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(header: &ScanDatabaseHeader) -> Result<Vec<u8>, WireEncodeErr> {
264 let mut out = Vec::with_capacity(96 + header.table_sections.len() * 25);
265 put_scan_database_header(&mut out, header)?;
266 Ok(out)
267}
268
269pub fn put_scan_database_header(
276 out: &mut Vec<u8>,
277 header: &ScanDatabaseHeader,
278) -> Result<(), WireEncodeErr> {
279 out.extend_from_slice(SCAN_DATABASE_HEADER_MAGIC);
280 put_u32(out, SCAN_DATABASE_HEADER_VERSION);
281 out.extend_from_slice(&header.pattern_set_digest);
282 put_string(out, &header.compiler_version)?;
283 put_u8(out, header.mode.tag());
284 put_len_u32(
285 out,
286 header.table_sections.len(),
287 "scan database section count ",
288 )?;
289 for section in &header.table_sections {
290 put_u8(out, section.kind.tag());
291 put_u64(out, section.offset);
292 put_u64(out, section.byte_len);
293 put_u64(out, section.section_digest);
294 }
295 put_len_u32(
296 out,
297 header.unsupported_features.len(),
298 "scan database unsupported feature count ",
299 )?;
300 for unsupported in &header.unsupported_features {
301 put_u32(out, unsupported.pattern_index);
302 put_string(out, &unsupported.feature)?;
303 }
304 put_u64(out, header.compatibility.construct_tier_digest);
305 put_u64(out, header.compatibility.dialect_digest);
306 put_u8(out, header.compatibility.reader_compatibility.tag());
307 Ok(())
308}
309
310pub fn decode_scan_database_header(bytes: &[u8]) -> Result<ScanDatabaseHeader, String> {
318 let mut reader = Reader {
319 bytes,
320 pos: 0,
321 depth: 0,
322 };
323 let magic = reader.take(SCAN_DATABASE_HEADER_MAGIC.len())?;
324 if magic != SCAN_DATABASE_HEADER_MAGIC {
325 return Err(
326 "invalid scan database header magic. Fix: load a VSDH scan database header, not a VIR0 Program blob."
327 .to_string(),
328 );
329 }
330 let version = reader.u32()?;
331 if version != SCAN_DATABASE_HEADER_VERSION {
332 return Err(format!(
333 "scan database header version {version} is unsupported; expected {SCAN_DATABASE_HEADER_VERSION}. Fix: rebuild the scan database cache."
334 ));
335 }
336 let digest_bytes = reader.take(32)?;
337 let mut pattern_set_digest = [0u8; 32];
338 pattern_set_digest.copy_from_slice(digest_bytes);
339 let compiler_version = reader.string()?;
340 let mode = ScanDatabaseMode::from_tag(reader.u8()?)?;
341
342 let section_count =
343 reader.bounded_len(MAX_SCAN_DATABASE_SECTIONS, "scan database section count")?;
344 let mut table_sections = Vec::with_capacity(section_count);
345 for _ in 0..section_count {
346 table_sections.push(ScanDatabaseSectionHeader {
347 kind: ScanDatabaseSectionKind::from_tag(reader.u8()?)?,
348 offset: reader.u64()?,
349 byte_len: reader.u64()?,
350 section_digest: reader.u64()?,
351 });
352 }
353
354 let unsupported_feature_count = reader.bounded_len(
355 MAX_SCAN_DATABASE_UNSUPPORTED_FEATURES,
356 "scan database unsupported feature count",
357 )?;
358 let mut unsupported_features = Vec::with_capacity(unsupported_feature_count);
359 for _ in 0..unsupported_feature_count {
360 unsupported_features.push(UnsupportedScanFeature {
361 pattern_index: reader.u32()?,
362 feature: reader.string()?,
363 });
364 }
365
366 let compatibility = if reader.pos == bytes.len() {
367 legacy_compatibility_record(&unsupported_features)
368 } else {
369 ScanDatabaseCompatibilityRecord {
370 construct_tier_digest: reader.u64()?,
371 dialect_digest: reader.u64()?,
372 reader_compatibility: ScanDatabaseReaderCompatibility::from_tag(reader.u8()?)?,
373 }
374 };
375
376 if reader.pos != bytes.len() {
377 return Err(
378 "scan database header has trailing bytes. Fix: split the header from table payload sections before decoding."
379 .to_string(),
380 );
381 }
382
383 Ok(ScanDatabaseHeader {
384 pattern_set_digest,
385 compiler_version,
386 mode,
387 table_sections,
388 unsupported_features,
389 compatibility,
390 })
391}
392
393pub fn decode_compatible_scan_database_header(
399 bytes: &[u8],
400 expected_compiler_version: &str,
401 expected_mode: ScanDatabaseMode,
402) -> Result<ScanDatabaseHeader, String> {
403 let header = decode_scan_database_header(bytes)?;
404 header.validate_compatible(expected_compiler_version, expected_mode)?;
405 Ok(header)
406}
407
408pub fn decode_scan_database_header_with_compatibility(
415 bytes: &[u8],
416 expected_compiler_version: &str,
417 expected_mode: ScanDatabaseMode,
418 expected_construct_tier_digest: u64,
419 expected_dialect_digest: u64,
420 accepted_reader_compatibility: &[ScanDatabaseReaderCompatibility],
421) -> Result<ScanDatabaseHeader, String> {
422 let header =
423 decode_compatible_scan_database_header(bytes, expected_compiler_version, expected_mode)?;
424 header.validate_database_compatibility(
425 expected_construct_tier_digest,
426 expected_dialect_digest,
427 accepted_reader_compatibility,
428 )?;
429 Ok(header)
430}
431
432fn put_u64(out: &mut Vec<u8>, value: u64) {
433 out.extend_from_slice(&value.to_le_bytes());
434}
435
436fn legacy_compatibility_record(
437 unsupported_features: &[UnsupportedScanFeature],
438) -> ScanDatabaseCompatibilityRecord {
439 ScanDatabaseCompatibilityRecord {
440 construct_tier_digest: 0,
441 dialect_digest: 0,
442 reader_compatibility: if unsupported_features.is_empty() {
443 ScanDatabaseReaderCompatibility::Compatible
444 } else {
445 ScanDatabaseReaderCompatibility::RequiresVerifier
446 },
447 }
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
453
454 const CONSTRUCT_TIER_DIGEST: u64 = 0x5ca1_c075_7e12;
455 const DIALECT_DIGEST: u64 = 0xd1a1_ec7;
456
457 fn header() -> ScanDatabaseHeader {
458 ScanDatabaseHeader {
459 pattern_set_digest: [7u8; 32],
460 compiler_version: "vyre-scan-compiler-test-v1".to_string(),
461 mode: ScanDatabaseMode::Streaming,
462 table_sections: vec![
463 ScanDatabaseSectionHeader {
464 kind: ScanDatabaseSectionKind::LiteralTable,
465 offset: 128,
466 byte_len: 64,
467 section_digest: 0x11,
468 },
469 ScanDatabaseSectionHeader {
470 kind: ScanDatabaseSectionKind::AutomataTable,
471 offset: 192,
472 byte_len: 256,
473 section_digest: 0x12,
474 },
475 ],
476 unsupported_features: vec![UnsupportedScanFeature {
477 pattern_index: 3,
478 feature: "Fix: unsupported backreference must stay verifier-only".to_string(),
479 }],
480 compatibility: ScanDatabaseCompatibilityRecord {
481 construct_tier_digest: CONSTRUCT_TIER_DIGEST,
482 dialect_digest: DIALECT_DIGEST,
483 reader_compatibility: ScanDatabaseReaderCompatibility::RequiresVerifier,
484 },
485 }
486 }
487
488 #[test]
489 fn scan_database_header_round_trips_all_fields() {
490 let original = header();
491 let bytes = encode_scan_database_header(&original).unwrap();
492 let decoded = decode_compatible_scan_database_header(
493 &bytes,
494 "vyre-scan-compiler-test-v1",
495 ScanDatabaseMode::Streaming,
496 )
497 .unwrap();
498
499 assert_eq!(decoded, original);
500 assert_eq!(decoded.section_count(), 2);
501 assert_eq!(decoded.unsupported_feature_count(), 1);
502 }
503
504 #[test]
505 fn scan_database_header_validates_construct_and_dialect_compatibility() {
506 let bytes = encode_scan_database_header(&header()).unwrap();
507 let decoded = decode_scan_database_header_with_compatibility(
508 &bytes,
509 "vyre-scan-compiler-test-v1",
510 ScanDatabaseMode::Streaming,
511 CONSTRUCT_TIER_DIGEST,
512 DIALECT_DIGEST,
513 &[ScanDatabaseReaderCompatibility::RequiresVerifier],
514 )
515 .unwrap();
516 assert_eq!(
517 decoded.compatibility.reader_compatibility,
518 ScanDatabaseReaderCompatibility::RequiresVerifier
519 );
520
521 let construct_error = decode_scan_database_header_with_compatibility(
522 &bytes,
523 "vyre-scan-compiler-test-v1",
524 ScanDatabaseMode::Streaming,
525 CONSTRUCT_TIER_DIGEST + 1,
526 DIALECT_DIGEST,
527 &[ScanDatabaseReaderCompatibility::RequiresVerifier],
528 )
529 .unwrap_err();
530 assert!(construct_error.contains("construct tier digest"));
531
532 let dialect_error = decode_scan_database_header_with_compatibility(
533 &bytes,
534 "vyre-scan-compiler-test-v1",
535 ScanDatabaseMode::Streaming,
536 CONSTRUCT_TIER_DIGEST,
537 DIALECT_DIGEST + 1,
538 &[ScanDatabaseReaderCompatibility::RequiresVerifier],
539 )
540 .unwrap_err();
541 assert!(dialect_error.contains("dialect digest"));
542 }
543
544 #[test]
545 fn scan_database_header_rejects_unaccepted_reader_compatibility() {
546 let bytes = encode_scan_database_header(&header()).unwrap();
547 let error = decode_scan_database_header_with_compatibility(
548 &bytes,
549 "vyre-scan-compiler-test-v1",
550 ScanDatabaseMode::Streaming,
551 CONSTRUCT_TIER_DIGEST,
552 DIALECT_DIGEST,
553 &[ScanDatabaseReaderCompatibility::Compatible],
554 )
555 .unwrap_err();
556
557 assert!(error.contains("reader compatibility"));
558 }
559
560 #[test]
561 fn scan_database_header_decodes_legacy_headers_with_conservative_compatibility() {
562 let mut legacy_bytes = encode_scan_database_header(&header()).unwrap();
563 legacy_bytes.truncate(legacy_bytes.len() - 17);
564 let decoded = decode_compatible_scan_database_header(
565 &legacy_bytes,
566 "vyre-scan-compiler-test-v1",
567 ScanDatabaseMode::Streaming,
568 )
569 .unwrap();
570
571 assert_eq!(decoded.compatibility.construct_tier_digest, 0);
572 assert_eq!(decoded.compatibility.dialect_digest, 0);
573 assert_eq!(
574 decoded.compatibility.reader_compatibility,
575 ScanDatabaseReaderCompatibility::RequiresVerifier
576 );
577 assert_eq!(decoded.unsupported_feature_count(), 1);
578 }
579
580 #[test]
581 fn scan_database_header_rejects_incompatible_compiler_or_mode() {
582 let bytes = encode_scan_database_header(&header()).unwrap();
583
584 let compiler_error = decode_compatible_scan_database_header(
585 &bytes,
586 "vyre-scan-compiler-test-v2",
587 ScanDatabaseMode::Streaming,
588 )
589 .unwrap_err();
590 assert!(compiler_error.contains("compiler version"));
591
592 let mode_error = decode_compatible_scan_database_header(
593 &bytes,
594 "vyre-scan-compiler-test-v1",
595 ScanDatabaseMode::Block,
596 )
597 .unwrap_err();
598 assert!(mode_error.contains("mode"));
599 }
600
601 #[test]
602 fn scan_database_header_rejects_wrong_blob_family() {
603 let error = decode_scan_database_header(b"VIR0").unwrap_err();
604 assert!(error.contains("VSDH"));
605 }
606}