use std::fs::File;
use std::io::Read;
use std::path::Path;
use quick_xml::events::{BytesStart, Event};
use quick_xml::{Reader, XmlVersion};
use crate::error::{Error, Result};
use crate::header::Header;
use crate::keyword::FitsKeyword;
use crate::property::Property;
use crate::value::Value;
pub(crate) const SIGNATURE: &[u8; 8] = b"XISF0100";
pub(crate) const MAX_HEADER_LEN: usize = 8 * 1024 * 1024;
impl Header {
pub fn parse(bytes: &[u8]) -> Result<Self> {
if bytes.len() < 16 {
return Err(Error::TooSmall {
needed: 16,
got: bytes.len(),
});
}
if &bytes[0..8] != SIGNATURE {
return Err(Error::InvalidSignature);
}
let xml_len = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]) as usize;
if xml_len > MAX_HEADER_LEN {
return Err(Error::HeaderTooLarge {
len: xml_len,
max: MAX_HEADER_LEN,
});
}
let end = 16 + xml_len;
if bytes.len() < end {
return Err(Error::TooSmall {
needed: end,
got: bytes.len(),
});
}
let xml = std::str::from_utf8(&bytes[16..end])?;
parse_xml(xml)
}
pub fn read_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let mut file = File::open(path)?;
let mut preamble = [0u8; 16];
file.read_exact(&mut preamble)?;
if &preamble[0..8] != SIGNATURE {
return Err(Error::InvalidSignature);
}
let xml_len =
u32::from_le_bytes([preamble[8], preamble[9], preamble[10], preamble[11]]) as usize;
if xml_len > MAX_HEADER_LEN {
return Err(Error::HeaderTooLarge {
len: xml_len,
max: MAX_HEADER_LEN,
});
}
let mut buf = vec![0u8; 16 + xml_len];
buf[..16].copy_from_slice(&preamble);
file.read_exact(&mut buf[16..])?;
Self::parse(&buf)
}
}
struct OpenProperty {
id: Option<String>,
prop: Property,
has_value_attr: bool,
}
fn parse_xml(xml: &str) -> Result<Header> {
let mut reader = Reader::from_str(xml);
let mut header = Header::new();
let mut open_property: Option<OpenProperty> = None;
loop {
match reader.read_event()? {
Event::Empty(e) => {
let local = e.local_name();
let tag = local.as_ref();
if tag.eq_ignore_ascii_case(b"FITSKeyword") {
header.keywords.push(parse_keyword(&e)?);
} else if tag.eq_ignore_ascii_case(b"Property") {
let (id, prop, _) = parse_property(&e)?;
if let Some(id) = id {
header.properties.insert(id, prop);
}
}
}
Event::Start(e) => {
let local = e.local_name();
let tag = local.as_ref();
if tag.eq_ignore_ascii_case(b"FITSKeyword") {
header.keywords.push(parse_keyword(&e)?);
} else if tag.eq_ignore_ascii_case(b"Property") {
let (id, prop, has_value_attr) = parse_property(&e)?;
open_property = Some(OpenProperty {
id,
prop,
has_value_attr,
});
}
}
Event::Text(t) => {
if let Some(open) = open_property.as_mut() {
if !open.has_value_attr {
let text = t
.xml_content(XmlVersion::Implicit1_0)
.map_err(quick_xml::Error::from)?;
open.prop.value.push_str(&text);
}
}
}
Event::CData(c) => {
if let Some(open) = open_property.as_mut() {
if !open.has_value_attr {
let text = c.decode().map_err(quick_xml::Error::from)?;
open.prop.value.push_str(&text);
}
}
}
Event::End(e) if e.local_name().as_ref().eq_ignore_ascii_case(b"Property") => {
if let Some(OpenProperty {
id: Some(id), prop, ..
}) = open_property.take()
{
header.properties.insert(id, prop);
}
}
Event::Eof => break,
_ => {}
}
}
Ok(header)
}
fn parse_keyword(e: &BytesStart) -> Result<FitsKeyword> {
let mut kw = FitsKeyword::default();
for attr in e.attributes() {
let attr = attr?;
let value = attr.normalized_value(XmlVersion::Implicit1_0)?;
match attr.key.as_ref() {
k if k.eq_ignore_ascii_case(b"name") => kw.name = value.into_owned(),
k if k.eq_ignore_ascii_case(b"value") => {
kw.value = classify_value(&value);
}
k if k.eq_ignore_ascii_case(b"comment") => kw.comment = value.into_owned(),
_ => {}
}
}
Ok(kw)
}
fn parse_property(e: &BytesStart) -> Result<(Option<String>, Property, bool)> {
let mut id = None;
let mut prop = Property::default();
let mut has_value_attr = false;
for attr in e.attributes() {
let attr = attr?;
let raw = attr.normalized_value(XmlVersion::Implicit1_0)?;
match attr.key.as_ref() {
k if k.eq_ignore_ascii_case(b"id") => id = Some(raw.into_owned()),
k if k.eq_ignore_ascii_case(b"type") => prop.type_ = raw.into_owned(),
k if k.eq_ignore_ascii_case(b"value") => {
prop.value = raw.into_owned();
has_value_attr = true;
}
k if k.eq_ignore_ascii_case(b"comment") => prop.comment = raw.into_owned(),
k if k.eq_ignore_ascii_case(b"format") => prop.format = raw.into_owned(),
_ => {}
}
}
Ok((id, prop, has_value_attr))
}
fn classify_value(text: &str) -> Value {
let bytes = text.as_bytes();
if bytes.len() >= 2 && bytes[0] == b'\'' && bytes[bytes.len() - 1] == b'\'' {
Value::Str(text[1..text.len() - 1].to_owned())
} else {
Value::Literal(text.to_owned())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn container(xml: &str, reserved: [u8; 4]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(SIGNATURE);
out.extend_from_slice(&(u32::try_from(xml.len()).unwrap()).to_le_bytes());
out.extend_from_slice(&reserved);
out.extend_from_slice(xml.as_bytes());
out
}
#[test]
fn classify_quoted_and_bare() {
assert_eq!(classify_value("'M31'"), Value::Str("M31".to_owned()));
assert_eq!(classify_value("''"), Value::Str(String::new()));
assert_eq!(classify_value("'''"), Value::Str("'".to_owned()));
assert_eq!(classify_value("300"), Value::Literal("300".to_owned()));
assert_eq!(classify_value("'"), Value::Literal("'".to_owned()));
assert_eq!(classify_value(""), Value::Literal(String::new()));
}
#[test]
fn too_small_preamble() {
assert!(matches!(
Header::parse(b"XISF01"),
Err(Error::TooSmall { needed: 16, got: 6 })
));
}
#[test]
fn too_small_for_declared_length() {
let mut bytes = container("<xisf/>", [0; 4]);
bytes[8..12].copy_from_slice(&100_u32.to_le_bytes()); assert!(matches!(
Header::parse(&bytes),
Err(Error::TooSmall { needed: 116, .. })
));
}
#[test]
fn header_too_large_is_rejected() {
let mut bytes = container("<xisf/>", [0; 4]);
let over = u32::try_from(MAX_HEADER_LEN + 1).unwrap();
bytes[8..12].copy_from_slice(&over.to_le_bytes());
assert!(matches!(
Header::parse(&bytes),
Err(Error::HeaderTooLarge { max, .. }) if max == MAX_HEADER_LEN
));
}
#[test]
fn invalid_utf8_is_rejected() {
let mut bytes = container("<xisf></xisf>", [0; 4]);
bytes[20] = 0xFF;
assert!(matches!(Header::parse(&bytes), Err(Error::Utf8(_))));
}
#[test]
fn malformed_xml_is_rejected() {
let bytes = container("<xisf><Image></xisf>", [0; 4]);
assert!(matches!(Header::parse(&bytes), Err(Error::Xml(_))));
}
#[test]
fn reserved_bytes_are_ignored() {
let bytes = container("<xisf/>", [0xDE, 0xAD, 0xBE, 0xEF]);
assert!(Header::parse(&bytes).is_ok());
}
#[test]
fn attribute_names_are_case_insensitive() {
let xml = r#"<xisf><FITSKeyword NAME="GAIN" VALUE="100" COMMENT="c"/></xisf>"#;
let h = Header::parse(&container(xml, [0; 4])).unwrap();
assert_eq!(h.get_i64("GAIN").unwrap(), Some(100));
assert_eq!(h.keywords()[0].comment, "c");
}
#[test]
fn unknown_attributes_and_elements_are_skipped() {
let xml = r#"<xisf>
<Metadata><Property id="XISF:CreatorApplication" type="String" value="PixInsight"/></Metadata>
<Image geometry="256:256:1" sampleFormat="UInt16" colorSpace="Gray" location="attachment:4096:131072">
<FITSKeyword name="GAIN" value="100" comment="" unknown="x"/>
<Resolution horizontal="72" vertical="72"/>
</Image>
</xisf>"#;
let h = Header::parse(&container(xml, [0; 4])).unwrap();
assert_eq!(h.get_i64("GAIN").unwrap(), Some(100));
assert_eq!(h.property("XISF:CreatorApplication"), Some("PixInsight"));
}
}