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}