Skip to main content

lace_sans_io/records/
metadata.rs

1//! Borrowed views over a validated record's canonical non-payload prefix.
2//!
3//! The prefix holds every glyph, structural and application field line and
4//! ends with the Blob's blank line. Facts, links, `By`/Mark and containment
5//! all come from it; no accessor reads Blob data.
6
7use 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/// One borrowed Plex application-field occurrence.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct FieldOccurrence<'a> {
23    pub name: &'a str,
24    /// Zero-based occurrence index within this field name.
25    pub index: usize,
26    pub value: &'a str,
27}
28
29/// Iterator over canonical Plex application fields.
30#[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/// One indexed occurrence returned by direct field lookup.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub struct IndexedField<'a> {
69    pub index: usize,
70    pub value: &'a str,
71}
72
73/// Borrowed view of a validated record's canonical non-payload prefix.
74///
75/// Accessors parse the trusted prefix on demand, in time proportional to the
76/// header; they never read Blob data.
77#[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    /// `prefix` is the complete canonical non-payload prefix of a validated
94    /// record, as located by [`trusted_data_offset`].
95    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    /// The canonical bytes before the Blob data.
104    pub fn prefix(self) -> &'a [u8] {
105        self.prefix
106    }
107
108    pub fn hash(self) -> RecordHash {
109        // SAFETY: the prefix starts with a validated canonical glyph line.
110        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        // SAFETY: canonical hash text is ASCII.
119        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            // SAFETY: validation established canonical text and an on-curve key.
133            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        // SAFETY: validated Data-Length digits are ASCII.
148        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        // Retain the matched name from the record, not the caller's lookup text.
173        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
275/// Locate the data offset of trusted committed record bytes: check the glyph
276/// line, find the blank line ending the prefix and check that `Data-Length`
277/// accounts for exactly the remaining bytes. Hashes and Marks are not rerun.
278pub(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
283/// [`trusted_data_offset`] of a trusted record `canonical_len` bytes long,
284/// given only its first bytes `head`: `None` when `head` ends before the
285/// prefix does.
286pub(super) fn trusted_head_data_offset(
287    head: &[u8],
288    canonical_len: usize,
289) -> Result<Option<usize>, RecordError> {
290    trusted_glyph_hash(head)?;
291    // Field and structural lines are never empty, so the first empty line is
292    // the Blob's blank line.
293    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
309/// The digits of the final `Data-Length` line of a prefix ending in `\n\n`.
310fn 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    // SAFETY: committed rows were validated at admission; the lead, separator
332    // and suffix checks above guard against a misaligned trusted range.
333    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        // SAFETY: validated field lines are UTF-8; drop the final LF.
361        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    // SAFETY: validated structural values are ASCII.
397    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}