Skip to main content

pdfrum_crypt/
permissions.rs

1//! What a document permits, decoded from the `/P` word.
2//!
3//! ISO 32000-1 table 22 numbers the bits from 1, and only eight of the
4//! thirty-two carry meaning; the rest are reserved and must be preserved
5//! rather than interpreted. The decode lives here, next to the `/P` value it
6//! decodes, and not in the crates that ask the questions — no caller should
7//! be spelling `bits & 0x100` for a bit it has no business knowing the
8//! number of.
9
10/// One bit's number in ISO 32000-1 table 22, **1-indexed as the table writes
11/// it**.
12///
13/// The table's own numbering is the contract: a reader checking this file
14/// against §7.6.4 table 22 reads "bit position 3" there and finds `3` here.
15/// The shift is `bit - 1`, applied in exactly one place below, which is the
16/// only arithmetic in this module.
17const fn bit(word: u32, position: u32) -> bool {
18    word & (1 << (position - 1)) != 0
19}
20
21/// The same, as a mask to build a word from.
22const fn mask(position: u32) -> u32 {
23    1 << (position - 1)
24}
25
26/// What a document's security handler permits.
27///
28/// Eight questions, not a bitfield — a caller asks "may I print?", never "is
29/// bit 3 set?". A struct of booleans rather than a `bitflags`-shaped newtype,
30/// because the answers do not compose into a set.
31///
32/// The reserved bits are *not* dropped: [`Permissions::from_bits`] ignores
33/// them and [`Permissions::bits`] reconstructs only the eight it knows, so a
34/// writer that must reproduce a document's `/P` verbatim keeps the original
35/// word — which is what `pdfrum-edit` does, copying `/Encrypt` rather than
36/// rebuilding it.
37///
38/// ```
39/// use pdfrum_crypt::Permissions;
40///
41/// // `/P -1` — every bit set, which is what an unrestricted document says.
42/// // (The standard handler forces `0xFFFF_FFFC` for a `/P` of -4, which
43/// // differs only in the two reserved low bits and decodes the same.)
44/// let all = Permissions::from_bits(0xFFFF_FFFF);
45/// assert_eq!(all, Permissions::ALL);
46/// assert!(all.print);
47///
48/// // Bit 3 alone: printing, and nothing else.
49/// let print_only = Permissions::from_bits(0b100);
50/// assert!(print_only.print);
51/// assert!(!print_only.copy);
52/// ```
53// Eight booleans is exactly the shape this type wants, against the
54// `bitflags`-newtype alternative: table 22's bits are eight independent questions with reserved holes
55// between them, not a set that composes, and `struct_excessive_bools`'s usual
56// advice — collapse them into an enum or a flags type — is the design that was
57// weighed and declined. The lint is right about most structs and wrong about
58// this one.
59#[allow(clippy::struct_excessive_bools)]
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
61pub struct Permissions {
62    /// Bit 3 — print the document.
63    ///
64    /// Revision 3 and above qualify this: with [`print_high_quality`] clear,
65    /// printing is permitted only in a degraded form.
66    ///
67    /// [`print_high_quality`]: Permissions::print_high_quality
68    pub print: bool,
69    /// Bit 4 — modify the contents by operations other than those controlled
70    /// by bits 6, 9 and 11.
71    pub modify: bool,
72    /// Bit 5 — copy or otherwise extract text and graphics.
73    pub copy: bool,
74    /// Bit 6 — add or modify text annotations and fill in interactive form
75    /// fields.
76    ///
77    /// The table couples the two: a document granting this grants both. Bit 9
78    /// then grants form filling *without* annotation editing, which is why
79    /// [`fill_form`] is a separate field and not implied by this one.
80    ///
81    /// [`fill_form`]: Permissions::fill_form
82    pub annotate: bool,
83    /// Bit 9 — fill in interactive form fields, including signature fields,
84    /// even when bit 6 is clear.
85    pub fill_form: bool,
86    /// Bit 10 — extract text and graphics for accessibility.
87    pub extract: bool,
88    /// Bit 11 — assemble the document: insert, rotate or delete pages, and
89    /// create bookmarks or thumbnails, even when bit 4 is clear.
90    pub assemble: bool,
91    /// Bit 12 — print at high resolution rather than as a degraded image.
92    pub print_high_quality: bool,
93}
94
95impl Permissions {
96    /// Every permission granted — an unencrypted document, and an
97    /// owner-authenticated one.
98    pub const ALL: Self = Self {
99        print: true,
100        modify: true,
101        copy: true,
102        annotate: true,
103        fill_form: true,
104        extract: true,
105        assemble: true,
106        print_high_quality: true,
107    };
108
109    /// Nothing granted.
110    pub const NONE: Self = Self {
111        print: false,
112        modify: false,
113        copy: false,
114        annotate: false,
115        fill_form: false,
116        extract: false,
117        assemble: false,
118        print_high_quality: false,
119    };
120
121    /// Decode the `/P` word (ISO 32000-1 §7.6.4 table 22).
122    ///
123    /// Reserved bits are ignored rather than refused: a damaged or
124    /// forward-dated file sets bits this table does not name, and the answer
125    /// to "may I print?" does not depend on them.
126    #[must_use]
127    pub const fn from_bits(bits: u32) -> Self {
128        Self {
129            print: bit(bits, 3),
130            modify: bit(bits, 4),
131            copy: bit(bits, 5),
132            annotate: bit(bits, 6),
133            fill_form: bit(bits, 9),
134            extract: bit(bits, 10),
135            assemble: bit(bits, 11),
136            print_high_quality: bit(bits, 12),
137        }
138    }
139
140    /// The eight bits back as a word, for a writer that must emit `/P`.
141    ///
142    /// **Not** a round trip of [`from_bits`]: reserved bits the input carried
143    /// are not here, because this type never held them. A save that must
144    /// reproduce a document's `/P` exactly keeps the original word instead —
145    /// which is what `pdfrum-edit` does today, copying the whole `/Encrypt`
146    /// dictionary.
147    ///
148    /// [`from_bits`]: Permissions::from_bits
149    #[must_use]
150    pub const fn bits(self) -> u32 {
151        let mut word = 0;
152        if self.print {
153            word |= mask(3);
154        }
155        if self.modify {
156            word |= mask(4);
157        }
158        if self.copy {
159            word |= mask(5);
160        }
161        if self.annotate {
162            word |= mask(6);
163        }
164        if self.fill_form {
165            word |= mask(9);
166        }
167        if self.extract {
168            word |= mask(10);
169        }
170        if self.assemble {
171            word |= mask(11);
172        }
173        if self.print_high_quality {
174            word |= mask(12);
175        }
176        word
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::Permissions;
183
184    /// ISO 32000-1 §7.6.4 table 22, written as the table writes it: **bit
185    /// numbers, 1-indexed**, never masks. A reviewer with the standard open
186    /// checks this column against the page, and nothing here computes
187    /// `1 << (n - 1)` — that is the code under test's job, and repeating it in
188    /// the test would test the test.
189    const TABLE_22: [(u32, &str); 8] = [
190        (3, "print"),
191        (4, "modify"),
192        (5, "copy"),
193        (6, "annotate"),
194        (9, "fill_form"),
195        (10, "extract"),
196        (11, "assemble"),
197        (12, "print_high_quality"),
198    ];
199
200    /// The field a name selects, so the table above can drive the assertions.
201    fn field(p: Permissions, name: &str) -> bool {
202        match name {
203            "print" => p.print,
204            "modify" => p.modify,
205            "copy" => p.copy,
206            "annotate" => p.annotate,
207            "fill_form" => p.fill_form,
208            "extract" => p.extract,
209            "assemble" => p.assemble,
210            "print_high_quality" => p.print_high_quality,
211            other => panic!("no field {other}"),
212        }
213    }
214
215    // Each bit, alone, grants exactly its own permission and no other. This is
216    // the assertion that catches an off-by-one in the 1-indexing: bit 3 set
217    // alone must read as `print`, not as `modify`.
218    #[test]
219    fn each_table_22_bit_grants_exactly_its_own_permission() {
220        for (position, name) in TABLE_22 {
221            let word = 1u32 << (position - 1);
222            let p = Permissions::from_bits(word);
223            for (_, other) in TABLE_22 {
224                assert_eq!(
225                    field(p, other),
226                    other == name,
227                    "bit {position} ({name}) should grant only {name}, but {other} read \
228                     {}",
229                    field(p, other)
230                );
231            }
232        }
233    }
234
235    // `/P -1` — every bit set — is the unrestricted document, which is what
236    // `SecurityHandler::Identity` reports and what an owner-unlocked handler
237    // is forced to.
238    #[test]
239    fn every_bit_set_is_all() {
240        assert_eq!(Permissions::from_bits(0xFFFF_FFFF), Permissions::ALL);
241    }
242
243    #[test]
244    fn no_bit_set_is_none() {
245        assert_eq!(Permissions::from_bits(0), Permissions::NONE);
246    }
247
248    // Reserved bits are ignored, not refused. Bits 1, 2, 7, 8 and 13 upward
249    // carry no meaning in table 22; a file setting all of them and nothing
250    // else grants nothing.
251    #[test]
252    fn reserved_bits_grant_nothing() {
253        let named: u32 = TABLE_22
254            .iter()
255            .fold(0, |acc, (position, _)| acc | (1 << (position - 1)));
256        assert_eq!(Permissions::from_bits(!named), Permissions::NONE);
257    }
258
259    // `bits()` reconstructs the eight named bits and only those, so it
260    // round-trips a word that had no reserved bits set — and drops the ones it
261    // did, which is documented and is why `pdfrum-edit` copies `/Encrypt`
262    // rather than rebuilding it.
263    #[test]
264    fn bits_round_trips_the_named_bits_and_drops_the_rest() {
265        let named: u32 = TABLE_22
266            .iter()
267            .fold(0, |acc, (position, _)| acc | (1 << (position - 1)));
268        assert_eq!(Permissions::ALL.bits(), named);
269        assert_eq!(Permissions::NONE.bits(), 0);
270        assert_eq!(Permissions::from_bits(0xFFFF_FFFF).bits(), named);
271        for (position, _) in TABLE_22 {
272            let word = 1u32 << (position - 1);
273            assert_eq!(Permissions::from_bits(word).bits(), word, "bit {position}");
274        }
275    }
276
277    // The standard handler's own reading of `/P 4092`, which is the value
278    // `docs` and the C++ both use as the worked example: bits 3 through 12
279    // set, everything else clear. Table 22 names eight of those ten, and the
280    // two it does not (7 and 8) are reserved.
281    #[test]
282    fn the_worked_example_p_4092_grants_everything_named() {
283        assert_eq!(Permissions::from_bits(4092), Permissions::ALL);
284    }
285}