1use std::fmt;
8
9use super::validator::RecordError;
10use crate::hash::{RecordHash, RecordKind};
11use crate::typed_text::{
12 BLOB_RECORD_HASH_LEAD, ByValue, PLEX_RECORD_HASH_LEAD, SEAL_RECORD_HASH_LEAD,
13};
14use crate::util::{
15 BY_LINE_PREFIX, DATA_LENGTH_PREFIX, LENGTH_FIELD, MARK_LINE_PREFIX, MAX_RECORD_METADATA_BYTES,
16 RECORD_GLYPH_LINE_HASH_END, RECORD_GLYPH_LINE_HASH_START, RECORD_GLYPH_LINE_LEN,
17 RECORD_GLYPH_LINE_PREFIX, TYPE_FIELD, with_usize_decimal_str,
18};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct FieldOccurrence<'a> {
23 pub name: &'a str,
24 pub index: usize,
26 pub value: &'a str,
27}
28
29#[derive(Debug, Clone)]
31pub struct ApplicationFieldIter<'a> {
32 remaining: &'a str,
33 previous_name: Option<&'a str>,
34 index: usize,
35}
36
37impl<'a> Iterator for ApplicationFieldIter<'a> {
38 type Item = FieldOccurrence<'a>;
39
40 fn next(&mut self) -> Option<Self::Item> {
41 if self.remaining.is_empty() {
42 return None;
43 }
44 let (line, rest) = match self.remaining.find('\n') {
45 Some(lf) => (&self.remaining[..lf], &self.remaining[lf + 1..]),
46 None => (self.remaining, ""),
47 };
48 self.remaining = rest;
49 let (name, value) = line
50 .split_once(": ")
51 .expect("validated application field contains separator");
52 if self.previous_name == Some(name) {
53 self.index += 1;
54 } else {
55 self.previous_name = Some(name);
56 self.index = 0;
57 }
58 Some(FieldOccurrence {
59 name,
60 index: self.index,
61 value,
62 })
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub struct IndexedField<'a> {
69 pub index: usize,
70 pub value: &'a str,
71}
72
73#[derive(Clone, Copy)]
78pub struct RecordMetadata<'a> {
79 prefix: &'a [u8],
80}
81
82impl fmt::Debug for RecordMetadata<'_> {
83 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
84 formatter
85 .debug_struct("RecordMetadata")
86 .field("hash", &self.hash())
87 .field("prefix_len", &self.prefix.len())
88 .finish()
89 }
90}
91
92impl<'a> RecordMetadata<'a> {
93 pub(super) fn new_trusted(prefix: &'a [u8]) -> Self {
96 Self { prefix }
97 }
98
99 fn parsed(self) -> ParsedPrefix<'a> {
100 parse_trusted_prefix(self.prefix).expect("record metadata is a trusted canonical prefix")
101 }
102
103 pub fn prefix(self) -> &'a [u8] {
105 self.prefix
106 }
107
108 pub fn hash(self) -> RecordHash {
109 unsafe {
111 RecordHash::from_validated_h3_bytes_unchecked(
112 &self.prefix[RECORD_GLYPH_LINE_HASH_START..RECORD_GLYPH_LINE_HASH_END],
113 )
114 }
115 }
116
117 pub fn hash_text(self) -> &'a str {
118 unsafe {
120 std::str::from_utf8_unchecked(
121 &self.prefix[RECORD_GLYPH_LINE_HASH_START..RECORD_GLYPH_LINE_HASH_END],
122 )
123 }
124 }
125
126 pub fn kind(self) -> RecordKind {
127 self.hash().kind()
128 }
129
130 pub fn by(self) -> Option<ByValue> {
131 self.parsed().by_value.map(|text| {
132 unsafe { ByValue::from_validated_bytes_unchecked(text.as_bytes()) }
134 })
135 }
136
137 pub fn mark(self) -> Option<&'a str> {
138 self.parsed().mark
139 }
140
141 pub fn data_length(self) -> usize {
142 self.parsed().data_length
143 }
144
145 pub fn data_length_text(self) -> &'a str {
146 let digits = data_length_digits(self.prefix).expect("trusted prefix ends with Data-Length");
147 unsafe { std::str::from_utf8_unchecked(digits) }
149 }
150
151 pub fn blob_hash(self) -> Option<RecordHash> {
152 self.parsed().blob_hash
153 }
154
155 pub fn plex_hash(self) -> Option<RecordHash> {
156 self.parsed().plex_hash
157 }
158
159 pub fn application_fields(self) -> ApplicationFieldIter<'a> {
160 ApplicationFieldIter {
161 remaining: self.parsed().application_fields,
162 previous_name: None,
163 index: 0,
164 }
165 }
166
167 pub fn fields(self, name: &str) -> impl Iterator<Item = IndexedField<'a>> + 'a {
168 let mut fields = self.application_fields();
169 let first = fields
170 .find(|field| field.name >= name)
171 .filter(|field| field.name == name);
172 let matched_name = first.map(|field| field.name);
174 first
175 .into_iter()
176 .chain(fields.take_while(move |field| Some(field.name) == matched_name))
177 .map(|field| IndexedField {
178 index: field.index,
179 value: field.value,
180 })
181 }
182
183 pub fn visit_record_links(self, mut visitor: impl FnMut(&str, usize, &str, &str)) {
184 for field in self.application_fields() {
185 if let Some((data, target)) =
186 crate::util::parse_record_link_text(field.name, field.value)
187 {
188 visitor(field.name, field.index, data, target);
189 }
190 }
191 }
192
193 pub(crate) fn visit_fields(self, mut visitor: impl FnMut(&str, usize, &str)) {
194 let parsed = self.parsed();
195 with_usize_decimal_str(self.prefix.len() + parsed.data_length, |value| {
196 visitor(LENGTH_FIELD, 0, value)
197 });
198 visitor(TYPE_FIELD, 0, parsed.kind.code());
199 for field in self.application_fields() {
200 visitor(field.name, field.index, field.value);
201 }
202 with_usize_decimal_str(parsed.data_length, |value| visitor("Data-Length", 0, value));
203 if let Some(value) = parsed.by_value {
204 visitor("By", 0, value);
205 }
206 if let Some(value) = parsed.mark {
207 visitor("Mark", 0, value);
208 }
209 }
210
211 pub(crate) fn visit_field_named(self, target: &str, mut visitor: impl FnMut(&str, &str)) {
212 let parsed = self.parsed();
213 match target {
214 LENGTH_FIELD => {
215 with_usize_decimal_str(self.prefix.len() + parsed.data_length, |value| {
216 visitor("0", value)
217 })
218 }
219 TYPE_FIELD => visitor("0", parsed.kind.code()),
220 "Data-Length" => {
221 with_usize_decimal_str(parsed.data_length, |value| visitor("0", value))
222 }
223 "By" => {
224 if let Some(value) = parsed.by_value {
225 visitor("0", value);
226 }
227 }
228 "Mark" => {
229 if let Some(value) = parsed.mark {
230 visitor("0", value);
231 }
232 }
233 _ => {}
234 }
235 for field in self.application_fields() {
236 if field.name == target {
237 with_usize_decimal_str(field.index, |index| visitor(index, field.value));
238 } else if field.name.as_bytes() > target.as_bytes() {
239 break;
240 }
241 }
242 }
243
244 pub(crate) fn has_field_named(self, target: &str) -> bool {
245 let mut found = false;
246 self.visit_field_named(target, |_, _| found = true);
247 found
248 }
249
250 pub(crate) fn has_field_exact(self, target: &str, expected: &str) -> bool {
251 let mut found = false;
252 self.visit_field_named(target, |_, value| {
253 if value == expected {
254 found = true;
255 }
256 });
257 found
258 }
259}
260
261struct ParsedPrefix<'a> {
262 kind: RecordKind,
263 data_length: usize,
264 blob_hash: Option<RecordHash>,
265 plex_hash: Option<RecordHash>,
266 application_fields: &'a str,
267 by_value: Option<&'a str>,
268 mark: Option<&'a str>,
269}
270
271fn trusted_error(message: &str) -> RecordError {
272 RecordError::InvalidFormat(format!("trusted record {message}"))
273}
274
275pub(super) fn trusted_data_offset(bytes: &[u8]) -> Result<usize, RecordError> {
279 trusted_head_data_offset(bytes, bytes.len())?
280 .ok_or_else(|| trusted_error("has no Blob blank line"))
281}
282
283pub(super) fn trusted_head_data_offset(
287 head: &[u8],
288 canonical_len: usize,
289) -> Result<Option<usize>, RecordError> {
290 trusted_glyph_hash(head)?;
291 let searched = &head[..head.len().min(MAX_RECORD_METADATA_BYTES)];
294 let Some(lf) = memchr::memmem::find(searched, b"\n\n") else {
295 if head.len() < canonical_len.min(MAX_RECORD_METADATA_BYTES) {
296 return Ok(None);
297 }
298 return Err(trusted_error("has no Blob blank line"));
299 };
300 let data_offset = lf + 2;
301 let digits = data_length_digits(&head[..data_offset])
302 .ok_or_else(|| trusted_error("has no Data-Length line"))?;
303 if data_offset.checked_add(parse_decimal(digits)?) != Some(canonical_len) {
304 return Err(trusted_error("length does not match Data-Length"));
305 }
306 Ok(Some(data_offset))
307}
308
309fn data_length_digits(prefix: &[u8]) -> Option<&[u8]> {
311 let line_end = prefix.len().checked_sub(2)?;
312 let line_start = memchr::memrchr(b'\n', &prefix[..line_end])? + 1;
313 prefix[line_start..line_end].strip_prefix(DATA_LENGTH_PREFIX)
314}
315
316fn trusted_glyph_hash(bytes: &[u8]) -> Result<RecordHash, RecordError> {
317 if bytes.len() < RECORD_GLYPH_LINE_LEN
318 || !bytes.starts_with(RECORD_GLYPH_LINE_PREFIX)
319 || bytes[RECORD_GLYPH_LINE_HASH_END] != b'\n'
320 {
321 return Err(trusted_error("has a malformed glyph line"));
322 }
323 match bytes[RECORD_GLYPH_LINE_HASH_START] {
324 BLOB_RECORD_HASH_LEAD | PLEX_RECORD_HASH_LEAD | SEAL_RECORD_HASH_LEAD => {}
325 other => return Err(RecordError::UnknownType((other as char).to_string())),
326 }
327 let text = &bytes[RECORD_GLYPH_LINE_HASH_START..RECORD_GLYPH_LINE_HASH_END];
328 if text[1] != b'.' || !text.ends_with(b".H3") {
329 return Err(trusted_error("has malformed hash text"));
330 }
331 Ok(unsafe { RecordHash::from_validated_h3_bytes_unchecked(text) })
334}
335
336fn parse_trusted_prefix(prefix: &[u8]) -> Result<ParsedPrefix<'_>, RecordError> {
337 let hash = trusted_glyph_hash(prefix)?;
338 let mut cursor = RECORD_GLYPH_LINE_LEN;
339 let mut by_value = None;
340 let mut mark = None;
341 let mut plex_hash = None;
342 if hash.kind() == RecordKind::Seal {
343 by_value = Some(take_prefixed_line(prefix, &mut cursor, BY_LINE_PREFIX)?);
344 mark = Some(take_prefixed_line(prefix, &mut cursor, MARK_LINE_PREFIX)?);
345 plex_hash = Some(trusted_glyph_hash(&prefix[cursor..])?);
346 cursor += RECORD_GLYPH_LINE_LEN;
347 }
348 let mut application_fields = "";
349 let mut blob_hash = None;
350 if hash.kind() != RecordKind::Blob {
351 let fields_start = cursor;
352 while !glyph_line_at(prefix, cursor) {
353 let lf = memchr::memchr(b'\n', &prefix[cursor..])
354 .ok_or_else(|| trusted_error("has an unterminated Plex field"))?;
355 cursor += lf + 1;
356 }
357 if cursor == fields_start {
358 return Err(trusted_error("Plex has no application fields"));
359 }
360 application_fields =
362 unsafe { std::str::from_utf8_unchecked(&prefix[fields_start..cursor - 1]) };
363 blob_hash = Some(trusted_glyph_hash(&prefix[cursor..])?);
364 cursor += RECORD_GLYPH_LINE_LEN;
365 }
366 let digits = prefix[cursor..]
367 .strip_prefix(DATA_LENGTH_PREFIX)
368 .and_then(|rest| rest.strip_suffix(b"\n\n"))
369 .ok_or_else(|| trusted_error("prefix does not end with Data-Length"))?;
370 Ok(ParsedPrefix {
371 kind: hash.kind(),
372 data_length: parse_decimal(digits)?,
373 blob_hash,
374 plex_hash,
375 application_fields,
376 by_value,
377 mark,
378 })
379}
380
381fn glyph_line_at(prefix: &[u8], cursor: usize) -> bool {
382 prefix[cursor..].starts_with(RECORD_GLYPH_LINE_PREFIX)
383}
384
385fn take_prefixed_line<'a>(
386 prefix: &'a [u8],
387 cursor: &mut usize,
388 line_prefix: &[u8],
389) -> Result<&'a str, RecordError> {
390 let rest = prefix[*cursor..]
391 .strip_prefix(line_prefix)
392 .ok_or_else(|| trusted_error("is missing a structural line"))?;
393 let value_len = memchr::memchr(b'\n', rest)
394 .ok_or_else(|| trusted_error("has an unterminated structural line"))?;
395 *cursor += line_prefix.len() + value_len + 1;
396 Ok(unsafe { std::str::from_utf8_unchecked(&rest[..value_len]) })
398}
399
400fn parse_decimal(digits: &[u8]) -> Result<usize, RecordError> {
401 if digits.is_empty() {
402 return Err(trusted_error("has an empty Data-Length"));
403 }
404 digits.iter().try_fold(0usize, |value, &byte| {
405 if !byte.is_ascii_digit() {
406 return Err(trusted_error("Data-Length contains a non-digit"));
407 }
408 value
409 .checked_mul(10)
410 .and_then(|value| value.checked_add(usize::from(byte - b'0')))
411 .ok_or_else(|| trusted_error("Data-Length overflows"))
412 })
413}