1mod caa;
8mod convert;
9mod inspect;
10mod observe;
11
12pub use convert::{
13 ConversionOptions, ConversionReport, FONTS_REQUIRE_HNC8, Fonts, ImageCounts, OmittedPage,
14 convert, convert_with_ttkn_response, native_font_mode, needs_fonts,
15};
16pub use inspect::{
17 DocumentInfo, InspectOptions, PageVisitor, Structure, index_pdf, inspect, inspect_pages,
18 read_outline,
19};
20pub use observe::Progress;
21
22use crate::{Cancellation, Limits, RangedSource, Result, read_exact_at};
23use std::ops::Range;
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
27pub enum InputFormat {
28 Pdf,
29 Caj,
30 Kdh,
31 Nh,
32 Hn,
33 C8,
34 Teb,
35 Caa,
37}
38
39impl InputFormat {
40 pub const fn name(self) -> &'static str {
42 match self {
43 Self::Pdf => "PDF",
44 Self::Caj => "CAJ",
45 Self::Kdh => "KDH",
46 Self::Nh => "NH",
47 Self::Hn => "HN",
48 Self::C8 => "C8",
49 Self::Teb => "TEB",
50 Self::Caa => "CAA",
51 }
52 }
53
54 pub const fn is_convertible(self) -> bool {
57 matches!(
58 self,
59 Self::Pdf | Self::Caj | Self::Kdh | Self::Hn | Self::C8
60 )
61 }
62}
63
64pub const SIGNATURE_BYTES: usize = 5;
67
68pub const PDF_HEADER_SEARCH_BYTES: usize = 1024;
71
72const PDF_SIGNATURE: &[u8] = b"%PDF-";
73
74#[derive(Clone, Copy, Debug, Eq, PartialEq)]
76pub struct Detection {
77 pub format: InputFormat,
78 pub header_offset: u64,
81 pub bytes_read: u64,
83}
84
85pub fn detect_format(prefix: &[u8]) -> Option<InputFormat> {
99 leading_format(prefix).or_else(|| searched_format(prefix).map(|(format, _)| format))
100}
101
102fn searched_format(prefix: &[u8]) -> Option<(InputFormat, usize)> {
103 let prefix = &prefix[..prefix.len().min(PDF_HEADER_SEARCH_BYTES)];
104 if caa::recognizes(prefix) {
105 Some((InputFormat::Caa, 0))
106 } else {
107 prefix
108 .windows(PDF_SIGNATURE.len())
109 .position(|window| window == PDF_SIGNATURE)
110 .map(|offset| (InputFormat::Pdf, offset))
111 }
112}
113
114fn leading_format(prefix: &[u8]) -> Option<InputFormat> {
115 const SIGNATURES: [(&[u8], InputFormat); 6] = [
116 (PDF_SIGNATURE, InputFormat::Pdf),
117 (b"CAJ", InputFormat::Caj),
118 (b"KDH", InputFormat::Kdh),
119 (b"HN", InputFormat::Hn),
120 (b"\xc8\0\0\0", InputFormat::C8),
121 (b"TEB", InputFormat::Teb),
122 ];
123 SIGNATURES
124 .iter()
125 .find(|(signature, _)| prefix.starts_with(signature))
126 .map(|&(_, format)| format)
127}
128
129pub fn detect_source<S: RangedSource, C: Cancellation>(
137 source: &mut S,
138 limits: &Limits,
139 cancellation: &C,
140) -> Result<Option<Detection>> {
141 limits.validate()?;
142 let mut prefix = [0; PDF_HEADER_SEARCH_BYTES];
143 let size = source.size();
144 let leading = size.min(SIGNATURE_BYTES as u64) as usize;
145 read_prefix(source, &mut prefix, 0..leading, limits, cancellation)?;
146 if let Some(format) = leading_format(&prefix[..leading]) {
147 return Ok(Some(Detection {
148 format,
149 header_offset: 0,
150 bytes_read: leading as u64,
151 }));
152 }
153 let searched = size.min(PDF_HEADER_SEARCH_BYTES as u64) as usize;
154 read_prefix(source, &mut prefix, leading..searched, limits, cancellation)?;
155 Ok(
156 searched_format(&prefix[..searched]).map(|(format, offset)| Detection {
157 format,
158 header_offset: offset as u64,
159 bytes_read: searched as u64,
160 }),
161 )
162}
163
164fn read_prefix<S: RangedSource, C: Cancellation>(
165 source: &mut S,
166 prefix: &mut [u8],
167 range: Range<usize>,
168 limits: &Limits,
169 cancellation: &C,
170) -> Result<()> {
171 let start = range.start;
172 for (index, chunk) in prefix[range].chunks_mut(limits.io_chunk_bytes).enumerate() {
173 let offset = start + index * limits.io_chunk_bytes;
174 read_exact_at(source, offset as u64, chunk, limits, cancellation)?;
175 }
176 Ok(())
177}
178
179fn pdf_range(size: u64, header_offset: u64) -> crate::pdf::PdfRange {
181 crate::pdf::PdfRange {
182 offset: header_offset,
183 length: size - header_offset,
184 }
185}
186
187#[derive(Clone, Debug, Eq, PartialEq)]
189pub struct Bookmark {
190 pub depth: u32,
192 pub title: String,
193 pub page_index: u32,
195}
196
197pub trait BookmarkVisitor {
199 fn visit(&mut self, bookmark: Bookmark) -> Result<()>;
200}
201
202#[cfg(test)]
203mod tests {
204 use super::{
205 Detection, InputFormat, PDF_HEADER_SEARCH_BYTES, SIGNATURE_BYTES, detect_format,
206 detect_source,
207 };
208 use crate::ErrorKind;
209 use crate::test_support::{CancelAfter, NEVER};
210 use crate::{Error, Limits, RangedSource, Result};
211
212 #[test]
213 fn detects_offset_zero_signatures_and_a_displaced_pdf_header() {
214 let cases: [(&[u8], Option<InputFormat>); 16] = [
215 (b"%PDF-1.7", Some(InputFormat::Pdf)),
216 (b"CAJ\0", Some(InputFormat::Caj)),
217 (b"KDH 2.00", Some(InputFormat::Kdh)),
218 (b"HN\0\0", Some(InputFormat::Hn)),
219 (b"\xc8\0\0\0\x01", Some(InputFormat::C8)),
220 (b"TEB", Some(InputFormat::Teb)),
221 (b" %PDF-", Some(InputFormat::Pdf)),
222 (b"\xef\xbb\xbf%PDF-1.4", Some(InputFormat::Pdf)),
223 (b"%PDF", None),
224 (b"\xc8\0\0", None),
225 (b"NH\0\0", None),
226 (b" CAJ", None),
227 (b"\nKDH 2.00", None),
228 (b" TEB", None),
229 (b"caj", None),
230 (b"", None),
231 ];
232 for (prefix, expected) in cases {
233 assert_eq!(detect_format(prefix), expected, "{prefix:?}");
234 }
235 }
236
237 #[test]
238 fn displaced_pdf_header_must_fit_in_the_searched_prefix() {
239 let last = PDF_HEADER_SEARCH_BYTES - b"%PDF-".len();
240 let mut prefix = vec![b'x'; last];
241 prefix.extend_from_slice(b"%PDF-1.7");
242 assert_eq!(detect_format(&prefix), Some(InputFormat::Pdf));
243 prefix.insert(0, b'x');
244 assert_eq!(detect_format(&prefix), None);
245 }
246
247 #[test]
248 fn signature_bytes_cover_the_longest_signature() {
249 assert_eq!(
250 detect_format(&b"%PDF-1.7"[..SIGNATURE_BYTES]),
251 Some(InputFormat::Pdf)
252 );
253 assert_eq!(detect_format(&b"%PDF-"[..SIGNATURE_BYTES - 1]), None);
254 }
255
256 struct Source {
258 bytes: Vec<u8>,
259 size: u64,
260 reads: Vec<(u64, usize)>,
261 }
262
263 impl Source {
264 fn new(bytes: Vec<u8>) -> Self {
265 Self {
266 size: bytes.len() as u64,
267 bytes,
268 reads: Vec::new(),
269 }
270 }
271 }
272
273 impl RangedSource for Source {
274 fn size(&self) -> u64 {
275 self.size
276 }
277
278 fn read_at(&mut self, offset: u64, destination: &mut [u8]) -> Result<usize> {
279 self.reads.push((offset, destination.len()));
280 let start = (offset as usize).min(self.bytes.len());
281 let available = &self.bytes[start..];
282 let count = available.len().min(destination.len());
283 destination[..count].copy_from_slice(&available[..count]);
284 Ok(count)
285 }
286 }
287
288 fn detect(source: &mut Source, limits: &Limits) -> Result<Option<Detection>> {
289 detect_source(source, limits, &NEVER)
290 }
291
292 fn detection(format: InputFormat, header_offset: u64, bytes_read: u64) -> Option<Detection> {
293 Some(Detection {
294 format,
295 header_offset,
296 bytes_read,
297 })
298 }
299
300 #[test]
301 fn offset_zero_signatures_read_only_the_signature_prefix() {
302 let mut source = Source::new(b"CAJ\0 and %PDF- later".to_vec());
303 let found = detect(&mut source, &Limits::default()).unwrap();
304 assert_eq!(found, detection(InputFormat::Caj, 0, 5));
305 assert_eq!(source.reads, [(0, SIGNATURE_BYTES)]);
306
307 let mut short = Source::new(b"HN".to_vec());
308 let found = detect(&mut short, &Limits::default()).unwrap();
309 assert_eq!(found, detection(InputFormat::Hn, 0, 2));
310 }
311
312 #[test]
313 fn displaced_pdf_headers_are_located_within_the_bounded_prefix() {
314 let mut junk = vec![b'#'; 100];
315 junk.push(b'\n');
316 for (prefix, offset) in [
317 (b"\n".as_slice(), 1),
318 (b"\xef\xbb\xbf".as_slice(), 3),
319 (junk.as_slice(), 101),
320 ] {
321 let mut bytes = prefix.to_vec();
322 bytes.extend_from_slice(&[b"%PDF-1.7\n".as_slice(), &[b' '; 4096]].concat());
323 let mut source = Source::new(bytes);
324 let found = detect(&mut source, &Limits::default()).unwrap();
325 let searched = PDF_HEADER_SEARCH_BYTES as u64;
326 assert_eq!(found, detection(InputFormat::Pdf, offset, searched));
327 let tail = PDF_HEADER_SEARCH_BYTES - SIGNATURE_BYTES;
328 assert_eq!(source.reads, [(0, SIGNATURE_BYTES), (5, tail)]);
329 }
330 }
331
332 #[test]
333 fn the_search_reads_in_io_chunks_and_stops_at_the_source_end() {
334 let limits = Limits {
335 io_chunk_bytes: 4,
336 ..Limits::default()
337 };
338 let mut source = Source::new(b"\r\n%PDF-1.0".to_vec());
339 let found = detect(&mut source, &limits).unwrap();
340 assert_eq!(found, detection(InputFormat::Pdf, 2, 10));
341 assert_eq!(source.reads, [(0, 4), (4, 1), (5, 4), (9, 1)]);
342 }
343
344 #[test]
345 fn a_header_beyond_the_searched_prefix_or_an_empty_source_is_unrecognized() {
346 let mut bytes = vec![0; PDF_HEADER_SEARCH_BYTES - 4];
347 bytes.extend_from_slice(b"%PDF-1.7\n");
348 let mut late = Source::new(bytes);
349 assert_eq!(detect(&mut late, &Limits::default()).unwrap(), None);
350 assert_eq!(late.reads.last(), Some(&(5, PDF_HEADER_SEARCH_BYTES - 5)));
351
352 let mut empty = Source::new(Vec::new());
353 assert_eq!(detect(&mut empty, &Limits::default()).unwrap(), None);
354 assert!(empty.reads.is_empty());
355 }
356
357 #[test]
358 fn invalid_limits_short_sources_and_cancellation_are_errors() {
359 let mut source = Source::new(b"%PDF-1.7".to_vec());
360 let zero_chunk = Limits {
361 io_chunk_bytes: 0,
362 ..Limits::default()
363 };
364 assert!(detect(&mut source, &zero_chunk).is_err());
365 assert!(source.reads.is_empty());
366
367 let mut short = Source::new(b"\n\n\n\n\n%PD".to_vec());
368 short.size = 64;
369 let error = detect(&mut short, &Limits::default()).unwrap_err();
370 assert!(
371 matches!(
372 error,
373 Error {
374 kind: ErrorKind::Truncated { .. },
375 offset: Some(5),
376 ..
377 }
378 ),
379 "{error:?}"
380 );
381
382 let mut source = Source::new(b"%PDF-1.7".to_vec());
383 let error =
384 detect_source(&mut source, &Limits::default(), &CancelAfter::always()).unwrap_err();
385 assert!(
386 matches!(
387 error,
388 Error {
389 kind: ErrorKind::Cancelled,
390 ..
391 }
392 ),
393 "{error:?}"
394 );
395 }
396
397 const CAA: &[u8] = include_bytes!(concat!(
398 env!("CARGO_MANIFEST_DIR"),
399 "/../../tests/fixtures/target_descriptor.caa"
400 ));
401
402 #[test]
403 fn caa_requires_complete_observed_fields_within_the_probe_bound() {
404 let text = std::str::from_utf8(CAA).unwrap();
405 for bytes in [
406 CAA.to_vec(),
407 text.replace("DOCTYPE=NH", "DOCTYPE=KDH")
408 .replace('\n', "\r\n")
409 .into_bytes(),
410 [CAA, b"\r\n \t"].concat(),
411 ] {
412 assert_eq!(detect_format(&bytes), Some(InputFormat::Caa));
413 }
414 for end in 0..CAA.len() {
415 assert_eq!(detect_format(&CAA[..end]), None, "prefix {end}");
416 }
417 for (from, to) in [
418 ("[TARGET]", "[OTHER]"),
419 ("A1=1", "A1="),
420 ("D1=1", "D1=-1"),
421 ("A2=QQ==", "A2="),
422 ("A2=QQ==", "A2=Q!"),
423 ("D2=Qg==", "D2=你好"),
424 ("B1=0", "B1=1"),
425 ("C2=", "C2=unknown"),
426 ("B2=\n", ""),
427 ("A1=1", "A1=1\nA1=1"),
428 ("DOCTYPE=NH", "DOCTYPE=CAS"),
429 ("DOCTYPE=NH", "DOCTYPE=NHX"),
430 ] {
431 assert_eq!(
432 detect_format(text.replace(from, to).as_bytes()),
433 None,
434 "{to}"
435 );
436 }
437 assert_eq!(detect_format(&[CAA, b"X=extra\n"].concat()), None);
438 assert_eq!(detect_format(&[CAA, &[0xff]].concat()), None);
439 let long = text.replace("A2=QQ==", &format!("A2={}", "A".repeat(1024)));
440 assert_eq!(detect_format(long.as_bytes()), None);
441 }
442
443 #[test]
444 fn caa_detection_is_chunked_cancellable_and_bounded() {
445 let limits = Limits {
446 io_chunk_bytes: 3,
447 ..Limits::default()
448 };
449 let mut source = Source::new(CAA.to_vec());
450 assert_eq!(
451 detect(&mut source, &limits).unwrap(),
452 detection(InputFormat::Caa, 0, CAA.len() as u64)
453 );
454 assert!(source.reads.iter().all(|(_, len)| *len <= 3));
455 assert_eq!(
456 source.reads.iter().map(|(_, len)| len).sum::<usize>(),
457 CAA.len()
458 );
459 let error = detect_source(&mut source, &limits, &CancelAfter::new(2)).unwrap_err();
460 assert!(matches!(error.kind, ErrorKind::Cancelled));
461
462 let mut source = Source::new([b"[TARGET]\n".as_slice(), &[b'A'; 2048]].concat());
463 assert_eq!(detect(&mut source, &limits).unwrap(), None);
464 assert_eq!(
465 source.reads.iter().map(|(_, len)| len).sum::<usize>(),
466 PDF_HEADER_SEARCH_BYTES
467 );
468 }
469
470 #[test]
471 fn caa_inspection_has_unknown_counts_and_conversion_writes_nothing() {
472 let limits = Limits::default();
473 let mut source = CAA;
474 let info = super::inspect(
475 &mut source,
476 &super::InspectOptions::default(),
477 &limits,
478 &mut crate::NeverCancel,
479 )
480 .unwrap();
481 assert_eq!(info.format, InputFormat::Caa);
482 assert_eq!(info.page_count, None);
483 assert_eq!(info.bookmark_count, None);
484 assert_eq!(info.has_outline, None);
485 assert_eq!(info.input_bytes_read, CAA.len() as u64);
486 assert!(!info.format.is_convertible());
487 let mut output = Vec::new();
488 let error = super::convert(
489 &mut source,
490 &mut output,
491 super::ConversionOptions::default(),
492 &limits,
493 &mut crate::NeverCancel,
494 )
495 .unwrap_err();
496 assert!(matches!(error.kind, ErrorKind::UnsupportedFormat));
497 assert!(output.is_empty());
498 }
499}