1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663 | //! Writes a document a reader shows nothing of until a password is typed in.
//!
//! Every string and every stream in the file is ciphertext, locked with AES
//! under a two-hundred-and-fifty-six-bit key. The user password is set, so a
//! reader asks for it before it shows a single page, and the password is what
//! the key is worked out from: a reader given the wrong one shows nothing at
//! all. The page states the password, so that the file can be opened by whoever
//! is handed it.
//!
//! Once it is open, the file grants printing, at the resolution the page was
//! drawn at, and reading aloud; it withholds copying, changing and taking pages
//! out — as requests a reader honours, not as locks. The author's password
//! lifts them and opens the file as well, and the page states that one too.
//!
//! The document that opens with nothing typed is `write_restricted`, and this
//! page names it in words rather than by that name.
//!
//! The page is written in the language `HQF_PDF_LANG` names. The passwords are
//! not: they are strings typed into a reader, and a translated password opens
//! nothing.
//!
//! Usage: `cargo run --example write_locked -- tmp/locked.pdf [font.ttf]`
//! `HQF_PDF_LANG=fr cargo run --example write_locked`
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use hqf_pdf::content::Content;
use hqf_pdf::cos::Name;
use hqf_pdf::{Document, Encryption, Font, FontHandle, Page, Permissions};
#[path = "shared/out.rs"]
mod out;
#[path = "shared/licence.rs"]
mod licence;
#[path = "shared/language.rs"]
mod language;
use language::Language;
/// The font the example draws with when none is given on the command line: the
/// one committed for the tests, so that the example runs on any machine.
fn default_font() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fonts")
.join("DejaVuSans.ttf")
}
/// How far in from the left edge of the sheet every line is set, in points.
const LEFT: f64 = 72.0;
/// The size the heading is set at, in points.
const HEADING_SIZE: f64 = 18.0;
/// Where the baseline of the first line of the heading sits, in points up from
/// the foot of the sheet.
const HEADING_BASELINE: f64 = 760.0;
/// How far one line of the heading sits below the one before it, in points.
const HEADING_LEADING: f64 = 22.0;
/// The size the body is set at, in points.
const BODY_SIZE: f64 = 11.0;
/// Where the baseline of the first line of the body sits, in points up from the
/// foot of the sheet.
const FIRST_BASELINE: f64 = 700.0;
/// How far one line of the body sits below the one before it, in points.
const LEADING: f64 = 18.0;
/// The size the heading over each closing section is set at, in points.
const SECTION_SIZE: f64 = 13.0;
/// Where the baseline of the heading over the other document sits, in points up
/// from the foot of the sheet.
const COMPANION_HEADING_BASELINE: f64 = 520.0;
/// Where the baseline of the first line about the other document sits, in
/// points up from the foot of the sheet.
const COMPANION_BASELINE: f64 = 492.0;
/// Where the baseline of the heading over the closing note sits, in points up
/// from the foot of the sheet.
const NOTE_HEADING_BASELINE: f64 = 420.0;
/// Where the baseline of the first line of the note sits, in points up from the
/// foot of the sheet.
const NOTE_BASELINE: f64 = 392.0;
/// The password a reader asks for before it shows a page, which the page states
/// so that the file can be opened at all. It stands outside the words: a
/// password is typed into a reader, and a translated one opens nothing.
const USER_PASSWORD: &str = "the reader";
/// The author's password, which the page states so that what the file withholds
/// can be lifted. It opens the file as well: the encryption dictionary of a
/// locked document works the same key out of it that the other password gives.
/// It stands outside the words for the same reason the other one does.
const OWNER_PASSWORD: &str = "the owner";
/// The thirty-two bytes the file key is built from.
///
/// They are fixed here, so that the example writes the same file on every run
/// and one build can be compared with the last. **A program takes them from its
/// operating system** — `getrandom` in Rust, `os.urandom(32)` in Python. A file
/// locked under a seed anybody can read is a file anybody opens.
const SEED: [u8; 32] = [
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
0x0F, 0x1E, 0x2D, 0x3C, 0x4B, 0x5A, 0x69, 0x78, 0x87, 0x96, 0xA5, 0xB4, 0xC3, 0xD2, 0xE1, 0xF0,
];
/// The words the page is written in, one set per language.
///
/// The passwords are not among them: they are typed into a reader, not
/// translated.
#[derive(Debug)]
struct Words {
/// What the document is called, set over two lines at the head of the page
/// and joined by a space in what the file says of itself.
heading: [&'static str; 2],
/// The body of the page, one line to a line.
body: [&'static str; 7],
/// What stands before the password the file asks for.
user_password_label: &'static str,
/// What stands before the author's password.
password_label: &'static str,
/// What stands over the lines about the other document.
companion_heading: &'static str,
/// What the other document is, and how it differs from this one.
companion: [&'static str; 3],
/// What stands over the closing note.
note_heading: &'static str,
/// The closing note, which says where the key comes from.
note: [&'static str; 4],
}
impl Words {
/// The words the page is written in, in `language`.
fn of(language: Language) -> &'static Self {
language::pick(&WORDS, language)
}
/// What the file says of itself: the two lines of the heading, in a row.
fn title(&self) -> String {
self.heading.join(" ")
}
}
/// The page in English.
const ENGLISH: Words = Words {
heading: ["A document that asks for a password", "before it opens"],
body: [
"This file shows nothing until a password is typed in. Type the",
"wrong one and no page appears at all.",
"Everything written in it is scrambled, under a key of two",
"hundred and fifty-six bits, and the password is what that key",
"is worked out from.",
"Once it is open, it asks the reading software to let it be",
"printed and read out loud, but not copied or changed.",
],
user_password_label: "The password this document asks for:",
password_label: "The author's password opens it too, and lifts the request:",
companion_heading: "The other document of this pair",
companion: [
"A second document was written beside this one. That one opens",
"on its own, with nothing to type in, and only asks that it not",
"be copied or changed.",
],
note_heading: "Where the key comes from",
note: [
"The thirty-two bytes the key is built from are fixed in this",
"example, so that it writes the same file on every run and one",
"build can be compared with the last. A program takes them from",
"its operating system: a seed anybody can read locks nothing.",
],
};
/// The page in French.
const FRENCH: Words = Words {
heading: ["Un document qui demande un mot de passe", "pour s'ouvrir"],
body: [
"Ce fichier ne montre rien tant qu'un mot de passe n'a pas été",
"tapé. Tapez le mauvais et aucune page n'apparaît.",
"Tout ce qu'il contient est brouillé, sous une clé de deux cent",
"cinquante-six bits, et c'est le mot de passe qui permet de",
"retrouver cette clé.",
"Une fois ouvert, il demande au logiciel de lecture de laisser",
"imprimer et lire à voix haute, mais ni copier ni modifier.",
],
user_password_label: "Le mot de passe que ce document demande :",
password_label: "Le mot de passe de l'auteur l'ouvre aussi, et lève la demande :",
companion_heading: "L'autre document de la paire",
companion: [
"Un second document a été écrit à côté de celui-ci. Lui s'ouvre",
"tout seul, sans rien à taper, et demande seulement qu'on ne le",
"copie ni ne le modifie.",
],
note_heading: "D'où vient la clé",
note: [
"Les trente-deux octets dont la clé est tirée sont figés dans cet",
"exemple, pour qu'il écrive le même fichier à chaque fois et qu'une",
"version se compare à la précédente. Un programme les prend à son",
"système : une graine que tout le monde peut lire ne ferme rien.",
],
};
/// Every language the example is written in. A language is added by writing its
/// own set of words and naming it here.
static WORDS: [(Language, &Words); 2] =
[(Language::English, &ENGLISH), (Language::French, &FRENCH)];
/// Every line of the body, the last two of which state the password the file
/// asks for and the author's.
fn lines(words: &Words) -> Vec<String> {
let mut all: Vec<String> = words.body.iter().map(|line| (*line).to_owned()).collect();
all.push(format!("{} {USER_PASSWORD}", words.user_password_label));
all.push(format!("{} {OWNER_PASSWORD}", words.password_label));
all
}
/// What the document is locked with: the password a reader asks for, the
/// author's, and what a reader is asked to allow — printing at the resolution
/// the page was drawn at, and reading aloud.
fn encryption() -> Encryption {
Encryption::new(SEED)
.user_password(USER_PASSWORD)
.owner_password(OWNER_PASSWORD)
.permissions(
Permissions::new()
.printing()
.printing_at_full_resolution()
.extracting_for_accessibility(),
)
}
/// Every line the page draws, in the order they are drawn: what it says, the
/// size it is set at, and where its baseline sits in points up from the foot of
/// the sheet.
fn drawing(words: &Words) -> Vec<(String, f64, f64)> {
let mut drawn = Vec::new();
let mut baseline = HEADING_BASELINE;
for line in words.heading {
drawn.push((line.to_owned(), HEADING_SIZE, baseline));
baseline -= HEADING_LEADING;
}
baseline = FIRST_BASELINE;
for line in lines(words) {
drawn.push((line, BODY_SIZE, baseline));
baseline -= LEADING;
}
drawn.push((
words.companion_heading.to_owned(),
SECTION_SIZE,
COMPANION_HEADING_BASELINE,
));
baseline = COMPANION_BASELINE;
for line in words.companion {
drawn.push((line.to_owned(), BODY_SIZE, baseline));
baseline -= LEADING;
}
drawn.push((
words.note_heading.to_owned(),
SECTION_SIZE,
NOTE_HEADING_BASELINE,
));
baseline = NOTE_BASELINE;
for line in words.note {
drawn.push((line.to_owned(), BODY_SIZE, baseline));
baseline -= LEADING;
}
drawn
}
/// The page: every line set by its own origin, each in an object of its own.
fn drawn(font: &FontHandle, lines: &[(String, f64, f64)]) -> Result<Content, hqf_pdf::Error> {
let mut content = Content::new();
for (line, size, baseline) in lines {
content.begin_text();
content.set_font(font.name(), *size)?;
content.text_origin(LEFT, *baseline)?;
content.show_glyphs(&font.glyphs(line));
content.end_text();
}
Ok(content)
}
/// The document and its one page, before it is locked.
fn document(words: &Words, font: Font) -> Result<Document, hqf_pdf::Error> {
let mut doc = Document::new();
doc.set_license(licence::licensed());
doc.set_info(Name::new("Title"), &words.title());
let handle = doc.add_font(font);
let mut page = Page::a4();
page.content = drawn(&handle, &drawing(words))?.into_bytes();
doc.add_page(page)?;
Ok(doc)
}
/// Writes `bytes` to `path`, making the directory it goes in if it is not
/// there, and says what was written.
fn written(path: &str, bytes: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
if let Some(parent) = Path::new(path).parent() {
fs::create_dir_all(parent)?;
}
fs::write(path, bytes)?;
println!("wrote {path}: {} bytes", bytes.len());
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let language = Language::from_environment()?;
let words = Words::of(language);
let mut args = env::args().skip(1);
// A named file is written as named; the default one carries the language,
// so the two languages do not overwrite each other in `tmp/`.
let out = args
.next()
.unwrap_or_else(|| language.file_name(&out::default_path("locked")));
let font_path = args.next().map_or_else(default_font, PathBuf::from);
let mut doc = document(words, Font::parse(fs::read(&font_path)?)?)?;
doc.protect(encryption());
written(&out, &doc.to_bytes()?)
}
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
use std::process::Command;
use hqf_pdf::read::Reader;
use hqf_pdf::{Document, Encryption, Font};
use super::{
LEFT, OWNER_PASSWORD, USER_PASSWORD, WORDS, Words, default_font, document, drawing,
encryption, language,
};
/// The width of the sheet, in points: A4, which the page is.
const SHEET_WIDTH: f64 = 595.276;
/// The room a line set from the left margin has before it reaches the
/// margin on the other side of the sheet.
const PAPER: f64 = SHEET_WIDTH - 2.0 * LEFT;
/// The lines two languages are allowed to write the same way. There are
/// none: the passwords are held outside the words.
const SPARED: [&str; 0] = [];
/// The committed font, parsed.
fn a_font() -> Font {
Font::parse(std::fs::read(default_font()).expect("the committed font is there"))
.expect("the committed font parses")
}
/// The bytes the example writes for `words`, protected or in the clear.
fn written(words: &Words, protected: bool) -> Vec<u8> {
let mut doc = document(words, a_font()).expect("the document is built");
if protected {
doc.protect(encryption());
}
doc.to_bytes().expect("the document writes")
}
#[test]
fn every_language_writes_the_page_in_its_own_words() {
let untranslated = language::untranslated_lines(&WORDS, &SPARED);
assert!(
untranslated.is_empty(),
"the page says these in more than one language: {untranslated:?}"
);
}
/// Nothing on the page is broken to a width: a line longer than the paper
/// runs off the edge of it.
#[test]
fn every_language_writes_lines_that_fit_the_room_they_have() {
let mut doc = Document::new();
let font = doc.add_font(a_font());
for (named, words) in WORDS {
for (line, size, _) in drawing(words) {
let measured = font.measure(&line, size);
assert!(
measured <= PAPER,
"the {} page draws {line:?} over {measured:.1} points, and \
it has {PAPER:.1}",
named.code()
);
}
}
}
/// The page draws no line over the one below it: every baseline sits lower
/// than the one before, by at least the size the line above is set at.
#[test]
fn the_page_draws_no_line_over_the_one_below_it() {
for (named, words) in WORDS {
for pair in drawing(words).windows(2) {
let (above, size, higher) = &pair[0];
let (below, _, lower) = &pair[1];
assert!(
higher - lower >= *size,
"the {} page sets {above:?} at {higher} and {below:?} at \
{lower}, and the first is {size} points tall",
named.code()
);
}
}
}
/// The bytes a file states its title as, however it spells them: plain
/// where the words are, sixteen bits to a letter where they are not.
fn stated_title(bytes: &[u8]) -> Vec<u8> {
let key = b"/Title (";
let at = bytes
.windows(key.len())
.position(|window| window == key)
.expect("the file states a title")
+ key.len();
let mut stated = Vec::new();
let mut escaped = false;
for byte in &bytes[at..] {
if escaped {
escaped = false;
} else if *byte == b'\\' {
escaped = true;
} else if *byte == b')' {
break;
}
stated.push(*byte);
}
stated
}
/// The file names the handler it was locked with and carries nothing of
/// what it states its title to be. The same document written in the clear
/// carries it plainly, which is what says the reading looks in the right
/// place.
#[test]
fn the_file_carries_nothing_it_draws_in_the_clear() {
for (named, words) in WORDS {
let code = named.code();
let plain = written(words, false);
let stated = stated_title(&plain);
assert!(
stated.len() > 8,
"the {code} document states its title where a file states one"
);
assert!(
!String::from_utf8_lossy(&plain).contains("/Encrypt"),
"and names no handler"
);
let locked = written(words, true);
let text = String::from_utf8_lossy(&locked);
assert!(text.contains("/Encrypt"), "the {code} file is protected");
assert!(text.contains("/AESV3"), "and names the cipher it used");
assert!(
!locked
.windows(stated.len())
.any(|window| window == stated.as_slice()),
"the {code} file carries its title in the clear"
);
}
}
/// The file does not open until the password its page states is given, and
/// the author's opens it too. What comes back out of it is what it says of
/// itself, which noise would not spell.
#[test]
fn the_file_opens_only_under_a_password_its_page_states() {
for (named, words) in WORDS {
let code = named.code();
let bytes = written(words, true);
assert!(
Reader::with_password(bytes.clone(), "").is_err(),
"the {code} file opened on nothing"
);
assert!(
Reader::with_password(bytes.clone(), "not the password").is_err(),
"the {code} file opened on another password"
);
for password in [USER_PASSWORD, OWNER_PASSWORD] {
let reader =
Reader::with_password(bytes.clone(), password).unwrap_or_else(|error| {
panic!("the {code} file opens on {password:?}: {error}")
});
assert_eq!(
reader.information().title.unwrap_or_default(),
words.title(),
"the {code} file states the title its page carries"
);
}
}
}
/// The page says printing and reading aloud are allowed and the rest is
/// withheld; the file states the same thing, as the flags of table 22 with
/// every reserved bit set — 0xFFFFF0C0, which is -3904 — plus bit 3 for
/// printing, which is 4, bit 10 for reading aloud, which is 512, and bit 12
/// for printing at the resolution the page was drawn at, which is 2048. A
/// file granting bit 3 without bit 12 is printed as a coarse picture.
#[test]
fn what_the_page_says_is_allowed_is_what_the_file_states() {
let bytes = written(Words::of(language::Language::English), true);
assert_eq!(-3904 + 4 + 512 + 2048, -1340, "the flags add up");
assert!(
String::from_utf8_lossy(&bytes).contains("/P -1340"),
"the file states what it grants"
);
}
/// The same document written twice is the same file, which is what lets one
/// build be compared with the last; another key writes another file.
#[test]
fn the_same_seed_writes_the_same_file_and_another_writes_another() {
let words = Words::of(language::Language::English);
assert_eq!(
written(words, true),
written(words, true),
"a protected document is still the same file"
);
let mut other = document(words, a_font()).expect("the document is built");
other.protect(
Encryption::new([1u8; 32])
.user_password(USER_PASSWORD)
.owner_password(OWNER_PASSWORD),
);
assert_ne!(
written(words, true),
other.to_bytes().expect("the document writes"),
"another key writes another file"
);
}
/// Where a reader that is not ours is handed what it is to read.
fn scratch(name: &str) -> PathBuf {
let mut at = std::env::temp_dir();
at.push(format!(
"hqf-pdf-write-locked-{}-{name}",
std::process::id()
));
at
}
/// Whether a program is on the machine.
fn installed(program: &str) -> bool {
Command::new(program)
.arg("-v")
.output()
.is_ok_and(|out| out.status.success() || !out.stderr.is_empty())
}
/// What `program` printed about `file`, and whether it managed to read it.
fn asked(program: &str, file: &Path, arguments: &[&str]) -> (bool, String) {
let out = Command::new(program)
.args(arguments)
.arg(file)
.output()
.unwrap_or_else(|error| panic!("{program} runs: {error}"));
(
out.status.success(),
String::from_utf8_lossy(&out.stdout).into_owned(),
)
}
/// What poppler gets off the pages of `file` under the passwords
/// `arguments` names, and whether it managed to read it at all.
fn text_of(file: &Path, arguments: &[&str]) -> (bool, String) {
let out = Command::new("pdftotext")
.args(arguments)
.arg(file)
.arg("-")
.output()
.expect("pdftotext runs");
(
out.status.success(),
String::from_utf8_lossy(&out.stdout).into_owned(),
)
}
/// Poppler, which shares no code with this library, is refused the file
/// until it is given the password the page states, and is refused it under
/// any other password. That is the page's own claim measured from outside:
/// our own reader agreeing with our own writer proves only that the two
/// were written from one reading of one clause.
#[test]
fn a_reader_that_is_not_ours_is_asked_for_the_password_the_page_states() {
if !installed("pdfinfo") {
println!("skipped: poppler is not on this machine");
return;
}
let words = Words::of(language::Language::English);
let at = scratch("asks.pdf");
std::fs::write(&at, written(words, true)).expect("the file is written");
let (opened, said) = asked("pdfinfo", &at, &[]);
assert!(
!opened,
"poppler opened the file with nothing typed, and said {said}"
);
let (opened, said) = asked("pdfinfo", &at, &["-upw", "not the password"]);
assert!(
!opened,
"poppler opened the file under another password, and said {said}"
);
let (opened, said) = asked("pdfinfo", &at, &["-upw", USER_PASSWORD]);
assert!(
opened,
"poppler opened the file once it was given the password"
);
assert!(
said.contains(&format!("Title: {}", words.title())),
"and read its title back, and said {said}"
);
assert!(
said.contains("Encrypted: yes"),
"and says the file is protected, and said {said}"
);
if installed("pdftotext") {
let drawn = words.body[0];
let (read, got) = text_of(&at, &[]);
assert!(
!read || !got.contains(drawn),
"poppler got the page off the file with nothing typed, and got \
{got:?}"
);
let (read, got) = text_of(&at, &["-upw", USER_PASSWORD]);
assert!(
read && got.contains(drawn),
"poppler got the page off it once it was given the password, \
and got {got:?}"
);
} else {
println!("skipped: pdftotext is not on this machine");
}
std::fs::remove_file(&at).ok();
}
}
|