Skip to main content

vole_document/encode/
mod.rs

1//! Encoding: candidate portfolio, complete-cost court, and decode-before-commit.
2
3pub mod candidates;
4pub mod court;
5
6/// Encoder-only search governance, compiled only under the non-default,
7/// dependency-free `dsfb-search` feature. Encoder-only: the decode path never
8/// imports it (see `docs/adr/0022-encoder-only-search-governance.md`).
9#[cfg(feature = "dsfb-search")]
10pub mod governor;
11
12use crate::accounting::CostBreakdown;
13use crate::encode::candidates::CandidateKind;
14use crate::error::{Error, Result};
15use crate::integrity::{sha256, to_hex};
16use crate::limits::Limits;
17
18/// A report describing encode-time decisions and physical attribution.
19#[derive(Debug, Clone)]
20pub struct EncodeReport {
21    /// Winning candidate family.
22    pub kind: CandidateKind,
23    /// Source length in bytes.
24    pub source_len: u64,
25    /// Serialized `.voldoc` length in bytes.
26    pub encoded_len: u64,
27    /// Physical byte attribution of the serialized descriptor.
28    pub cost: CostBreakdown,
29    /// Lower-case hex SHA-256 of the source.
30    pub sha256_hex: String,
31    /// Number of candidates evaluated.
32    pub candidates_evaluated: u32,
33    /// Reconstruction work (DRA instruction count) of the winner.
34    pub graph_ops: usize,
35}
36
37impl EncodeReport {
38    /// Source bytes divided by encoded bytes (0.0 when the source is empty).
39    pub fn compression_ratio(&self) -> f64 {
40        if self.encoded_len == 0 {
41            return 0.0;
42        }
43        self.source_len as f64 / self.encoded_len as f64
44    }
45}
46
47/// Encode `input` exactly, returning the serialized descriptor and a report.
48///
49/// Every returned byte sequence has already been round-tripped through the
50/// normative decoder and byte-compared against `input`.
51pub fn encode(input: &[u8], limits: Limits) -> Result<(Vec<u8>, EncodeReport)> {
52    encode_with(input, limits, None)
53}
54
55/// Encode `input` exactly, optionally forcing a single candidate family.
56///
57/// With `force == None` this is the ordinary complete-cost court over every
58/// proposed candidate. With `force == Some(kind)` the candidate set is first
59/// filtered to proposals of exactly that kind, then the *same* court runs over
60/// the filtered set: the forced lane is still serialized, decoded, and
61/// byte-compared before it may be returned, so forcing never weakens exactness
62/// and determinism.
63///
64/// Returns [`Error::usage`] when `force` names a kind that this input does not
65/// propose (for example PDF_CHANNELS on a non-PDF, or BYTE_RANS on empty input),
66/// rather than silently substituting another lane. That failure is the honest
67/// signal that the mechanism does not apply.
68pub fn encode_with(
69    input: &[u8],
70    limits: Limits,
71    force: Option<CandidateKind>,
72) -> Result<(Vec<u8>, EncodeReport)> {
73    let mut court = court::Court::new();
74    if let Some(kind) = force {
75        // Forced lane: build only that family (no portfolio), so e.g. `--force raw`
76        // for a runtime ingest costs one candidate, not the whole search.
77        match candidates::propose_forced(input, limits, kind)? {
78            Some(c) => court.offer(input, c, limits)?,
79            None => {
80                return Err(Error::usage(format!(
81                    "candidate {kind:?} is not proposed for this input"
82                )));
83            }
84        }
85    } else {
86        // Stream the portfolio through the court one candidate at a time: only the
87        // current best (plus the candidate being priced) stays resident, bounding
88        // encoder memory even when every candidate holds a source-sized payload.
89        candidates::propose_each(input, limits, |c| court.offer(input, c, limits))?;
90    }
91    let result = court.finish()?;
92    let report = EncodeReport {
93        kind: result.kind,
94        source_len: input.len() as u64,
95        encoded_len: result.bytes.len() as u64,
96        cost: result.cost,
97        sha256_hex: to_hex(&sha256(input)),
98        candidates_evaluated: result.candidates_evaluated,
99        graph_ops: result.graph_ops,
100    };
101    Ok((result.bytes, report))
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn encode_decode_identity() {
110        let input: Vec<u8> = (0..=255u8).cycle().take(10_000).collect();
111        let (bytes, report) = encode(&input, Limits::DEFAULT).unwrap();
112        assert_eq!(report.source_len, input.len() as u64);
113        assert_eq!(report.encoded_len, bytes.len() as u64);
114        let (out, _) = crate::materialize::decode_to_bytes(&bytes, Limits::DEFAULT).unwrap();
115        assert_eq!(out, input);
116    }
117
118    #[test]
119    fn deterministic_output() {
120        let input = b"determinism check".to_vec();
121        let (a, _) = encode(&input, Limits::DEFAULT).unwrap();
122        let (b, _) = encode(&input, Limits::DEFAULT).unwrap();
123        assert_eq!(a, b);
124    }
125
126    #[test]
127    fn encode_with_none_matches_encode() {
128        let input = b"the court must not care how it was invoked".repeat(40);
129        let (auto, auto_report) = encode(&input, Limits::DEFAULT).unwrap();
130        let (explicit, explicit_report) = encode_with(&input, Limits::DEFAULT, None).unwrap();
131        assert_eq!(auto, explicit, "None must equal the unforced court");
132        assert_eq!(auto_report.kind, explicit_report.kind);
133        assert_eq!(auto_report.encoded_len, explicit_report.encoded_len);
134        assert_eq!(
135            auto_report.candidates_evaluated,
136            explicit_report.candidates_evaluated
137        );
138    }
139
140    #[test]
141    fn encode_with_force_raw_is_exact() {
142        // RAW normally loses to RLE on this input, so forcing RAW proves the
143        // filter really overrides the court's own choice.
144        let input = vec![0u8; 8192];
145        let (_, auto_report) = encode(&input, Limits::DEFAULT).unwrap();
146        assert_ne!(auto_report.kind, CandidateKind::Raw);
147
148        let (bytes, report) =
149            encode_with(&input, Limits::DEFAULT, Some(CandidateKind::Raw)).unwrap();
150        assert_eq!(report.kind, CandidateKind::Raw);
151        assert_eq!(report.candidates_evaluated, 1);
152        let (out, _) = crate::materialize::decode_to_bytes(&bytes, Limits::DEFAULT).unwrap();
153        assert_eq!(out, input);
154    }
155
156    #[cfg(feature = "rans")]
157    #[test]
158    fn encode_with_force_byte_rans_is_exact() {
159        let input = b"The quick brown fox jumps over the lazy dog. ".repeat(800);
160        let (bytes, report) =
161            encode_with(&input, Limits::DEFAULT, Some(CandidateKind::ByteRans)).unwrap();
162        assert_eq!(report.kind, CandidateKind::ByteRans);
163        assert_eq!(report.candidates_evaluated, 1);
164        let (out, _) = crate::materialize::decode_to_bytes(&bytes, Limits::DEFAULT).unwrap();
165        assert_eq!(out, input);
166    }
167
168    #[test]
169    fn encode_with_unproposed_kind_errors() {
170        // A non-PDF cannot propose PDF_CHANNELS, so forcing it must fail with a
171        // typed Usage error rather than silently falling back to another lane.
172        let input = b"definitely not a pdf, just text".to_vec();
173        let err = encode_with(&input, Limits::DEFAULT, Some(CandidateKind::PdfChannels))
174            .expect_err("PDF_CHANNELS is not proposed for a non-PDF");
175        assert_eq!(err.class(), crate::error::ErrorClass::Usage);
176        assert!(
177            err.message().contains("is not proposed"),
178            "unexpected message: {}",
179            err.message()
180        );
181    }
182}