use std::collections::BTreeMap;
use crate::checksum::verify_adler32;
use crate::cursor::Cursor;
use crate::encoding::TextEncoding;
use crate::error::{Error, Result};
use crate::limits::MAX_HEADER_XML_BYTES;
use crate::source::FileSource;
use crate::types::{ContainerKind, Header};
#[derive(Debug, Clone)]
pub struct HeaderSection {
pub header: Header,
pub keyword_section_offset: u64,
}
pub fn parse_header(source: &FileSource, kind: ContainerKind) -> Result<HeaderSection> {
let len_bytes = source.read_exact_at(0, 4, "header length")?;
let mut cursor = Cursor::new(&len_bytes);
let xml_len = cursor.read_u32_be("header length")? as usize;
let total_len = 4usize
.checked_add(xml_len)
.and_then(|value| value.checked_add(4))
.ok_or(Error::InvalidFormat("header size overflow"))?;
let bytes = source.read_exact_at(0, total_len, "header section")?;
parse_header_bytes(&bytes, kind)
}
pub fn parse_header_bytes(bytes: &[u8], kind: ContainerKind) -> Result<HeaderSection> {
let mut cursor = Cursor::new(bytes);
let xml_len = cursor.read_u32_be("header length")? as usize;
if xml_len > MAX_HEADER_XML_BYTES {
return Err(Error::LimitExceeded {
limit: "header_xml_bytes",
value: xml_len as u64,
max: MAX_HEADER_XML_BYTES as u64,
});
}
let xml_bytes = cursor.read_bytes(xml_len, "header xml")?;
let expected = cursor.read_u32_le("header checksum")?;
verify_adler32("header xml", xml_bytes, expected)?;
let raw_xml = TextEncoding::Utf16Le.decode(xml_bytes, "header xml")?;
let raw_xml = raw_xml.trim_matches('\0').trim().to_owned();
let (tag_name, attributes) = parse_single_tag(&raw_xml)?;
build_header_section(kind, xml_len, raw_xml, tag_name, attributes)
}
fn build_header_section(
kind: ContainerKind,
xml_len: usize,
raw_xml: String,
tag_name: String,
attributes: BTreeMap<String, String>,
) -> Result<HeaderSection> {
match (kind, tag_name.as_str()) {
(ContainerKind::Mdx, "Dictionary") | (ContainerKind::Mdd, "Library_Data") => {}
_ => return Err(Error::InvalidFormat("unexpected top-level header tag")),
}
let generated_by_engine_version = attributes
.get("GeneratedByEngineVersion")
.cloned()
.ok_or(Error::InvalidFormat("missing GeneratedByEngineVersion"))?;
let required_engine_version = attributes
.get("RequiredEngineVersion")
.cloned()
.unwrap_or_else(|| generated_by_engine_version.clone());
let encrypted = attributes
.get("Encrypted")
.and_then(|value| {
if value.eq_ignore_ascii_case("yes") {
Some(1)
} else if value.eq_ignore_ascii_case("no") {
Some(0)
} else {
value.parse::<u8>().ok()
}
})
.unwrap_or(0);
let header = Header {
raw_xml,
tag_name,
generated_by_engine_version,
required_engine_version,
encoding_label: attributes
.get("Encoding")
.cloned()
.filter(|value| !value.is_empty()),
format: attributes
.get("Format")
.cloned()
.filter(|value| !value.is_empty()),
key_case_sensitive: parse_yes_no(attributes.get("KeyCaseSensitive")),
strip_key: parse_yes_no(attributes.get("StripKey")),
encrypted,
register_by: attributes
.get("RegisterBy")
.cloned()
.filter(|value| !value.is_empty()),
reg_code: attributes
.get("RegCode")
.cloned()
.filter(|value| !value.is_empty()),
description: attributes
.get("Description")
.cloned()
.filter(|value| !value.is_empty()),
title: attributes
.get("Title")
.cloned()
.filter(|value| !value.is_empty()),
creation_date: attributes
.get("CreationDate")
.cloned()
.filter(|value| !value.is_empty()),
attributes,
};
Ok(HeaderSection {
header,
keyword_section_offset: 4 + xml_len as u64 + 4,
})
}
fn parse_single_tag(xml: &str) -> Result<(String, BTreeMap<String, String>)> {
let bytes = xml.as_bytes();
if !bytes.starts_with(b"<") {
return Err(Error::InvalidFormat("header xml must start with '<'"));
}
let mut index = 1usize;
while index < bytes.len() && !bytes[index].is_ascii_whitespace() && bytes[index] != b'>' {
index += 1;
}
let tag_name = xml[1..index].to_owned();
let mut attrs = BTreeMap::new();
loop {
skip_ascii_whitespace(bytes, &mut index);
if index >= bytes.len() {
return Err(Error::InvalidFormat("unterminated header xml tag"));
}
match bytes[index] {
b'/' => {
index += 1;
if bytes.get(index) == Some(&b'>') {
break;
}
return Err(Error::InvalidFormat("invalid self-closing header tag"));
}
b'>' => break,
_ => {}
}
let name_start = index;
while index < bytes.len()
&& !bytes[index].is_ascii_whitespace()
&& bytes[index] != b'='
&& bytes[index] != b'>'
{
index += 1;
}
let name = xml[name_start..index].to_owned();
skip_ascii_whitespace(bytes, &mut index);
if bytes.get(index) != Some(&b'=') {
return Err(Error::InvalidFormat("expected '=' in header xml attribute"));
}
index += 1;
skip_ascii_whitespace(bytes, &mut index);
if bytes.get(index) != Some(&b'"') {
return Err(Error::InvalidFormat("expected quoted header xml attribute"));
}
index += 1;
let value_start = index;
while index < bytes.len() && bytes[index] != b'"' {
index += 1;
}
if index >= bytes.len() {
return Err(Error::InvalidFormat(
"unterminated header xml attribute value",
));
}
let value = unescape_xml(&xml[value_start..index])?;
attrs.insert(name, value);
index += 1;
}
Ok((tag_name, attrs))
}
fn skip_ascii_whitespace(bytes: &[u8], index: &mut usize) {
while *index < bytes.len() && bytes[*index].is_ascii_whitespace() {
*index += 1;
}
}
fn parse_yes_no(value: Option<&String>) -> bool {
value
.map(|value| value.eq_ignore_ascii_case("yes"))
.unwrap_or(false)
}
fn unescape_xml(value: &str) -> Result<String> {
let mut out = String::with_capacity(value.len());
let mut chars = value.chars().peekable();
while let Some(ch) = chars.next() {
if ch != '&' {
out.push(ch);
continue;
}
let mut entity = String::new();
while let Some(next) = chars.next() {
if next == ';' {
break;
}
entity.push(next);
}
match entity.as_str() {
"amp" => out.push('&'),
"lt" => out.push('<'),
"gt" => out.push('>'),
"quot" => out.push('"'),
"apos" => out.push('\''),
_ if entity.starts_with("#x") => {
let codepoint = u32::from_str_radix(&entity[2..], 16)
.map_err(|_| Error::InvalidFormat("invalid hex xml entity"))?;
let ch = char::from_u32(codepoint)
.ok_or(Error::InvalidFormat("invalid hex xml codepoint"))?;
out.push(ch);
}
_ if entity.starts_with('#') => {
let codepoint = entity[1..]
.parse::<u32>()
.map_err(|_| Error::InvalidFormat("invalid decimal xml entity"))?;
let ch = char::from_u32(codepoint)
.ok_or(Error::InvalidFormat("invalid decimal xml codepoint"))?;
out.push(ch);
}
_ => return Err(Error::InvalidFormat("unsupported xml entity")),
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_single_tag_attributes() {
let xml =
r#"<Dictionary Encoding="UTF-8" StripKey="Yes" Description="a <b>c</b>"/>"#;
let (tag, attrs) = parse_single_tag(xml).unwrap();
assert_eq!(tag, "Dictionary");
assert_eq!(attrs.get("Encoding").unwrap(), "UTF-8");
assert_eq!(attrs.get("StripKey").unwrap(), "Yes");
assert_eq!(attrs.get("Description").unwrap(), "a <b>c</b>");
}
}