1use std::collections::BTreeMap;
12use std::fs;
13use std::io::Write;
14use std::path::{Path, PathBuf};
15
16use fsqlite_error::{FrankenError, Result};
17use fsqlite_types::{ObjectId, Oti, SymbolRecord, SymbolRecordFlags, source_symbol_count};
18use tracing::{debug, error, info, warn};
19use xxhash_rust::xxh3::xxh3_64;
20
21const BEAD_ID: &str = "bd-1hi.24";
22const LOGGING_STANDARD_BEAD: &str = "bd-1fpm";
23
24pub const SYMBOL_SEGMENT_MAGIC: [u8; 4] = *b"FSSY";
26pub const SYMBOL_SEGMENT_VERSION: u32 = 1;
28pub const SYMBOL_SEGMENT_HEADER_BYTES: usize = 40;
30
31const SYMBOL_SEGMENT_HASH_INPUT_BYTES: usize = 32;
32
33const SYMBOL_RECORD_HEADER_BYTES: usize = 51;
36const SYMBOL_RECORD_TRAILER_BYTES: usize = 25;
37const SYMBOL_SIZE_FIELD_OFFSET: usize = 47;
38const SYMBOL_SIZE_FIELD_BYTES: usize = 4;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct SymbolSegmentHeader {
51 pub segment_id: u64,
53 pub epoch_id: u64,
55 pub created_at: u64,
57}
58
59impl SymbolSegmentHeader {
60 #[must_use]
62 pub const fn new(segment_id: u64, epoch_id: u64, created_at: u64) -> Self {
63 Self {
64 segment_id,
65 epoch_id,
66 created_at,
67 }
68 }
69
70 #[must_use]
72 pub fn encode(&self) -> [u8; SYMBOL_SEGMENT_HEADER_BYTES] {
73 let mut out = [0_u8; SYMBOL_SEGMENT_HEADER_BYTES];
74 out[0..4].copy_from_slice(&SYMBOL_SEGMENT_MAGIC);
75 out[4..8].copy_from_slice(&SYMBOL_SEGMENT_VERSION.to_le_bytes());
76 out[8..16].copy_from_slice(&self.segment_id.to_le_bytes());
77 out[16..24].copy_from_slice(&self.epoch_id.to_le_bytes());
78 out[24..32].copy_from_slice(&self.created_at.to_le_bytes());
79 let checksum = xxh3_64(&out[..SYMBOL_SEGMENT_HASH_INPUT_BYTES]);
80 out[32..40].copy_from_slice(&checksum.to_le_bytes());
81 out
82 }
83
84 pub fn decode(bytes: &[u8]) -> Result<Self> {
86 if bytes.len() < SYMBOL_SEGMENT_HEADER_BYTES {
87 return Err(FrankenError::DatabaseCorrupt {
88 detail: format!(
89 "symbol segment header too short: expected {SYMBOL_SEGMENT_HEADER_BYTES}, got {}",
90 bytes.len()
91 ),
92 });
93 }
94
95 if bytes[0..4] != SYMBOL_SEGMENT_MAGIC {
96 return Err(FrankenError::DatabaseCorrupt {
97 detail: format!("invalid symbol segment magic: {:02X?}", &bytes[0..4]),
98 });
99 }
100
101 let version = read_u32_at(bytes, 4, "version")?;
102 if version != SYMBOL_SEGMENT_VERSION {
103 return Err(FrankenError::DatabaseCorrupt {
104 detail: format!(
105 "unsupported symbol segment version {version}, expected {SYMBOL_SEGMENT_VERSION}"
106 ),
107 });
108 }
109
110 let segment_id = read_u64_at(bytes, 8, "segment_id")?;
111 let epoch_id = read_u64_at(bytes, 16, "epoch_id")?;
112 let created_at = read_u64_at(bytes, 24, "created_at")?;
113 let stored_checksum = read_u64_at(bytes, 32, "header_xxh3")?;
114 let computed_checksum = xxh3_64(&bytes[..SYMBOL_SEGMENT_HASH_INPUT_BYTES]);
115
116 if stored_checksum != computed_checksum {
117 return Err(FrankenError::DatabaseCorrupt {
118 detail: format!(
119 "symbol segment header checksum mismatch: stored {stored_checksum:#018X}, computed {computed_checksum:#018X}"
120 ),
121 });
122 }
123
124 Ok(Self {
125 segment_id,
126 epoch_id,
127 created_at,
128 })
129 }
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
134pub struct SymbolLogOffset {
135 pub segment_id: u64,
137 pub offset_bytes: u64,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct SymbolLogRecord {
144 pub offset: SymbolLogOffset,
146 pub record: SymbolRecord,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct SymbolSegmentScan {
153 pub header: SymbolSegmentHeader,
155 pub records: Vec<SymbolLogRecord>,
157 pub torn_tail: bool,
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub struct AlignedSymbolIndexEntry {
164 pub offset: SymbolLogOffset,
166 pub logical_len: u32,
168 pub padded_len: u32,
170}
171
172#[derive(Debug, Clone)]
174pub struct SymbolLogManager {
175 symbols_dir: PathBuf,
176 active_header: SymbolSegmentHeader,
177}
178
179impl SymbolLogManager {
180 pub fn new(
182 symbols_dir: &Path,
183 active_segment_id: u64,
184 epoch_id: u64,
185 created_at: u64,
186 ) -> Result<Self> {
187 let active_header = SymbolSegmentHeader::new(active_segment_id, epoch_id, created_at);
188 let segment_path = symbol_segment_path(symbols_dir, active_segment_id);
189 ensure_symbol_segment(&segment_path, active_header)?;
190
191 info!(
192 bead_id = BEAD_ID,
193 logging_standard = LOGGING_STANDARD_BEAD,
194 segment_id = active_segment_id,
195 epoch_id,
196 "opened symbol log manager"
197 );
198
199 Ok(Self {
200 symbols_dir: symbols_dir.to_path_buf(),
201 active_header,
202 })
203 }
204
205 #[must_use]
207 pub const fn active_segment_id(&self) -> u64 {
208 self.active_header.segment_id
209 }
210
211 #[must_use]
213 pub fn active_segment_path(&self) -> PathBuf {
214 symbol_segment_path(&self.symbols_dir, self.active_header.segment_id)
215 }
216
217 pub fn append(&self, record: &SymbolRecord) -> Result<SymbolLogOffset> {
219 append_symbol_record(&self.symbols_dir, self.active_header, record)
220 }
221
222 pub fn append_to_segment(
226 &self,
227 segment_id: u64,
228 record: &SymbolRecord,
229 ) -> Result<SymbolLogOffset> {
230 if segment_id != self.active_header.segment_id {
231 warn!(
232 bead_id = BEAD_ID,
233 logging_standard = LOGGING_STANDARD_BEAD,
234 requested_segment = segment_id,
235 active_segment = self.active_header.segment_id,
236 "append rejected because segment is immutable"
237 );
238 return Err(FrankenError::Internal(format!(
239 "segment {segment_id} is immutable; active segment is {}",
240 self.active_header.segment_id
241 )));
242 }
243 self.append(record)
244 }
245
246 pub fn rotate(
248 &mut self,
249 next_segment_id: u64,
250 next_epoch_id: u64,
251 next_created_at: u64,
252 ) -> Result<()> {
253 if next_segment_id <= self.active_header.segment_id {
254 return Err(FrankenError::Internal(format!(
255 "next segment id {next_segment_id} must be greater than current {}",
256 self.active_header.segment_id
257 )));
258 }
259
260 let next_header = SymbolSegmentHeader::new(next_segment_id, next_epoch_id, next_created_at);
261 let next_path = symbol_segment_path(&self.symbols_dir, next_segment_id);
262 ensure_symbol_segment(&next_path, next_header)?;
263 self.active_header = next_header;
264
265 info!(
266 bead_id = BEAD_ID,
267 logging_standard = LOGGING_STANDARD_BEAD,
268 segment_id = next_segment_id,
269 epoch_id = next_epoch_id,
270 "rotated symbol log segment"
271 );
272
273 Ok(())
274 }
275}
276
277#[must_use]
279pub fn symbol_segment_path(symbols_dir: &Path, segment_id: u64) -> PathBuf {
280 symbols_dir.join(format!("segment-{segment_id:06}.log"))
281}
282
283pub fn ensure_symbol_segment(segment_path: &Path, header: SymbolSegmentHeader) -> Result<()> {
285 if let Some(parent) = segment_path.parent()
286 && !parent.as_os_str().is_empty()
287 {
288 fs::create_dir_all(parent)?;
289 }
290
291 match fs::OpenOptions::new()
292 .write(true)
293 .create_new(true)
294 .open(segment_path)
295 {
296 Ok(mut file) => {
297 file.write_all(&header.encode())?;
298 file.sync_all()?;
299 info!(
300 bead_id = BEAD_ID,
301 logging_standard = LOGGING_STANDARD_BEAD,
302 path = %segment_path.display(),
303 segment_id = header.segment_id,
304 epoch_id = header.epoch_id,
305 "created symbol segment"
306 );
307 return Ok(());
308 }
309 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
310 }
312 Err(e) => return Err(e.into()),
313 }
314
315 let bytes = fs::read(segment_path)?;
316 if bytes.len() < SYMBOL_SEGMENT_HEADER_BYTES {
317 return Err(FrankenError::DatabaseCorrupt {
318 detail: format!(
319 "existing segment {} shorter than header: {} bytes",
320 segment_path.display(),
321 bytes.len()
322 ),
323 });
324 }
325
326 let existing = SymbolSegmentHeader::decode(&bytes[..SYMBOL_SEGMENT_HEADER_BYTES])?;
327 if existing != header {
328 return Err(FrankenError::DatabaseCorrupt {
329 detail: format!(
330 "segment header mismatch for {}: existing={existing:?}, requested={header:?}",
331 segment_path.display()
332 ),
333 });
334 }
335
336 Ok(())
337}
338
339pub fn append_symbol_record(
341 symbols_dir: &Path,
342 header: SymbolSegmentHeader,
343 record: &SymbolRecord,
344) -> Result<SymbolLogOffset> {
345 let segment_path = symbol_segment_path(symbols_dir, header.segment_id);
346 ensure_symbol_segment(&segment_path, header)?;
347
348 let current_len = file_len_usize(&segment_path)?;
349 if current_len < SYMBOL_SEGMENT_HEADER_BYTES {
350 return Err(FrankenError::DatabaseCorrupt {
351 detail: format!(
352 "segment {} length {} shorter than header",
353 segment_path.display(),
354 current_len
355 ),
356 });
357 }
358
359 let offset_bytes = usize_to_u64(
360 current_len - SYMBOL_SEGMENT_HEADER_BYTES,
361 "symbol log offset",
362 )?;
363
364 let mut file = fs::OpenOptions::new().append(true).open(&segment_path)?;
365 let record_bytes = record.to_bytes();
366 file.write_all(&record_bytes)?;
367 file.sync_data()?;
368
369 debug!(
370 bead_id = BEAD_ID,
371 logging_standard = LOGGING_STANDARD_BEAD,
372 path = %segment_path.display(),
373 segment_id = header.segment_id,
374 offset_bytes,
375 logical_len = record_bytes.len(),
376 "appended packed symbol record"
377 );
378
379 Ok(SymbolLogOffset {
380 segment_id: header.segment_id,
381 offset_bytes,
382 })
383}
384
385pub fn append_symbol_record_aligned(
389 symbols_dir: &Path,
390 header: SymbolSegmentHeader,
391 record: &SymbolRecord,
392 sector_size: u32,
393) -> Result<AlignedSymbolIndexEntry> {
394 if sector_size == 0 {
395 return Err(FrankenError::Internal(
396 "sector_size must be non-zero for aligned symbol append".to_owned(),
397 ));
398 }
399
400 let segment_path = symbol_segment_path(symbols_dir, header.segment_id);
401 ensure_symbol_segment(&segment_path, header)?;
402
403 let current_len = file_len_usize(&segment_path)?;
404 if current_len < SYMBOL_SEGMENT_HEADER_BYTES {
405 return Err(FrankenError::DatabaseCorrupt {
406 detail: format!(
407 "segment {} length {} shorter than header",
408 segment_path.display(),
409 current_len
410 ),
411 });
412 }
413
414 let record_bytes = record.to_bytes();
415 let logical_len = record_bytes.len();
416 let alignment_bytes = u32_to_usize(sector_size, "sector_size")?;
417 let padded_len = align_up(logical_len, alignment_bytes)?;
418 let padding = padded_len.saturating_sub(logical_len);
419
420 let offset = SymbolLogOffset {
421 segment_id: header.segment_id,
422 offset_bytes: usize_to_u64(
423 current_len - SYMBOL_SEGMENT_HEADER_BYTES,
424 "symbol log offset",
425 )?,
426 };
427
428 let mut file = fs::OpenOptions::new().append(true).open(&segment_path)?;
429 file.write_all(&record_bytes)?;
430 if padding > 0 {
431 file.write_all(&vec![0_u8; padding])?;
432 }
433 file.sync_data()?;
434
435 let entry = AlignedSymbolIndexEntry {
436 offset,
437 logical_len: usize_to_u32(logical_len, "logical_len")?,
438 padded_len: usize_to_u32(padded_len, "padded_len")?,
439 };
440
441 debug!(
442 bead_id = BEAD_ID,
443 logging_standard = LOGGING_STANDARD_BEAD,
444 path = %segment_path.display(),
445 segment_id = header.segment_id,
446 offset_bytes = offset.offset_bytes,
447 logical_len = entry.logical_len,
448 padded_len = entry.padded_len,
449 sector_size,
450 "appended aligned symbol record"
451 );
452
453 Ok(entry)
454}
455
456pub fn scan_symbol_segment(segment_path: &Path) -> Result<SymbolSegmentScan> {
458 let bytes = fs::read(segment_path)?;
459 if bytes.len() < SYMBOL_SEGMENT_HEADER_BYTES {
460 return Err(FrankenError::DatabaseCorrupt {
461 detail: format!(
462 "segment {} shorter than header: {} bytes",
463 segment_path.display(),
464 bytes.len()
465 ),
466 });
467 }
468
469 let header = SymbolSegmentHeader::decode(&bytes[..SYMBOL_SEGMENT_HEADER_BYTES])?;
470 let mut cursor = SYMBOL_SEGMENT_HEADER_BYTES;
471 let mut records = Vec::new();
472 let mut torn_tail = false;
473
474 while cursor < bytes.len() {
475 let parsed = parse_symbol_record_at(&bytes, header.segment_id, cursor)?;
476 let Some((record, len)) = parsed else {
477 torn_tail = true;
478 warn!(
479 bead_id = BEAD_ID,
480 logging_standard = LOGGING_STANDARD_BEAD,
481 path = %segment_path.display(),
482 segment_id = header.segment_id,
483 absolute_offset = cursor,
484 "detected torn tail while scanning symbol segment"
485 );
486 break;
487 };
488 records.push(record);
489 cursor = cursor
490 .checked_add(len)
491 .ok_or_else(|| FrankenError::DatabaseCorrupt {
492 detail: "cursor overflow while scanning symbol segment".to_owned(),
493 })?;
494 }
495
496 info!(
497 bead_id = BEAD_ID,
498 logging_standard = LOGGING_STANDARD_BEAD,
499 path = %segment_path.display(),
500 segment_id = header.segment_id,
501 record_count = records.len(),
502 torn_tail,
503 "scanned symbol segment"
504 );
505
506 Ok(SymbolSegmentScan {
507 header,
508 records,
509 torn_tail,
510 })
511}
512
513pub fn read_symbol_record_at_offset(
515 segment_path: &Path,
516 offset: SymbolLogOffset,
517) -> Result<SymbolRecord> {
518 let bytes = fs::read(segment_path)?;
519 if bytes.len() < SYMBOL_SEGMENT_HEADER_BYTES {
520 return Err(FrankenError::DatabaseCorrupt {
521 detail: format!(
522 "segment {} shorter than header: {} bytes",
523 segment_path.display(),
524 bytes.len()
525 ),
526 });
527 }
528
529 let header = SymbolSegmentHeader::decode(&bytes[..SYMBOL_SEGMENT_HEADER_BYTES])?;
530 if header.segment_id != offset.segment_id {
531 return Err(FrankenError::DatabaseCorrupt {
532 detail: format!(
533 "segment id mismatch: locator={}, header={}",
534 offset.segment_id, header.segment_id
535 ),
536 });
537 }
538
539 let offset_usize = u64_to_usize(offset.offset_bytes, "offset_bytes")?;
540 let absolute_offset = SYMBOL_SEGMENT_HEADER_BYTES
541 .checked_add(offset_usize)
542 .ok_or_else(|| FrankenError::DatabaseCorrupt {
543 detail: "absolute offset overflow while reading symbol record".to_owned(),
544 })?;
545
546 let Some((record, _)) = parse_symbol_record_at(&bytes, header.segment_id, absolute_offset)?
547 else {
548 return Err(FrankenError::DatabaseCorrupt {
549 detail: format!(
550 "no complete symbol record at offset {} in {}",
551 offset.offset_bytes,
552 segment_path.display()
553 ),
554 });
555 };
556
557 Ok(record.record)
558}
559
560pub fn read_aligned_symbol_record(
562 segment_path: &Path,
563 entry: AlignedSymbolIndexEntry,
564) -> Result<SymbolRecord> {
565 let bytes = fs::read(segment_path)?;
566 if bytes.len() < SYMBOL_SEGMENT_HEADER_BYTES {
567 return Err(FrankenError::DatabaseCorrupt {
568 detail: format!(
569 "segment {} shorter than header: {} bytes",
570 segment_path.display(),
571 bytes.len()
572 ),
573 });
574 }
575
576 let header = SymbolSegmentHeader::decode(&bytes[..SYMBOL_SEGMENT_HEADER_BYTES])?;
577 if header.segment_id != entry.offset.segment_id {
578 return Err(FrankenError::DatabaseCorrupt {
579 detail: format!(
580 "segment id mismatch: locator={}, header={}",
581 entry.offset.segment_id, header.segment_id
582 ),
583 });
584 }
585
586 let offset_usize = u64_to_usize(entry.offset.offset_bytes, "offset_bytes")?;
587 let absolute_offset = SYMBOL_SEGMENT_HEADER_BYTES
588 .checked_add(offset_usize)
589 .ok_or_else(|| FrankenError::DatabaseCorrupt {
590 detail: "absolute offset overflow while reading aligned symbol".to_owned(),
591 })?;
592 let logical_len = u32_to_usize(entry.logical_len, "logical_len")?;
593 let padded_len = u32_to_usize(entry.padded_len, "padded_len")?;
594 if logical_len > padded_len {
595 return Err(FrankenError::DatabaseCorrupt {
596 detail: format!(
597 "invalid aligned index entry: logical_len {} exceeds padded_len {}",
598 entry.logical_len, entry.padded_len
599 ),
600 });
601 }
602 let padded_end =
603 absolute_offset
604 .checked_add(padded_len)
605 .ok_or_else(|| FrankenError::DatabaseCorrupt {
606 detail: "aligned padded read overflow".to_owned(),
607 })?;
608 if padded_end > bytes.len() {
609 return Err(FrankenError::DatabaseCorrupt {
610 detail: format!(
611 "aligned symbol padded range out of bounds: end={}, file_len={}",
612 padded_end,
613 bytes.len()
614 ),
615 });
616 }
617 let end =
618 absolute_offset
619 .checked_add(logical_len)
620 .ok_or_else(|| FrankenError::DatabaseCorrupt {
621 detail: "aligned logical read overflow".to_owned(),
622 })?;
623 if end > padded_end {
624 return Err(FrankenError::DatabaseCorrupt {
625 detail: format!(
626 "aligned logical range exceeds padded slot: logical_end={}, padded_end={}",
627 end, padded_end
628 ),
629 });
630 }
631
632 SymbolRecord::from_bytes(&bytes[absolute_offset..end]).map_err(|err| {
633 error!(
634 bead_id = BEAD_ID,
635 logging_standard = LOGGING_STANDARD_BEAD,
636 path = %segment_path.display(),
637 offset_bytes = entry.offset.offset_bytes,
638 error = %err,
639 "failed to decode aligned symbol record"
640 );
641 FrankenError::DatabaseCorrupt {
642 detail: format!(
643 "invalid aligned SymbolRecord at offset {}: {err}",
644 entry.offset.offset_bytes
645 ),
646 }
647 })
648}
649
650pub fn rebuild_object_locator(
652 symbols_dir: &Path,
653) -> Result<BTreeMap<ObjectId, Vec<SymbolLogOffset>>> {
654 let mut locator: BTreeMap<ObjectId, Vec<SymbolLogOffset>> = BTreeMap::new();
655 let segments = sorted_segment_paths(symbols_dir)?;
656
657 for (segment_id, path) in segments {
658 let scan = scan_symbol_segment(&path)?;
659 for row in scan.records {
660 locator
661 .entry(row.record.object_id)
662 .or_default()
663 .push(row.offset);
664 }
665 if scan.torn_tail {
666 warn!(
667 bead_id = BEAD_ID,
668 logging_standard = LOGGING_STANDARD_BEAD,
669 segment_id,
670 path = %path.display(),
671 "locator rebuild ignored torn tail in segment"
672 );
673 }
674 }
675
676 for offsets in locator.values_mut() {
677 offsets.sort_unstable();
678 }
679
680 info!(
681 bead_id = BEAD_ID,
682 logging_standard = LOGGING_STANDARD_BEAD,
683 objects = locator.len(),
684 "rebuilt object locator from symbol segments"
685 );
686
687 Ok(locator)
688}
689
690#[derive(Debug, Clone, PartialEq, Eq)]
696pub struct SystematicRunLocator {
697 pub object_id: ObjectId,
699 pub segment_id: u64,
701 pub esi_start: u32,
703 pub esi_end_inclusive: u32,
705 pub offsets: Vec<SymbolLogOffset>,
707}
708
709impl SystematicRunLocator {
710 #[must_use]
712 pub fn source_symbol_count(&self) -> usize {
713 self.offsets.len()
714 }
715}
716
717pub fn rebuild_systematic_run_locator(
728 symbols_dir: &Path,
729) -> Result<BTreeMap<ObjectId, SystematicRunLocator>> {
730 let mut locator: BTreeMap<ObjectId, SystematicRunLocator> = BTreeMap::new();
731 let segments = sorted_segment_paths(symbols_dir)?;
732
733 for (segment_id, path) in segments {
734 let scan = scan_symbol_segment(&path)?;
735 let rows = &scan.records;
736
737 for start_idx in 0..rows.len() {
738 let start = &rows[start_idx].record;
739 if start.esi != 0
740 || !start
741 .flags
742 .contains(SymbolRecordFlags::SYSTEMATIC_RUN_START)
743 {
744 continue;
745 }
746
747 match build_systematic_run_locator(rows, start_idx) {
748 Ok(run) => {
749 locator.insert(run.object_id, run);
750 }
751 Err(detail) => {
752 warn!(
753 bead_id = BEAD_ID,
754 logging_standard = LOGGING_STANDARD_BEAD,
755 segment_id,
756 path = %path.display(),
757 start_offset = rows[start_idx].offset.offset_bytes,
758 start_object_id = %start.object_id,
759 reason = %detail,
760 "invalid systematic run start; fast-path must fall back"
761 );
762 }
763 }
764 }
765
766 if scan.torn_tail {
767 warn!(
768 bead_id = BEAD_ID,
769 logging_standard = LOGGING_STANDARD_BEAD,
770 segment_id,
771 path = %path.display(),
772 "systematic-run locator rebuild ignored torn tail in segment"
773 );
774 }
775 }
776
777 info!(
778 bead_id = BEAD_ID,
779 logging_standard = LOGGING_STANDARD_BEAD,
780 objects = locator.len(),
781 "rebuilt systematic run locator from symbol segments"
782 );
783
784 Ok(locator)
785}
786
787#[derive(Debug, Clone, Copy)]
798struct SystematicFastPathPlan {
799 source_symbols: usize,
800 symbol_size: usize,
801 transfer_len: usize,
802 total_len: usize,
803}
804
805#[derive(Debug, Clone, Copy)]
806struct SystematicFastPathExpectations<'a> {
807 run: &'a SystematicRunLocator,
808 object_id: ObjectId,
809 oti: Oti,
810 symbol_size: usize,
811 auth_epoch_key: Option<&'a [u8; 32]>,
812}
813
814fn fast_path_unavailable(object_id: ObjectId, detail: &str) {
815 warn!(
816 bead_id = BEAD_ID,
817 logging_standard = LOGGING_STANDARD_BEAD,
818 object_id = %object_id,
819 detail,
820 "systematic fast path unavailable"
821 );
822}
823
824fn fast_path_unavailable_esi(object_id: ObjectId, expected_esi: u32, detail: &str) {
825 warn!(
826 bead_id = BEAD_ID,
827 logging_standard = LOGGING_STANDARD_BEAD,
828 object_id = %object_id,
829 expected_esi,
830 detail,
831 "systematic fast path unavailable"
832 );
833}
834
835pub fn read_systematic_fast_path(
836 symbols_dir: &Path,
837 run: &SystematicRunLocator,
838 object_id: ObjectId,
839 oti: Oti,
840 auth_epoch_key: Option<&[u8; 32]>,
841) -> Result<Option<Vec<u8>>> {
842 let Some(plan) = build_systematic_fast_path_plan(run, object_id, oti) else {
843 return Ok(None);
844 };
845 if plan.source_symbols == 0 {
846 return Ok(Some(Vec::new()));
847 }
848
849 let Some((bytes, _header)) = load_systematic_fast_path_segment(symbols_dir, run, object_id)?
850 else {
851 return Ok(None);
852 };
853
854 let expectations = SystematicFastPathExpectations {
855 run,
856 object_id,
857 oti,
858 symbol_size: plan.symbol_size,
859 auth_epoch_key,
860 };
861 let mut out = vec![0_u8; plan.total_len];
862
863 for (index, offset) in run.offsets.iter().copied().enumerate() {
864 let Ok(expected_esi) = u32::try_from(index) else {
865 fast_path_unavailable(object_id, "index does not fit ESI");
866 return Ok(None);
867 };
868 let Some(parsed) =
869 read_systematic_fast_path_record(&bytes, &expectations, offset, expected_esi)
870 else {
871 return Ok(None);
872 };
873
874 let Some(start) = index.checked_mul(plan.symbol_size) else {
875 fast_path_unavailable_esi(object_id, expected_esi, "output offset overflow");
876 return Ok(None);
877 };
878 let Some(end) = start.checked_add(plan.symbol_size) else {
879 fast_path_unavailable_esi(object_id, expected_esi, "output end overflow");
880 return Ok(None);
881 };
882 if end > out.len() {
883 fast_path_unavailable_esi(object_id, expected_esi, "output bounds check failed");
884 return Ok(None);
885 }
886 out[start..end].copy_from_slice(&parsed.symbol_data);
887 }
888
889 out.truncate(plan.transfer_len);
890 Ok(Some(out))
891}
892
893fn build_systematic_fast_path_plan(
894 run: &SystematicRunLocator,
895 object_id: ObjectId,
896 oti: Oti,
897) -> Option<SystematicFastPathPlan> {
898 let source_symbols = match source_symbol_count(oti) {
899 Ok(value) => value,
900 Err(err) => {
901 let detail = format!("invalid source symbol count: {err}");
902 fast_path_unavailable(object_id, &detail);
903 return None;
904 }
905 };
906 if source_symbols == 0 {
907 return Some(SystematicFastPathPlan {
908 source_symbols,
909 symbol_size: 0,
910 transfer_len: 0,
911 total_len: 0,
912 });
913 }
914 if run.object_id != object_id {
915 fast_path_unavailable(object_id, "locator object mismatch");
916 return None;
917 }
918 if run.esi_start != 0 {
919 fast_path_unavailable(object_id, "run does not start at ESI 0");
920 return None;
921 }
922 if run.offsets.len() != source_symbols {
923 let detail = format!(
924 "locator offset count mismatch: expected={source_symbols} found={}",
925 run.offsets.len()
926 );
927 fast_path_unavailable(object_id, &detail);
928 return None;
929 }
930 let Ok(expected_end) = u32::try_from(source_symbols.saturating_sub(1)) else {
931 fast_path_unavailable(object_id, "source symbol count exceeds ESI range");
932 return None;
933 };
934 if run.esi_end_inclusive != expected_end {
935 fast_path_unavailable(object_id, "locator ESI range mismatch");
936 return None;
937 }
938
939 let Ok(symbol_size) = usize::try_from(oti.t) else {
940 fast_path_unavailable(object_id, "invalid OTI.t");
941 return None;
942 };
943 let Ok(transfer_len) = usize::try_from(oti.f) else {
944 fast_path_unavailable(object_id, "invalid OTI.f");
945 return None;
946 };
947 let Some(total_len) = source_symbols.checked_mul(symbol_size) else {
948 fast_path_unavailable(object_id, "reconstruction size overflow");
949 return None;
950 };
951
952 Some(SystematicFastPathPlan {
953 source_symbols,
954 symbol_size,
955 transfer_len,
956 total_len,
957 })
958}
959
960fn load_systematic_fast_path_segment(
961 symbols_dir: &Path,
962 run: &SystematicRunLocator,
963 object_id: ObjectId,
964) -> Result<Option<(Vec<u8>, SymbolSegmentHeader)>> {
965 let segment_path = symbol_segment_path(symbols_dir, run.segment_id);
966 if !segment_path.exists() {
967 fast_path_unavailable(object_id, "locator segment missing");
968 return Ok(None);
969 }
970
971 let bytes = fs::read(&segment_path)?;
972 if bytes.len() < SYMBOL_SEGMENT_HEADER_BYTES {
973 fast_path_unavailable(object_id, "segment shorter than header");
974 return Ok(None);
975 }
976
977 let header = match SymbolSegmentHeader::decode(&bytes[..SYMBOL_SEGMENT_HEADER_BYTES]) {
978 Ok(value) => value,
979 Err(err) => {
980 let detail = format!("invalid segment header: {err}");
981 fast_path_unavailable(object_id, &detail);
982 return Ok(None);
983 }
984 };
985 if header.segment_id != run.segment_id {
986 fast_path_unavailable(object_id, "segment id mismatch");
987 return Ok(None);
988 }
989
990 Ok(Some((bytes, header)))
991}
992
993fn read_systematic_fast_path_record(
994 bytes: &[u8],
995 expectations: &SystematicFastPathExpectations<'_>,
996 offset: SymbolLogOffset,
997 expected_esi: u32,
998) -> Option<SymbolRecord> {
999 if offset.segment_id != expectations.run.segment_id {
1000 fast_path_unavailable_esi(
1001 expectations.object_id,
1002 expected_esi,
1003 "wrong segment in offset",
1004 );
1005 return None;
1006 }
1007
1008 let Ok(offset_usize) = usize::try_from(offset.offset_bytes) else {
1009 fast_path_unavailable_esi(expectations.object_id, expected_esi, "bad record offset");
1010 return None;
1011 };
1012 let Some(absolute_offset) = SYMBOL_SEGMENT_HEADER_BYTES.checked_add(offset_usize) else {
1013 fast_path_unavailable_esi(
1014 expectations.object_id,
1015 expected_esi,
1016 "absolute offset overflow",
1017 );
1018 return None;
1019 };
1020
1021 let parsed = match parse_symbol_record_at(bytes, expectations.run.segment_id, absolute_offset) {
1022 Ok(Some((row, _))) => row.record,
1023 Ok(None) => {
1024 fast_path_unavailable_esi(
1025 expectations.object_id,
1026 expected_esi,
1027 "missing symbol record at offset",
1028 );
1029 return None;
1030 }
1031 Err(err) => {
1032 let detail = format!("invalid symbol record: {err}");
1033 fast_path_unavailable_esi(expectations.object_id, expected_esi, &detail);
1034 return None;
1035 }
1036 };
1037
1038 if parsed.object_id != expectations.object_id {
1039 fast_path_unavailable_esi(expectations.object_id, expected_esi, "object mismatch");
1040 return None;
1041 }
1042 if parsed.oti != expectations.oti {
1043 fast_path_unavailable_esi(expectations.object_id, expected_esi, "OTI mismatch");
1044 return None;
1045 }
1046 if parsed.esi != expected_esi {
1047 fast_path_unavailable_esi(expectations.object_id, expected_esi, "non-contiguous ESI");
1048 return None;
1049 }
1050 if parsed.symbol_data.len() != expectations.symbol_size {
1051 fast_path_unavailable_esi(expectations.object_id, expected_esi, "symbol size mismatch");
1052 return None;
1053 }
1054 if !parsed.verify_integrity() {
1055 fast_path_unavailable_esi(
1056 expectations.object_id,
1057 expected_esi,
1058 "integrity check failed",
1059 );
1060 return None;
1061 }
1062 if parsed.auth_tag != [0_u8; 16] {
1063 let Some(epoch_key) = expectations.auth_epoch_key else {
1064 fast_path_unavailable_esi(
1065 expectations.object_id,
1066 expected_esi,
1067 "auth tag present but no epoch key provided",
1068 );
1069 return None;
1070 };
1071 if !parsed.verify_auth(epoch_key) {
1072 fast_path_unavailable_esi(expectations.object_id, expected_esi, "auth check failed");
1073 return None;
1074 }
1075 }
1076
1077 Some(parsed)
1078}
1079
1080fn build_systematic_run_locator(
1081 rows: &[SymbolLogRecord],
1082 start_idx: usize,
1083) -> std::result::Result<SystematicRunLocator, String> {
1084 let start_row = rows
1085 .get(start_idx)
1086 .ok_or_else(|| format!("run start index {start_idx} out of bounds"))?;
1087 let start = &start_row.record;
1088 let source_symbols = source_symbol_count(start.oti)
1089 .map_err(|err| format!("invalid source symbol count at run start: {err}"))?;
1090 if source_symbols == 0 {
1091 return Err("source symbol count is zero".to_owned());
1092 }
1093 let source_symbols_u32 = u32::try_from(source_symbols)
1094 .map_err(|_| format!("source symbol count does not fit u32: {source_symbols}"))?;
1095 let end_exclusive = start_idx
1096 .checked_add(source_symbols)
1097 .ok_or_else(|| "systematic run index overflow".to_owned())?;
1098 if end_exclusive > rows.len() {
1099 return Err(format!(
1100 "incomplete systematic run: need {} rows from index {}, have {}",
1101 source_symbols,
1102 start_idx,
1103 rows.len().saturating_sub(start_idx)
1104 ));
1105 }
1106
1107 let mut offsets = Vec::with_capacity(source_symbols);
1108 for relative in 0..source_symbols {
1109 let row = &rows[start_idx + relative];
1110 let rec = &row.record;
1111 let expected_esi = u32::try_from(relative).map_err(|_| {
1112 format!("relative index {relative} exceeds u32::MAX in systematic run locator")
1113 })?;
1114
1115 if rec.object_id != start.object_id {
1116 return Err(format!(
1117 "object boundary at relative={} expected={} found={}",
1118 relative, start.object_id, rec.object_id
1119 ));
1120 }
1121 if rec.oti != start.oti {
1122 return Err(format!(
1123 "OTI mismatch at relative={} expected={:?} found={:?}",
1124 relative, start.oti, rec.oti
1125 ));
1126 }
1127 if rec.esi != expected_esi {
1128 return Err(format!(
1129 "non-contiguous ESI at relative={} expected={} found={}",
1130 relative, expected_esi, rec.esi
1131 ));
1132 }
1133 if relative == 0 {
1134 if !rec.flags.contains(SymbolRecordFlags::SYSTEMATIC_RUN_START) {
1135 return Err("missing SYSTEMATIC_RUN_START on ESI 0".to_owned());
1136 }
1137 } else if rec.flags.contains(SymbolRecordFlags::SYSTEMATIC_RUN_START) {
1138 return Err(format!(
1139 "unexpected SYSTEMATIC_RUN_START on non-zero ESI {}",
1140 rec.esi
1141 ));
1142 }
1143
1144 offsets.push(row.offset);
1145 }
1146
1147 Ok(SystematicRunLocator {
1148 object_id: start.object_id,
1149 segment_id: start_row.offset.segment_id,
1150 esi_start: 0,
1151 esi_end_inclusive: source_symbols_u32.saturating_sub(1),
1152 offsets,
1153 })
1154}
1155
1156fn parse_symbol_record_at(
1157 bytes: &[u8],
1158 segment_id: u64,
1159 absolute_offset: usize,
1160) -> Result<Option<(SymbolLogRecord, usize)>> {
1161 if absolute_offset >= bytes.len() {
1162 return Ok(None);
1163 }
1164
1165 let Some(record_len) = record_wire_len_at(bytes, absolute_offset)? else {
1166 return Ok(None);
1167 };
1168
1169 let end =
1170 absolute_offset
1171 .checked_add(record_len)
1172 .ok_or_else(|| FrankenError::DatabaseCorrupt {
1173 detail: "record end overflow while parsing symbol record".to_owned(),
1174 })?;
1175 let record = SymbolRecord::from_bytes(&bytes[absolute_offset..end]).map_err(|err| {
1176 error!(
1177 bead_id = BEAD_ID,
1178 logging_standard = LOGGING_STANDARD_BEAD,
1179 segment_id,
1180 absolute_offset,
1181 error = %err,
1182 "failed to decode SymbolRecord during scan"
1183 );
1184 FrankenError::DatabaseCorrupt {
1185 detail: format!("invalid SymbolRecord at absolute offset {absolute_offset}: {err}"),
1186 }
1187 })?;
1188
1189 let offset_without_header = absolute_offset
1190 .checked_sub(SYMBOL_SEGMENT_HEADER_BYTES)
1191 .ok_or_else(|| FrankenError::DatabaseCorrupt {
1192 detail: format!(
1193 "record offset {absolute_offset} precedes segment header of {SYMBOL_SEGMENT_HEADER_BYTES} bytes"
1194 ),
1195 })?;
1196
1197 let offset = SymbolLogOffset {
1198 segment_id,
1199 offset_bytes: usize_to_u64(offset_without_header, "offset_without_header")?,
1200 };
1201
1202 Ok(Some((SymbolLogRecord { offset, record }, record_len)))
1203}
1204
1205fn record_wire_len_at(bytes: &[u8], absolute_offset: usize) -> Result<Option<usize>> {
1206 let remaining = bytes.len().saturating_sub(absolute_offset);
1207 if remaining < SYMBOL_RECORD_HEADER_BYTES {
1208 return Ok(None);
1209 }
1210
1211 let size_start = absolute_offset
1212 .checked_add(SYMBOL_SIZE_FIELD_OFFSET)
1213 .ok_or_else(|| FrankenError::DatabaseCorrupt {
1214 detail: "symbol size field offset overflow".to_owned(),
1215 })?;
1216 let size_end = size_start
1217 .checked_add(SYMBOL_SIZE_FIELD_BYTES)
1218 .ok_or_else(|| FrankenError::DatabaseCorrupt {
1219 detail: "symbol size field end overflow".to_owned(),
1220 })?;
1221 let symbol_size_u32 = read_u32_at(bytes, size_start, "symbol_size")?;
1222 let symbol_size = u32_to_usize(symbol_size_u32, "symbol_size")?;
1223
1224 let total_len = SYMBOL_RECORD_HEADER_BYTES
1225 .checked_add(symbol_size)
1226 .and_then(|v| v.checked_add(SYMBOL_RECORD_TRAILER_BYTES))
1227 .ok_or_else(|| FrankenError::DatabaseCorrupt {
1228 detail: "symbol record size overflow".to_owned(),
1229 })?;
1230 if remaining < total_len {
1231 return Ok(None);
1232 }
1233
1234 if size_end > bytes.len() {
1235 return Err(FrankenError::DatabaseCorrupt {
1236 detail: format!(
1237 "symbol size field out of bounds: end={}, file_len={}",
1238 size_end,
1239 bytes.len()
1240 ),
1241 });
1242 }
1243
1244 Ok(Some(total_len))
1245}
1246
1247fn sorted_segment_paths(symbols_dir: &Path) -> Result<Vec<(u64, PathBuf)>> {
1248 if !symbols_dir.exists() {
1249 return Ok(Vec::new());
1250 }
1251
1252 let mut segments = Vec::new();
1253 for entry in fs::read_dir(symbols_dir)? {
1254 let entry = entry?;
1255 if !entry.file_type()?.is_file() {
1256 continue;
1257 }
1258 let name = entry.file_name();
1259 let Some(name) = name.to_str() else {
1260 continue;
1261 };
1262 let Some(segment_id) = parse_segment_id_from_name(name) else {
1263 continue;
1264 };
1265 segments.push((segment_id, entry.path()));
1266 }
1267 segments.sort_by_key(|(segment_id, _)| *segment_id);
1268 Ok(segments)
1269}
1270
1271fn parse_segment_id_from_name(file_name: &str) -> Option<u64> {
1272 let prefix = "segment-";
1273 let suffix = ".log";
1274 if !file_name.starts_with(prefix) || !file_name.ends_with(suffix) {
1275 return None;
1276 }
1277 let id_text = &file_name[prefix.len()..file_name.len() - suffix.len()];
1278 id_text.parse::<u64>().ok()
1279}
1280
1281fn file_len_usize(path: &Path) -> Result<usize> {
1282 let len = fs::metadata(path)?.len();
1283 u64_to_usize(len, "file length")
1284}
1285
1286fn align_up(value: usize, alignment: usize) -> Result<usize> {
1287 if alignment == 0 {
1288 return Err(FrankenError::Internal(
1289 "alignment must be non-zero".to_owned(),
1290 ));
1291 }
1292 let remainder = value % alignment;
1293 if remainder == 0 {
1294 return Ok(value);
1295 }
1296 value
1297 .checked_add(alignment - remainder)
1298 .ok_or_else(|| FrankenError::DatabaseCorrupt {
1299 detail: "alignment overflow".to_owned(),
1300 })
1301}
1302
1303fn read_u32_at(bytes: &[u8], start: usize, field: &str) -> Result<u32> {
1304 let end = start
1305 .checked_add(4)
1306 .ok_or_else(|| FrankenError::DatabaseCorrupt {
1307 detail: format!("overflow while reading field {field}"),
1308 })?;
1309 let slice = bytes
1310 .get(start..end)
1311 .ok_or_else(|| FrankenError::DatabaseCorrupt {
1312 detail: format!(
1313 "field {field} out of bounds: start={start}, end={end}, len={}",
1314 bytes.len()
1315 ),
1316 })?;
1317 let array: [u8; 4] = slice
1318 .try_into()
1319 .map_err(|_| FrankenError::DatabaseCorrupt {
1320 detail: format!("failed to parse field {field}"),
1321 })?;
1322 Ok(u32::from_le_bytes(array))
1323}
1324
1325fn read_u64_at(bytes: &[u8], start: usize, field: &str) -> Result<u64> {
1326 let end = start
1327 .checked_add(8)
1328 .ok_or_else(|| FrankenError::DatabaseCorrupt {
1329 detail: format!("overflow while reading field {field}"),
1330 })?;
1331 let slice = bytes
1332 .get(start..end)
1333 .ok_or_else(|| FrankenError::DatabaseCorrupt {
1334 detail: format!(
1335 "field {field} out of bounds: start={start}, end={end}, len={}",
1336 bytes.len()
1337 ),
1338 })?;
1339 let array: [u8; 8] = slice
1340 .try_into()
1341 .map_err(|_| FrankenError::DatabaseCorrupt {
1342 detail: format!("failed to parse field {field}"),
1343 })?;
1344 Ok(u64::from_le_bytes(array))
1345}
1346
1347fn u64_to_usize(value: u64, what: &str) -> Result<usize> {
1348 usize::try_from(value).map_err(|_| FrankenError::DatabaseCorrupt {
1349 detail: format!("{what} does not fit in usize: {value}"),
1350 })
1351}
1352
1353fn usize_to_u64(value: usize, what: &str) -> Result<u64> {
1354 u64::try_from(value).map_err(|_| FrankenError::DatabaseCorrupt {
1355 detail: format!("{what} does not fit in u64: {value}"),
1356 })
1357}
1358
1359fn u32_to_usize(value: u32, what: &str) -> Result<usize> {
1360 usize::try_from(value).map_err(|_| FrankenError::DatabaseCorrupt {
1361 detail: format!("{what} does not fit in usize: {value}"),
1362 })
1363}
1364
1365fn usize_to_u32(value: usize, what: &str) -> Result<u32> {
1366 u32::try_from(value).map_err(|_| FrankenError::DatabaseCorrupt {
1367 detail: format!("{what} does not fit in u32: {value}"),
1368 })
1369}
1370
1371#[cfg(test)]
1372mod tests {
1373 use std::collections::BTreeMap;
1374 use std::fs::OpenOptions;
1375 use std::io::Write;
1376
1377 use fsqlite_types::{ObjectId, Oti, SymbolRecordFlags};
1378 use tempfile::tempdir;
1379
1380 use super::*;
1381
1382 const BD_1HI_24_COMPLIANCE_SENTINEL: &str = "test_bd_1hi_24_unit_compliance_gate prop_bd_1hi_24_structure_compliance \
1383 test_e2e_bd_1hi_24_compliance DEBUG INFO WARN ERROR bd-1fpm";
1384
1385 fn test_record(object_seed: u8, esi: u32, symbol_size: u32, fill: u8) -> SymbolRecord {
1386 let symbol_len = usize::try_from(symbol_size).expect("symbol_size fits usize for tests");
1387 let oti = Oti {
1388 f: u64::from(symbol_size),
1389 al: 1,
1390 t: symbol_size,
1391 z: 1,
1392 n: 1,
1393 };
1394 let mut data = vec![fill; symbol_len];
1395 data[0] = object_seed;
1396 SymbolRecord::new(
1397 ObjectId::from_bytes([object_seed; 16]),
1398 oti,
1399 esi,
1400 data,
1401 SymbolRecordFlags::empty(),
1402 )
1403 }
1404
1405 fn systematic_record(
1406 object_seed: u8,
1407 oti: Oti,
1408 esi: u32,
1409 fill: u8,
1410 systematic_start: bool,
1411 ) -> SymbolRecord {
1412 let symbol_len = usize::try_from(oti.t).expect("OTI.t fits usize for tests");
1413 let mut data = vec![fill; symbol_len];
1414 if let Some(first) = data.first_mut() {
1415 let esi_tag = u8::try_from(esi).unwrap_or(0);
1416 *first = object_seed.wrapping_add(esi_tag);
1417 }
1418 let flags = if systematic_start {
1419 SymbolRecordFlags::SYSTEMATIC_RUN_START
1420 } else {
1421 SymbolRecordFlags::empty()
1422 };
1423 SymbolRecord::new(
1424 ObjectId::from_bytes([object_seed; 16]),
1425 oti,
1426 esi,
1427 data,
1428 flags,
1429 )
1430 }
1431
1432 #[test]
1433 fn test_symbol_segment_header_encode_decode() {
1434 let header = SymbolSegmentHeader::new(17, 42, 1_731_000_000);
1435 let bytes = header.encode();
1436 assert_eq!(bytes.len(), SYMBOL_SEGMENT_HEADER_BYTES);
1437 let decoded = SymbolSegmentHeader::decode(&bytes).expect("decode header");
1438 assert_eq!(decoded, header);
1439 }
1440
1441 #[test]
1442 fn test_symbol_segment_header_magic() {
1443 let header = SymbolSegmentHeader::new(3, 7, 99);
1444 let mut bytes = header.encode();
1445 bytes[0] = b'X';
1446 let err = SymbolSegmentHeader::decode(&bytes).expect_err("bad magic must fail");
1447 assert!(err.to_string().contains("invalid symbol segment magic"));
1448 }
1449
1450 #[test]
1451 fn test_symbol_log_append_records() {
1452 let dir = tempdir().expect("tempdir");
1453 let mut manager = SymbolLogManager::new(dir.path(), 1, 42, 100).expect("manager");
1454 let sizes = [1024_u32, 1536, 2048, 3072, 4096];
1455 for (idx, size) in sizes.into_iter().enumerate() {
1456 let idx_u32 = u32::try_from(idx).expect("test index fits u32");
1457 let seed = u8::try_from(idx + 1).expect("test index fits u8");
1458 let rec = test_record(seed, idx_u32, size, 0xA0);
1459 manager.append(&rec).expect("append record");
1460 }
1461
1462 let scan = scan_symbol_segment(&manager.active_segment_path()).expect("scan segment");
1463 assert_eq!(scan.records.len(), 5);
1464 assert!(!scan.torn_tail);
1465 assert_eq!(scan.records[0].record.symbol_data.len(), 1024);
1466 assert_eq!(scan.records[4].record.symbol_data.len(), 4096);
1467 manager.rotate(2, 43, 200).expect("rotation succeeds");
1468 }
1469
1470 #[test]
1471 fn test_symbol_log_torn_tail_recovery() {
1472 let dir = tempdir().expect("tempdir");
1473 let manager = SymbolLogManager::new(dir.path(), 1, 42, 100).expect("manager");
1474 for idx in 0_u32..3_u32 {
1475 let seed = u8::try_from(idx + 1).expect("small index fits u8");
1476 let rec = test_record(seed, idx, 1024, 0xB0);
1477 manager.append(&rec).expect("append record");
1478 }
1479
1480 let partial = test_record(9, 9, 1024, 0xCC).to_bytes();
1481 let partial_len = partial.len() / 2;
1482 let mut file = OpenOptions::new()
1483 .append(true)
1484 .open(manager.active_segment_path())
1485 .expect("open for append");
1486 file.write_all(&partial[..partial_len])
1487 .expect("write partial record");
1488 file.sync_data().expect("sync partial tail");
1489
1490 let scan = scan_symbol_segment(&manager.active_segment_path()).expect("scan segment");
1491 assert_eq!(scan.records.len(), 3);
1492 assert!(scan.torn_tail);
1493 }
1494
1495 #[test]
1496 fn test_locator_offset_computation() {
1497 let dir = tempdir().expect("tempdir");
1498 let manager = SymbolLogManager::new(dir.path(), 1, 42, 100).expect("manager");
1499 let record = test_record(7, 11, 2048, 0x44);
1500 let offset = manager.append(&record).expect("append record");
1501
1502 let loaded = read_symbol_record_at_offset(&manager.active_segment_path(), offset)
1503 .expect("read by offset");
1504 assert_eq!(loaded.object_id, record.object_id);
1505 assert_eq!(loaded.esi, record.esi);
1506 assert_eq!(loaded.symbol_data, record.symbol_data);
1507 }
1508
1509 #[test]
1510 fn test_locator_cache_rebuild() {
1511 let dir = tempdir().expect("tempdir");
1512 let mut manager = SymbolLogManager::new(dir.path(), 1, 42, 100).expect("manager");
1513
1514 let record_alpha_first = test_record(1, 0, 1024, 0x01);
1515 let record_bravo_first = test_record(2, 1, 1024, 0x02);
1516 manager.append(&record_alpha_first).expect("append a1");
1517 manager.append(&record_bravo_first).expect("append b1");
1518
1519 manager.rotate(2, 43, 200).expect("rotate");
1520 let record_alpha_second = test_record(1, 2, 1024, 0x03);
1521 let record_charlie_second = test_record(3, 3, 1024, 0x04);
1522 manager.append(&record_alpha_second).expect("append a2");
1523 manager.append(&record_charlie_second).expect("append c2");
1524
1525 let locator = rebuild_object_locator(dir.path()).expect("rebuild locator");
1526 assert_eq!(locator.len(), 3);
1527 assert_eq!(
1528 locator
1529 .get(&ObjectId::from_bytes([1_u8; 16]))
1530 .expect("object 1 exists")
1531 .len(),
1532 2
1533 );
1534 assert_eq!(
1535 locator
1536 .get(&ObjectId::from_bytes([2_u8; 16]))
1537 .expect("object 2 exists")
1538 .len(),
1539 1
1540 );
1541 assert_eq!(
1542 locator
1543 .get(&ObjectId::from_bytes([3_u8; 16]))
1544 .expect("object 3 exists")
1545 .len(),
1546 1
1547 );
1548 }
1549
1550 #[test]
1551 fn test_locator_cache_missing() {
1552 let dir = tempdir().expect("tempdir");
1553 let manager = SymbolLogManager::new(dir.path(), 1, 42, 100).expect("manager");
1554 let rec = test_record(9, 0, 1024, 0x55);
1555 manager.append(&rec).expect("append");
1556
1557 let locator = rebuild_object_locator(dir.path()).expect("rebuild from scan");
1558 assert_eq!(locator.len(), 1);
1559 assert!(locator.contains_key(&ObjectId::from_bytes([9_u8; 16])));
1560 }
1561
1562 #[test]
1563 fn test_systematic_run_locator_rebuild_happy_path() {
1564 let dir = tempdir().expect("tempdir");
1565 let manager = SymbolLogManager::new(dir.path(), 1, 42, 100).expect("manager");
1566 let oti = Oti {
1567 f: 64_u64 * 3,
1568 al: 1,
1569 t: 64,
1570 z: 1,
1571 n: 1,
1572 };
1573 let object_id = ObjectId::from_bytes([7_u8; 16]);
1574
1575 let r0 = systematic_record(7, oti, 0, 0xA1, true);
1576 let r1 = systematic_record(7, oti, 1, 0xA2, false);
1577 let r2 = systematic_record(7, oti, 2, 0xA3, false);
1578 let repair = systematic_record(7, oti, 3, 0xAF, false);
1579
1580 let o0 = manager.append(&r0).expect("append esi0");
1581 let o1 = manager.append(&r1).expect("append esi1");
1582 let o2 = manager.append(&r2).expect("append esi2");
1583 let _o3 = manager.append(&repair).expect("append repair");
1584
1585 let locator =
1586 rebuild_systematic_run_locator(dir.path()).expect("rebuild systematic locator");
1587 let run = locator.get(&object_id).expect("run must exist");
1588 assert_eq!(run.segment_id, 1);
1589 assert_eq!(run.esi_start, 0);
1590 assert_eq!(run.esi_end_inclusive, 2);
1591 assert_eq!(run.source_symbol_count(), 3);
1592 assert_eq!(run.offsets, vec![o0, o1, o2]);
1593 }
1594
1595 #[test]
1596 fn test_systematic_run_locator_missing_symbol_is_ignored() {
1597 let dir = tempdir().expect("tempdir");
1598 let manager = SymbolLogManager::new(dir.path(), 1, 42, 100).expect("manager");
1599 let oti = Oti {
1600 f: 64_u64 * 3,
1601 al: 1,
1602 t: 64,
1603 z: 1,
1604 n: 1,
1605 };
1606 let object_id = ObjectId::from_bytes([8_u8; 16]);
1607
1608 manager
1609 .append(&systematic_record(8, oti, 0, 0xB1, true))
1610 .expect("append esi0");
1611 manager
1612 .append(&systematic_record(8, oti, 2, 0xB3, false))
1613 .expect("append esi2");
1614
1615 let locator =
1616 rebuild_systematic_run_locator(dir.path()).expect("rebuild systematic locator");
1617 assert!(
1618 !locator.contains_key(&object_id),
1619 "incomplete run must not be indexed as fast-path eligible"
1620 );
1621 }
1622
1623 #[test]
1624 fn test_systematic_run_locator_interleaved_object_is_ignored() {
1625 let dir = tempdir().expect("tempdir");
1626 let manager = SymbolLogManager::new(dir.path(), 1, 42, 100).expect("manager");
1627 let oti = Oti {
1628 f: 64_u64 * 2,
1629 al: 1,
1630 t: 64,
1631 z: 1,
1632 n: 1,
1633 };
1634 let object_a = ObjectId::from_bytes([11_u8; 16]);
1635 let object_b = ObjectId::from_bytes([12_u8; 16]);
1636
1637 manager
1638 .append(&systematic_record(11, oti, 0, 0xC1, true))
1639 .expect("append A esi0");
1640 manager
1641 .append(&systematic_record(12, oti, 0, 0xD1, true))
1642 .expect("append B esi0");
1643 manager
1644 .append(&systematic_record(11, oti, 1, 0xC2, false))
1645 .expect("append A esi1");
1646
1647 let locator =
1648 rebuild_systematic_run_locator(dir.path()).expect("rebuild systematic locator");
1649 assert!(
1650 !locator.contains_key(&object_a),
1651 "interleaved run must be rejected for fast-path"
1652 );
1653 assert!(
1654 !locator.contains_key(&object_b),
1655 "single-symbol run with K=2 must be rejected as incomplete"
1656 );
1657 }
1658
1659 #[test]
1660 fn test_systematic_run_locator_prefers_newest_complete_run() {
1661 let dir = tempdir().expect("tempdir");
1662 let mut manager = SymbolLogManager::new(dir.path(), 1, 42, 100).expect("manager");
1663 let oti = Oti {
1664 f: 64_u64 * 2,
1665 al: 1,
1666 t: 64,
1667 z: 1,
1668 n: 1,
1669 };
1670 let object_id = ObjectId::from_bytes([13_u8; 16]);
1671
1672 manager
1673 .append(&systematic_record(13, oti, 0, 0xE1, true))
1674 .expect("append seg1 esi0");
1675 manager
1676 .append(&systematic_record(13, oti, 1, 0xE2, false))
1677 .expect("append seg1 esi1");
1678
1679 manager.rotate(2, 43, 200).expect("rotate");
1680 let newer_o0 = manager
1681 .append(&systematic_record(13, oti, 0, 0xF1, true))
1682 .expect("append seg2 esi0");
1683 let newer_o1 = manager
1684 .append(&systematic_record(13, oti, 1, 0xF2, false))
1685 .expect("append seg2 esi1");
1686
1687 let locator =
1688 rebuild_systematic_run_locator(dir.path()).expect("rebuild systematic locator");
1689 let run = locator.get(&object_id).expect("run exists");
1690 assert_eq!(
1691 run.segment_id, 2,
1692 "newest complete run should win in append-order locator rebuild"
1693 );
1694 assert_eq!(run.offsets, vec![newer_o0, newer_o1]);
1695 }
1696
1697 #[test]
1698 fn test_systematic_fast_path_success() {
1699 let dir = tempdir().expect("tempdir");
1700 let manager = SymbolLogManager::new(dir.path(), 1, 42, 100).expect("manager");
1701 let oti = Oti {
1702 f: 64_u64 * 3 - 11,
1703 al: 1,
1704 t: 64,
1705 z: 1,
1706 n: 1,
1707 };
1708 let object_id = ObjectId::from_bytes([21_u8; 16]);
1709
1710 let r0 = systematic_record(21, oti, 0, 0x11, true);
1711 let r1 = systematic_record(21, oti, 1, 0x22, false);
1712 let r2 = systematic_record(21, oti, 2, 0x33, false);
1713 manager.append(&r0).expect("append esi0");
1714 manager.append(&r1).expect("append esi1");
1715 manager.append(&r2).expect("append esi2");
1716 manager
1717 .append(&systematic_record(21, oti, 3, 0x44, false))
1718 .expect("append repair");
1719
1720 let runs = rebuild_systematic_run_locator(dir.path()).expect("rebuild runs");
1721 let run = runs.get(&object_id).expect("run exists");
1722 let maybe_payload = read_systematic_fast_path(dir.path(), run, object_id, oti, None)
1723 .expect("fast-path read");
1724 let payload = maybe_payload.expect("fast path should reconstruct");
1725
1726 let mut expected = Vec::new();
1727 expected.extend_from_slice(&r0.symbol_data);
1728 expected.extend_from_slice(&r1.symbol_data);
1729 expected.extend_from_slice(&r2.symbol_data);
1730 expected.truncate(usize::try_from(oti.f).expect("f fits usize"));
1731 assert_eq!(payload, expected);
1732 }
1733
1734 #[test]
1735 fn test_systematic_fast_path_corrupt_symbol_requires_fallback() {
1736 let dir = tempdir().expect("tempdir");
1737 let manager = SymbolLogManager::new(dir.path(), 1, 42, 100).expect("manager");
1738 let oti = Oti {
1739 f: 64_u64 * 3 - 7,
1740 al: 1,
1741 t: 64,
1742 z: 1,
1743 n: 1,
1744 };
1745 let object_id = ObjectId::from_bytes([22_u8; 16]);
1746
1747 let r0 = systematic_record(22, oti, 0, 0x51, true);
1748 let r1 = systematic_record(22, oti, 1, 0x52, false);
1749 let r2 = systematic_record(22, oti, 2, 0x53, false);
1750 manager.append(&r0).expect("append esi0");
1751 let r1_offset = manager.append(&r1).expect("append esi1");
1752 manager.append(&r2).expect("append esi2");
1753
1754 let runs = rebuild_systematic_run_locator(dir.path()).expect("rebuild runs");
1755 let run = runs.get(&object_id).expect("run exists").clone();
1756
1757 let segment_path = symbol_segment_path(dir.path(), r1_offset.segment_id);
1758 let mut bytes = fs::read(&segment_path).expect("read segment bytes");
1759 let record_offset = usize::try_from(r1_offset.offset_bytes).expect("offset fits usize");
1760 let absolute_record_offset = SYMBOL_SEGMENT_HEADER_BYTES
1761 .checked_add(record_offset)
1762 .expect("absolute offset");
1763 let data_byte_offset = absolute_record_offset
1764 .checked_add(SYMBOL_RECORD_HEADER_BYTES)
1765 .expect("data offset");
1766 bytes[data_byte_offset] ^= 0xFF;
1767 fs::write(&segment_path, bytes).expect("write corrupted segment");
1768
1769 let result = read_systematic_fast_path(dir.path(), &run, object_id, oti, None)
1770 .expect("fast-path read should not hard-fail on corrupt symbol");
1771 assert!(
1772 result.is_none(),
1773 "corrupt symbol should force fallback path"
1774 );
1775 }
1776
1777 #[test]
1778 fn test_systematic_fast_path_missing_symbol_requires_fallback() {
1779 let dir = tempdir().expect("tempdir");
1780 let manager = SymbolLogManager::new(dir.path(), 1, 42, 100).expect("manager");
1781 let oti = Oti {
1782 f: 64_u64 * 3 - 3,
1783 al: 1,
1784 t: 64,
1785 z: 1,
1786 n: 1,
1787 };
1788 let object_id = ObjectId::from_bytes([23_u8; 16]);
1789
1790 manager
1791 .append(&systematic_record(23, oti, 0, 0x61, true))
1792 .expect("append esi0");
1793 manager
1794 .append(&systematic_record(23, oti, 1, 0x62, false))
1795 .expect("append esi1");
1796 manager
1797 .append(&systematic_record(23, oti, 2, 0x63, false))
1798 .expect("append esi2");
1799
1800 let runs = rebuild_systematic_run_locator(dir.path()).expect("rebuild runs");
1801 let mut run = runs.get(&object_id).expect("run exists").clone();
1802 run.offsets[1].offset_bytes = run.offsets[1].offset_bytes.saturating_add(1_000_000);
1803
1804 let result = read_systematic_fast_path(dir.path(), &run, object_id, oti, None)
1805 .expect("fast-path read should not hard-fail on missing symbol");
1806 assert!(
1807 result.is_none(),
1808 "missing symbol should force fallback path"
1809 );
1810 }
1811
1812 #[test]
1813 fn test_systematic_fast_path_auth_failure_requires_fallback() {
1814 let dir = tempdir().expect("tempdir");
1815 let manager = SymbolLogManager::new(dir.path(), 1, 42, 100).expect("manager");
1816 let oti = Oti {
1817 f: 64_u64 * 3 - 5,
1818 al: 1,
1819 t: 64,
1820 z: 1,
1821 n: 1,
1822 };
1823 let object_id = ObjectId::from_bytes([24_u8; 16]);
1824 let auth_epoch_key = [0xA5_u8; 32];
1825 let wrong_epoch_key = [0x5A_u8; 32];
1826
1827 let r0 = systematic_record(24, oti, 0, 0x71, true).with_auth_tag(&auth_epoch_key);
1828 let r1 = systematic_record(24, oti, 1, 0x72, false).with_auth_tag(&auth_epoch_key);
1829 let r2 = systematic_record(24, oti, 2, 0x73, false).with_auth_tag(&auth_epoch_key);
1830 manager.append(&r0).expect("append esi0");
1831 manager.append(&r1).expect("append esi1");
1832 manager.append(&r2).expect("append esi2");
1833
1834 let runs = rebuild_systematic_run_locator(dir.path()).expect("rebuild runs");
1835 let run = runs.get(&object_id).expect("run exists");
1836
1837 let wrong_key_result =
1838 read_systematic_fast_path(dir.path(), run, object_id, oti, Some(&wrong_epoch_key))
1839 .expect("fast-path read with wrong key");
1840 assert!(
1841 wrong_key_result.is_none(),
1842 "auth mismatch should force fallback path"
1843 );
1844
1845 let correct_key_result =
1846 read_systematic_fast_path(dir.path(), run, object_id, oti, Some(&auth_epoch_key))
1847 .expect("fast-path read with correct key");
1848 assert!(
1849 correct_key_result.is_some(),
1850 "correct auth key should keep fast path eligible"
1851 );
1852 }
1853
1854 #[test]
1855 fn test_epoch_id_stored() {
1856 let dir = tempdir().expect("tempdir");
1857 let manager = SymbolLogManager::new(dir.path(), 1, 42, 123_456).expect("manager");
1858 let bytes = fs::read(manager.active_segment_path()).expect("read segment bytes");
1859 let header = SymbolSegmentHeader::decode(&bytes[..SYMBOL_SEGMENT_HEADER_BYTES])
1860 .expect("decode header");
1861 assert_eq!(header.epoch_id, 42);
1862 }
1863
1864 #[test]
1865 fn test_immutable_rotated_segments() {
1866 let dir = tempdir().expect("tempdir");
1867 let mut manager = SymbolLogManager::new(dir.path(), 1, 42, 100).expect("manager");
1868 manager
1869 .append(&test_record(1, 0, 1024, 0x11))
1870 .expect("append segment 1");
1871 manager.rotate(2, 43, 200).expect("rotate");
1872
1873 let err = manager
1874 .append_to_segment(1, &test_record(2, 1, 1024, 0x22))
1875 .expect_err("rotated segment should be immutable");
1876 assert!(err.to_string().contains("immutable"));
1877 }
1878
1879 #[test]
1880 fn test_variable_size_records() {
1881 let dir = tempdir().expect("tempdir");
1882 let manager = SymbolLogManager::new(dir.path(), 1, 42, 100).expect("manager");
1883 for (idx, size) in [1024_u32, 4096, 65_536].into_iter().enumerate() {
1884 let idx_u32 = u32::try_from(idx).expect("small test index fits u32");
1885 let seed = u8::try_from(idx + 1).expect("small test index fits u8");
1886 let rec = test_record(seed, idx_u32, size, 0x66);
1887 manager.append(&rec).expect("append variable-size record");
1888 }
1889
1890 let scan = scan_symbol_segment(&manager.active_segment_path()).expect("scan");
1891 assert_eq!(scan.records.len(), 3);
1892 assert_eq!(scan.records[0].record.symbol_data.len(), 1024);
1893 assert_eq!(scan.records[1].record.symbol_data.len(), 4096);
1894 assert_eq!(scan.records[2].record.symbol_data.len(), 65_536);
1895 }
1896
1897 #[test]
1898 fn test_no_o_direct_requirement() {
1899 let dir = tempdir().expect("tempdir");
1900 let manager = SymbolLogManager::new(dir.path(), 1, 42, 100).expect("manager");
1901 manager
1902 .append(&test_record(4, 0, 1024, 0x77))
1903 .expect("buffered append succeeds");
1904 let scan =
1905 scan_symbol_segment(&manager.active_segment_path()).expect("buffered scan succeeds");
1906 assert_eq!(scan.records.len(), 1);
1907 assert!(!scan.torn_tail);
1908 }
1909
1910 #[test]
1911 fn test_aligned_variant_optional() {
1912 let dir = tempdir().expect("tempdir");
1913 let header = SymbolSegmentHeader::new(1, 42, 100);
1914 let record = test_record(5, 0, 1024, 0x88);
1915 let entry = append_symbol_record_aligned(dir.path(), header, &record, 4096)
1916 .expect("aligned append");
1917
1918 assert_eq!(u64::from(entry.padded_len) % 4096, 0);
1919 assert!(entry.padded_len >= entry.logical_len);
1920
1921 let segment_path = symbol_segment_path(dir.path(), 1);
1922 let loaded = read_aligned_symbol_record(&segment_path, entry).expect("read aligned");
1923 assert_eq!(loaded.object_id, record.object_id);
1924 assert_eq!(loaded.esi, record.esi);
1925 assert_eq!(loaded.frame_xxh3, record.frame_xxh3);
1926 assert!(loaded.verify_integrity());
1927 }
1928
1929 #[test]
1930 fn test_aligned_read_rejects_inconsistent_index_entry() {
1931 let dir = tempdir().expect("tempdir");
1932 let header = SymbolSegmentHeader::new(1, 42, 100);
1933 let record = test_record(6, 0, 1024, 0x99);
1934 let entry = append_symbol_record_aligned(dir.path(), header, &record, 4096)
1935 .expect("aligned append");
1936
1937 let segment_path = symbol_segment_path(dir.path(), 1);
1938 let mut bad = entry;
1939 bad.padded_len = bad.logical_len.saturating_sub(1);
1940
1941 let err =
1942 read_aligned_symbol_record(&segment_path, bad).expect_err("must reject bad index");
1943 let FrankenError::DatabaseCorrupt { detail } = err else {
1944 panic!("expected DatabaseCorrupt for inconsistent aligned index");
1945 };
1946 assert!(
1947 detail.contains("logical_len") && detail.contains("padded_len"),
1948 "unexpected detail: {detail}"
1949 );
1950 }
1951
1952 #[test]
1953 fn test_bd_1hi_24_unit_compliance_gate() {
1954 assert_eq!(SYMBOL_SEGMENT_HEADER_BYTES, 40);
1955 assert_eq!(SYMBOL_SEGMENT_MAGIC, *b"FSSY");
1956 for token in [
1957 "test_bd_1hi_24_unit_compliance_gate",
1958 "prop_bd_1hi_24_structure_compliance",
1959 "test_e2e_bd_1hi_24_compliance",
1960 "DEBUG",
1961 "INFO",
1962 "WARN",
1963 "ERROR",
1964 "bd-1fpm",
1965 ] {
1966 assert!(BD_1HI_24_COMPLIANCE_SENTINEL.contains(token));
1967 }
1968 }
1969
1970 #[test]
1971 fn prop_bd_1hi_24_structure_compliance() {
1972 let dir = tempdir().expect("tempdir");
1973 let mut manager = SymbolLogManager::new(dir.path(), 1, 42, 100).expect("manager");
1974 let mut expected: BTreeMap<ObjectId, Vec<SymbolLogOffset>> = BTreeMap::new();
1975
1976 for segment_index in 0_u64..3_u64 {
1977 if segment_index > 0 {
1978 manager
1979 .rotate(segment_index + 1, 42 + segment_index, 100 + segment_index)
1980 .expect("rotate");
1981 }
1982 for record_index in 0_u32..6_u32 {
1983 let object_seed = u8::try_from((segment_index + u64::from(record_index)) % 4)
1984 .expect("small seed");
1985 let fill = u8::try_from(0x90_u64 + segment_index + u64::from(record_index))
1986 .expect("small fill");
1987 let rec = test_record(object_seed, record_index, 1024, fill);
1988 let offset = manager.append(&rec).expect("append");
1989 expected.entry(rec.object_id).or_default().push(offset);
1990 }
1991 }
1992
1993 for offsets in expected.values_mut() {
1994 offsets.sort_unstable();
1995 }
1996
1997 let rebuilt = rebuild_object_locator(dir.path()).expect("rebuild locator");
1998 assert_eq!(rebuilt, expected);
1999 }
2000
2001 #[test]
2002 fn test_e2e_bd_1hi_24_compliance() {
2003 let dir = tempdir().expect("tempdir");
2004 let mut manager = SymbolLogManager::new(dir.path(), 1, 42, 100).expect("manager");
2005 let mut written = Vec::new();
2006
2007 let rec_a = test_record(1, 0, 1024, 0x11);
2008 let rec_b = test_record(2, 1, 2048, 0x22);
2009 written.push((
2010 rec_a.object_id,
2011 manager.append(&rec_a).expect("append rec_a to segment 1"),
2012 ));
2013 written.push((
2014 rec_b.object_id,
2015 manager.append(&rec_b).expect("append rec_b to segment 1"),
2016 ));
2017
2018 manager.rotate(2, 43, 200).expect("rotate");
2019 let rec_c = test_record(1, 2, 4096, 0x33);
2020 let rec_d = test_record(3, 3, 1024, 0x44);
2021 written.push((
2022 rec_c.object_id,
2023 manager.append(&rec_c).expect("append rec_c to segment 2"),
2024 ));
2025 written.push((
2026 rec_d.object_id,
2027 manager.append(&rec_d).expect("append rec_d to segment 2"),
2028 ));
2029
2030 let locator = rebuild_object_locator(dir.path()).expect("rebuild locator");
2031 assert_eq!(locator.len(), 3);
2032
2033 for (object_id, offset) in &written {
2034 let path = symbol_segment_path(dir.path(), offset.segment_id);
2035 let loaded = read_symbol_record_at_offset(&path, *offset).expect("direct offset read");
2036 assert_eq!(&loaded.object_id, object_id);
2037 }
2038
2039 let active_scan_before =
2040 scan_symbol_segment(&manager.active_segment_path()).expect("scan active before crash");
2041 let active_count_before = active_scan_before.records.len();
2042
2043 let crash_partial = test_record(9, 99, 1024, 0xEE).to_bytes();
2044 let partial_len = crash_partial.len() / 2;
2045 let mut file = OpenOptions::new()
2046 .append(true)
2047 .open(manager.active_segment_path())
2048 .expect("open active segment for crash tail");
2049 file.write_all(&crash_partial[..partial_len])
2050 .expect("append torn tail");
2051 file.sync_data().expect("sync torn tail");
2052
2053 let active_scan_after =
2054 scan_symbol_segment(&manager.active_segment_path()).expect("scan active after crash");
2055 assert_eq!(active_scan_after.records.len(), active_count_before);
2056 assert!(active_scan_after.torn_tail);
2057 }
2058}