strypt_core/pipeline.rs
1//! The end-to-end operations front-ends call.
2//!
3//! Detection, dispatch, the handler, and — the part that is easy to leave out and expensive
4//! to add later — the verification pass. Front-ends should call these rather than reaching
5//! for a handler directly, because the checks that make a result trustworthy live here.
6
7use std::path::Path;
8
9use crate::detect::{Format, detect};
10use crate::error::{Result, StryptError};
11use crate::formats::{StripOptions, Stripped};
12use crate::io::{AtomicWrite, Limits, Overwrite, Permissions, read_bounded};
13use crate::registry::handler_for;
14use crate::report::{InspectOptions, MetadataReport, StripReport};
15
16/// Report what metadata `data` contains, without modifying anything.
17///
18/// # Errors
19///
20/// Returns [`StryptError::UnsupportedFormat`] or [`StryptError::UnrecognisedFormat`] when
21/// there is no handler, and the handler's own errors otherwise. There is no variant of this
22/// function that quietly returns an empty report for a file it did not understand.
23pub fn inspect_bytes(data: &[u8], options: &InspectOptions) -> Result<MetadataReport> {
24 let format = detect(data)?;
25 handler(format)?.inspect(data, options)
26}
27
28/// Produce a sanitised copy of `data` in memory, verified before it is returned.
29///
30/// The output is re-inspected here, and metadata that survived the strip fails the whole
31/// operation. That check is the reason this function exists rather than callers using a
32/// handler directly: it converts an entire class of handler bug — one that removes less than
33/// it reports — from a silent leak into a loud refusal (`docs/ARCHITECTURE.md` §1 stage 5).
34///
35/// What it cannot do is find metadata the inspector does not know to look for. It makes the
36/// tool internally consistent, not omniscient, and `docs/THREAT_MODEL.md` §4.8 says so to
37/// users in those words.
38///
39/// # Errors
40///
41/// [`StryptError::VerificationFailed`] if anything survived; otherwise the handler's errors.
42pub fn strip_bytes(data: &[u8], options: &StripOptions) -> Result<Stripped> {
43 let format = detect(data)?;
44 let handler = handler(format)?;
45 let stripped = handler.strip(data, options)?;
46
47 // Verify through the same top-level path a user would: detect the output afresh rather
48 // than assuming it is still the format we started with. A handler that emitted something
49 // unparseable should fail here, not at the point where the user opens the file.
50 let verified_format = detect(&stripped.bytes)?;
51 if verified_format != format {
52 return Err(StryptError::VerificationFailed {
53 format,
54 residual: 0,
55 });
56 }
57 let residual = handler
58 .inspect(&stripped.bytes, &InspectOptions::names_only())?
59 .findings
60 .len();
61 if residual > 0 {
62 return Err(StryptError::VerificationFailed { format, residual });
63 }
64 Ok(stripped)
65}
66
67/// Report what metadata the file at `path` contains.
68///
69/// # Errors
70///
71/// As [`inspect_bytes`], plus [`StryptError::Io`] and [`StryptError::InputTooLarge`].
72pub fn inspect_file(
73 path: &Path,
74 limits: Limits,
75 options: &InspectOptions,
76) -> Result<MetadataReport> {
77 let data = read_bounded(path, limits)?;
78 inspect_bytes(&data, options)
79}
80
81/// Strip `input` and write the result to `output`.
82///
83/// Nothing is written unless the whole operation succeeds and the output passes verification:
84/// on any failure the destination is left exactly as it was, and no partial file is left
85/// anywhere for a user to mistake for a clean copy.
86///
87/// # Errors
88///
89/// As [`strip_bytes`], plus [`StryptError::Io`] if the output cannot be written.
90pub fn strip_file(
91 input: &Path,
92 output: &Path,
93 limits: Limits,
94 overwrite: Overwrite,
95 options: &StripOptions,
96) -> Result<StripReport> {
97 let data = read_bounded(input, limits)?;
98 strip_bytes_to_file(&data, output, overwrite, options)
99}
100
101/// [`strip_file`] for bytes already read, so a front-end that inspects first reads the file once.
102///
103/// # Errors
104///
105/// As [`strip_bytes`], plus [`StryptError::Io`] if the output cannot be written.
106pub fn strip_bytes_to_file(
107 data: &[u8],
108 output: &Path,
109 overwrite: Overwrite,
110 options: &StripOptions,
111) -> Result<StripReport> {
112 let stripped = strip_bytes(data, options)?;
113
114 let mut writer = AtomicWrite::begin(output, overwrite, Permissions::OwnerOnly)?;
115 writer.write_all(&stripped.bytes)?;
116 writer.commit()?;
117 Ok(stripped.report)
118}
119
120/// The handler for `format`, or a reported refusal.
121fn handler(format: Format) -> Result<&'static dyn crate::formats::MetadataHandler> {
122 handler_for(format).ok_or(StryptError::UnsupportedFormat {
123 format: match format {
124 // A format strypt recognises but has not implemented yet is still a refusal. The
125 // one outcome that must never exist is a success message about a file that was
126 // copied through untouched (`docs/THREAT_MODEL.md` §5.4).
127 Format::Jpeg
128 | Format::Png
129 | Format::Webp
130 | Format::Pdf
131 | Format::Tiff
132 | Format::Gif
133 | Format::Heif
134 | Format::Avif
135 | Format::Docx
136 | Format::Xlsx
137 | Format::Pptx
138 | Format::Odt
139 | Format::Ods
140 | Format::Odp
141 | Format::Svg
142 | Format::Jxl
143 | Format::Flac
144 | Format::Wav
145 | Format::Mp3
146 | Format::Ogg
147 | Format::Opus
148 | Format::OggFlac
149 | Format::Mp4
150 | Format::M4a => crate::error::UnsupportedKind::NotYetImplemented(format),
151 },
152 })
153}