Skip to main content

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}