1use crate::error::Error;
21use pdfrum_common::{DiagKind, Diagnostics, Severity, hex_digit};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Container {
28 Pfb,
30 Pfa,
32 Bare,
34}
35
36#[derive(Debug, Clone)]
38pub struct Split {
39 pub container: Container,
41 pub clear: Vec<u8>,
44 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#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct FontFile {
63 pub program: Vec<u8>,
66 pub length1: u32,
68 pub length2: u32,
72 pub length3: u32,
75}
76
77#[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
94fn 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 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
154fn 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
178fn trailer_start(bytes: &[u8], after: usize) -> usize {
188 const TRAILER_ZEROS: usize = 512;
189 let tail = bytes.get(after..).unwrap_or_default();
190 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 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
217pub 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
246fn 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
257fn 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 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 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 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 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
349fn 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 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
383fn find_eexec(bytes: &[u8]) -> Option<usize> {
391 let mut i = 0usize;
392 while i < bytes.len() {
393 match bytes.get(i).copied() {
394 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 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
440fn 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
458fn 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 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 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 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 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 #[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 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); 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 assert_eq!(file.program.len() + 20, wrapped.len());
612 }
613
614 #[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 #[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 #[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 #[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}