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}