//! Telling a FictionBook from any other XML. //! //! A FictionBook is an XML document whose root element is `FictionBook`. //! The root comes after the prolog — a declaration, comments, perhaps a //! doctype — so it is found by stepping over those rather than by matching //! at a fixed offset. //! //! The walk is over bytes, not text. A great many FictionBooks are written in //! windows-1251, which is not UTF-8 but spells its markup in ASCII all the //! same, and a reader that insisted on UTF-8 would call every one of them //! binary. use crate::BOM; /// What the root element of a FictionBook is called. const ROOT: &[u8] = b"FictionBook"; /// Whether the front of a file is the start of a FictionBook. /// /// `head` is however much of the front the caller read. A root element that /// does not begin inside it is not found, so the answer is no rather than a /// guess. pub fn is_fictionbook(head: &[u8]) -> bool { root_element(head).is_some_and(|name| local_name(name) == ROOT) } /// The name of the first element, after the prolog. fn root_element(head: &[u8]) -> Option<&[u8]> { let mut rest = head.strip_prefix(BOM).unwrap_or(head); loop { rest = rest.trim_ascii_start(); if let Some(after) = rest.strip_prefix(b"")?; } else if let Some(after) = rest.strip_prefix(b"")?; } else if let Some(after) = rest.strip_prefix(b"' | b'/') || after[end].is_ascii_whitespace(); return (end > 0 && ends_properly).then_some(&after[..end]); } } } /// What follows the first `marker`, if it is there at all. fn after_marker<'a>(rest: &'a [u8], marker: &[u8]) -> Option<&'a [u8]> { rest.windows(marker.len()) .position(|window| window == marker) .map(|at| &rest[at + marker.len()..]) } /// What follows a doctype, whose internal subset may hold `>` of its own. fn after_declaration(rest: &[u8]) -> Option<&[u8]> { let mut depth = 0usize; let mut quote = None; for (at, &byte) in rest.iter().enumerate() { match quote { Some(open) if byte == open => quote = None, Some(_) => {} None => match byte { b'"' | b'\'' => quote = Some(byte), b'[' => depth += 1, b']' => depth = depth.saturating_sub(1), b'>' if depth == 0 => return Some(&rest[at + 1..]), _ => {} }, } } None } /// The name without its namespace prefix. fn local_name(name: &[u8]) -> &[u8] { match name.iter().rposition(|byte| *byte == b':') { Some(at) => &name[at + 1..], None => name, } } fn is_name_byte(byte: u8) -> bool { byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.' | b':') || byte >= 0x80 } #[cfg(test)] mod tests { use super::*; const BOOK: &[u8] = b"\n\ \n\ The Mill"; #[test] fn a_fictionbook_is_recognised() { assert!(is_fictionbook(BOOK)); assert!(is_fictionbook(b"")); } #[test] fn the_prolog_is_stepped_over() { let head = b"\xef\xbb\xbf \r\n\ \n\ the mill\"> ]>\n\ \n\ "; assert!(is_fictionbook(head)); } #[test] fn a_prefixed_root_is_still_a_fictionbook() { assert!(is_fictionbook( b"" )); } #[test] fn markup_need_not_be_utf8() { // windows-1251 for "Мельница" in the title. let head = b"\ \xcc\xe5\xeb\xfc\xed\xe8\xf6\xe0"; assert!(is_fictionbook(head)); } #[test] fn other_xml_is_not_a_fictionbook() { assert!(!is_fictionbook( b"\n" )); assert!(!is_fictionbook(b"")); assert!(!is_fictionbook(b"")); assert!(!is_fictionbook(b"text before ")); assert!(!is_fictionbook(b"")); } #[test] fn a_root_the_window_cut_short_is_not_taken() { assert!(!is_fictionbook(b"")); } #[test] fn a_truncated_or_mangled_prolog_does_not_panic() { let head = b"\xef\xbb\xbf'> ] \"]\">\ "; assert!(is_fictionbook(head)); for cut in 0..head.len() { let _ = is_fictionbook(&head[..cut]); } for byte in 0..=255u8 { let _ = is_fictionbook(&[b'<', byte]); let _ = is_fictionbook(&[b'<', b'!', byte, b'>']); } } }