1use std::sync::Arc;
10
11use crate::core::listing_symbols::ListingSymbol;
12use crate::core::{AdvertisementField, AdvertisementRecord};
13use crate::records::{
14 RECORD_HASH_TEXT_LEN, RecordHash, RecordKind, validate_projected_field_name_value,
15};
16
17use super::framing::{IltpError, IltpItem, malformed};
18
19pub(super) const CONTROL_LINE_MAX_EXCLUDING_LF: usize =
22 1 + crate::records::MAX_FIELD_LINE_BYTES_EXCLUDING_LF;
23pub const OFFER_LINE_BYTES: u64 = RECORD_HASH_TEXT_LEN as u64 + 1;
25pub const CHANGE_LINE_BYTES: u64 = 2 + OFFER_LINE_BYTES;
27pub const EMPTY_LISTING_BYTES: u64 = (COMPLETE_LABEL.len() + 1) as u64;
29pub const RESPONSE_FRAMING_BYTES: u64 = RESPONSE_LABEL.len() as u64 + 1;
32pub const ROUND_END_BYTES: u64 = ROUND_END_LABEL.len() as u64;
34
35const COMPLETE_LABEL: &str = "::offers\n";
36const NO_PROOF_LABEL: &str = "::as unknown\n";
37const POLICY_KNOWN_LABEL: &str = "::policy-known\n";
38const POLICY_MISSING_LABEL: &str = "::policy-missing\n";
39const ROUND_END_LABEL: &str = "::round-end\n";
40const PROOF_LABEL: &str = "::as\n";
41const RESPONSE_LABEL: &str = "::response\n";
42const POLICY_BODY_LABEL: &str = "::policy-body\n";
43const WANT_LABEL: &str = "::want\n";
44const SYMBOLS_LABEL: &str = "::symbols\n";
45const POLICY_PREFIX: &str = "::policy ";
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum ListingForm {
50 Full,
52 Difference,
54 Delta,
57}
58
59impl ListingForm {
60 pub fn label(self) -> &'static str {
62 match self {
63 Self::Full => COMPLETE_LABEL,
64 Self::Difference => "::offers-unshared\n",
65 Self::Delta => "::offers-changed\n",
66 }
67 }
68
69 pub fn is_change(self) -> bool {
71 self == Self::Delta
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum ListingRow {
78 Withdrawn(RecordHash),
79 Added(Arc<AdvertisementRecord>),
80}
81
82impl ListingRow {
83 pub fn hash(&self) -> RecordHash {
84 match self {
85 Self::Withdrawn(hash) => *hash,
86 Self::Added(entry) => entry.record_hash(),
87 }
88 }
89
90 pub fn rendered_bytes(&self, form: ListingForm) -> u64 {
92 match self {
93 Self::Withdrawn(_) => CHANGE_LINE_BYTES,
94 Self::Added(entry) => entry.canonical_bytes() + if form.is_change() { 2 } else { 0 },
95 }
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum RecordLabel {
102 Proof,
103 PolicyBody,
104 Response,
105}
106
107impl RecordLabel {
108 fn label(self) -> &'static str {
109 match self {
110 Self::Proof => PROOF_LABEL,
111 Self::PolicyBody => POLICY_BODY_LABEL,
112 Self::Response => RESPONSE_LABEL,
113 }
114 }
115
116 pub fn item_len(self, canonical_len: usize) -> usize {
119 self.label().len() + canonical_len + 1
120 }
121}
122
123pub fn entry_len(fields: &[AdvertisementField]) -> u64 {
126 fields.iter().fold(OFFER_LINE_BYTES, |len, field| {
127 len + (1 + field.name.len() + 2 + field.value.len() + 1) as u64
128 })
129}
130
131pub(super) fn item_len(item: &IltpItem) -> usize {
133 let hash_line = RECORD_HASH_TEXT_LEN + 1;
134 match item {
135 IltpItem::SessionPreface(preface) => super::framing::preface_len(preface),
136 IltpItem::NoProof => NO_PROOF_LABEL.len(),
137 IltpItem::Policy(_) => POLICY_PREFIX.len() + hash_line,
138 IltpItem::PolicyKnown => POLICY_KNOWN_LABEL.len(),
139 IltpItem::PolicyMissing => POLICY_MISSING_LABEL.len(),
140 IltpItem::Listing { form, rows } => {
141 form.label().len()
142 + rows
143 .iter()
144 .map(|row| row.rendered_bytes(*form) as usize)
145 .sum::<usize>()
146 + 1
147 }
148 IltpItem::Symbols(symbols) => {
149 SYMBOLS_LABEL.len()
150 + symbols
151 .iter()
152 .map(|symbol| symbol.row_len() + 1)
153 .sum::<usize>()
154 + 1
155 }
156 IltpItem::Want(hashes) => WANT_LABEL.len() + hashes.len() * hash_line + 1,
157 IltpItem::RoundEnd => ROUND_END_LABEL.len(),
158 IltpItem::Record(label, record) => label.item_len(record.canonical_len()),
159 }
160}
161
162#[derive(Debug, Default, Clone, Copy)]
164pub(super) struct LineCursor {
165 stage: Stage,
166 row: usize,
167 field: usize,
169}
170
171#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
172enum Stage {
173 #[default]
174 Label,
175 Rows,
176 Done,
177}
178
179pub(super) fn next_line(item: &IltpItem, cursor: &mut LineCursor, out: &mut Vec<u8>) -> bool {
183 out.clear();
184 match cursor.stage {
185 Stage::Label => {
186 let list = write_label(item, out);
187 cursor.stage = if list { Stage::Rows } else { Stage::Done };
188 true
189 }
190 Stage::Rows => {
191 if !write_row(item, cursor, out) {
192 out.push(b'\n');
193 cursor.stage = Stage::Done;
194 }
195 true
196 }
197 Stage::Done => false,
198 }
199}
200
201fn write_label(item: &IltpItem, out: &mut Vec<u8>) -> bool {
203 let mut push = |text: &str| out.extend_from_slice(text.as_bytes());
204 match item {
205 IltpItem::SessionPreface(_) => unreachable!("prefaces render through framing"),
206 IltpItem::NoProof => push(NO_PROOF_LABEL),
207 IltpItem::Policy(operand) => {
208 push(POLICY_PREFIX);
209 push(operand.as_str());
210 push("\n");
211 }
212 IltpItem::PolicyKnown => push(POLICY_KNOWN_LABEL),
213 IltpItem::PolicyMissing => push(POLICY_MISSING_LABEL),
214 IltpItem::RoundEnd => push(ROUND_END_LABEL),
215 IltpItem::Listing { form, .. } => {
216 push(form.label());
217 return true;
218 }
219 IltpItem::Symbols(_) => {
220 push(SYMBOLS_LABEL);
221 return true;
222 }
223 IltpItem::Want(_) => {
224 push(WANT_LABEL);
225 return true;
226 }
227 IltpItem::Record(label, _) => push(label.label()),
228 }
229 false
230}
231
232fn write_hash_line(lead: &str, hash: &RecordHash, out: &mut Vec<u8>) {
233 out.extend_from_slice(lead.as_bytes());
234 out.extend_from_slice(hash.as_str().as_bytes());
235 out.push(b'\n');
236}
237
238fn write_row(item: &IltpItem, cursor: &mut LineCursor, out: &mut Vec<u8>) -> bool {
241 let row = cursor.row;
242 match item {
243 IltpItem::Listing { form, rows } => match rows.get(row) {
244 None => return false,
245 Some(ListingRow::Withdrawn(hash)) => {
246 debug_assert!(form.is_change(), "only ::offers-changed withdraws");
247 write_hash_line("- ", hash, out);
248 }
249 Some(ListingRow::Added(entry)) => {
250 match cursor.field.checked_sub(1) {
251 None => {
252 let lead = if form.is_change() { "+ " } else { "" };
253 write_hash_line(lead, &entry.record_hash(), out);
254 }
255 Some(index) => {
256 let field = &entry.fields()[index];
257 out.push(b'\t');
258 out.extend_from_slice(field.name.as_bytes());
259 out.extend_from_slice(b": ");
260 out.extend_from_slice(field.value.as_bytes());
261 out.push(b'\n');
262 }
263 }
264 cursor.field += 1;
265 if cursor.field > entry.fields().len() {
266 cursor.field = 0;
267 cursor.row += 1;
268 }
269 return true;
270 }
271 },
272 IltpItem::Symbols(symbols) => match symbols.get(row) {
273 None => return false,
274 Some(symbol) => {
275 symbol.write_row(out);
276 out.push(b'\n');
277 }
278 },
279 IltpItem::Want(hashes) => match hashes.get(row) {
280 None => return false,
281 Some(hash) => write_hash_line("", hash, out),
282 },
283 _ => return false,
284 }
285 cursor.row += 1;
286 true
287}
288
289pub(super) enum Opened {
291 Item(IltpItem),
293 List(List),
295 Record(RecordLabel),
297}
298
299pub(super) fn open(line: &[u8]) -> Result<Opened, IltpError> {
301 let text = std::str::from_utf8(line).map_err(|_| malformed("ILTP label is not UTF-8"))?;
302 let item = match text {
303 "::as unknown" => IltpItem::NoProof,
304 "::policy-known" => IltpItem::PolicyKnown,
305 "::policy-missing" => IltpItem::PolicyMissing,
306 "::round-end" => IltpItem::RoundEnd,
307 "::as" => return Ok(Opened::Record(RecordLabel::Proof)),
308 "::policy-body" => return Ok(Opened::Record(RecordLabel::PolicyBody)),
309 "::response" => return Ok(Opened::Record(RecordLabel::Response)),
310 "::want" => return Ok(Opened::List(List::Want(Vec::new()))),
311 "::symbols" => return Ok(Opened::List(List::Symbols(Vec::new()))),
312 "::offers" => return Ok(listing(ListingForm::Full)),
313 "::offers-changed" => return Ok(listing(ListingForm::Delta)),
314 "::offers-unshared" => return Ok(listing(ListingForm::Difference)),
315 _ => {
316 let Some(rest) = text.strip_prefix(POLICY_PREFIX) else {
317 return Err(malformed("unknown ILTP label"));
318 };
319 let operand = parse_hash(rest)?;
320 if operand.kind() != RecordKind::Blob {
321 return Err(malformed("operand hash is not a Blob hash"));
322 }
323 IltpItem::Policy(operand)
324 }
325 };
326 Ok(Opened::Item(item))
327}
328
329fn listing(form: ListingForm) -> Opened {
330 Opened::List(List::Listing(ListingList {
331 form,
332 rows: Vec::new(),
333 entry: None,
334 }))
335}
336
337fn parse_hash(text: &str) -> Result<RecordHash, IltpError> {
338 RecordHash::parse(text).map_err(|error| malformed(format!("invalid record hash: {error}")))
339}
340
341pub(super) enum List {
343 Listing(ListingList),
344 Symbols(Vec<ListingSymbol>),
345 Want(Vec<RecordHash>),
346}
347
348pub(super) struct ListingList {
349 form: ListingForm,
350 rows: Vec<ListingRow>,
351 entry: Option<(RecordHash, Vec<AdvertisementField>)>,
353}
354
355impl List {
356 pub(super) fn push_line(&mut self, line: &[u8]) -> Result<(), IltpError> {
358 match self {
359 Self::Listing(list) => list.push_line(line)?,
360 Self::Symbols(symbols) => symbols.push(
361 ListingSymbol::parse_row(ascii(line)?)
362 .map_err(|error| malformed(error.to_string()))?,
363 ),
364 Self::Want(hashes) => {
365 let hash = parse_hash(ascii(line)?)?;
366 if hashes.last().is_some_and(|last| *last >= hash) {
367 return Err(malformed(
368 "requested hashes are not in strictly increasing order",
369 ));
370 }
371 hashes.push(hash);
372 }
373 }
374 Ok(())
375 }
376
377 pub(super) fn finish(self) -> Result<IltpItem, IltpError> {
379 Ok(match self {
380 Self::Listing(list) => list.finish(),
381 Self::Symbols(symbols) => {
382 if symbols.is_empty() {
383 return Err(malformed("::symbols has no rows"));
384 }
385 IltpItem::Symbols(symbols)
386 }
387 Self::Want(hashes) => IltpItem::Want(hashes.into()),
388 })
389 }
390}
391
392impl ListingList {
393 fn push_line(&mut self, line: &[u8]) -> Result<(), IltpError> {
394 if let Some(field) = line.strip_prefix(b"\t") {
395 let Some((_, fields)) = &mut self.entry else {
396 return Err(malformed("field line does not follow an offer"));
397 };
398 let text =
399 std::str::from_utf8(field).map_err(|_| malformed("field line is not UTF-8"))?;
400 let (name, value) = text
401 .split_once(": ")
402 .ok_or_else(|| malformed("field line has no ': ' separator"))?;
403 validate_projected_field_name_value(name, value)
404 .map_err(|error| malformed(format!("invalid advertised field: {error}")))?;
405 let index = match fields.last() {
406 Some(last) if last.name.as_bytes() > name.as_bytes() => {
407 return Err(malformed("advertised field names decrease"));
408 }
409 Some(last) if last.name == name => last.index.parse::<usize>().unwrap_or(0) + 1,
410 _ => 0,
411 };
412 fields.push(AdvertisementField::new(name, index.to_string(), value));
413 return Ok(());
414 }
415 self.finish_entry();
416 let (added, hash) = match (self.form.is_change(), line) {
417 (true, [b'+', b' ', hash @ ..]) => (true, hash),
418 (true, [b'-', b' ', hash @ ..]) => (false, hash),
419 (true, _) => return Err(malformed("::offers-changed rows start with '+ ' or '- '")),
420 (false, [b'+' | b'-', b' ', ..]) => {
421 return Err(malformed("a complete listing holds bare offer hashes"));
422 }
423 (false, hash) => (true, hash),
424 };
425 let hash = parse_hash(ascii(hash)?)?;
426 if self.rows.last().is_some_and(|last| last.hash() >= hash) {
427 return Err(malformed(
428 "listing rows are not in strictly increasing hash order",
429 ));
430 }
431 if added {
432 self.entry = Some((hash, Vec::new()));
433 } else {
434 self.rows.push(ListingRow::Withdrawn(hash));
435 }
436 Ok(())
437 }
438
439 fn finish_entry(&mut self) {
440 if let Some((hash, fields)) = self.entry.take() {
441 self.rows
442 .push(ListingRow::Added(Arc::new(AdvertisementRecord::new(
443 hash, fields,
444 ))));
445 }
446 }
447
448 fn finish(mut self) -> IltpItem {
449 self.finish_entry();
450 IltpItem::Listing {
451 form: self.form,
452 rows: self.rows,
453 }
454 }
455}
456
457fn ascii(line: &[u8]) -> Result<&str, IltpError> {
458 if !line.is_ascii() {
459 return Err(malformed("ILTP row is not ASCII"));
460 }
461 Ok(std::str::from_utf8(line).expect("ASCII is UTF-8"))
462}