1use super::inflate::{ExactInflate, InflateFault, InflateFaultKind};
10use super::{Hnc8Reader, Location, Variant, read_fixed};
11use crate::fallible::{len_u64, reserve_exact};
12use crate::hnc8::TEXT_DECODER_RESERVATION_BYTES;
13use crate::{Cancellation, Error, ErrorKind, RangedSource, Result};
14use std::fmt;
15
16pub(super) mod links;
17
18pub const MAX_APPLICATION_INFO_BYTES: u32 = 1024 * 1024;
20pub const MAX_APPLICATION_INFO_FIELD_BYTES: usize = 4096;
22
23const MARKER: &[u8] = b"APPINFOSIGN ";
24const MARKER_NAME: &[u8] = b"APPINFOSIGN";
25const TRAILER_WINDOW: u64 = 32;
27const HEADER_BYTES: u64 = 8;
28const CHUNK_BYTES: usize = 64 * 1024;
29const MAX_DEPTH: usize = 16;
30const BOM: &str = "\u{feff}";
31pub(super) const TRAILER_DEFECTS: [&str; 2] = [
34 "application-info trailer: marker is not followed by a final decimal offset",
35 "application-info trailer: offset overflows 64 bits",
36];
37
38#[derive(Clone, Debug, Default, Eq, PartialEq)]
41pub struct ApplicationInfo {
42 pub doi: Option<String>,
45 pub url: Option<String>,
47 pub note_count: u32,
49}
50
51#[derive(Clone, Copy, Debug, Eq, PartialEq)]
53pub struct ApplicationInfoDefect {
54 pub offset: u64,
56 pub field: &'static str,
57 pub reason: &'static str,
58}
59
60impl From<&Error> for ApplicationInfoDefect {
63 fn from(error: &Error) -> Self {
64 let (field, reason) = match &error.kind {
65 ErrorKind::Malformed => error
66 .reason
67 .split_once(": ")
68 .unwrap_or((error.reason, "malformed")),
69 ErrorKind::UnsupportedFormat => (error.reason, "unsupported"),
70 ErrorKind::Encrypted => (error.reason, "encrypted"),
71 ErrorKind::Truncated { .. } => (error.reason, "truncated"),
72 ErrorKind::LimitExceeded { resource, .. } => (*resource, "limit"),
73 ErrorKind::Io(_) => (error.reason, "source"),
74 ErrorKind::Cancelled => (error.reason, "cancelled"),
75 };
76 Self {
77 offset: error.offset.unwrap_or(0),
78 field,
79 reason,
80 }
81 }
82}
83
84impl fmt::Display for ApplicationInfoDefect {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 write!(
87 f,
88 "ignored C8 application-info package at byte {}: {}: {}",
89 self.offset, self.field, self.reason
90 )
91 }
92}
93
94#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
96pub enum ApplicationInfoStatus {
97 #[default]
99 Absent,
100 Read,
103 Ignored(ApplicationInfoDefect),
105}
106
107#[derive(Clone, Debug, Default, Eq, PartialEq)]
109pub struct ApplicationInfoReport {
110 pub info: Option<ApplicationInfo>,
111 pub status: ApplicationInfoStatus,
112}
113
114impl<S: RangedSource, C: Cancellation> Hnc8Reader<'_, S, C> {
115 pub fn application_info_report(&mut self) -> Result<ApplicationInfoReport> {
118 Ok(match self.application_info() {
119 Ok(Some(info)) => ApplicationInfoReport {
120 info: Some(info),
121 status: ApplicationInfoStatus::Read,
122 },
123 Ok(None) => ApplicationInfoReport::default(),
124 Err(error) if matches!(error.kind, ErrorKind::Cancelled) => return Err(error),
125 Err(error) => ApplicationInfoReport {
126 info: None,
127 status: ApplicationInfoStatus::Ignored(ApplicationInfoDefect::from(&error)),
128 },
129 })
130 }
131
132 pub fn application_info(&mut self) -> Result<Option<ApplicationInfo>> {
141 let Some((xml, loc)) = self.application_info_xml()? else {
142 return Ok(None);
143 };
144 scan(&xml, loc).map(Some)
145 }
146
147 fn application_info_xml(&mut self) -> Result<Option<(Vec<u8>, Location)>> {
148 if self.header.variant != Variant::C8 {
149 return Ok(None);
150 }
151 let loc = Location {
152 variant: Some(Variant::C8),
153 offset: 0,
154 page: None,
155 image: None,
156 };
157 if self.cancellation.is_cancelled() {
158 return Err(loc.cancelled());
159 }
160 let Some(package) = self.find_package(loc)? else {
161 return Ok(None);
162 };
163 let xml = self.inflate(package, loc.at(package.offset))?;
164 Ok(Some((xml, loc.at(package.offset))))
165 }
166
167 pub(super) fn application_info_trailer(&mut self) -> Result<Option<Trailer>> {
173 let loc = Location {
174 variant: Some(self.header.variant),
175 offset: 0,
176 page: None,
177 image: None,
178 };
179 let size = self.source.size();
180 let window_start = size.saturating_sub(TRAILER_WINDOW);
181 let mut window = [0; TRAILER_WINDOW as usize];
182 let window = &mut window[..(size - window_start) as usize];
183 let field = "application-info trailer";
184 read_fixed(
185 self.source,
186 self.limits,
187 self.cancellation,
188 window_start,
189 window,
190 loc.at(window_start),
191 field,
192 )?;
193 let digits = window
194 .iter()
195 .rev()
196 .take_while(|byte| byte.is_ascii_digit())
197 .count();
198 let marker_at = window.len().checked_sub(digits + MARKER.len());
199 if digits == 0 || marker_at.is_none_or(|at| &window[at..at + MARKER.len()] != MARKER) {
200 return match window
201 .windows(MARKER_NAME.len())
202 .position(|part| part == MARKER_NAME)
203 {
204 Some(at) => Err(loc
205 .at(window_start + len_u64(at))
206 .malformed(TRAILER_DEFECTS[0])),
207 None => Ok(None),
208 };
209 }
210 let marker = window_start + len_u64(marker_at.expect("checked marker position"));
211 let start = std::str::from_utf8(&window[window.len() - digits..])
212 .expect("ASCII digits")
213 .parse::<u64>()
214 .map_err(|_| loc.at(marker).malformed(TRAILER_DEFECTS[1]))?;
215 Ok(Some(Trailer { marker, start }))
216 }
217
218 fn find_package(&mut self, loc: Location) -> Result<Option<Package>> {
220 let Some(Trailer { marker, start }) = self.application_info_trailer()? else {
221 return Ok(None);
222 };
223 let lengths = "application-info lengths";
224 if start < self.header.page_index.offset + self.header.page_index.length {
225 return Err(loc.at(marker).malformed(
226 "application-info lengths: package starts inside the header or page index",
227 ));
228 }
229 if start > marker || marker - start < HEADER_BYTES {
230 return Err(loc.at(marker).truncated(
231 lengths,
232 HEADER_BYTES,
233 marker.saturating_sub(start),
234 ));
235 }
236 let mut header = [0; HEADER_BYTES as usize];
237 read_fixed(
238 self.source,
239 self.limits,
240 self.cancellation,
241 start,
242 &mut header,
243 loc.at(start),
244 lengths,
245 )?;
246 let decoded = u32::from_le_bytes(header[..4].try_into().expect("fixed field width"));
247 let compressed = u32::from_le_bytes(header[4..].try_into().expect("fixed field width"));
248 for (resource, value) in [
249 ("application-info decoded bytes", decoded),
250 ("application-info compressed bytes", compressed),
251 ] {
252 if value > MAX_APPLICATION_INFO_BYTES {
253 return Err(loc.at(start).limit(
254 resource,
255 u64::from(MAX_APPLICATION_INFO_BYTES),
256 u64::from(value),
257 ));
258 }
259 }
260 if decoded == 0 {
261 return Err(loc
262 .at(start)
263 .malformed("application-info lengths: declared empty package"));
264 }
265 let offset = start + HEADER_BYTES;
266 if offset + u64::from(compressed) != marker {
267 return Err(loc.at(start).malformed(
268 "application-info lengths: compressed length does not end at the marker",
269 ));
270 }
271 Ok(Some(Package {
272 offset,
273 compressed: u64::from(compressed),
274 decoded: u64::from(decoded),
275 }))
276 }
277
278 fn inflate(&mut self, package: Package, loc: Location) -> Result<Vec<u8>> {
280 let Package {
281 compressed,
282 decoded,
283 ..
284 } = package;
285 let field = "application-info zlib stream";
286 let chunk = len_u64(self.limits.io_chunk_bytes.min(CHUNK_BYTES)).min(compressed);
287 let planned = (decoded + 1) + chunk + TEXT_DECODER_RESERVATION_BYTES;
289 let resource = "application-info allocation bytes";
290 if planned > self.limits.max_allocation_bytes {
291 return Err(loc.limit(resource, self.limits.max_allocation_bytes, planned));
292 }
293 let refused = || loc.limit(resource, self.limits.max_allocation_bytes, planned);
294 let mut input = Vec::new();
295 reserve_exact(&mut input, chunk as usize, refused())?;
296 input.resize(chunk as usize, 0);
297 let mut output = Vec::new();
298 reserve_exact(&mut output, decoded as usize + 1, refused())?;
299 output.resize(decoded as usize + 1, 0);
300 let mut inflate = ExactInflate::new(package.offset, compressed, decoded);
301 let fault = |fault: InflateFault| {
302 let reason = match fault.kind {
303 InflateFaultKind::Invalid => {
304 "application-info zlib stream: invalid stream or checksum"
305 }
306 InflateFaultKind::Excess => {
307 "application-info zlib stream: output exceeds the declared length"
308 }
309 InflateFaultKind::EndMismatch => {
310 "application-info zlib stream: stream ends before the declared lengths"
311 }
312 InflateFaultKind::Stalled => "application-info zlib stream: truncated stream",
313 };
314 loc.at(fault.offset).malformed(reason)
315 };
316 loop {
317 if self.cancellation.is_cancelled() {
318 return Err(loc.at(inflate.position()).cancelled());
319 }
320 if let Some((at, length)) = inflate.next_read(input.len()) {
321 read_fixed(
322 self.source,
323 self.limits,
324 self.cancellation,
325 at,
326 &mut input[..length],
327 loc.at(at),
328 field,
329 )?;
330 }
331 let window = &mut output[inflate.total_out() as usize..];
332 let step = inflate.step(&input, window).map_err(fault)?;
333 inflate.check_length(&step).map_err(fault)?;
334 if inflate.finished(&step).map_err(fault)? {
335 output.truncate(decoded as usize);
336 return Ok(output);
337 }
338 }
339 }
340}
341
342#[derive(Clone, Copy)]
345pub(super) struct Trailer {
346 pub(super) marker: u64,
347 pub(super) start: u64,
348}
349
350#[derive(Clone, Copy)]
352struct Package {
353 offset: u64,
354 compressed: u64,
355 decoded: u64,
356}
357
358#[derive(Clone, Copy, Eq, PartialEq)]
359enum Field {
360 Doi,
361 Url,
362}
363
364fn is_space(byte: u8) -> bool {
365 matches!(byte, b' ' | b'\t' | b'\r' | b'\n')
366}
367
368fn is_name_byte(byte: u8) -> bool {
369 byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b':') || byte >= 0x80
370}
371
372fn scan(bytes: &[u8], loc: Location) -> Result<ApplicationInfo> {
376 scan_with(bytes, loc, &mut |_| Ok(()))
377}
378
379enum Event<'a> {
381 Start {
382 parents: &'a [&'a str],
383 name: &'a str,
384 empty: bool,
385 tag: &'a str,
386 },
387 End {
388 path: &'a [&'a str],
389 },
390 Text {
391 path: &'a [&'a str],
392 text: &'a str,
393 },
394}
395
396fn scan_with(
397 bytes: &[u8],
398 loc: Location,
399 observe: &mut impl FnMut(Event<'_>) -> Result<()>,
400) -> Result<ApplicationInfo> {
401 let bad = |reason| loc.malformed(reason);
402 let xml = std::str::from_utf8(bytes)
403 .map_err(|_| bad("application-info XML: package is not UTF-8"))?;
404 let body = xml.strip_prefix(BOM).unwrap_or(xml);
405 let mut info = ApplicationInfo::default();
406 let mut stack = [""; MAX_DEPTH];
407 let mut depth = 0;
408 let mut root_closed = false;
409 let mut seen = [false; 2];
410 let mut capture: Option<(Field, String)> = None;
411 let mut rest = body;
412 while !rest.is_empty() {
413 if !rest.starts_with('<') {
414 let end = rest.find('<').unwrap_or(rest.len());
415 let text = &rest[..end];
416 observe(Event::Text {
417 path: &stack[..depth],
418 text,
419 })?;
420 if let Some((_, value)) = &mut capture {
421 decode_text(text, value, loc)?;
422 } else if depth == 0 && !text.bytes().all(is_space) {
423 return Err(bad("application-info XML: text outside the root element"));
424 }
425 rest = &rest[end..];
426 } else if let Some(tail) = rest.strip_prefix("<?") {
427 let end = tail.find("?>").ok_or(bad(
428 "application-info XML: unterminated processing instruction",
429 ))?;
430 if rest.len() == body.len() && tail.starts_with("xml") {
431 check_encoding(&tail[3..end])
432 .ok_or(bad("application-info XML: declared encoding is not UTF-8"))?;
433 }
434 rest = &tail[end + 2..];
435 } else if let Some(tail) = rest.strip_prefix("<!--") {
436 let end = tail
437 .find("-->")
438 .ok_or(bad("application-info XML: unterminated comment"))?;
439 rest = &tail[end + 3..];
440 } else if let Some(tail) = rest.strip_prefix("<![CDATA[") {
441 let end = tail
442 .find("]]>")
443 .ok_or(bad("application-info XML: unterminated character data"))?;
444 if depth == 0 {
445 return Err(bad(
446 "application-info XML: character data outside the root element",
447 ));
448 }
449 if let Some((_, value)) = &mut capture {
450 push(value, &tail[..end], loc)?;
451 }
452 observe(Event::Text {
453 path: &stack[..depth],
454 text: &tail[..end],
455 })?;
456 rest = &tail[end + 3..];
457 } else if rest.starts_with("<!") {
458 return Err(bad("application-info XML: declarations are not accepted"));
459 } else if let Some(tail) = rest.strip_prefix("</") {
460 let end = tail
461 .find('>')
462 .ok_or(bad("application-info XML: unterminated end tag"))?;
463 let name = tail[..end].trim_end_matches([' ', '\t', '\r', '\n']);
464 if depth == 0 || stack[depth - 1] != name {
465 return Err(bad(
466 "application-info XML: end tag does not match the open element",
467 ));
468 }
469 observe(Event::End {
470 path: &stack[..depth],
471 })?;
472 depth -= 1;
473 root_closed = depth == 0;
474 if let Some((field, value)) = capture.take() {
475 store(&mut info, field, value);
476 }
477 rest = &tail[end + 1..];
478 } else {
479 let (name, empty, length) =
480 start_tag(rest).ok_or(bad("application-info XML: malformed start tag"))?;
481 if capture.is_some() {
482 return Err(bad(
483 "application-info XML: unexpected element inside DOI or DURL",
484 ));
485 }
486 if depth == 0 && (root_closed || name != "Package") {
487 return Err(bad("application-info XML: root element is not one Package"));
488 }
489 let path = &stack[..depth];
490 observe(Event::Start {
491 parents: path,
492 name,
493 empty,
494 tag: &rest[..length],
495 })?;
496 let field = match name {
497 "DOI" => Some(Field::Doi),
498 "DURL" => Some(Field::Url),
499 _ => None,
500 };
501 if let Some(field) = field.filter(|_| path == ["Package", "FileProperty-Package"]) {
502 if std::mem::replace(&mut seen[field as usize], true) {
503 return Err(bad("application-info XML: duplicate DOI or DURL"));
504 }
505 if !empty {
506 capture = Some((field, String::new()));
507 }
508 } else if name == "Item" && path == ["Package", "Note-Package", "NoteItems"] {
509 info.note_count += 1;
511 }
512 if !empty {
513 if depth == MAX_DEPTH {
514 return Err(bad("application-info XML: elements nest too deeply"));
515 }
516 stack[depth] = name;
517 depth += 1;
518 } else if depth == 0 {
519 root_closed = true;
520 }
521 rest = &rest[length..];
522 }
523 }
524 if !root_closed || depth != 0 {
525 return Err(bad(
526 "application-info XML: root element is missing or unclosed",
527 ));
528 }
529 Ok(info)
530}
531
532fn store(info: &mut ApplicationInfo, field: Field, value: String) {
533 let trimmed = value.trim_matches([' ', '\t', '\r', '\n']);
534 let value = (!trimmed.is_empty()).then(|| trimmed.to_owned());
535 match field {
536 Field::Doi => info.doi = value,
537 Field::Url => info.url = value,
538 }
539}
540
541fn start_tag(tag: &str) -> Option<(&str, bool, usize)> {
544 let bytes = tag.as_bytes();
545 let mut at = 1;
546 while bytes.get(at).copied().is_some_and(is_name_byte) {
547 at += 1;
548 }
549 let name = &tag[1..at];
550 if name.is_empty() {
551 return None;
552 }
553 loop {
554 let spaced = bytes.get(at).copied().is_some_and(is_space);
555 while bytes.get(at).copied().is_some_and(is_space) {
556 at += 1;
557 }
558 match *bytes.get(at)? {
559 b'>' => return Some((name, false, at + 1)),
560 b'/' => return (bytes.get(at + 1) == Some(&b'>')).then_some((name, true, at + 2)),
561 _ if !spaced => return None,
562 _ => {}
563 }
564 let attribute = at;
565 while bytes.get(at).copied().is_some_and(is_name_byte) {
566 at += 1;
567 }
568 while bytes.get(at).copied().is_some_and(is_space) {
569 at += 1;
570 }
571 if at == attribute || bytes.get(at) != Some(&b'=') {
572 return None;
573 }
574 at += 1;
575 while bytes.get(at).copied().is_some_and(is_space) {
576 at += 1;
577 }
578 let quote = *bytes.get(at).filter(|byte| matches!(byte, b'"' | b'\''))?;
579 let value = tag[at + 1..].find(char::from(quote))?;
580 if tag[at + 1..at + 1 + value].contains('<') {
581 return None;
582 }
583 at += value + 2;
584 }
585}
586
587fn check_encoding(declaration: &str) -> Option<()> {
589 let Some(at) = declaration.find("encoding") else {
590 return Some(());
591 };
592 let value = declaration[at + 8..]
593 .trim_start()
594 .strip_prefix('=')?
595 .trim_start();
596 let quote = value.chars().next().filter(|c| matches!(c, '"' | '\''))?;
597 let value = &value[1..];
598 let end = value.find(quote)?;
599 value[..end].eq_ignore_ascii_case("UTF-8").then_some(())
600}
601
602fn push(value: &mut String, text: &str, loc: Location) -> Result<()> {
603 let total = value.len() + text.len();
604 if total > MAX_APPLICATION_INFO_FIELD_BYTES {
605 return Err(loc.limit(
606 "application-info field bytes",
607 len_u64(MAX_APPLICATION_INFO_FIELD_BYTES),
608 len_u64(total),
609 ));
610 }
611 value.push_str(text);
612 Ok(())
613}
614
615fn decode_text(mut text: &str, value: &mut String, loc: Location) -> Result<()> {
618 while let Some(at) = text.find('&') {
619 push(value, &text[..at], loc)?;
620 let tail = &text[at + 1..];
621 let end = tail
622 .find(';')
623 .ok_or(loc.malformed("application-info XML: unterminated entity reference"))?;
624 let decoded = match &tail[..end] {
625 "lt" => Some('<'),
626 "gt" => Some('>'),
627 "amp" => Some('&'),
628 "quot" => Some('"'),
629 "apos" => Some('\''),
630 reference => character_reference(reference),
631 }
632 .ok_or(loc.malformed("application-info XML: unknown entity or character reference"))?;
633 push(value, decoded.encode_utf8(&mut [0; 4]), loc)?;
634 text = &tail[end + 1..];
635 }
636 push(value, text, loc)
637}
638
639fn character_reference(reference: &str) -> Option<char> {
641 let (digits, radix) = match reference.strip_prefix("#x") {
642 Some(hex) => (hex, 16),
643 None => (reference.strip_prefix('#')?, 10),
644 };
645 if digits.is_empty() || !digits.chars().all(|c| c.is_digit(radix)) {
646 return None;
647 }
648 u32::from_str_radix(digits, radix)
649 .ok()
650 .and_then(char::from_u32)
651 .filter(|&c| c != '\0')
652}
653
654#[cfg(test)]
655mod tests;