1use super::WireEncodeErr;
8use crate::serial::wire::framing::{put_len_u32, put_string, put_u32, put_u8};
9use crate::serial::wire::Reader;
10use serde::{Deserialize, Serialize};
11
12pub const SCAN_DATABASE_HEADER_MAGIC: &[u8; 4] = b"VSDH";
14pub const SCAN_DATABASE_HEADER_VERSION: u32 = 1;
16pub const MAX_SCAN_DATABASE_SECTIONS: usize = 4_096;
18pub const MAX_SCAN_DATABASE_UNSUPPORTED_FEATURES: usize = 4_096;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub enum ScanDatabaseMode {
24 Block,
26 Streaming,
28 Vectored,
30}
31
32impl ScanDatabaseMode {
33 const fn tag(self) -> u8 {
34 match self {
35 Self::Block => 1,
36 Self::Streaming => 2,
37 Self::Vectored => 3,
38 }
39 }
40
41 fn from_tag(tag: u8) -> Result<Self, String> {
42 match tag {
43 1 => Ok(Self::Block),
44 2 => Ok(Self::Streaming),
45 3 => Ok(Self::Vectored),
46 _ => Err(format!(
47 "scan database mode tag {tag} is unsupported. Fix: recompile the scan database with a compatible Vyre scan compiler."
48 )),
49 }
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
55pub enum ScanDatabaseSectionKind {
56 LiteralTable,
58 AutomataTable,
60 VerifierFragments,
62 OutputLayout,
64 StreamingState,
66 RelationSeeds,
68}
69
70impl ScanDatabaseSectionKind {
71 const fn tag(self) -> u8 {
72 match self {
73 Self::LiteralTable => 1,
74 Self::AutomataTable => 2,
75 Self::VerifierFragments => 3,
76 Self::OutputLayout => 4,
77 Self::StreamingState => 5,
78 Self::RelationSeeds => 6,
79 }
80 }
81
82 fn from_tag(tag: u8) -> Result<Self, String> {
83 match tag {
84 1 => Ok(Self::LiteralTable),
85 2 => Ok(Self::AutomataTable),
86 3 => Ok(Self::VerifierFragments),
87 4 => Ok(Self::OutputLayout),
88 5 => Ok(Self::StreamingState),
89 6 => Ok(Self::RelationSeeds),
90 _ => Err(format!(
91 "scan database section tag {tag} is unsupported. Fix: recompile the scan database with a compatible Vyre scan compiler."
92 )),
93 }
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
99pub struct ScanDatabaseSectionHeader {
100 pub kind: ScanDatabaseSectionKind,
102 pub offset: u64,
104 pub byte_len: u64,
106 pub section_digest: u64,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
112pub struct UnsupportedScanFeature {
113 pub pattern_index: u32,
115 pub feature: String,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
121pub enum ScanDatabaseReaderCompatibility {
122 Compatible,
124 RequiresVerifier,
126 Incompatible,
128}
129
130impl ScanDatabaseReaderCompatibility {
131 const fn tag(self) -> u8 {
132 match self {
133 Self::Compatible => 1,
134 Self::RequiresVerifier => 2,
135 Self::Incompatible => 3,
136 }
137 }
138
139 fn from_tag(tag: u8) -> Result<Self, String> {
140 match tag {
141 1 => Ok(Self::Compatible),
142 2 => Ok(Self::RequiresVerifier),
143 3 => Ok(Self::Incompatible),
144 _ => Err(format!(
145 "scan database reader compatibility tag {tag} is unsupported. Fix: rebuild the scan database cache with a compatible Vyre scan compiler."
146 )),
147 }
148 }
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
153pub struct ScanDatabaseCompatibilityRecord {
154 pub construct_tier_digest: u64,
156 pub dialect_digest: u64,
158 pub reader_compatibility: ScanDatabaseReaderCompatibility,
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
164pub struct ScanDatabaseHeader {
165 pub pattern_set_digest: [u8; 32],
167 pub compiler_version: String,
169 pub mode: ScanDatabaseMode,
171 pub table_sections: Vec<ScanDatabaseSectionHeader>,
173 pub unsupported_features: Vec<UnsupportedScanFeature>,
175 pub compatibility: ScanDatabaseCompatibilityRecord,
177}
178
179impl ScanDatabaseHeader {
180 #[must_use]
182 pub fn section_count(&self) -> usize {
183 self.table_sections.len()
184 }
185
186 #[must_use]
188 pub fn unsupported_feature_count(&self) -> usize {
189 self.unsupported_features.len()
190 }
191
192 pub fn validate_compatible(
200 &self,
201 expected_compiler_version: &str,
202 expected_mode: ScanDatabaseMode,
203 ) -> Result<(), String> {
204 if self.compiler_version != expected_compiler_version {
205 return Err(format!(
206 "scan database compiler version `{}` is incompatible with expected `{expected_compiler_version}`. Fix: rebuild the scan database cache with the current compiler.",
207 self.compiler_version
208 ));
209 }
210 if self.mode != expected_mode {
211 return Err(format!(
212 "scan database mode {:?} is incompatible with expected {:?}. Fix: rebuild the scan database cache for the requested scan mode.",
213 self.mode, expected_mode
214 ));
215 }
216 Ok(())
217 }
218
219 pub fn validate_database_compatibility(
227 &self,
228 expected_construct_tier_digest: u64,
229 expected_dialect_digest: u64,
230 accepted_reader_compatibility: &[ScanDatabaseReaderCompatibility],
231 ) -> Result<(), String> {
232 if self.compatibility.construct_tier_digest != expected_construct_tier_digest {
233 return Err(format!(
234 "scan database construct tier digest {:#x} is incompatible with expected {expected_construct_tier_digest:#x}. Fix: rebuild the scan database cache from the current construct tier matrix.",
235 self.compatibility.construct_tier_digest
236 ));
237 }
238 if self.compatibility.dialect_digest != expected_dialect_digest {
239 return Err(format!(
240 "scan database dialect digest {:#x} is incompatible with expected {expected_dialect_digest:#x}. Fix: rebuild the scan database cache from the current regex dialect lattice.",
241 self.compatibility.dialect_digest
242 ));
243 }
244 if !accepted_reader_compatibility
245 .iter()
246 .any(|accepted| *accepted == self.compatibility.reader_compatibility)
247 {
248 return Err(format!(
249 "scan database reader compatibility {:?} is not accepted by this reader. Fix: choose a verifier-capable reader or rebuild the scan database.",
250 self.compatibility.reader_compatibility
251 ));
252 }
253 Ok(())
254 }
255}
256
257pub fn encode_scan_database_header(
264 header: &ScanDatabaseHeader,
265) -> Result<Vec<u8>, WireEncodeErr> {
266 let mut out = Vec::with_capacity(96 + header.table_sections.len() * 25);
267 put_scan_database_header(&mut out, header)?;
268 Ok(out)
269}
270
271pub fn put_scan_database_header(
278 out: &mut Vec<u8>,
279 header: &ScanDatabaseHeader,
280) -> Result<(), WireEncodeErr> {
281 out.extend_from_slice(SCAN_DATABASE_HEADER_MAGIC);
282 put_u32(out, SCAN_DATABASE_HEADER_VERSION);
283 out.extend_from_slice(&header.pattern_set_digest);
284 put_string(out, &header.compiler_version)?;
285 put_u8(out, header.mode.tag());
286 put_len_u32(
287 out,
288 header.table_sections.len(),
289 "scan database section count ",
290 )?;
291 for section in &header.table_sections {
292 put_u8(out, section.kind.tag());
293 put_u64(out, section.offset);
294 put_u64(out, section.byte_len);
295 put_u64(out, section.section_digest);
296 }
297 put_len_u32(
298 out,
299 header.unsupported_features.len(),
300 "scan database unsupported feature count ",
301 )?;
302 for unsupported in &header.unsupported_features {
303 put_u32(out, unsupported.pattern_index);
304 put_string(out, &unsupported.feature)?;
305 }
306 put_u64(out, header.compatibility.construct_tier_digest);
307 put_u64(out, header.compatibility.dialect_digest);
308 put_u8(out, header.compatibility.reader_compatibility.tag());
309 Ok(())
310}
311
312pub fn decode_scan_database_header(bytes: &[u8]) -> Result<ScanDatabaseHeader, String> {
320 let mut reader = Reader {
321 bytes,
322 pos: 0,
323 depth: 0,
324 };
325 let magic = reader.take(SCAN_DATABASE_HEADER_MAGIC.len())?;
326 if magic != SCAN_DATABASE_HEADER_MAGIC {
327 return Err(
328 "invalid scan database header magic. Fix: load a VSDH scan database header, not a VIR0 Program blob."
329 .to_string(),
330 );
331 }
332 let version = reader.u32()?;
333 if version != SCAN_DATABASE_HEADER_VERSION {
334 return Err(format!(
335 "scan database header version {version} is unsupported; expected {SCAN_DATABASE_HEADER_VERSION}. Fix: rebuild the scan database cache."
336 ));
337 }
338 let digest_bytes = reader.take(32)?;
339 let mut pattern_set_digest = [0u8; 32];
340 pattern_set_digest.copy_from_slice(digest_bytes);
341 let compiler_version = reader.string()?;
342 let mode = ScanDatabaseMode::from_tag(reader.u8()?)?;
343
344 let section_count = reader.bounded_len(
345 MAX_SCAN_DATABASE_SECTIONS,
346 "scan database section count",
347 )?;
348 let mut table_sections = Vec::with_capacity(section_count);
349 for _ in 0..section_count {
350 table_sections.push(ScanDatabaseSectionHeader {
351 kind: ScanDatabaseSectionKind::from_tag(reader.u8()?)?,
352 offset: reader.u64()?,
353 byte_len: reader.u64()?,
354 section_digest: reader.u64()?,
355 });
356 }
357
358 let unsupported_feature_count = reader.bounded_len(
359 MAX_SCAN_DATABASE_UNSUPPORTED_FEATURES,
360 "scan database unsupported feature count",
361 )?;
362 let mut unsupported_features = Vec::with_capacity(unsupported_feature_count);
363 for _ in 0..unsupported_feature_count {
364 unsupported_features.push(UnsupportedScanFeature {
365 pattern_index: reader.u32()?,
366 feature: reader.string()?,
367 });
368 }
369
370 let compatibility = if reader.pos == bytes.len() {
371 legacy_compatibility_record(&unsupported_features)
372 } else {
373 ScanDatabaseCompatibilityRecord {
374 construct_tier_digest: reader.u64()?,
375 dialect_digest: reader.u64()?,
376 reader_compatibility: ScanDatabaseReaderCompatibility::from_tag(reader.u8()?)?,
377 }
378 };
379
380 if reader.pos != bytes.len() {
381 return Err(
382 "scan database header has trailing bytes. Fix: split the header from table payload sections before decoding."
383 .to_string(),
384 );
385 }
386
387 Ok(ScanDatabaseHeader {
388 pattern_set_digest,
389 compiler_version,
390 mode,
391 table_sections,
392 unsupported_features,
393 compatibility,
394 })
395}
396
397pub fn decode_compatible_scan_database_header(
403 bytes: &[u8],
404 expected_compiler_version: &str,
405 expected_mode: ScanDatabaseMode,
406) -> Result<ScanDatabaseHeader, String> {
407 let header = decode_scan_database_header(bytes)?;
408 header.validate_compatible(expected_compiler_version, expected_mode)?;
409 Ok(header)
410}
411
412pub fn decode_scan_database_header_with_compatibility(
419 bytes: &[u8],
420 expected_compiler_version: &str,
421 expected_mode: ScanDatabaseMode,
422 expected_construct_tier_digest: u64,
423 expected_dialect_digest: u64,
424 accepted_reader_compatibility: &[ScanDatabaseReaderCompatibility],
425) -> Result<ScanDatabaseHeader, String> {
426 let header =
427 decode_compatible_scan_database_header(bytes, expected_compiler_version, expected_mode)?;
428 header.validate_database_compatibility(
429 expected_construct_tier_digest,
430 expected_dialect_digest,
431 accepted_reader_compatibility,
432 )?;
433 Ok(header)
434}
435
436fn put_u64(out: &mut Vec<u8>, value: u64) {
437 out.extend_from_slice(&value.to_le_bytes());
438}
439
440fn legacy_compatibility_record(
441 unsupported_features: &[UnsupportedScanFeature],
442) -> ScanDatabaseCompatibilityRecord {
443 ScanDatabaseCompatibilityRecord {
444 construct_tier_digest: 0,
445 dialect_digest: 0,
446 reader_compatibility: if unsupported_features.is_empty() {
447 ScanDatabaseReaderCompatibility::Compatible
448 } else {
449 ScanDatabaseReaderCompatibility::RequiresVerifier
450 },
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 const CONSTRUCT_TIER_DIGEST: u64 = 0x5ca1_c075_7e12;
459 const DIALECT_DIGEST: u64 = 0xd1a1_ec7;
460
461 fn header() -> ScanDatabaseHeader {
462 ScanDatabaseHeader {
463 pattern_set_digest: [7u8; 32],
464 compiler_version: "vyre-scan-compiler-test-v1".to_string(),
465 mode: ScanDatabaseMode::Streaming,
466 table_sections: vec![
467 ScanDatabaseSectionHeader {
468 kind: ScanDatabaseSectionKind::LiteralTable,
469 offset: 128,
470 byte_len: 64,
471 section_digest: 0x11,
472 },
473 ScanDatabaseSectionHeader {
474 kind: ScanDatabaseSectionKind::AutomataTable,
475 offset: 192,
476 byte_len: 256,
477 section_digest: 0x12,
478 },
479 ],
480 unsupported_features: vec![UnsupportedScanFeature {
481 pattern_index: 3,
482 feature: "Fix: unsupported backreference must stay verifier-only".to_string(),
483 }],
484 compatibility: ScanDatabaseCompatibilityRecord {
485 construct_tier_digest: CONSTRUCT_TIER_DIGEST,
486 dialect_digest: DIALECT_DIGEST,
487 reader_compatibility: ScanDatabaseReaderCompatibility::RequiresVerifier,
488 },
489 }
490 }
491
492 #[test]
493 fn scan_database_header_round_trips_all_fields() {
494 let original = header();
495 let bytes = encode_scan_database_header(&original).unwrap();
496 let decoded = decode_compatible_scan_database_header(
497 &bytes,
498 "vyre-scan-compiler-test-v1",
499 ScanDatabaseMode::Streaming,
500 )
501 .unwrap();
502
503 assert_eq!(decoded, original);
504 assert_eq!(decoded.section_count(), 2);
505 assert_eq!(decoded.unsupported_feature_count(), 1);
506 }
507
508 #[test]
509 fn scan_database_header_validates_construct_and_dialect_compatibility() {
510 let bytes = encode_scan_database_header(&header()).unwrap();
511 let decoded = decode_scan_database_header_with_compatibility(
512 &bytes,
513 "vyre-scan-compiler-test-v1",
514 ScanDatabaseMode::Streaming,
515 CONSTRUCT_TIER_DIGEST,
516 DIALECT_DIGEST,
517 &[ScanDatabaseReaderCompatibility::RequiresVerifier],
518 )
519 .unwrap();
520 assert_eq!(
521 decoded.compatibility.reader_compatibility,
522 ScanDatabaseReaderCompatibility::RequiresVerifier
523 );
524
525 let construct_error = decode_scan_database_header_with_compatibility(
526 &bytes,
527 "vyre-scan-compiler-test-v1",
528 ScanDatabaseMode::Streaming,
529 CONSTRUCT_TIER_DIGEST + 1,
530 DIALECT_DIGEST,
531 &[ScanDatabaseReaderCompatibility::RequiresVerifier],
532 )
533 .unwrap_err();
534 assert!(construct_error.contains("construct tier digest"));
535
536 let dialect_error = decode_scan_database_header_with_compatibility(
537 &bytes,
538 "vyre-scan-compiler-test-v1",
539 ScanDatabaseMode::Streaming,
540 CONSTRUCT_TIER_DIGEST,
541 DIALECT_DIGEST + 1,
542 &[ScanDatabaseReaderCompatibility::RequiresVerifier],
543 )
544 .unwrap_err();
545 assert!(dialect_error.contains("dialect digest"));
546 }
547
548 #[test]
549 fn scan_database_header_rejects_unaccepted_reader_compatibility() {
550 let bytes = encode_scan_database_header(&header()).unwrap();
551 let error = decode_scan_database_header_with_compatibility(
552 &bytes,
553 "vyre-scan-compiler-test-v1",
554 ScanDatabaseMode::Streaming,
555 CONSTRUCT_TIER_DIGEST,
556 DIALECT_DIGEST,
557 &[ScanDatabaseReaderCompatibility::Compatible],
558 )
559 .unwrap_err();
560
561 assert!(error.contains("reader compatibility"));
562 }
563
564 #[test]
565 fn scan_database_header_decodes_legacy_headers_with_conservative_compatibility() {
566 let mut legacy_bytes = encode_scan_database_header(&header()).unwrap();
567 legacy_bytes.truncate(legacy_bytes.len() - 17);
568 let decoded = decode_compatible_scan_database_header(
569 &legacy_bytes,
570 "vyre-scan-compiler-test-v1",
571 ScanDatabaseMode::Streaming,
572 )
573 .unwrap();
574
575 assert_eq!(decoded.compatibility.construct_tier_digest, 0);
576 assert_eq!(decoded.compatibility.dialect_digest, 0);
577 assert_eq!(
578 decoded.compatibility.reader_compatibility,
579 ScanDatabaseReaderCompatibility::RequiresVerifier
580 );
581 assert_eq!(decoded.unsupported_feature_count(), 1);
582 }
583
584 #[test]
585 fn scan_database_header_rejects_incompatible_compiler_or_mode() {
586 let bytes = encode_scan_database_header(&header()).unwrap();
587
588 let compiler_error = decode_compatible_scan_database_header(
589 &bytes,
590 "vyre-scan-compiler-test-v2",
591 ScanDatabaseMode::Streaming,
592 )
593 .unwrap_err();
594 assert!(compiler_error.contains("compiler version"));
595
596 let mode_error = decode_compatible_scan_database_header(
597 &bytes,
598 "vyre-scan-compiler-test-v1",
599 ScanDatabaseMode::Block,
600 )
601 .unwrap_err();
602 assert!(mode_error.contains("mode"));
603 }
604
605 #[test]
606 fn scan_database_header_rejects_wrong_blob_family() {
607 let error = decode_scan_database_header(b"VIR0").unwrap_err();
608 assert!(error.contains("VSDH"));
609 }
610}