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}