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 jpeg;
32pub mod pdf;
33pub mod png;
34pub mod webp;
35mod xmp;
36
37/// Sanitised bytes and an account of what was done to produce them.
38#[derive(Debug, Clone)]
39#[non_exhaustive]
40pub struct Stripped {
41    /// The sanitised file. Not yet written anywhere: the pipeline verifies it first.
42    pub bytes: Vec<u8>,
43    /// What was removed, what was kept, and any caveats.
44    pub report: StripReport,
45}
46
47/// Ceilings a handler applies while parsing.
48///
49/// Separate from [`crate::io::Limits`], which bounds how much is *read*. These bound what a
50/// parser will do with what it read — the difference between refusing a 4 GB file and
51/// refusing a 4 KB file that describes four billion objects.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53#[non_exhaustive]
54pub struct ParseLimits {
55    /// Maximum nesting depth.
56    ///
57    /// Stack overflow aborts the process; it is not a catchable panic, so it cannot be
58    /// handled after the fact and must be prevented by construction
59    /// (`docs/ARCHITECTURE.md` §5.1).
60    pub max_depth: u32,
61    /// Maximum number of structural items — objects, segments, chunks — in one file.
62    pub max_items: u32,
63    /// Maximum bytes a single compressed structure may expand to.
64    ///
65    /// **Reserved: no handler in this release uses it.** Neither PNG nor WebP decompresses
66    /// anything — PNG's compressed text chunks are removed without being inflated (ADR-0022)
67    /// — and the PDF handler declines to inflate a filtered metadata stream for the same
68    /// reason. It is kept rather than removed because Phase 2's ZIP-container formats cannot
69    /// be attempted without it, and because a limit that exists is easier to review than one
70    /// invented under deadline. It is documented as reserved rather than left looking
71    /// enforced.
72    pub max_expanded_bytes: u64,
73}
74
75impl Default for ParseLimits {
76    fn default() -> Self {
77        Self {
78            // Comfortably past what real documents nest to, far short of what blows a stack.
79            max_depth: 64,
80            // A large real PDF runs to tens of thousands of objects; a million is a refusal.
81            max_items: 1_000_000,
82            max_expanded_bytes: 256 * 1024 * 1024,
83        }
84    }
85}
86
87/// What a caller wants from a strip.
88#[derive(Debug, Clone, Default, PartialEq, Eq)]
89#[non_exhaustive]
90pub struct StripOptions {
91    /// Whether the returned report names the values that were removed.
92    ///
93    /// Off by default, for the reason in [`crate::report`]: a report is easy to redirect into
94    /// a file, and a file naming everything just removed is a durable copy of the secret.
95    pub inspect: InspectOptions,
96    /// Parser ceilings.
97    pub limits: ParseLimits,
98}
99
100/// Detection, reporting, and removal for one file format.
101///
102/// Implementations sit directly on attacker-controlled bytes and must uphold the invariants
103/// in `docs/ARCHITECTURE.md` §3: `inspect` never mutates, nothing panics, failure is total
104/// rather than partial, resources are bounded, the payload is preserved, and anything `strip`
105/// claims to remove is something `inspect` can detect — without which the verification pass
106/// would be checking nothing.
107pub trait MetadataHandler: Send + Sync {
108    /// Stable identifier, matching [`Format::id`].
109    fn name(&self) -> &'static str;
110
111    /// The format this handler is responsible for.
112    fn format(&self) -> Format;
113
114    /// Report what metadata the file contains, without modifying anything.
115    ///
116    /// An unparseable region is a [`crate::report::Note`], not necessarily an error: a file
117    /// strypt only partly understands is still worth telling the user about, provided the
118    /// report says plainly which part was not understood.
119    ///
120    /// # Errors
121    ///
122    /// Returns an error when the file's structure is unusable, or when a parse ceiling is hit.
123    fn inspect(&self, input: &[u8], options: &InspectOptions) -> Result<MetadataReport>;
124
125    /// Produce a sanitised copy.
126    ///
127    /// # Errors
128    ///
129    /// Returns an error rather than partially-sanitised bytes. There is no half-success.
130    fn strip(&self, input: &[u8], options: &StripOptions) -> Result<Stripped>;
131}