1use std::fmt;
4
5use std::sync::Arc;
6
7use crate::core::listing_symbols::ListingSymbol;
8use crate::records::{Record, RecordError, RecordHash, b64a_decode, b64a_encode};
9
10use super::control::{ListingForm, ListingRow, RecordLabel};
11
12use super::stream_encoding::{StreamEncoding, StreamEncodingError, StreamOffers};
13
14pub(super) const KNOT_PREFIX: &[u8] = "🪢: ".as_bytes();
15pub const SESSION_PREFACE_MAX_BYTES: usize = 512;
17pub(super) const KNOT_LINE: &str = "🪢: iltp/0.13\n";
19pub const PORT_MAX_BYTES: usize =
21 crate::records::MAX_FIELD_LINE_BYTES_EXCLUDING_LF - "Port: ".len();
22const OFFER_TOKEN_MAX_BYTES: usize = 16;
23pub const ITEM_MAX_BYTES: usize = 64 * 1024 * 1024;
25#[derive(Clone, PartialEq, Eq, Hash)]
27pub struct SessionNonceText(String);
28
29impl SessionNonceText {
30 pub fn from_bytes(bytes: [u8; 32]) -> Result<Self, IltpError> {
31 if bytes.iter().all(|byte| *byte == 0) {
32 return Err(malformed("session nonce must not be all-zero"));
33 }
34 Ok(Self(b64a_encode(&bytes)))
35 }
36
37 pub fn parse(text: &str) -> Result<Self, IltpError> {
38 if text.len() != 43 || !text.is_ascii() {
39 return Err(malformed(
40 "session nonce must be exactly 43 canonical B64A characters",
41 ));
42 }
43 let decoded =
44 b64a_decode(text).map_err(|_| malformed("session nonce is not canonical B64A"))?;
45 let bytes: [u8; 32] = decoded
46 .try_into()
47 .map_err(|_| malformed("session nonce must encode exactly 32 bytes"))?;
48 if bytes.iter().all(|byte| *byte == 0) || b64a_encode(&bytes) != text {
49 return Err(malformed(
50 "session nonce must encode canonical nonzero 32-byte data",
51 ));
52 }
53 Ok(Self(text.to_owned()))
54 }
55
56 pub fn as_str(&self) -> &str {
57 &self.0
58 }
59 pub fn bytes(&self) -> [u8; 32] {
60 b64a_decode(&self.0)
61 .expect("validated nonce text")
62 .try_into()
63 .expect("validated nonce width")
64 }
65}
66
67impl fmt::Debug for SessionNonceText {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 f.debug_tuple("SessionNonceText").field(&self.0).finish()
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Hash)]
77pub struct PortText(String);
78
79impl PortText {
80 pub fn parse(text: &str) -> Result<Self, IltpError> {
81 let valid = (1..=PORT_MAX_BYTES).contains(&text.len())
82 && text.split('/').all(|segment| {
83 !segment.is_empty() && segment.bytes().all(|byte| byte.is_ascii_graphic())
84 });
85 if !valid {
86 return Err(malformed(
87 "port must be nonempty printable-ASCII segments separated by '/', at most 122 bytes",
88 ));
89 }
90 Ok(Self(text.to_owned()))
91 }
92
93 pub fn as_str(&self) -> &str {
94 &self.0
95 }
96}
97
98impl fmt::Display for PortText {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 f.write_str(&self.0)
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct SessionPreface {
108 pub nonce: SessionNonceText,
109 pub offers: StreamOffers,
110 pub port: Option<PortText>,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
116pub enum IltpItem {
117 SessionPreface(SessionPreface),
118 NoProof,
120 Policy(RecordHash),
122 PolicyKnown,
124 PolicyMissing,
126 Listing {
128 form: ListingForm,
129 rows: Vec<ListingRow>,
130 },
131 Symbols(Vec<ListingSymbol>),
133 Want(Arc<[RecordHash]>),
135 RoundEnd,
137 Record(RecordLabel, Record),
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
144pub enum IltpError {
145 Record(RecordError),
146 Truncated(IltpTruncation),
147 ItemTooLarge,
148 Malformed(String),
149 StreamEncoding(StreamEncodingError),
150 Local(&'static str),
152}
153
154impl IltpError {
155 pub fn is_local(&self) -> bool {
158 matches!(
159 self,
160 Self::StreamEncoding(StreamEncodingError::Local(_)) | Self::Local(_)
161 )
162 }
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166pub enum IltpTruncation {
167 Preface,
168 Control,
169 Record,
170 StreamItem,
171 StreamEncoding,
173}
174
175impl fmt::Display for IltpError {
176 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177 match self {
178 Self::Record(err) => write!(f, "record validation failed: {err}"),
179 Self::Truncated(kind) => write!(f, "truncated ILTP {} at end of stream", kind.label()),
180 Self::ItemTooLarge => f.write_str("ILTP item exceeds 64 MiB hard cap"),
181 Self::Malformed(msg) => f.write_str(msg),
182 Self::StreamEncoding(err) => err.fmt(f),
183 Self::Local(message) => f.write_str(message),
184 }
185 }
186}
187
188impl IltpTruncation {
189 fn label(self) -> &'static str {
190 match self {
191 Self::Preface => "preface",
192 Self::Control => "control item",
193 Self::Record => "record",
194 Self::StreamItem => "stream item",
195 Self::StreamEncoding => "stream encoding before its end mark",
196 }
197 }
198}
199
200impl std::error::Error for IltpError {}
201
202impl From<RecordError> for IltpError {
203 fn from(value: RecordError) -> Self {
204 Self::Record(value)
205 }
206}
207
208impl From<StreamEncodingError> for IltpError {
209 fn from(value: StreamEncodingError) -> Self {
210 Self::StreamEncoding(value)
211 }
212}
213
214mod incremental;
215pub(super) use incremental::{Framed, ItemFramer, PrefaceRule};
216
217#[cfg(any(test, feature = "internal"))]
220#[doc(hidden)]
221pub fn encode_item(item: &IltpItem, out: &mut Vec<u8>) -> Result<(), IltpError> {
222 if let IltpItem::SessionPreface(preface) = item {
223 write_preface(preface, out);
224 return Ok(());
225 }
226 let mut cursor = super::control::LineCursor::default();
227 let mut line = Vec::new();
228 while super::control::next_line(item, &mut cursor, &mut line) {
229 out.extend_from_slice(&line);
230 }
231 if let IltpItem::Record(_, record) = item {
232 out.extend_from_slice(
233 &record
234 .to_vec(crate::records::MAX_H3_RECORD_BYTES)
235 .map_err(|_| IltpError::Local("record bytes could not be read"))?,
236 );
237 out.push(b'\n');
238 }
239 Ok(())
240}
241
242pub(super) fn preface_len(preface: &SessionPreface) -> usize {
244 let mut len = 0;
245 preface_lines(preface, |line| {
246 len += line.iter().map(|part| part.len()).sum::<usize>()
247 });
248 len
249}
250
251pub(super) fn write_preface(preface: &SessionPreface, out: &mut Vec<u8>) {
252 preface_lines(preface, |line| {
253 for part in line {
254 out.extend_from_slice(part.as_bytes());
255 }
256 });
257}
258
259fn preface_lines(preface: &SessionPreface, mut line: impl FnMut(&[&str])) {
262 fn list(line: &mut impl FnMut(&[&str]), name: &'static str, encodings: &[StreamEncoding]) {
263 for (index, encoding) in encodings.iter().enumerate() {
264 let lead = if index == 0 { name } else { "," };
265 let end = if index + 1 == encodings.len() {
266 "\n"
267 } else {
268 ""
269 };
270 line(&[lead, encoding.token(), end]);
271 }
272 }
273 line(&[KNOT_LINE]);
274 line(&["Nonce: ", preface.nonce.as_str(), "\n"]);
275 list(&mut line, "Decode: ", preface.offers.decode());
276 list(&mut line, "Encode: ", preface.offers.encode());
277 if let Some(port) = &preface.port {
278 line(&["Port: ", port.as_str(), "\n"]);
279 }
280 line(&["\n"]);
281}
282
283#[derive(Debug, Default)]
285pub(super) struct PrefaceFields {
286 names: Vec<String>,
287 nonce: Option<SessionNonceText>,
288 decode: Vec<StreamEncoding>,
289 encode: Vec<StreamEncoding>,
290 port: Option<PortText>,
291}
292
293impl PrefaceFields {
294 pub(super) fn push_line(&mut self, line: &[u8]) -> Result<(), IltpError> {
297 let text = std::str::from_utf8(&line[..line.len() - 1])
298 .map_err(|_| malformed("preface field line is not UTF-8"))?;
299 let (name, value) = crate::util::validate_field_line(text)
300 .map_err(|error| malformed(format!("invalid preface field line: {error}")))?;
301 if self.names.iter().any(|seen| seen == name) {
302 return Err(malformed(format!("preface field {name} repeats")));
303 }
304 self.names.push(name.to_owned());
305 match name {
306 "Nonce" => self.nonce = Some(SessionNonceText::parse(value)?),
307 "Decode" => {
308 let tokens = offer_tokens(value)?;
309 if tokens.windows(2).any(|pair| pair[0] >= pair[1]) {
310 return Err(malformed(
311 "preface Decode tokens must be in strictly ascending order",
312 ));
313 }
314 self.decode = StreamOffers::known(tokens);
315 }
316 "Encode" => {
317 let tokens = offer_tokens(value)?;
318 if tokens
319 .iter()
320 .enumerate()
321 .any(|(index, token)| tokens[..index].contains(token))
322 {
323 return Err(malformed("preface Encode tokens must not repeat"));
324 }
325 self.encode = StreamOffers::known(tokens);
326 }
327 "Port" => self.port = Some(PortText::parse(value)?),
328 _ => {}
329 }
330 Ok(())
331 }
332
333 pub(super) fn finish(self) -> Result<SessionPreface, IltpError> {
334 Ok(SessionPreface {
335 nonce: self
336 .nonce
337 .ok_or_else(|| malformed("session preface has no Nonce field"))?,
338 offers: StreamOffers::from_lists(self.decode, self.encode),
339 port: self.port,
340 })
341 }
342}
343
344fn offer_tokens(list: &str) -> Result<Vec<&str>, IltpError> {
345 list.split(',')
346 .map(|token| {
347 let valid = (1..=OFFER_TOKEN_MAX_BYTES).contains(&token.len())
348 && token
349 .bytes()
350 .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit());
351 if !valid {
352 Err(malformed("preface offer token must match [a-z0-9]{1,16}"))
353 } else if token == StreamEncoding::Identity.token() {
354 Err(malformed("preface offers must not list identity"))
355 } else {
356 Ok(token)
357 }
358 })
359 .collect()
360}
361
362pub(super) fn malformed(message: impl Into<String>) -> IltpError {
363 IltpError::Malformed(message.into())
364}