Skip to main content

faf_fafb/
section.rs

1//! FAFB Section Entry and Section Table
2//!
3//! The section table is located at the end of the file (at section_table_offset).
4//! Each entry is 16 bytes and describes one section's location and metadata.
5//!
6//! ## Section Entry Layout (16 bytes)
7//!
8//! ```text
9//! Offset  Size  Field
10//! ------  ----  -----
11//! 0       1     section_name_index (string table index)
12//! 1       1     priority
13//! 2       4     offset
14//! 6       4     length
15//! 10      2     token_count
16//! 12      4     flags (bits 0-1 = classification, bits 2+ section-specific)
17//! ------  ----
18//! Total: 16 bytes
19//! ```
20
21use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
22use std::io::{Read, Write};
23
24use super::canon::{CLASSIFICATION_MASK, ChunkClassification};
25use super::error::{FafbError, FafbResult};
26use super::priority::Priority;
27
28/// Size of a single section entry in bytes
29pub const SECTION_ENTRY_SIZE: usize = 16;
30
31/// A single section entry in the section table
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct SectionEntry {
34    /// String-table index of this section's name
35    pub name_index: u8,
36    /// Truncation priority (0-255, higher = more important)
37    pub priority: Priority,
38    /// Byte offset to section data (from start of file)
39    pub offset: u32,
40    /// Section data length in bytes
41    pub length: u32,
42    /// Pre-computed token count estimate
43    pub token_count: u16,
44    /// Section flags: bits 0-1 = classification, bits 2+ section-specific
45    pub flags: u32,
46}
47
48impl SectionEntry {
49    /// Create a new section entry. Callers normally set the real priority via
50    /// `with_priority` (from the canonical chunk table); the default is medium.
51    pub fn new(name_index: u8, offset: u32, length: u32) -> Self {
52        Self {
53            name_index,
54            priority: Priority::medium(),
55            offset,
56            length,
57            token_count: estimate_tokens(length),
58            flags: 0,
59        }
60    }
61
62    /// Create with explicit priority
63    pub fn with_priority(mut self, priority: Priority) -> Self {
64        self.priority = priority;
65        self
66    }
67
68    /// Create with explicit token count
69    pub fn with_token_count(mut self, count: u16) -> Self {
70        self.token_count = count;
71        self
72    }
73
74    /// Create with section-specific flags
75    pub fn with_flags(mut self, flags: u32) -> Self {
76        self.flags = flags;
77        self
78    }
79
80    /// Set classification in the low 2 bits of flags
81    pub fn with_classification(mut self, classification: ChunkClassification) -> Self {
82        // Clear low 2 bits, then set classification
83        self.flags = (self.flags & !CLASSIFICATION_MASK) | classification.bits();
84        self
85    }
86
87    /// Get the classification from the low 2 bits of flags
88    pub fn classification(&self) -> ChunkClassification {
89        ChunkClassification::from_bits(self.flags)
90    }
91
92    /// Get section-specific flags (bits 2+, excluding classification)
93    pub fn section_flags(&self) -> u32 {
94        self.flags & !CLASSIFICATION_MASK
95    }
96
97    /// Write entry to a byte buffer
98    pub fn write<W: Write>(&self, writer: &mut W) -> FafbResult<()> {
99        writer.write_u8(self.name_index)?;
100        writer.write_u8(self.priority.value())?;
101        writer.write_u32::<LittleEndian>(self.offset)?;
102        writer.write_u32::<LittleEndian>(self.length)?;
103        writer.write_u16::<LittleEndian>(self.token_count)?;
104        writer.write_u32::<LittleEndian>(self.flags)?;
105        Ok(())
106    }
107
108    /// Write entry to a new `Vec<u8>`
109    pub fn to_bytes(&self) -> FafbResult<Vec<u8>> {
110        let mut buf = Vec::with_capacity(SECTION_ENTRY_SIZE);
111        self.write(&mut buf)?;
112        Ok(buf)
113    }
114
115    /// Read entry from a byte buffer
116    pub fn read<R: Read>(reader: &mut R) -> FafbResult<Self> {
117        let name_index = reader.read_u8()?;
118        let priority = Priority::from(reader.read_u8()?);
119        let offset = reader.read_u32::<LittleEndian>()?;
120        let length = reader.read_u32::<LittleEndian>()?;
121        let token_count = reader.read_u16::<LittleEndian>()?;
122        let flags = reader.read_u32::<LittleEndian>()?;
123
124        Ok(Self {
125            name_index,
126            priority,
127            offset,
128            length,
129            token_count,
130            flags,
131        })
132    }
133
134    /// Read entry from a byte slice
135    pub fn from_bytes(data: &[u8]) -> FafbResult<Self> {
136        if data.len() < SECTION_ENTRY_SIZE {
137            return Err(FafbError::FileTooSmall {
138                expected: SECTION_ENTRY_SIZE,
139                actual: data.len(),
140            });
141        }
142        let mut cursor = std::io::Cursor::new(data);
143        Self::read(&mut cursor)
144    }
145
146    /// Check if this section's data range is valid within a file
147    pub fn validate_bounds(&self, file_size: u32) -> FafbResult<()> {
148        // WHY: checked_add prevents integer overflow attacks where offset + length wraps
149        // around u32::MAX to produce a small "end" that passes the bounds check
150        // Example attack: offset=0xFFFFFF00, length=0x200 would wrap to 0x100
151        let end =
152            self.offset
153                .checked_add(self.length)
154                .ok_or(FafbError::InvalidSectionTableOffset {
155                    offset: self.offset,
156                    file_size,
157                })?;
158
159        // WHY: Bounds check prevents reading past file end - memory safety
160        if end > file_size {
161            return Err(FafbError::InvalidSectionTableOffset {
162                offset: self.offset,
163                file_size,
164            });
165        }
166
167        Ok(())
168    }
169}
170
171/// The section table containing all section entries
172#[derive(Debug, Clone, Default)]
173pub struct SectionTable {
174    entries: Vec<SectionEntry>,
175}
176
177impl SectionTable {
178    /// Create an empty section table
179    pub fn new() -> Self {
180        Self {
181            entries: Vec::new(),
182        }
183    }
184
185    /// Create with pre-allocated capacity
186    pub fn with_capacity(capacity: usize) -> Self {
187        Self {
188            entries: Vec::with_capacity(capacity),
189        }
190    }
191
192    /// Add a section entry
193    pub fn push(&mut self, entry: SectionEntry) {
194        self.entries.push(entry);
195    }
196
197    /// Get number of sections
198    pub fn len(&self) -> usize {
199        self.entries.len()
200    }
201
202    /// Check if table is empty
203    pub fn is_empty(&self) -> bool {
204        self.entries.is_empty()
205    }
206
207    /// Get entry by index
208    pub fn get(&self, index: usize) -> Option<&SectionEntry> {
209        self.entries.get(index)
210    }
211
212    /// Get entry by its string-table name index
213    pub fn get_by_name_index(&self, name_index: u8) -> Option<&SectionEntry> {
214        self.entries.iter().find(|e| e.name_index == name_index)
215    }
216
217    /// Get all entries
218    pub fn entries(&self) -> &[SectionEntry] {
219        &self.entries
220    }
221
222    /// Get entries sorted by priority (highest first)
223    pub fn entries_by_priority(&self) -> Vec<&SectionEntry> {
224        let mut sorted: Vec<_> = self.entries.iter().collect();
225        sorted.sort_by_key(|e| std::cmp::Reverse(e.priority));
226        sorted
227    }
228
229    /// Get entries that fit within a token budget by dropping whole priority
230    /// tiers from the tail (64, then 128, then 150–200). Original table order
231    /// is preserved. Critical (255) entries always remain.
232    ///
233    /// This is prefix truncation, not priority-first greedy: a hole in
234    /// canonical order is not a rendering of this Content ID.
235    pub fn entries_within_budget(&self, budget: u16) -> Vec<&SectionEntry> {
236        let budget = budget as u32;
237        let mut keep: Vec<&SectionEntry> = self.entries.iter().collect();
238        for tier in [TierDrop::P64, TierDrop::P128, TierDrop::P150_200] {
239            if token_sum(&keep) <= budget {
240                break;
241            }
242            keep.retain(|e| e.priority.is_critical() || !tier.matches(e.priority.value()));
243        }
244        keep
245    }
246
247    /// Calculate total token count
248    pub fn total_tokens(&self) -> u32 {
249        self.entries.iter().map(|e| e.token_count as u32).sum()
250    }
251
252    /// Calculate total size in bytes (for section table only)
253    pub fn table_size(&self) -> usize {
254        self.entries.len() * SECTION_ENTRY_SIZE
255    }
256
257    /// Write section table to a byte buffer
258    pub fn write<W: Write>(&self, writer: &mut W) -> FafbResult<()> {
259        for entry in &self.entries {
260            entry.write(writer)?;
261        }
262        Ok(())
263    }
264
265    /// Write section table to a new `Vec<u8>`
266    pub fn to_bytes(&self) -> FafbResult<Vec<u8>> {
267        let mut buf = Vec::with_capacity(self.table_size());
268        self.write(&mut buf)?;
269        Ok(buf)
270    }
271
272    /// Read section table from a byte buffer
273    pub fn read<R: Read>(reader: &mut R, count: usize) -> FafbResult<Self> {
274        let mut entries = Vec::with_capacity(count);
275        for _ in 0..count {
276            entries.push(SectionEntry::read(reader)?);
277        }
278        Ok(Self { entries })
279    }
280
281    /// Read section table from a byte slice
282    pub fn from_bytes(data: &[u8], count: usize) -> FafbResult<Self> {
283        let expected_size = count * SECTION_ENTRY_SIZE;
284        if data.len() < expected_size {
285            return Err(FafbError::FileTooSmall {
286                expected: expected_size,
287                actual: data.len(),
288            });
289        }
290        let mut cursor = std::io::Cursor::new(data);
291        Self::read(&mut cursor, count)
292    }
293
294    /// Validate all entries against file size
295    pub fn validate_bounds(&self, file_size: u32) -> FafbResult<()> {
296        for entry in &self.entries {
297            entry.validate_bounds(file_size)?;
298        }
299        Ok(())
300    }
301}
302
303impl IntoIterator for SectionTable {
304    type Item = SectionEntry;
305    type IntoIter = std::vec::IntoIter<SectionEntry>;
306
307    fn into_iter(self) -> Self::IntoIter {
308        self.entries.into_iter()
309    }
310}
311
312impl<'a> IntoIterator for &'a SectionTable {
313    type Item = &'a SectionEntry;
314    type IntoIter = std::slice::Iter<'a, SectionEntry>;
315
316    fn into_iter(self) -> Self::IntoIter {
317        self.entries.iter()
318    }
319}
320
321fn token_sum(entries: &[&SectionEntry]) -> u32 {
322    entries.iter().map(|e| e.token_count as u32).sum()
323}
324
325#[derive(Clone, Copy)]
326enum TierDrop {
327    P64,
328    P128,
329    P150_200,
330}
331
332impl TierDrop {
333    fn matches(self, priority: u8) -> bool {
334        match self {
335            Self::P64 => priority == 64,
336            Self::P128 => priority == 128,
337            Self::P150_200 => (150..=200).contains(&priority),
338        }
339    }
340}
341
342/// Estimate token count from byte length
343/// Rough estimate: ~4 bytes per token for English text
344fn estimate_tokens(byte_length: u32) -> u16 {
345    // WHY: 4 bytes/token is the empirical average for English prose in BPE tokenizers
346    // Code tends to be slightly higher (3-3.5), YAML slightly lower (4-5)
347    // WHY: u16::MAX cap prevents overflow - sections >256KB truncate to max tokens
348    // This is acceptable because such large sections will likely be truncated anyway
349    std::cmp::min(byte_length / 4, u16::MAX as u32) as u16
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355
356    #[test]
357    fn test_section_entry_size() {
358        let entry = SectionEntry::new(0, 32, 100);
359        let bytes = entry.to_bytes().unwrap();
360        assert_eq!(bytes.len(), SECTION_ENTRY_SIZE);
361        assert_eq!(bytes.len(), 16);
362    }
363
364    #[test]
365    fn test_section_entry_roundtrip() {
366        let original = SectionEntry::new(5, 64, 256)
367            .with_priority(Priority::high())
368            .with_token_count(100)
369            .with_flags(0xDEADBEEF);
370
371        let bytes = original.to_bytes().unwrap();
372        let recovered = SectionEntry::from_bytes(&bytes).unwrap();
373
374        assert_eq!(original.name_index, recovered.name_index);
375        assert_eq!(original.priority, recovered.priority);
376        assert_eq!(original.offset, recovered.offset);
377        assert_eq!(original.length, recovered.length);
378        assert_eq!(original.token_count, recovered.token_count);
379        assert_eq!(original.flags, recovered.flags);
380    }
381
382    #[test]
383    fn test_token_estimation() {
384        assert_eq!(estimate_tokens(0), 0);
385        assert_eq!(estimate_tokens(4), 1);
386        assert_eq!(estimate_tokens(100), 25);
387        assert_eq!(estimate_tokens(1000), 250);
388    }
389
390    #[test]
391    fn test_token_estimation_cap() {
392        // Should cap at u16::MAX
393        let huge = estimate_tokens(u32::MAX);
394        assert_eq!(huge, u16::MAX);
395    }
396
397    #[test]
398    fn test_section_table_empty() {
399        let table = SectionTable::new();
400        assert!(table.is_empty());
401        assert_eq!(table.len(), 0);
402        assert_eq!(table.table_size(), 0);
403    }
404
405    #[test]
406    fn test_section_table_push() {
407        let mut table = SectionTable::new();
408        table.push(SectionEntry::new(0, 32, 100));
409        table.push(SectionEntry::new(1, 132, 200));
410
411        assert_eq!(table.len(), 2);
412        assert_eq!(table.table_size(), 32);
413    }
414
415    #[test]
416    fn test_section_table_roundtrip() {
417        let mut original = SectionTable::new();
418        original.push(SectionEntry::new(0, 32, 100));
419        original.push(SectionEntry::new(1, 132, 200));
420        original.push(SectionEntry::new(2, 332, 500));
421
422        let bytes = original.to_bytes().unwrap();
423        assert_eq!(bytes.len(), 48); // 3 × 16 bytes
424
425        let recovered = SectionTable::from_bytes(&bytes, 3).unwrap();
426        assert_eq!(recovered.len(), 3);
427
428        for (orig, recv) in original.entries().iter().zip(recovered.entries().iter()) {
429            assert_eq!(orig.name_index, recv.name_index);
430            assert_eq!(orig.offset, recv.offset);
431            assert_eq!(orig.length, recv.length);
432        }
433    }
434
435    #[test]
436    fn test_section_table_get_by_name_index() {
437        let mut table = SectionTable::new();
438        table.push(SectionEntry::new(0, 32, 100));
439        table.push(SectionEntry::new(1, 132, 200));
440
441        let first = table.get_by_name_index(0);
442        assert!(first.is_some());
443        assert_eq!(first.unwrap().offset, 32);
444
445        assert!(table.get_by_name_index(9).is_none());
446    }
447
448    #[test]
449    fn test_section_table_priority_sorting() {
450        let mut table = SectionTable::new();
451        table.push(SectionEntry::new(2, 0, 100).with_priority(Priority::low()));
452        table.push(SectionEntry::new(0, 0, 100).with_priority(Priority::critical()));
453        table.push(SectionEntry::new(1, 0, 100).with_priority(Priority::high()));
454
455        let sorted = table.entries_by_priority();
456        assert_eq!(sorted[0].name_index, 0); // Critical first
457        assert_eq!(sorted[1].name_index, 1); // High second
458        assert_eq!(sorted[2].name_index, 2); // Low last
459    }
460
461    #[test]
462    fn test_section_table_budget() {
463        let mut table = SectionTable::new();
464        table.push(
465            SectionEntry::new(0, 0, 100)
466                .with_priority(Priority::critical())
467                .with_token_count(50),
468        );
469        table.push(
470            SectionEntry::new(1, 0, 200)
471                .with_priority(Priority::high())
472                .with_token_count(100),
473        );
474        table.push(
475            SectionEntry::new(2, 0, 1000)
476                .with_priority(Priority::low())
477                .with_token_count(500),
478        );
479
480        // Budget of 200: drop the 64-tier (500 tokens); critical + high remain.
481        let within_budget = table.entries_within_budget(200);
482        assert_eq!(within_budget.len(), 2);
483
484        // The critical entry must always be included
485        assert!(within_budget.iter().any(|e| e.name_index == 0));
486        // Original table order, not priority-sorted.
487        assert_eq!(within_budget[0].name_index, 0);
488        assert_eq!(within_budget[1].name_index, 1);
489    }
490
491    #[test]
492    fn test_budget_does_not_punch_holes() {
493        // Canonical-ish order: about(150), stack(200), architecture(128).
494        // Greedy-by-priority with budget 90 would keep stack+architecture and
495        // drop about — a hole. Tier-tail drops whole bands from the tail.
496        let mut table = SectionTable::new();
497        table.push(
498            SectionEntry::new(0, 0, 80)
499                .with_priority(Priority::new(150))
500                .with_token_count(80),
501        );
502        table.push(
503            SectionEntry::new(1, 0, 40)
504                .with_priority(Priority::high())
505                .with_token_count(40),
506        );
507        table.push(
508            SectionEntry::new(2, 0, 40)
509                .with_priority(Priority::medium())
510                .with_token_count(40),
511        );
512
513        let kept = table.entries_within_budget(90);
514        // Drop 128, still over; drop 150–200 → nothing left. Never stack
515        // without the earlier `about` row.
516        assert!(kept.is_empty());
517    }
518
519    #[test]
520    fn test_section_table_total_tokens() {
521        let mut table = SectionTable::new();
522        table.push(SectionEntry::new(0, 0, 100).with_token_count(50));
523        table.push(SectionEntry::new(1, 0, 200).with_token_count(100));
524
525        assert_eq!(table.total_tokens(), 150);
526    }
527
528    #[test]
529    fn test_section_entry_validate_bounds() {
530        let entry = SectionEntry::new(0, 100, 50);
531
532        // Valid: offset 100, length 50, file size 200
533        assert!(entry.validate_bounds(200).is_ok());
534
535        // Invalid: offset 100, length 50 = end 150, but file only 100
536        assert!(entry.validate_bounds(100).is_err());
537    }
538
539    #[test]
540    fn test_unknown_name_index_preserved() {
541        // The IFF rule: a reader must carry unknown names through untouched
542        let entry = SectionEntry::new(0x99, 0, 100)
543            .with_priority(Priority::medium())
544            .with_token_count(25);
545
546        let bytes = entry.to_bytes().unwrap();
547        let recovered = SectionEntry::from_bytes(&bytes).unwrap();
548
549        assert_eq!(recovered.name_index, 0x99);
550    }
551}