Skip to main content

setzer_import/scrub/
mod.rs

1//! The scrambled copy of a manuscript.
2//!
3//! An author whose import fails can send a copy of the manuscript with every
4//! letter replaced and the structure kept: most import bugs survive, none of
5//! the prose does. `scrub` makes the copy, `check` imports the original and
6//! the copy and says whether the copy still shows the problem.
7//!
8//! What a copy keeps and changes:
9//!
10//! - **Letters** change, one for one: a vowel for a vowel, a consonant for a
11//!   consonant, an umlaut for an umlaut, ß for ß, upper case for upper case.
12//!   A word keeps its length, and is replaced the same way wherever it
13//!   stands in the file (so a title page and the document properties still
14//!   agree, and a contents entry still matches its heading).
15//! - **Everything else** stays: digits, punctuation, quotation marks,
16//!   dashes, spaces, no-break and zero-width characters, soft hyphens,
17//!   combining accents, line breaks, markup.
18//! - **The words the importer reads** stay, because it decides by them:
19//!   its heading, section and number words, "END"/"Ende", months, "by"/
20//!   "von", the starts of dedications and copyright lines, and the function
21//!   words it counts to tell English from German (`vocab`, `lang`: taken
22//!   from the importer itself). Heading-type words stay only in short lines
23//!   and only where the importer reads them; in running text they are
24//!   replaced like any other word.
25//! - **A .docx** is rewritten entry by entry: text in `document.xml`, notes,
26//!   headers, footers, comments, text boxes and the document properties is
27//!   replaced in place; every picture becomes a blank one of the same
28//!   format, pixel size, resolution, colour mode and rotation tag
29//!   (`image`); every other entry is copied byte for byte.
30//! - **Markdown** keeps its front-matter keys and language, link and image
31//!   targets, footnote labels, fences and attributes.
32//!
33//! The replacement is deterministic: its seed is a hash of the file, so the
34//! same file always gives the same copy (tests are stable) and two files
35//! give two different mappings. It is not a cipher: the original letters
36//! are not in the copy, only a random choice of the same kind, so nothing
37//! can be undone from the copy alone; and without the original there is no
38//! seed to find, only a hash of text nobody else has.
39//!
40//! A file the scrubber cannot read is an error, never a repaired copy.
41
42mod check;
43mod docx;
44mod image;
45mod markdown;
46mod words;
47mod xml;
48
49use std::path::Path;
50
51pub use check::{Verdict, check};
52pub use markdown::ScrubbedMarkdown;
53
54/// Why a scrambled copy could not be made. The app then offers the
55/// original.
56#[derive(Debug, thiserror::Error)]
57pub enum ScrubError {
58    #[error("this is not a Word document (.docx): {0}")]
59    NotDocx(String),
60    #[error("not UTF-8 text")]
61    NotUtf8,
62    /// A part of the file that is damaged or in a form the scrubber does not
63    /// handle: it would have to guess, and a guess is not a copy.
64    #[error("{part} cannot be scrambled: {why}")]
65    Part { part: String, why: String },
66    #[error("{0} is too large")]
67    TooLarge(String),
68    /// A picture that cannot be blanked (damaged, or too large).
69    #[error("the picture {name} cannot be blanked: {why}")]
70    Picture { name: String, why: String },
71}
72
73/// What kind of manuscript a file is.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum Kind {
76    Docx,
77    Markdown,
78}
79
80impl Kind {
81    /// By file extension (`.docx`; `.md`, `.markdown`), as `read_manuscript`
82    /// does; `None` for any other file.
83    pub fn of_path(path: &Path) -> Option<Self> {
84        match path
85            .extension()
86            .and_then(|e| e.to_str())?
87            .to_ascii_lowercase()
88            .as_str()
89        {
90            "docx" => Some(Kind::Docx),
91            "md" | "markdown" => Some(Kind::Markdown),
92            _ => None,
93        }
94    }
95}
96
97/// A scrambled manuscript: the file's bytes, and for Markdown the blank
98/// pictures it refers to (see [`ScrubbedMarkdown::images`]); a .docx holds
99/// its own.
100#[derive(Debug, Clone, PartialEq)]
101pub struct Scrubbed {
102    pub bytes: Vec<u8>,
103    pub images: Vec<(String, Vec<u8>)>,
104}
105
106/// The scrambled copy of a manuscript's bytes. `images` gives the bytes of
107/// a picture a Markdown manuscript refers to, by the path written in it
108/// (as for `markdown::import`); a .docx does not use it.
109pub fn scrub(
110    kind: Kind,
111    bytes: &[u8],
112    images: impl FnMut(&str) -> Option<Vec<u8>>,
113) -> Result<Scrubbed, ScrubError> {
114    match kind {
115        Kind::Docx => Ok(Scrubbed {
116            bytes: scrub_docx(bytes)?,
117            images: Vec::new(),
118        }),
119        Kind::Markdown => {
120            let md = scrub_markdown(bytes, images)?;
121            Ok(Scrubbed {
122                bytes: md.text.into_bytes(),
123                images: md.images,
124            })
125        }
126    }
127}
128
129/// The scrambled copy of a .docx.
130pub fn scrub_docx(bytes: &[u8]) -> Result<Vec<u8>, ScrubError> {
131    docx::scrub(bytes)
132}
133
134/// The scrambled copy of a Markdown manuscript, with a blank picture for
135/// each one the original's import found.
136pub fn scrub_markdown(
137    bytes: &[u8],
138    images: impl FnMut(&str) -> Option<Vec<u8>>,
139) -> Result<ScrubbedMarkdown, ScrubError> {
140    markdown::scrub(bytes, images)
141}