1use std::fmt;
6
7use b64a::{b64a_decode_into, b64a_encoded_len};
8use schnorr_blake3::{Mark, VerificationKey};
9
10use crate::hash::{RecordHash, RecordKind};
11use crate::typed_text::ByValue;
12use crate::util::{
13 MAX_BLOB_SIZE, MAX_FIELD_LINE_BYTES_EXCLUDING_LF, MAX_PLEX_FIELDS, MAX_RECORD_METADATA_BYTES,
14 RECORD_GLYPH, RECORD_GLYPH_LINE_PREFIX, validate_application_field_line, validate_field_line,
15};
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum RecordError {
20 InvalidFormat(String),
21 HashMismatch { expected: String, computed: String },
22 MarkInvalid,
23 UnknownType(String),
24 MissingField(String),
25}
26
27impl fmt::Display for RecordError {
28 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
29 match self {
30 Self::InvalidFormat(message) => write!(formatter, "invalid record format: {message}"),
31 Self::HashMismatch { expected, computed } => {
32 write!(
33 formatter,
34 "hash mismatch: expected {expected}, got {computed}"
35 )
36 }
37 Self::MarkInvalid => formatter.write_str("mark verification failed"),
38 Self::UnknownType(kind) => write!(formatter, "unknown record type: {kind}"),
39 Self::MissingField(field) => write!(formatter, "missing field: {field}"),
40 }
41 }
42}
43
44impl std::error::Error for RecordError {}
45
46fn invalid(message: impl Into<String>) -> RecordError {
47 RecordError::InvalidFormat(message.into())
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51enum Phase {
52 Glyph(Option<RecordKind>),
54 By,
55 Mark,
56 PlexFields,
57 DataLength,
58 Blank,
59 Body,
60}
61
62pub struct RecordValidator {
68 prefix: Vec<u8>,
69 line_start: usize,
70 phase: Phase,
71 digests: Vec<(RecordHash, blake3::Hasher)>,
75 previous_name: Option<(usize, usize)>,
76 field_count: usize,
77 by: Option<VerificationKey>,
78 mark: Option<Mark>,
79 data_length: usize,
80 data_received: usize,
81 complete: bool,
82 failure: Option<RecordError>,
83}
84
85impl Default for RecordValidator {
86 fn default() -> Self {
87 Self::new()
88 }
89}
90
91impl fmt::Debug for RecordValidator {
92 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
93 formatter
94 .debug_struct("RecordValidator")
95 .field("phase", &self.phase)
96 .field("prefix_len", &self.prefix.len())
97 .field("data_received", &self.data_received)
98 .field("complete", &self.complete)
99 .field("failure", &self.failure)
100 .finish()
101 }
102}
103
104impl RecordValidator {
105 pub fn new() -> Self {
106 Self {
107 prefix: Vec::new(),
108 line_start: 0,
109 phase: Phase::Glyph(None),
110 digests: Vec::new(),
111 previous_name: None,
112 field_count: 0,
113 by: None,
114 mark: None,
115 data_length: 0,
116 data_received: 0,
117 complete: false,
118 failure: None,
119 }
120 }
121
122 pub fn feed(&mut self, input: &[u8]) -> Result<usize, RecordError> {
125 if let Some(failure) = &self.failure {
126 return Err(failure.clone());
127 }
128 let result = self.consume(input);
129 if let Err(failure) = &result {
130 self.failure = Some(failure.clone());
131 }
132 result
133 }
134
135 pub fn canonical_len(&self) -> Option<usize> {
138 (self.phase == Phase::Body).then(|| self.prefix.len() + self.data_length)
139 }
140
141 pub fn prefix(&self) -> &[u8] {
144 &self.prefix
145 }
146
147 pub fn is_complete(&self) -> bool {
149 self.complete
150 }
151
152 pub fn finish(mut self) -> Result<ValidatedHeader, RecordError> {
154 self.take_header()
155 }
156
157 pub(crate) fn take_header(&mut self) -> Result<ValidatedHeader, RecordError> {
160 if let Some(failure) = &self.failure {
161 return Err(failure.clone());
162 }
163 if !self.complete {
164 return Err(invalid("truncated record"));
165 }
166 Ok(ValidatedHeader {
167 hash: self.digests[0].0,
168 canonical_len: self.prefix.len() + self.data_length,
169 prefix: std::mem::take(&mut self.prefix),
170 })
171 }
172
173 pub(crate) fn reset(&mut self) {
175 let mut digests = std::mem::take(&mut self.digests);
176 digests.clear();
177 *self = Self {
178 digests,
179 ..Self::new()
180 };
181 }
182
183 fn consume(&mut self, input: &[u8]) -> Result<usize, RecordError> {
184 let mut consumed = 0;
185 while !self.complete && consumed < input.len() {
186 let rest = &input[consumed..];
187 let count = if self.phase == Phase::Body {
188 let count = (self.data_length - self.data_received).min(rest.len());
189 self.hash(&rest[..count]);
190 self.data_received += count;
191 count
192 } else {
193 self.consume_line_bytes(rest)?
194 };
195 consumed += count;
196 if self.phase == Phase::Body && self.data_received == self.data_length {
197 self.verify()?;
198 self.complete = true;
199 }
200 }
201 Ok(consumed)
202 }
203
204 fn consume_line_bytes(&mut self, input: &[u8]) -> Result<usize, RecordError> {
206 let (count, has_lf) = match memchr::memchr(b'\n', input) {
207 Some(lf) => (lf + 1, true),
208 None => (input.len(), false),
209 };
210 let line_len = self.prefix.len() - self.line_start + count - usize::from(has_lf);
211 if line_len > MAX_FIELD_LINE_BYTES_EXCLUDING_LF {
212 return Err(invalid("field line too long"));
213 }
214 let len = self.prefix.len() + count;
215 if len > MAX_RECORD_METADATA_BYTES {
216 return Err(invalid("record metadata too long"));
217 }
218 if len > self.prefix.capacity() {
220 let capacity = (self.prefix.capacity() * 2).clamp(len, MAX_RECORD_METADATA_BYTES);
221 self.prefix.reserve_exact(capacity - self.prefix.len());
222 }
223 let bytes = &input[..count];
224 self.hash(bytes);
225 self.prefix.extend_from_slice(bytes);
226 if has_lf {
227 self.finish_line()?;
228 self.line_start = self.prefix.len();
229 }
230 Ok(count)
231 }
232
233 fn hash(&mut self, bytes: &[u8]) {
234 for (_, context) in &mut self.digests {
235 context.update(bytes);
236 }
237 }
238
239 fn finish_line(&mut self) -> Result<(), RecordError> {
240 let start = self.line_start;
241 let end = self.prefix.len() - 1;
242 let line = std::str::from_utf8(&self.prefix[start..end])
243 .map_err(|_| invalid("record header line is not UTF-8"))?;
244 match self.phase {
245 Phase::Glyph(expected) => {
246 let hash = parse_glyph(line)?;
247 if expected.is_some_and(|kind| kind != hash.kind()) {
248 return Err(invalid("a Seal must embed a Plex"));
249 }
250 self.phase = match hash.kind() {
251 RecordKind::Blob => Phase::DataLength,
252 RecordKind::Plex => Phase::PlexFields,
253 RecordKind::Seal => Phase::By,
254 };
255 self.open(hash);
256 }
257 Phase::By => {
258 let value = structural_value(line, "By")?;
259 let (_, key) = ByValue::parse_with_key(value)
260 .map_err(|error| invalid(format!("invalid By value: {error}")))?;
261 self.by = Some(key);
262 self.phase = Phase::Mark;
263 }
264 Phase::Mark => {
265 self.mark = Some(parse_mark(structural_value(line, "Mark")?)?);
266 self.phase = Phase::Glyph(Some(RecordKind::Plex));
267 }
268 Phase::PlexFields => {
269 if line.as_bytes().starts_with(RECORD_GLYPH_LINE_PREFIX) {
270 let hash = parse_glyph(line)?;
271 if hash.kind() != RecordKind::Blob {
272 return Err(invalid("a Plex must embed a Blob"));
273 }
274 if self.field_count == 0 {
275 return Err(invalid("Plex has no application fields"));
276 }
277 self.phase = Phase::DataLength;
278 self.open(hash);
279 } else {
280 let (name, _) = validate_application_field_line(line).map_err(|error| {
281 invalid(format!("invalid Plex application field: {error}"))
282 })?;
283 if self.field_count == MAX_PLEX_FIELDS {
284 return Err(invalid("too many Plex fields"));
285 }
286 if self
287 .previous_name
288 .is_some_and(|(previous_start, previous_end)| {
289 name.as_bytes() < &self.prefix[previous_start..previous_end]
290 })
291 {
292 return Err(invalid("Plex fields are out of order"));
293 }
294 self.previous_name = Some((start, start + name.len()));
295 self.field_count += 1;
296 }
297 }
298 Phase::DataLength => {
299 let value = structural_value(line, "Data-Length")?;
300 if !value.bytes().all(|byte| byte.is_ascii_digit())
301 || (value.len() > 1 && value.starts_with('0'))
302 {
303 return Err(invalid("invalid Data-Length"));
304 }
305 self.data_length = value
306 .parse::<usize>()
307 .ok()
308 .filter(|&length| length <= MAX_BLOB_SIZE)
309 .ok_or_else(|| invalid("Blob data exceeds maximum"))?;
310 self.phase = Phase::Blank;
311 }
312 Phase::Blank => {
313 if !line.is_empty() {
314 return Err(invalid("missing Blob blank line"));
315 }
316 self.phase = Phase::Body;
317 }
318 Phase::Body => unreachable!("body bytes are not header lines"),
319 }
320 Ok(())
321 }
322
323 fn open(&mut self, hash: RecordHash) {
325 if self.digests.is_empty() {
326 self.digests.reserve_exact(3);
327 }
328 self.digests.push((hash, blake3::Hasher::new()));
329 }
330
331 fn verify(&mut self) -> Result<(), RecordError> {
332 for (expected, context) in &self.digests {
333 let computed = RecordHash::from_digest(expected.kind(), context.finalize().into());
334 if computed != *expected {
335 return Err(RecordError::HashMismatch {
336 expected: expected.to_string(),
337 computed: computed.to_string(),
338 });
339 }
340 }
341 if let (Some(key), Some(mark)) = (self.by, self.mark) {
342 let plex = self.digests[1].0;
343 if !key.verify_digest(plex.digest(), mark) {
344 return Err(RecordError::MarkInvalid);
345 }
346 }
347 Ok(())
348 }
349}
350
351#[derive(Debug, Clone, PartialEq, Eq)]
354pub struct ValidatedHeader {
355 hash: RecordHash,
356 canonical_len: usize,
357 prefix: Vec<u8>,
358}
359
360impl ValidatedHeader {
361 pub fn hash(&self) -> RecordHash {
362 self.hash
363 }
364
365 pub fn canonical_len(&self) -> usize {
366 self.canonical_len
367 }
368
369 pub fn prefix(&self) -> &[u8] {
371 &self.prefix
372 }
373}
374
375fn parse_glyph(line: &str) -> Result<RecordHash, RecordError> {
376 let (name, value) = validate_field_line(line)
377 .map_err(|error| invalid(format!("invalid record glyph line: {error}")))?;
378 if name != RECORD_GLYPH {
379 return Err(invalid("invalid record glyph name"));
380 }
381 RecordHash::parse(value).map_err(|error| invalid(format!("invalid record hash: {error}")))
382}
383
384fn structural_value<'a>(line: &'a str, expected: &str) -> Result<&'a str, RecordError> {
385 let (name, value) = validate_field_line(line)
386 .map_err(|error| invalid(format!("invalid {expected} line: {error}")))?;
387 if name != expected {
388 return Err(RecordError::MissingField(expected.into()));
389 }
390 Ok(value)
391}
392
393fn parse_mark(raw: &str) -> Result<Mark, RecordError> {
394 if raw.len() != b64a_encoded_len(64) {
395 return Err(invalid(format!(
396 "Mark must encode 64 bytes, got length {}",
397 raw.len()
398 )));
399 }
400 let mut bytes = [0u8; 64];
401 b64a_decode_into(raw, &mut bytes)
402 .map_err(|error| invalid(format!("invalid Mark B64A: {error}")))?;
403 Ok(Mark::from_bytes(bytes))
404}
405
406#[cfg(test)]
407#[path = "validator_tests.rs"]
408mod tests;