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}