Skip to main content

apr_format/
validate.rs

1//! Structural validation — the Structure-category half of the validator split
2//! (issue #2231 Stage 1 spike, part b).
3//!
4//! These checks operate on **bytes only** (magic, version, header size, flags)
5//! and have NO dependency on `f32` tensor data. They are the cleanly-separable
6//! subset of `aprender-core/src/format/validation_impl.rs`'s `Category::Structure`
7//! checks (`check_magic`, `check_gguf_version`, `check_header_size`,
8//! `check_version`, `check_flags`, `AprHeader::parse`).
9//!
10//! The Physics-category checks (`check_no_nan`, `check_no_inf`, `validate_tensors`,
11//! `TensorStats::compute`) need `&[f32]` and are deliberately NOT moved here —
12//! they stay in `aprender-core` (decision: converter/physics stay in core).
13
14use crate::types::{Header, HEADER_SIZE, MAGIC};
15
16/// Outcome of a single structural check.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum StructureCheck {
19    /// The check passed.
20    Pass,
21    /// The check failed (with a static reason).
22    Fail,
23}
24
25impl StructureCheck {
26    /// True iff this check passed.
27    #[must_use]
28    pub fn is_pass(self) -> bool {
29        matches!(self, StructureCheck::Pass)
30    }
31}
32
33/// Check that the leading bytes carry the v1 `APRN` magic.
34#[must_use]
35pub fn check_magic(data: &[u8]) -> StructureCheck {
36    if data.len() >= 4 && data[0..4] == MAGIC {
37        StructureCheck::Pass
38    } else {
39        StructureCheck::Fail
40    }
41}
42
43/// Check that the file is at least one full header in size.
44#[must_use]
45pub fn check_header_size(data: &[u8]) -> StructureCheck {
46    if data.len() >= HEADER_SIZE {
47        StructureCheck::Pass
48    } else {
49        StructureCheck::Fail
50    }
51}
52
53/// Check that a parsed header carries a supported major version.
54#[must_use]
55pub fn check_version(header: &Header) -> StructureCheck {
56    if header.version.0 <= crate::types::FORMAT_VERSION.0 {
57        StructureCheck::Pass
58    } else {
59        StructureCheck::Fail
60    }
61}
62
63/// Check that the header's flags byte parses (reserved high bit clear).
64#[must_use]
65pub fn check_flags(header: &Header) -> StructureCheck {
66    // `Flags::from_bits` masks the reserved bit; a round-trip that drops bits
67    // signals a dirty reserved bit.
68    if header.flags.bits() & 0b1000_0000 == 0 {
69        StructureCheck::Pass
70    } else {
71        StructureCheck::Fail
72    }
73}
74
75/// Run all structural (byte-only) checks against a candidate `.apr` buffer.
76///
77/// Returns `true` iff the structure is well-formed. Performs NO `f32` / tensor
78/// physics validation — that is the responsibility of `aprender-core`.
79#[must_use]
80pub fn validate_structure(data: &[u8]) -> bool {
81    if !check_magic(data).is_pass() || !check_header_size(data).is_pass() {
82        return false;
83    }
84    match Header::from_bytes(&data[..HEADER_SIZE]) {
85        Ok(header) => check_version(&header).is_pass() && check_flags(&header).is_pass(),
86        Err(_) => false,
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use crate::types::{Header, ModelType};
94
95    fn good_header_bytes() -> Vec<u8> {
96        let mut v = Header::new(ModelType::LinearRegression).to_bytes().to_vec();
97        v.resize(HEADER_SIZE, 0);
98        v
99    }
100
101    #[test]
102    fn test_check_magic_pass_and_fail() {
103        assert!(check_magic(&good_header_bytes()).is_pass());
104        assert!(!check_magic(b"GGUF").is_pass());
105    }
106
107    #[test]
108    fn test_validate_structure_round_trip() {
109        assert!(validate_structure(&good_header_bytes()));
110        let mut bad = good_header_bytes();
111        bad[0] = 0x00; // corrupt magic
112        assert!(!validate_structure(&bad));
113    }
114}