Skip to main content

flatr_core/
verify.rs

1// flatr/flatr_core/src/verify.rs
2use crate::read::ReadAt;
3
4// ── Error ─────────────────────────────────────────────────────────────────────
5
6#[derive(Debug, Clone, PartialEq)]
7pub enum VerifyError {
8    OutOfBounds    { context: &'static str, offset: usize, needed: usize, buf_len: usize },
9    BadOffset      { at: usize },
10    VTableTooSmall { vtable_size: usize },
11    FieldOutOfBounds { field_idx: usize, voff: usize, field_size: usize, object_size: usize },
12    InvalidUtf8    { at: usize },
13    VectorOverflow { at: usize, len: usize, elem_size: usize },
14    DepthLimitExceeded,
15}
16
17/// Unit on success — vtable positions are accumulated into the caller-supplied
18/// `out: &mut Vec<usize>` so the entire traversal shares one allocation.
19pub type VerifyResult = Result<(), VerifyError>;
20
21pub const MAX_DEPTH: usize = 64;
22
23// ── Trait ─────────────────────────────────────────────────────────────────────
24
25pub trait Verify {
26    const INLINE_SIZE: usize;
27
28    /// Verify the value at `offset`.  Every vtable position reached during
29    /// traversal is pushed onto `out`; duplicates are expected and deduplicated
30    /// by the caller once at the end.
31    fn verify_at(buf: &[u8], offset: usize, depth: usize, out: &mut Vec<usize>) -> VerifyResult;
32}
33
34// ── Entry point ───────────────────────────────────────────────────────────────
35
36/// Verify a finished buffer and return the unique vtable positions found.
37pub fn verify_root<T: Verify>(buf: &[u8]) -> Result<Vec<usize>, VerifyError> {
38    check_bounds(buf, 0, 4, "root offset")?;
39    let root_offset = u32::read_at(buf, 0) as usize;
40    let mut positions = Vec::new();
41    T::verify_at(buf, root_offset, MAX_DEPTH, &mut positions)?;
42    Ok(positions)
43}
44
45// ── Public helpers ────────────────────────────────────────────────────────────
46
47#[inline]
48pub fn verify_table_header(
49    buf: &[u8], t_pos: usize, depth: usize,
50) -> Result<(usize, usize, usize), VerifyError> {
51    if depth == 0 { return Err(VerifyError::DepthLimitExceeded); }
52
53    check_bounds(buf, t_pos, 4, "vtable pointer")?;
54    let jump  = i32::read_at(buf, t_pos);
55    let v_pos = checked_apply_i32(t_pos, jump)
56        .filter(|&v| v < buf.len())
57        .ok_or(VerifyError::BadOffset { at: t_pos })?;
58
59    check_bounds(buf, v_pos, 4, "vtable header")?;
60    let vtable_size = u16::read_at(buf, v_pos)     as usize;
61    let object_size = u16::read_at(buf, v_pos + 2) as usize;
62
63    if vtable_size < 4 { return Err(VerifyError::VTableTooSmall { vtable_size }); }
64    check_bounds(buf, v_pos, vtable_size, "vtable body")?;
65    check_bounds(buf, t_pos, object_size, "table object")?;
66
67    Ok((v_pos, vtable_size, object_size))
68}
69
70#[inline]
71pub fn verify_vtable_field(
72    buf: &[u8], v_pos: usize, vtable_size: usize,
73    t_pos: usize, object_size: usize,
74    field_idx: usize, field_size: usize,
75) -> Result<Option<usize>, VerifyError> {
76    let entry_off = 4 + field_idx * 2;
77    if entry_off + 2 > vtable_size { return Ok(None); }
78    let voff = u16::read_at(buf, v_pos + entry_off) as usize;
79    if voff == 0 { return Ok(None); }
80    if voff.saturating_add(field_size) > object_size {
81        return Err(VerifyError::FieldOutOfBounds { field_idx, voff, field_size, object_size });
82    }
83    Ok(Some(t_pos + voff))
84}
85
86#[inline]
87pub fn verify_string_field(buf: &[u8], field_pos: usize) -> VerifyResult {
88    check_bounds(buf, field_pos, 4, "string forward-offset")?;
89    let hdr_pos = field_pos.saturating_add(u32::read_at(buf, field_pos) as usize);
90    check_bounds(buf, hdr_pos, 4, "string length prefix")?;
91    let len = u32::read_at(buf, hdr_pos) as usize;
92    check_bounds(buf, hdr_pos + 4, len, "string bytes")?;
93    std::str::from_utf8(&buf[hdr_pos + 4..hdr_pos + 4 + len])
94        .map_err(|_| VerifyError::InvalidUtf8 { at: hdr_pos + 4 })?;
95    Ok(())
96}
97#[inline]
98pub fn verify_file_field(buf: &[u8], field_pos: usize) -> VerifyResult {
99    check_bounds(buf, field_pos, 4, "File forward-offset")?;
100    let hdr_pos = field_pos.saturating_add(u32::read_at(buf, field_pos) as usize);
101    check_bounds(buf, hdr_pos, 4, "File length prefix")?;
102    let len = u32::read_at(buf, hdr_pos) as usize;
103    check_bounds(buf, hdr_pos + 4, len, "File bytes")?;
104    std::str::from_utf8(&buf[hdr_pos + 4..hdr_pos + 4 + len])
105        .map_err(|_| VerifyError::InvalidUtf8 { at: hdr_pos + 4 })?;
106    Ok(())
107}
108#[inline]
109pub fn verify_scalar_array(buf: &[u8], field_pos: usize, elem_size: usize) -> VerifyResult {
110    check_bounds(buf, field_pos, 4, "array forward-offset")?;
111    let hdr_pos = field_pos.saturating_add(u32::read_at(buf, field_pos) as usize);
112    check_bounds(buf, hdr_pos, 4, "array length prefix")?;
113    let len = u32::read_at(buf, hdr_pos) as usize;
114    let total = len.checked_mul(elem_size)
115        .ok_or(VerifyError::VectorOverflow { at: hdr_pos, len, elem_size })?;
116    check_bounds(buf, hdr_pos + 4, total, "array data")
117}
118
119#[inline]
120pub fn verify_string_array(buf: &[u8], field_pos: usize) -> VerifyResult {
121    check_bounds(buf, field_pos, 4, "string-array forward-offset")?;
122    let hdr_pos = field_pos.saturating_add(u32::read_at(buf, field_pos) as usize);
123    check_bounds(buf, hdr_pos, 4, "string-array length prefix")?;
124    let len = u32::read_at(buf, hdr_pos) as usize;
125    let total = len.checked_mul(4)
126        .ok_or(VerifyError::VectorOverflow { at: hdr_pos, len, elem_size: 4 })?;
127    check_bounds(buf, hdr_pos + 4, total, "string-array offset table")?;
128    for i in 0..len {
129        verify_string_field(buf, hdr_pos + 4 + i * 4)?;
130    }
131    Ok(())
132}
133#[inline]
134pub fn verify_file_array(buf: &[u8], field_pos: usize) -> VerifyResult {
135    check_bounds(buf, field_pos, 4, "file-array forward-offset")?;
136    let hdr_pos = field_pos.saturating_add(u32::read_at(buf, field_pos) as usize);
137    check_bounds(buf, hdr_pos, 4, "file-array length prefix")?;
138    let len = u32::read_at(buf, hdr_pos) as usize;
139    let total = len.checked_mul(4)
140        .ok_or(VerifyError::VectorOverflow { at: hdr_pos, len, elem_size: 4 })?;
141    check_bounds(buf, hdr_pos + 4, total, "file-array offset table")?;
142    for i in 0..len {
143        verify_file_field(buf, hdr_pos + 4 + i * 4)?;
144    }
145    Ok(())
146}
147/// `out` receives one position per table element — duplicates are expected
148/// when elements share a vtable (the common case) and are deduplicated later.
149#[inline]
150pub fn verify_table_array<T: Verify>(
151    buf: &[u8], field_pos: usize, depth: usize, out: &mut Vec<usize>,
152) -> VerifyResult {
153    check_bounds(buf, field_pos, 4, "table-array forward-offset")?;
154    let hdr_pos = field_pos.saturating_add(u32::read_at(buf, field_pos) as usize);
155    check_bounds(buf, hdr_pos, 4, "table-array length prefix")?;
156    let len = u32::read_at(buf, hdr_pos) as usize;
157    let total = len.checked_mul(4)
158        .ok_or(VerifyError::VectorOverflow { at: hdr_pos, len, elem_size: 4 })?;
159    check_bounds(buf, hdr_pos + 4, total, "table-array offset table")?;
160    for i in 0..len {
161        let ep        = hdr_pos + 4 + i * 4;
162        let table_pos = ep.saturating_add(u32::read_at(buf, ep) as usize);
163        T::verify_at(buf, table_pos, depth, out)?;
164    }
165    Ok(())
166}
167
168#[inline]
169pub fn verify_table_field<T: Verify>(
170    buf: &[u8], field_pos: usize, depth: usize, out: &mut Vec<usize>,
171) -> VerifyResult {
172    check_bounds(buf, field_pos, 4, "table forward-offset")?;
173    let table_pos = field_pos.saturating_add(u32::read_at(buf, field_pos) as usize);
174    T::verify_at(buf, table_pos, depth, out)
175}
176
177// ── Scalar Verify impls ───────────────────────────────────────────────────────
178
179macro_rules! impl_verify_scalar {
180    ($($t:ty),*) => {$(
181        impl Verify for $t {
182            const INLINE_SIZE: usize = ::std::mem::size_of::<Self>();
183            #[inline(always)]
184            fn verify_at(buf: &[u8], offset: usize, _depth: usize, _out: &mut Vec<usize>) -> VerifyResult {
185                check_bounds(buf, offset, ::std::mem::size_of::<Self>(), "scalar")
186            }
187        }
188    )*};
189}
190impl_verify_scalar!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, f64, bool);
191
192// verify.rs — replace both impls
193
194impl Verify for &str {
195    const INLINE_SIZE: usize = 4;
196    #[inline]
197    fn verify_at(buf: &[u8], offset: usize, _depth: usize, _out: &mut Vec<usize>) -> VerifyResult {
198        // `offset` is the direct string header (length prefix), matching ReadAt::read_at.
199        // Callers in the table path use verify_string_field() directly and never reach here.
200        check_bounds(buf, offset, 4, "string length prefix")?;
201        let len = u32::read_at(buf, offset) as usize;
202        check_bounds(buf, offset + 4, len, "string bytes")?;
203        std::str::from_utf8(&buf[offset + 4..offset + 4 + len])
204            .map_err(|_| VerifyError::InvalidUtf8 { at: offset + 4 })?;
205        Ok(())
206    }
207}
208
209impl Verify for String {
210    const INLINE_SIZE: usize = 4;
211    #[inline]
212    fn verify_at(buf: &[u8], offset: usize, _depth: usize, _out: &mut Vec<usize>) -> VerifyResult {
213        check_bounds(buf, offset, 4, "string length prefix")?;
214        let len = u32::read_at(buf, offset) as usize;
215        check_bounds(buf, offset + 4, len, "string bytes")?;
216        std::str::from_utf8(&buf[offset + 4..offset + 4 + len])
217            .map_err(|_| VerifyError::InvalidUtf8 { at: offset + 4 })?;
218        Ok(())
219    }
220}
221
222// ── Internal primitives ───────────────────────────────────────────────────────
223
224#[inline(always)]
225pub fn check_bounds(buf: &[u8], offset: usize, needed: usize, ctx: &'static str) -> VerifyResult {
226    if offset.saturating_add(needed) > buf.len() {
227        Err(VerifyError::OutOfBounds { context: ctx, offset, needed, buf_len: buf.len() })
228    } else {
229        Ok(())
230    }
231}
232
233#[inline(always)]
234fn checked_apply_i32(base: usize, delta: i32) -> Option<usize> {
235    if delta >= 0 { base.checked_sub(delta as usize) }
236    else          { base.checked_add(delta.unsigned_abs() as usize) }
237}
238