Skip to main content

pdfrum_type1/
container.rs

1//! Getting from a `/FontFile` blob to two byte ranges: the ASCII clear portion
2//! and the still-encrypted private portion.
3//!
4//! A Type 1 font program arrives in one of three wrappers, and the font dict's
5//! `/Length1` `/Length2` `/Length3` cannot be trusted to describe them —
6//! PDFium ignores those keys entirely and so do we, sniffing the bytes instead.
7//!
8//! - **PFB**: a chain of `[0x80, type, len:u32le, data…]` records, type 1 for
9//!   ASCII text, 2 for binary, 3 for end-of-file. The clear part is the leading
10//!   text record; the binary records concatenated are the eexec ciphertext.
11//! - **PFA**: plain ASCII beginning `%!PS-AdobeFont` or `%!FontType1`, with the
12//!   private portion written as hexadecimal after the `eexec` keyword.
13//! - **bare**: neither marker. Treated as PFA-shaped, because that is what a
14//!   stripped-down embedded font usually is.
15//!
16//! The output is deliberately owned rather than borrowed: PFB's binary
17//! segments are non-contiguous and PFA's hex needs decoding, so a `&[u8]` view
18//! of the ciphertext does not exist in the general case.
19
20use crate::error::Error;
21use pdfrum_common::{DiagKind, Diagnostics, Severity, hex_digit};
22
23/// Which wrapper the bytes turned out to be in. Reported so callers (and
24/// tests) can tell a genuine PFA from a bare program that merely parses like
25/// one.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Container {
28    /// Segmented binary container (`0x80 0x01 …`).
29    Pfb,
30    /// ASCII container with a `%!PS-AdobeFont` or `%!FontType1` banner.
31    Pfa,
32    /// No recognisable banner; read as if it were PFA.
33    Bare,
34}
35
36/// A font program split into its two halves, ready for `eexec` decryption.
37#[derive(Debug, Clone)]
38pub struct Split {
39    /// Which wrapper this came out of.
40    pub container: Container,
41    /// The cleartext PostScript preamble: everything up to and including the
42    /// `eexec` keyword's whitespace.
43    pub clear: Vec<u8>,
44    /// The still-encrypted private portion, hex-decoded if it was hex.
45    pub cipher: Vec<u8>,
46}
47
48const PFB_MARKER: u8 = 0x80;
49const PFB_TEXT: u8 = 1;
50const PFB_BINARY: u8 = 2;
51const PFB_EOF: u8 = 3;
52
53/// A Type 1 program as a PDF `/FontFile` stream: the bytes to store, and the
54/// ISO 32000-1 §9.9 table 127 lengths that partition them.
55///
56/// The two are produced together because table 127 defines the three lengths
57/// as a *partition of the stream's decoded data* — `/Length1` the clear-text
58/// portion, `/Length2` the encrypted portion, `/Length3` the fixed-content
59/// (`cleartomark`) portion — so computing lengths for one byte string and
60/// storing another is the defect this type exists to make unrepresentable.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct FontFile {
63    /// The bytes to write as the stream's decoded data. `length1 + length2 +
64    /// length3 == program.len()` always holds.
65    pub program: Vec<u8>,
66    /// Bytes of clear-text ASCII, up to and including the `eexec` line.
67    pub length1: u32,
68    /// Bytes of the `eexec`-encrypted private portion, in whatever form it is
69    /// stored — binary for a PFB, and still hexadecimal for a PFA whose
70    /// private portion was written that way, which §9.9 permits.
71    pub length2: u32,
72    /// Bytes of the fixed 512-zeros-plus-`cleartomark` trailer, 0 when the
73    /// program carries none.
74    pub length3: u32,
75}
76
77/// Unwrap a Type 1 program into the `/FontFile` stream a PDF writer stores.
78///
79/// A PFB is a container, not a font program: its record headers and end marker
80/// are framing that must not reach the stream, so the record bodies are
81/// concatenated into the PFA-shaped raw program table 127 describes and the
82/// three lengths are those bodies' sizes. A PFA or bare program is stored
83/// as-is, with `/Length1` ending at the `eexec` boundary, `/Length3` covering
84/// a trailing `cleartomark` block when there is one, and `/Length2` the
85/// remainder. A program with no `eexec` at all is entirely `/Length1`.
86#[must_use]
87pub fn font_file(bytes: &[u8]) -> FontFile {
88    if bytes.first() == Some(&PFB_MARKER) {
89        return pfb_font_file(bytes);
90    }
91    ascii_font_file(bytes)
92}
93
94/// Walk PFB records, concatenating their bodies and measuring each class.
95///
96/// Truncation is tolerated the way [`split_pfb`] tolerates it: a body whose
97/// declared length overruns the blob contributes what is actually there, and
98/// the walk stops. The invariant survives, because every byte counted is a
99/// byte pushed.
100fn pfb_font_file(bytes: &[u8]) -> FontFile {
101    let mut out = FontFile {
102        program: Vec::with_capacity(bytes.len()),
103        length1: 0,
104        length2: 0,
105        length3: 0,
106    };
107    let mut seen_binary = false;
108    let mut at = 0usize;
109    while at < bytes.len() {
110        let Some(header) = bytes.get(at..at.saturating_add(6)) else {
111            break;
112        };
113        if header.first().copied() != Some(PFB_MARKER) {
114            break;
115        }
116        let Some(k @ (PFB_TEXT | PFB_BINARY)) = header.get(1).copied() else {
117            break;
118        };
119        let declared = le_u32(header.get(2..6).unwrap_or_default()) as usize;
120        let body_at = at.saturating_add(6);
121        let body = bytes
122            .get(body_at..body_at.saturating_add(declared))
123            .unwrap_or_else(|| bytes.get(body_at..).unwrap_or_default());
124        let n = len_u32(body.len());
125        out.program.extend_from_slice(body);
126        if k == PFB_TEXT {
127            if seen_binary {
128                out.length3 = out.length3.saturating_add(n);
129            } else {
130                out.length1 = out.length1.saturating_add(n);
131            }
132        } else {
133            seen_binary = true;
134            out.length2 = out.length2.saturating_add(n);
135        }
136        at = body_at.saturating_add(body.len());
137        if body.len() < declared {
138            break;
139        }
140    }
141    if out.program.is_empty() {
142        // Not a walkable record chain after all — store what we were given
143        // rather than an empty `/FontFile`, and describe it as clear text.
144        return FontFile {
145            program: bytes.to_vec(),
146            length1: len_u32(bytes.len()),
147            length2: 0,
148            length3: 0,
149        };
150    }
151    out
152}
153
154/// Measure an already-raw program: `eexec` splits `/Length1` from `/Length2`,
155/// and a trailing zeros block splits `/Length3` off the end.
156///
157/// The encrypted portion is **not** decoded. A PFA writes it in hexadecimal,
158/// and ISO 32000-1 §9.9 permits that in a `/FontFile` — the lengths describe
159/// the stored bytes, so hex stays hex and `/Length2` counts hex digits.
160fn ascii_font_file(bytes: &[u8]) -> FontFile {
161    let Some(key) = find_eexec(bytes) else {
162        return FontFile {
163            program: bytes.to_vec(),
164            length1: len_u32(bytes.len()),
165            length2: 0,
166            length3: 0,
167        };
168    };
169    let trailer = trailer_start(bytes, key);
170    FontFile {
171        program: bytes.to_vec(),
172        length1: len_u32(key),
173        length2: len_u32(trailer.saturating_sub(key)),
174        length3: len_u32(bytes.len().saturating_sub(trailer)),
175    }
176}
177
178/// Where the fixed-content trailer begins: the first of the 512 ASCII `0`
179/// digits that close a Type 1 program.
180///
181/// The Type 1 specification's trailer is exactly 512 zeros — conventionally
182/// eight 64-digit lines — followed by `cleartomark`. Counting the *maximal*
183/// trailing run of zeros and whitespace would be wrong: a private portion
184/// whose last hex digits happen to be zeros would be eaten into `/Length3`.
185/// So the count is exact, taken backwards from the last zero, and anything
186/// but a 512-zero block means the program has no trailer and `/Length3` is 0.
187fn trailer_start(bytes: &[u8], after: usize) -> usize {
188    const TRAILER_ZEROS: usize = 512;
189    let tail = bytes.get(after..).unwrap_or_default();
190    // Walk back over `cleartomark` and its whitespace to the last zero.
191    let mut end = tail.len();
192    while end > 0 && tail.get(end.saturating_sub(1)) != Some(&b'0') {
193        end = end.saturating_sub(1);
194    }
195    // Then back over exactly 512 zeros, tolerating the line breaks between.
196    let mut zeros = 0usize;
197    let mut at = end;
198    while at > 0 && zeros < TRAILER_ZEROS {
199        match tail.get(at.saturating_sub(1)) {
200            Some(b'0') => zeros = zeros.saturating_add(1),
201            Some(b) if b.is_ascii_whitespace() => {}
202            _ => break,
203        }
204        at = at.saturating_sub(1);
205    }
206    if zeros == TRAILER_ZEROS {
207        after.saturating_add(at)
208    } else {
209        bytes.len()
210    }
211}
212
213fn len_u32(n: usize) -> u32 {
214    u32::try_from(n).unwrap_or(u32::MAX)
215}
216
217/// Sniff the container and split the program.
218///
219/// Damage tolerance mirrors what a Type 1 rasterizer has to survive in the
220/// wild: a PFB whose final segment length overruns the blob is truncated to
221/// what is there, a missing type-3 terminator is not an error, and a PFA whose
222/// hex runs into `0000…0000 cleartomark` simply stops at the first non-hex
223/// byte. Each of those records a diagnostic.
224///
225/// # Errors
226///
227/// [`Error::Empty`] for an empty blob, [`Error::PfbSegment`] for a PFB whose
228/// *first* segment header is unusable (past that point truncation is a
229/// recovery, not a failure), and [`Error::NoEexec`] when no private portion can
230/// be located at all.
231pub fn split(bytes: &[u8], diags: &mut Diagnostics) -> Result<Split, Error> {
232    if bytes.is_empty() {
233        return Err(Error::Empty);
234    }
235    if bytes.first() == Some(&PFB_MARKER) {
236        return split_pfb(bytes, diags);
237    }
238    let container = if banner(bytes) {
239        Container::Pfa
240    } else {
241        Container::Bare
242    };
243    split_ascii(bytes, container, diags)
244}
245
246/// Whether the blob opens with one of the two ASCII Type 1 banners. The scan
247/// tolerates leading whitespace, which real files do carry.
248fn banner(bytes: &[u8]) -> bool {
249    let start = bytes
250        .iter()
251        .position(|b| !b.is_ascii_whitespace())
252        .unwrap_or(bytes.len());
253    let rest = bytes.get(start..).unwrap_or_default();
254    rest.starts_with(b"%!PS-AdobeFont") || rest.starts_with(b"%!FontType1")
255}
256
257/// Walk the `[0x80, type, len:u32le]` record chain.
258fn split_pfb(bytes: &[u8], diags: &mut Diagnostics) -> Result<Split, Error> {
259    let mut clear = Vec::new();
260    let mut cipher = Vec::new();
261    let mut at = 0usize;
262    let mut first = true;
263
264    while at < bytes.len() {
265        let Some(header) = bytes.get(at..at.saturating_add(6)) else {
266            // A trailing stub too short to be a header. Real files end on a
267            // type-3 record; this one did not.
268            diags.record(
269                Severity::Suspicious,
270                DiagKind::Type1PfbTruncated,
271                Some(at as u64),
272            );
273            break;
274        };
275        let (marker, kind) = (header.first().copied(), header.get(1).copied());
276        if marker != Some(PFB_MARKER) {
277            if first {
278                return Err(Error::PfbSegment { at });
279            }
280            diags.record(
281                Severity::Suspicious,
282                DiagKind::Type1PfbTruncated,
283                Some(at as u64),
284            );
285            break;
286        }
287        match kind {
288            Some(PFB_EOF) => break,
289            Some(k @ (PFB_TEXT | PFB_BINARY)) => {
290                let declared = le_u32(header.get(2..6).unwrap_or_default()) as usize;
291                let body_at = at.saturating_add(6);
292                let body = if let Some(b) = bytes.get(body_at..body_at.saturating_add(declared)) {
293                    b
294                } else {
295                    // Length overruns the blob: keep what is there.
296                    diags.record(
297                        Severity::Recovered,
298                        DiagKind::Type1PfbTruncated,
299                        Some(at as u64),
300                    );
301                    bytes.get(body_at..).unwrap_or_default()
302                };
303                if k == PFB_TEXT {
304                    // Only the preamble matters; the trailing 512 zeros and
305                    // `cleartomark` are a PostScript ritual, not font data,
306                    // and they arrive *after* the binary segments.
307                    if cipher.is_empty() {
308                        clear.extend_from_slice(body);
309                    }
310                } else {
311                    cipher.extend_from_slice(body);
312                }
313                at = body_at.saturating_add(body.len());
314                if body.len() < declared {
315                    break;
316                }
317            }
318            _ => {
319                if first {
320                    return Err(Error::PfbSegment { at });
321                }
322                diags.record(
323                    Severity::Suspicious,
324                    DiagKind::Type1PfbTruncated,
325                    Some(at as u64),
326                );
327                break;
328            }
329        }
330        first = false;
331    }
332
333    if cipher.is_empty() {
334        // A PFB whose text segment nevertheless holds an inline `eexec`
335        // section — malformed, but recoverable by reading it as PFA.
336        if let Ok(mut ascii) = split_ascii(&clear, Container::Pfb, diags) {
337            ascii.container = Container::Pfb;
338            return Ok(ascii);
339        }
340        return Err(Error::NoEexec);
341    }
342    Ok(Split {
343        container: Container::Pfb,
344        clear,
345        cipher,
346    })
347}
348
349/// Find `eexec` in an ASCII program and take everything after it, hex-decoding
350/// when the tail is hex.
351fn split_ascii(
352    bytes: &[u8],
353    container: Container,
354    diags: &mut Diagnostics,
355) -> Result<Split, Error> {
356    let key = find_eexec(bytes).ok_or(Error::NoEexec)?;
357    let clear = bytes.get(..key).unwrap_or_default().to_vec();
358    let tail = bytes.get(key..).unwrap_or_default();
359
360    // The private portion is hex when its first four *significant* bytes are
361    // all hex digits — the test FreeType uses, and it is reliable because a
362    // binary section's first four bytes are random ciphertext.
363    let significant: Vec<u8> = tail
364        .iter()
365        .copied()
366        .filter(|b| !b.is_ascii_whitespace())
367        .take(4)
368        .collect();
369    let is_hex = significant.len() == 4 && significant.iter().all(u8::is_ascii_hexdigit);
370
371    let cipher = if is_hex {
372        hex_decode(tail, diags)
373    } else {
374        tail.to_vec()
375    };
376    Ok(Split {
377        container,
378        clear,
379        cipher,
380    })
381}
382
383/// Locate the byte just past the `eexec` keyword and its following
384/// end-of-line.
385///
386/// The keyword is matched only at a token boundary, so `eexec` inside a
387/// comment or a `(string)` does not trigger. Exactly one EOL is consumed —
388/// `\r\n`, `\r` or `\n` — plus any run of spaces or tabs before it, matching
389/// what the Type 1 specification says the interpreter does.
390fn find_eexec(bytes: &[u8]) -> Option<usize> {
391    let mut i = 0usize;
392    while i < bytes.len() {
393        match bytes.get(i).copied() {
394            // Skip a `%` comment to end of line.
395            Some(b'%') => {
396                while i < bytes.len() && !matches!(bytes.get(i), Some(b'\r' | b'\n')) {
397                    i = i.saturating_add(1);
398                }
399            }
400            // Skip a `(…)` string, honouring nesting and backslash escapes.
401            Some(b'(') => {
402                let mut depth = 1usize;
403                i = i.saturating_add(1);
404                while i < bytes.len() && depth > 0 {
405                    match bytes.get(i).copied() {
406                        Some(b'\\') => i = i.saturating_add(1),
407                        Some(b'(') => depth = depth.saturating_add(1),
408                        Some(b')') => depth = depth.saturating_sub(1),
409                        _ => {}
410                    }
411                    i = i.saturating_add(1);
412                }
413            }
414            _ => {
415                if bytes.get(i..i.saturating_add(5)) == Some(b"eexec".as_slice())
416                    && before_is_boundary(bytes, i)
417                    && after_is_boundary(bytes, i.saturating_add(5))
418                {
419                    return Some(skip_one_eol(bytes, i.saturating_add(5)));
420                }
421                i = i.saturating_add(1);
422            }
423        }
424    }
425    None
426}
427
428fn before_is_boundary(bytes: &[u8], at: usize) -> bool {
429    at == 0
430        || at
431            .checked_sub(1)
432            .and_then(|p| bytes.get(p))
433            .is_some_and(|b| b.is_ascii_whitespace() || *b == b'/')
434}
435
436fn after_is_boundary(bytes: &[u8], at: usize) -> bool {
437    bytes.get(at).is_none_or(u8::is_ascii_whitespace)
438}
439
440/// Consume trailing blanks then exactly one line ending.
441fn skip_one_eol(bytes: &[u8], mut at: usize) -> usize {
442    while matches!(bytes.get(at), Some(b' ' | b'\t')) {
443        at = at.saturating_add(1);
444    }
445    match bytes.get(at) {
446        Some(b'\r') => {
447            at = at.saturating_add(1);
448            if bytes.get(at) == Some(&b'\n') {
449                at = at.saturating_add(1);
450            }
451        }
452        Some(b'\n') => at = at.saturating_add(1),
453        _ => {}
454    }
455    at
456}
457
458/// Decode hex until the first byte that is neither a hex digit nor whitespace.
459fn hex_decode(bytes: &[u8], diags: &mut Diagnostics) -> Vec<u8> {
460    let mut out = Vec::with_capacity(bytes.len() / 2);
461    let mut high: Option<u8> = None;
462    for (i, b) in bytes.iter().enumerate() {
463        if b.is_ascii_whitespace() {
464            continue;
465        }
466        let Some(nibble) = hex_digit(*b) else {
467            if i.saturating_add(1) < bytes.len() {
468                diags.record(
469                    Severity::Recovered,
470                    DiagKind::Type1HexTruncated,
471                    Some(i as u64),
472                );
473            }
474            break;
475        };
476        match high.take() {
477            None => high = Some(nibble),
478            Some(h) => out.push((h << 4) | nibble),
479        }
480    }
481    out
482}
483
484fn le_u32(b: &[u8]) -> u32 {
485    let g = |i: usize| u32::from(b.get(i).copied().unwrap_or(0));
486    g(0) | (g(1) << 8) | (g(2) << 16) | (g(3) << 24)
487}
488
489#[cfg(test)]
490#[allow(
491    clippy::indexing_slicing,
492    clippy::float_cmp,
493    clippy::cast_possible_truncation,
494    clippy::cast_sign_loss,
495    clippy::similar_names
496)]
497mod tests {
498    use super::{Container, split};
499    use pdfrum_common::{DiagKind, Diagnostics};
500
501    /// A minimal PFB: text segment, binary segment, EOF.
502    fn pfb(text: &[u8], binary: &[u8]) -> Vec<u8> {
503        let mut v = vec![0x80, 1];
504        v.extend_from_slice(&(text.len() as u32).to_le_bytes());
505        v.extend_from_slice(text);
506        v.extend_from_slice(&[0x80, 2]);
507        v.extend_from_slice(&(binary.len() as u32).to_le_bytes());
508        v.extend_from_slice(binary);
509        v.extend_from_slice(&[0x80, 3]);
510        v
511    }
512
513    #[test]
514    fn pfb_and_pfa_agree() {
515        let mut d = Diagnostics::default();
516        let clear = b"%!PS-AdobeFont-1.0: T 1\n/FontName /T def\ncurrentfile eexec\n";
517        let binary = b"\x01\x02\x03\x04rest-of-the-private-dict";
518
519        let from_pfb = split(&pfb(clear, binary), &mut d).unwrap();
520        assert_eq!(from_pfb.container, Container::Pfb);
521        assert_eq!(from_pfb.clear, clear);
522        assert_eq!(from_pfb.cipher, binary);
523
524        // The same font written as PFA: the binary section spelled in hex.
525        let mut pfa = clear.to_vec();
526        for b in binary {
527            pfa.extend_from_slice(format!("{b:02X}").as_bytes());
528        }
529        let from_pfa = split(&pfa, &mut d).unwrap();
530        assert_eq!(from_pfa.container, Container::Pfa);
531        assert_eq!(from_pfa.clear, clear);
532        assert_eq!(from_pfa.cipher, binary);
533    }
534
535    #[test]
536    fn truncated_pfb_segment_keeps_what_it_has() {
537        // Eat the two-byte EOF record and four bytes of the binary payload,
538        // so the declared length of 8 overruns what is there.
539        let mut good = pfb(b"%!PS-AdobeFont\n", b"abcdefgh");
540        good.truncate(good.len() - 6);
541        let mut d = Diagnostics::default();
542        let s = split(&good, &mut d).unwrap();
543        assert_eq!(s.cipher, b"abcd");
544        assert!(d.contains(&DiagKind::Type1PfbTruncated));
545    }
546
547    #[test]
548    fn bare_program_reads_as_pfa_shaped() {
549        let mut d = Diagnostics::default();
550        let s = split(
551            b"/FontName /T def\ncurrentfile eexec\n\x01\x02\x03\x04tail",
552            &mut d,
553        )
554        .unwrap();
555        assert_eq!(s.container, Container::Bare);
556        assert_eq!(s.cipher, b"\x01\x02\x03\x04tail");
557    }
558
559    #[test]
560    fn eexec_in_a_comment_or_string_is_not_the_keyword() {
561        let mut d = Diagnostics::default();
562        // The payload's first four bytes are not all hex digits, so it is read
563        // as binary rather than hex-decoded.
564        let s = split(
565            b"%!PS-AdobeFont\n% eexec here\n(eexec there) def\ncurrentfile eexec\r\n\x01\x02\x03\x04real",
566            &mut d,
567        )
568        .unwrap();
569        assert_eq!(s.cipher, b"\x01\x02\x03\x04real");
570        assert!(s.clear.ends_with(b"eexec\r\n"));
571    }
572
573    /// The partition ISO 32000-1 §9.9 table 127 asks for, on the wrapper that
574    /// breaks it: a PFB's framing is 6 bytes per record plus a 2-byte end
575    /// marker, and none of it is font data.
576    #[test]
577    fn pfb_font_file_drops_the_framing_and_partitions_what_is_left() {
578        let clear = b"%!PS-AdobeFont-1.0: T 1\ncurrentfile eexec\n";
579        let binary = b"\x01\x02\x03\x04private";
580        let mut wrapped = pfb(clear, binary);
581        // The trailer PFB files carry after the binary: a text record holding
582        // the 512 zeros and `cleartomark`.
583        let trailer = {
584            let mut t = vec![b'0'; 512];
585            t.extend_from_slice(b"\ncleartomark\n");
586            t
587        };
588        wrapped.truncate(wrapped.len() - 2); // drop the 0x80 0x03 marker
589        wrapped.extend_from_slice(&[0x80, 1]);
590        wrapped.extend_from_slice(&(trailer.len() as u32).to_le_bytes());
591        wrapped.extend_from_slice(&trailer);
592        wrapped.extend_from_slice(&[0x80, 3]);
593
594        let file = super::font_file(&wrapped);
595        assert_eq!(
596            file.program.len() as u32,
597            file.length1 + file.length2 + file.length3,
598            "the three lengths must partition the stored program"
599        );
600        assert!(file.program.starts_with(b"%!"));
601        assert_eq!(file.length1 as usize, clear.len());
602        assert_eq!(file.length2 as usize, binary.len());
603        assert_eq!(file.length3 as usize, trailer.len());
604        assert_eq!(&file.program[..clear.len()], clear);
605        assert_eq!(
606            &file.program[clear.len()..clear.len() + binary.len()],
607            binary
608        );
609        // 20 bytes of framing — three 6-byte headers and the 2-byte end
610        // marker — are gone.
611        assert_eq!(file.program.len() + 20, wrapped.len());
612    }
613
614    /// A PFA has no wrapper: it is stored as-is, hex private portion included,
615    /// with the lengths measured off the `eexec` boundary and the trailer.
616    #[test]
617    fn pfa_font_file_is_stored_as_is_with_hex_kept_hex() {
618        let mut pfa = b"%!PS-AdobeFont-1.0: T 1\ncurrentfile eexec\n".to_vec();
619        let head = pfa.len();
620        pfa.extend_from_slice(b"41424344454647484950\n");
621        let cipher = pfa.len() - head;
622        let mut trailer = vec![b'0'; 512];
623        trailer.extend_from_slice(b"\ncleartomark\n");
624        pfa.extend_from_slice(&trailer);
625
626        let file = super::font_file(&pfa);
627        assert_eq!(file.program, pfa, "a raw program is stored unchanged");
628        assert_eq!(
629            file.program.len() as u32,
630            file.length1 + file.length2 + file.length3
631        );
632        assert_eq!(file.length1 as usize, head);
633        assert_eq!(file.length2 as usize, cipher);
634        assert_eq!(file.length3 as usize, trailer.len());
635    }
636
637    /// No trailer, no `/Length3`: the partition still holds, with the
638    /// encrypted portion running to the end.
639    #[test]
640    fn a_program_without_a_trailer_has_length3_zero() {
641        let raw = b"%!FontType1\ncurrentfile eexec\n\x01\x02\x03\x04tail";
642        let file = super::font_file(raw);
643        assert_eq!(file.length3, 0);
644        assert_eq!(
645            file.program.len() as u32,
646            file.length1 + file.length2 + file.length3
647        );
648        assert_eq!(file.program, raw);
649    }
650
651    /// Bytes with no `eexec` at all are still stored and still partitioned —
652    /// as one clear-text portion, which is the only honest reading.
653    #[test]
654    fn a_program_without_eexec_is_all_length1() {
655        let file = super::font_file(b"not a font");
656        assert_eq!(file.length1, 10);
657        assert_eq!((file.length2, file.length3), (0, 0));
658        assert_eq!(file.program, b"not a font");
659    }
660
661    /// A truncated PFB keeps what it has, and the invariant survives: every
662    /// byte counted is a byte pushed.
663    #[test]
664    fn truncated_pfb_font_file_still_partitions() {
665        let mut good = pfb(b"%!PS-AdobeFont\ncurrentfile eexec\n", b"abcdefgh");
666        good.truncate(good.len() - 6);
667        let file = super::font_file(&good);
668        assert_eq!(
669            file.program.len() as u32,
670            file.length1 + file.length2 + file.length3
671        );
672        assert_eq!(file.length2, 4);
673    }
674
675    #[test]
676    fn hex_stops_at_the_first_non_hex_byte() {
677        let mut d = Diagnostics::default();
678        let s = split(b"%!FontType1\neexec\n4142 4344 zz9999", &mut d).unwrap();
679        assert_eq!(s.cipher, b"ABCD");
680        assert!(d.contains(&DiagKind::Type1HexTruncated));
681    }
682}