1use super::observe::{Observer, Progress, refused};
7use super::{Bookmark, BookmarkVisitor, Detection, InputFormat, pdf_range};
8use crate::caj::parse_metadata;
9use crate::hnc8::{
10 ApplicationInfoReport, ApplicationInfoTail, Header, Hnc8Reader, ImageRecord, OutlineReport,
11 PageRecord, TextStructure, Variant,
12};
13use crate::kdh::{HEADER_SIGNATURE, KdhPdfSource};
14use crate::pdf::PdfIndex;
15use crate::{CountingSource, Error, ErrorKind, Limits, RangedSource, Result, read_exact_at};
16
17#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
19pub struct InspectOptions {
20 pub format: Option<InputFormat>,
22 pub bookmarks: bool,
25 pub structure: bool,
27}
28
29#[derive(Clone, Debug, Eq, PartialEq)]
31pub enum Structure {
32 Kdh { signature: Vec<u8>, supported: bool },
35 Hnc8 {
37 header: Header,
38 page_row_bytes: u64,
39 application_info: Option<ApplicationInfoTail>,
40 },
41}
42
43#[derive(Clone, Debug, Eq, PartialEq)]
45pub struct DocumentInfo {
46 pub format: InputFormat,
47 pub variant: Option<Variant>,
49 pub page_count: Option<u32>,
52 pub has_outline: Option<bool>,
54 pub bookmark_count: Option<u32>,
57 pub bookmarks: Option<Vec<Bookmark>>,
60 pub outline: OutlineReport,
63 pub application_info: ApplicationInfoReport,
66 pub structure: Option<Structure>,
68 pub input_bytes_read: u64,
70}
71
72impl DocumentInfo {
73 fn unknown(format: InputFormat) -> Self {
74 Self {
75 format,
76 variant: None,
77 page_count: None,
78 has_outline: None,
79 bookmark_count: None,
80 bookmarks: None,
81 outline: OutlineReport::default(),
82 application_info: ApplicationInfoReport::default(),
83 structure: None,
84 input_bytes_read: 0,
85 }
86 }
87
88 fn indexed(&mut self, index: &PdfIndex) {
89 self.page_count = Some(index.pages().len() as u32);
91 self.has_outline = Some(index.has_outlines());
92 }
93}
94
95pub fn inspect<S: RangedSource>(
102 source: &mut S,
103 options: &InspectOptions,
104 limits: &Limits,
105 progress: &mut dyn Progress,
106) -> Result<DocumentInfo> {
107 let observer = Observer::new(progress);
108 let mut tracked = observer.track(source);
109 let detection = observer.resolve(&mut tracked, options.format, limits)?;
110 let mut input_bytes_read = 0;
111 let mut info = read_info(
112 &mut CountingSource::new(&mut tracked, &mut input_bytes_read),
113 detection,
114 options,
115 limits,
116 &observer,
117 )?;
118 info.input_bytes_read = input_bytes_read.saturating_add(detection.bytes_read);
119 Ok(info)
120}
121
122fn read_info<S: RangedSource>(
123 source: &mut S,
124 detection: Detection,
125 options: &InspectOptions,
126 limits: &Limits,
127 observer: &Observer<'_>,
128) -> Result<DocumentInfo> {
129 let mut info = DocumentInfo::unknown(detection.format);
130 match detection.format {
131 InputFormat::Pdf => {
132 let range = pdf_range(source.size(), detection.header_offset);
133 info.indexed(&PdfIndex::open(source, range, limits, observer)?);
134 }
135 InputFormat::Kdh => {
136 if options.structure {
137 let mut signature =
140 vec![0; source.size().min(HEADER_SIGNATURE.len() as u64) as usize];
141 read_exact_at(source, 0, &mut signature, limits, observer)?;
142 let supported = signature == HEADER_SIGNATURE;
143 info.structure = Some(Structure::Kdh {
144 signature,
145 supported,
146 });
147 if !supported {
148 return Ok(info);
149 }
150 }
151 let mut decoded = KdhPdfSource::open(source, limits, observer)?;
152 let range = pdf_range(decoded.size(), 0);
153 info.indexed(&PdfIndex::open(&mut decoded, range, limits, observer)?);
154 }
155 InputFormat::Caj => {
156 let metadata = parse_metadata(source, limits, observer)?;
157 info.page_count = Some(metadata.page_count);
158 info.has_outline = Some(!metadata.bookmarks.is_empty());
159 info.bookmark_count = Some(metadata.bookmarks.len() as u32);
161 info.bookmarks = options.bookmarks.then_some(metadata.bookmarks);
162 }
163 InputFormat::Hn | InputFormat::C8 => {
164 inspect_hnc8(source, &mut info, options, limits, observer)?;
165 }
166 InputFormat::Nh | InputFormat::Teb | InputFormat::Caa => {}
167 }
168 Ok(info)
169}
170
171fn inspect_hnc8<S: RangedSource>(
175 source: &mut S,
176 info: &mut DocumentInfo,
177 options: &InspectOptions,
178 limits: &Limits,
179 observer: &Observer<'_>,
180) -> Result<()> {
181 let mut reader = Hnc8Reader::open(source, limits, observer)?;
182 let header = reader.header();
183 info.variant = Some(header.variant);
184 info.page_count = Some(header.page_count);
185 if options.structure {
186 info.structure = Some(Structure::Hnc8 {
187 header,
188 page_row_bytes: reader.page_row_bytes(),
189 application_info: reader.application_info_tail()?,
190 });
191 }
192 info.application_info = reader.application_info_report()?;
193 let Some(count) = reader.declared_bookmark_count() else {
194 return Ok(());
195 };
196 let map = |page: u32| Some(page - 1);
197 let outline = if options.bookmarks {
198 let mut collected = Collected::new(count, limits)?;
199 let outline = reader.visit_bookmarks(64, header.page_count, map, &mut collected)?;
200 info.bookmarks = Some(collected.items);
201 outline
202 } else {
203 reader.visit_bookmarks(64, header.page_count, map, &mut Counted)?
204 };
205 info.bookmark_count = Some(outline.written);
206 info.has_outline = Some(outline.written != 0);
207 info.outline = outline;
208 Ok(())
209}
210
211struct Collected<'l> {
214 items: Vec<Bookmark>,
215 limits: &'l Limits,
216 bytes: u64,
217}
218
219impl<'l> Collected<'l> {
220 fn new(count: u32, limits: &'l Limits) -> Result<Self> {
223 let bytes = u64::from(count) * size_of::<Bookmark>() as u64;
224 let mut items = Vec::new();
225 if count <= limits.max_bookmarks {
226 limits.check_allocation(bytes)?;
227 items
228 .try_reserve_exact(count as usize)
229 .map_err(|_| limits.allocation_refused("HN-A outline metadata", bytes))?;
230 }
231 Ok(Self {
232 items,
233 limits,
234 bytes,
235 })
236 }
237}
238
239impl BookmarkVisitor for Collected<'_> {
240 fn visit(&mut self, bookmark: Bookmark) -> Result<()> {
241 self.bytes += bookmark.title.capacity() as u64;
242 self.limits.check_allocation(self.bytes)?;
243 self.items.push(bookmark);
244 Ok(())
245 }
246}
247
248struct Counted;
250
251impl BookmarkVisitor for Counted {
252 fn visit(&mut self, _: Bookmark) -> Result<()> {
253 Ok(())
254 }
255}
256
257pub trait PageVisitor {
260 fn begin(&mut self) -> Result<()>;
262 fn page(&mut self, number: u32, row: Option<&PageRecord>) -> Result<()>;
264 fn image(&mut self, image: &ImageRecord) -> Result<()>;
266 fn end_page(
270 &mut self,
271 text: Option<&TextStructure>,
272 text_error: Option<&Error>,
273 error: Option<&Error>,
274 ) -> Result<()>;
275 fn finish(&mut self) -> Result<()>;
277}
278
279pub fn inspect_pages<S: RangedSource>(
290 source: &mut S,
291 info: &DocumentInfo,
292 limits: &Limits,
293 progress: &mut dyn Progress,
294 visitor: &mut dyn PageVisitor,
295) -> Result<bool> {
296 let (Some(_), Some(page_count)) = (info.variant, info.page_count) else {
297 return Ok(false);
298 };
299 let observer = Observer::new(progress);
300 let mut source = observer.track(source);
301 visitor.begin()?;
302 for number in 1..=page_count {
303 let mut reader = Hnc8Reader::probe_at_page(&mut source, limits, &observer, number)?;
304 let row = match reader.next_page() {
305 Ok(row) => row.expect("a probe opens at a declared page"),
306 Err(error) => {
307 let error = page_error(error)?;
308 visitor.page(number, None)?;
309 visitor.end_page(None, None, Some(&error))?;
310 continue;
311 }
312 };
313 visitor.page(number, Some(&row))?;
314 let error = loop {
315 match reader.next_image() {
316 Ok(Some(image)) => visitor.image(&image)?,
317 Ok(None) => break None,
318 Err(error) => break Some(page_error(error)?),
319 }
320 };
321 if error.is_some() {
322 visitor.end_page(None, None, error.as_ref())?;
323 continue;
324 }
325 match reader.inspect_text() {
326 Ok(text) => visitor.end_page(Some(&text), None, None)?,
327 Err(error) => visitor.end_page(None, Some(&page_error(error)?), None)?,
328 }
329 }
330 visitor.finish()?;
331 Ok(true)
332}
333
334fn page_error(error: Error) -> Result<Error> {
337 match error.kind {
338 ErrorKind::Cancelled | ErrorKind::Io(_) => Err(error),
339 _ => Ok(error),
340 }
341}
342
343pub fn read_outline<S: RangedSource>(
346 source: &mut S,
347 limits: &Limits,
348 progress: &mut dyn Progress,
349) -> Result<Vec<Bookmark>> {
350 let observer = Observer::new(progress);
351 let mut source = observer.track(source);
352 match observer.resolve(&mut source, None, limits)?.format {
353 InputFormat::Caj => Ok(parse_metadata(&mut source, limits, &observer)?.bookmarks),
354 _ => Err(refused()),
355 }
356}
357
358pub fn index_pdf<S: RangedSource>(
362 source: &mut S,
363 limits: &Limits,
364 progress: &mut dyn Progress,
365) -> Result<PdfIndex> {
366 let observer = Observer::new(progress);
367 let mut source = observer.track(source);
368 let detection = observer.resolve(&mut source, None, limits)?;
369 match detection.format {
370 InputFormat::Pdf => {
371 let range = pdf_range(source.size(), detection.header_offset);
372 PdfIndex::open(&mut source, range, limits, &observer)
373 }
374 _ => Err(refused()),
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381 use crate::{Context, NeverCancel};
382
383 fn hn_a(count: usize, levels: impl Fn(usize) -> u8) -> Vec<u8> {
384 let mut bytes = vec![0; 0x15c + count * 308 + 20];
385 bytes[..8].copy_from_slice(&[72, 78, 0, 0, 0x90, 1, 0, 0]);
386 bytes[0x90] = 1;
387 bytes[0x158] = count as u8;
388 for number in 0..count {
389 let at = 0x15c + number * 308;
390 bytes[at..at + 4].copy_from_slice(b"Root");
391 bytes[at + 280] = levels(number);
392 bytes[at + 304] = 1;
393 }
394 bytes
395 }
396
397 fn listing() -> InspectOptions {
398 InspectOptions {
399 bookmarks: true,
400 ..InspectOptions::default()
401 }
402 }
403
404 #[test]
405 fn outline_collection_accounts_for_records_and_retained_titles() {
406 let bytes = hn_a(1, |_| b'1');
407 for (max_bookmarks, max_allocation_bytes) in
408 [(0, 4096), (1, 1), (1, size_of::<Bookmark>() as u64)]
409 {
410 let limits = Limits {
411 max_bookmarks,
412 max_allocation_bytes,
413 io_chunk_bytes: 1,
414 ..Limits::default()
415 };
416 let error = inspect(&mut &bytes[..], &listing(), &limits, &mut NeverCancel)
417 .unwrap_err()
418 .to_string();
419 assert!(error.contains("limit"), "{error}");
420 }
421 }
422
423 #[test]
424 fn hn_a_outlines_are_listed_or_counted_with_the_same_validation() {
425 let bytes = hn_a(3, |number| if number == 0 { b'1' } else { b'0' });
427 let listed = inspect(
428 &mut &bytes[..],
429 &listing(),
430 &Limits::default(),
431 &mut NeverCancel,
432 )
433 .unwrap();
434 let counted = inspect(
435 &mut &bytes[..],
436 &InspectOptions::default(),
437 &Limits::default(),
438 &mut NeverCancel,
439 )
440 .unwrap();
441 assert_eq!(listed.bookmarks.as_ref().map(Vec::len), Some(1));
442 assert_eq!(counted.bookmarks, None);
443 for info in [&listed, &counted] {
444 assert_eq!(info.variant, Some(Variant::HnA));
445 assert_eq!((info.page_count, info.bookmark_count), (Some(1), Some(1)));
446 assert_eq!(info.has_outline, Some(true));
447 assert_eq!(info.outline.defects, 2);
448 assert!(info.input_bytes_read > 0);
449 }
450 }
451
452 #[test]
453 fn only_repeating_page_failures_end_the_page_report() {
454 let page = Context::Hnc8 {
455 variant: None,
456 page: Some(2),
457 image: None,
458 segment: None,
459 stage: None,
460 };
461 let io = std::io::Error::other("disk failed");
462 for error in [Error::cancelled(), Error::from(ErrorKind::Io(io))] {
463 assert!(page_error(error.at(7).within(page)).is_err());
464 }
465 let error = Error::malformed(7, "page has unread image records").within(page);
466 assert_eq!(
467 page_error(error).unwrap().to_string(),
468 "malformed HN/C8 at byte 7, page 2: page has unread image records"
469 );
470 }
471
472 #[test]
473 fn families_without_page_records_skip_the_page_report() {
474 struct Refuse;
475 impl PageVisitor for Refuse {
476 fn begin(&mut self) -> Result<()> {
477 unreachable!()
478 }
479 fn page(&mut self, _: u32, _: Option<&PageRecord>) -> Result<()> {
480 unreachable!()
481 }
482 fn image(&mut self, _: &ImageRecord) -> Result<()> {
483 unreachable!()
484 }
485 fn end_page(
486 &mut self,
487 _: Option<&TextStructure>,
488 _: Option<&Error>,
489 _: Option<&Error>,
490 ) -> Result<()> {
491 unreachable!()
492 }
493 fn finish(&mut self) -> Result<()> {
494 unreachable!()
495 }
496 }
497 let info = DocumentInfo::unknown(InputFormat::Teb);
498 let reported = inspect_pages(
499 &mut &b"TEB"[..],
500 &info,
501 &Limits::default(),
502 &mut NeverCancel,
503 &mut Refuse,
504 );
505 assert!(!reported.unwrap());
506 }
507}