Skip to main content

strypt_core/formats/
mod.rs

1//! Format handlers.
2//!
3//! Organised by **file format, not by dependency**: someone looking for how WebP is handled
4//! opens `webp.rs`. If the crate underneath a handler is ever swapped, the change stops at
5//! that module boundary and nothing else in the tree moves. A module must never be named
6//! after the crate it wraps.
7//!
8//! # Why handlers take a slice and return a buffer
9//!
10//! `docs/ARCHITECTURE.md` §3 sketched this trait over a `ReadSeek`. Phase 1 refines it to
11//! `&[u8]` in and `Vec<u8>` out, recorded as ADR-0017. Two reasons:
12//!
13//! - **Output must be verified before it can reach the disk.** The verification pass
14//!   (`docs/ARCHITECTURE.md` §1) re-inspects what the handler produced and fails if metadata
15//!   survived. Streaming straight to the destination would mean unverified — possibly
16//!   partially-sanitised — bytes had already been written by the time the check ran, which is
17//!   exactly the outcome the fail-closed rule exists to prevent.
18//! - **A slice makes the panic-freedom lints enforceable.** Seek-driven parsing spreads
19//!   bounds checking across every read; a slice concentrates it in
20//!   [`crate::bytes::Reader`], where `indexing_slicing` and `arithmetic_side_effects` can be
21//!   denied and actually mean something (ADR-0006).
22//!
23//! The cost is that a file is held in memory, which is why ingest is bounded before a handler
24//! ever sees it ([`crate::io::Limits`]).
25
26use crate::detect::Format;
27use crate::error::Result;
28use crate::report::{InspectOptions, MetadataReport, StripReport};
29
30mod exif;
31pub mod flac;
32pub mod gif;
33pub mod heif;
34pub mod jpeg;
35pub mod jxl;
36pub mod mp3;
37pub mod mp4;
38pub mod odf;
39pub mod ogg;
40pub mod ooxml;
41pub mod pdf;
42pub mod png;
43pub mod svg;
44pub(crate) mod tags;
45pub mod tiff;
46pub(crate) mod vorbis;
47pub mod wav;
48pub mod webp;
49mod xml;
50pub(crate) mod xmp;
51
52/// Sanitised bytes and an account of what was done to produce them.
53#[derive(Debug, Clone)]
54#[non_exhaustive]
55pub struct Stripped {
56    /// The sanitised file. Not yet written anywhere: the pipeline verifies it first.
57    pub bytes: Vec<u8>,
58    /// What was removed, what was kept, and any caveats.
59    pub report: StripReport,
60}
61
62/// Ceilings a handler applies while parsing.
63///
64/// Separate from [`crate::io::Limits`], which bounds how much is *read*. These bound what a
65/// parser will do with what it read — the difference between refusing a 4 GB file and
66/// refusing a 4 KB file that describes four billion objects.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68#[non_exhaustive]
69pub struct ParseLimits {
70    /// Maximum nesting depth.
71    ///
72    /// Stack overflow aborts the process; it is not a catchable panic, so it cannot be
73    /// handled after the fact and must be prevented by construction
74    /// (`docs/ARCHITECTURE.md` §5.1).
75    pub max_depth: u32,
76    /// Maximum number of structural items — objects, segments, chunks — in one file.
77    pub max_items: u32,
78    /// Maximum bytes a single compressed structure may expand to.
79    ///
80    /// **Load-bearing as of the OOXML handler.** It was reserved through Phase 1, because no
81    /// handler decompressed anything: PNG's compressed text chunks are removed without being
82    /// inflated (ADR-0022), and the PDF handler declines to inflate a filtered metadata stream
83    /// for the same reason. The ZIP container layer is the first code here to inflate, and it
84    /// spends this ceiling as an allowance shared across a whole archive — so that a hundred
85    /// entries each individually within it cannot collectively exceed it, which is the shape of
86    /// every archive bomb that gets past a naive limit (ADR-0028, ADR-0029).
87    ///
88    /// The ceiling is enforced *as output is produced*, never checked afterwards. A limit
89    /// tested after decompressing is not a limit — the memory is already committed by the time
90    /// it fails.
91    pub max_expanded_bytes: u64,
92}
93
94impl Default for ParseLimits {
95    fn default() -> Self {
96        Self {
97            // Comfortably past what real documents nest to, far short of what blows a stack.
98            max_depth: 64,
99            // A large real PDF runs to tens of thousands of objects; a million is a refusal.
100            max_items: 1_000_000,
101            max_expanded_bytes: 256 * 1024 * 1024,
102        }
103    }
104}
105
106/// What a caller wants from a strip.
107#[derive(Debug, Clone, Default, PartialEq, Eq)]
108#[non_exhaustive]
109pub struct StripOptions {
110    /// Whether the returned report names the values that were removed.
111    ///
112    /// Off by default, for the reason in [`crate::report`]: a report is easy to redirect into
113    /// a file, and a file naming everything just removed is a durable copy of the secret.
114    pub inspect: InspectOptions,
115    /// Parser ceilings.
116    pub limits: ParseLimits,
117}
118
119/// Detection, reporting, and removal for one file format.
120///
121/// Implementations sit directly on attacker-controlled bytes and must uphold the invariants
122/// in `docs/ARCHITECTURE.md` §3: `inspect` never mutates, nothing panics, failure is total
123/// rather than partial, resources are bounded, the payload is preserved, and anything `strip`
124/// claims to remove is something `inspect` can detect — without which the verification pass
125/// would be checking nothing.
126pub trait MetadataHandler: Send + Sync {
127    /// Stable identifier, matching [`Format::id`].
128    fn name(&self) -> &'static str;
129
130    /// The format this handler is responsible for.
131    fn format(&self) -> Format;
132
133    /// Report what metadata the file contains, without modifying anything.
134    ///
135    /// An unparseable region is a [`crate::report::Note`], not necessarily an error: a file
136    /// strypt only partly understands is still worth telling the user about, provided the
137    /// report says plainly which part was not understood.
138    ///
139    /// # Errors
140    ///
141    /// Returns an error when the file's structure is unusable, or when a parse ceiling is hit.
142    fn inspect(&self, input: &[u8], options: &InspectOptions) -> Result<MetadataReport>;
143
144    /// Produce a sanitised copy.
145    ///
146    /// # Errors
147    ///
148    /// Returns an error rather than partially-sanitised bytes. There is no half-success.
149    fn strip(&self, input: &[u8], options: &StripOptions) -> Result<Stripped>;
150}