1use std::fs::File;
4use std::io::Read;
5use std::path::Path;
6
7use quick_xml::events::{BytesStart, Event};
8use quick_xml::{Reader, XmlVersion};
9
10use crate::error::{Error, Result};
11use crate::header::Header;
12use crate::keyword::FitsKeyword;
13use crate::property::Property;
14use crate::value::Value;
15
16pub(crate) const SIGNATURE: &[u8; 8] = b"XISF0100";
18
19pub(crate) const MAX_HEADER_LEN: usize = 8 * 1024 * 1024;
21
22impl Header {
23 pub fn parse(bytes: &[u8]) -> Result<Self> {
50 let (start, end) = split_preamble(bytes)?;
51 let xml = std::str::from_utf8(&bytes[start..end])?;
52 parse_xml(xml)
53 }
54
55 pub fn read_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
76 let mut file = File::open(path)?;
77
78 let mut preamble = [0u8; 16];
79 file.read_exact(&mut preamble)?;
80 if &preamble[0..8] != SIGNATURE {
81 return Err(Error::InvalidSignature);
82 }
83 let xml_len =
84 u32::from_le_bytes([preamble[8], preamble[9], preamble[10], preamble[11]]) as usize;
85 if xml_len > MAX_HEADER_LEN {
86 return Err(Error::HeaderTooLarge {
87 len: xml_len,
88 max: MAX_HEADER_LEN,
89 });
90 }
91
92 let mut buf = vec![0u8; 16 + xml_len];
93 buf[..16].copy_from_slice(&preamble);
94 file.read_exact(&mut buf[16..])?;
95 Self::parse(&buf)
96 }
97}
98
99pub(crate) fn split_preamble(bytes: &[u8]) -> Result<(usize, usize)> {
102 if bytes.len() < 16 {
103 return Err(Error::TooSmall {
104 needed: 16,
105 got: bytes.len(),
106 });
107 }
108 if &bytes[0..8] != SIGNATURE {
109 return Err(Error::InvalidSignature);
110 }
111 let xml_len = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]) as usize;
112 if xml_len > MAX_HEADER_LEN {
114 return Err(Error::HeaderTooLarge {
115 len: xml_len,
116 max: MAX_HEADER_LEN,
117 });
118 }
119 let end = 16 + xml_len;
120 if bytes.len() < end {
121 return Err(Error::TooSmall {
122 needed: end,
123 got: bytes.len(),
124 });
125 }
126 Ok((16, end))
127}
128
129struct OpenProperty {
133 id: Option<String>,
134 prop: Property,
135 has_value_attr: bool,
136 span_start: usize,
139}
140
141pub(crate) struct XmlIndex {
146 pub keyword_spans: Vec<(usize, usize)>,
150 pub property_spans: Vec<(String, usize, usize)>,
153 pub image: std::result::Result<ImageInfo, String>,
157}
158
159pub(crate) struct ImageInfo {
162 pub location_span: (usize, usize),
165 pub offset: usize,
166 pub size: usize,
167 pub insertion_point: Option<usize>,
171}
172
173fn parse_xml(xml: &str) -> Result<Header> {
176 parse_xml_with_index(xml).map(|(header, _)| header)
177}
178
179pub(crate) fn parse_xml_with_index(xml: &str) -> Result<(Header, XmlIndex)> {
182 let mut reader = Reader::from_str(xml);
183 let mut header = Header::new();
184 let mut open_property: Option<OpenProperty> = None;
185 let mut keyword_spans = Vec::new();
186 let mut property_spans = Vec::new();
187
188 let mut image_count = 0usize;
189 let mut image_end_start: Option<usize> = None;
190 let mut locations: Vec<(bool, usize, usize, usize, usize)> = Vec::new();
195
196 loop {
197 let start = reader.buffer_position() as usize;
198 let event = reader.read_event()?;
199 let end = reader.buffer_position() as usize;
200 match event {
201 Event::Empty(e) => {
202 let local = e.local_name();
203 let tag = local.as_ref();
204 if tag.eq_ignore_ascii_case(b"FITSKeyword") {
205 if let Some(kw) = parse_keyword(&e)? {
206 header.keywords.push(kw);
207 keyword_spans.push((start, end));
208 }
209 } else if tag.eq_ignore_ascii_case(b"Property") {
210 let (id, prop, _) = parse_property(&e)?;
211 if let Some(id) = id {
212 property_spans.push((id.clone(), start, end));
213 header.properties.insert(id, prop);
214 }
215 } else {
216 if tag.eq_ignore_ascii_case(b"Image") {
217 image_count += 1;
218 }
219 record_location(xml, start, end, tag, &mut locations);
220 }
221 }
222 Event::Start(e) => {
223 let local = e.local_name();
224 let tag = local.as_ref();
225 if tag.eq_ignore_ascii_case(b"FITSKeyword") {
226 if let Some(kw) = parse_keyword(&e)? {
227 header.keywords.push(kw);
228 keyword_spans.push((start, end));
229 }
230 } else if tag.eq_ignore_ascii_case(b"Property") {
231 let (id, prop, has_value_attr) = parse_property(&e)?;
232 open_property = Some(OpenProperty {
233 id,
234 prop,
235 has_value_attr,
236 span_start: start,
237 });
238 } else {
239 if tag.eq_ignore_ascii_case(b"Image") {
240 image_count += 1;
241 }
242 record_location(xml, start, end, tag, &mut locations);
243 }
244 }
245 Event::Text(t) => {
246 if let Some(open) = open_property.as_mut() {
247 if !open.has_value_attr {
248 let text = t
249 .xml_content(XmlVersion::Implicit1_0)
250 .map_err(quick_xml::Error::from)?;
251 open.prop.value.push_str(&text);
252 }
253 }
254 }
255 Event::CData(c) => {
256 if let Some(open) = open_property.as_mut() {
257 if !open.has_value_attr {
258 let text = c.decode().map_err(quick_xml::Error::from)?;
259 open.prop.value.push_str(&text);
260 }
261 }
262 }
263 Event::End(e) => {
264 let local = e.local_name();
265 let tag = local.as_ref();
266 if tag.eq_ignore_ascii_case(b"Property") {
267 if let Some(open) = open_property.take() {
268 if let Some(id) = open.id {
269 property_spans.push((id.clone(), open.span_start, end));
270 header.properties.insert(id, open.prop);
271 }
272 }
273 } else if tag.eq_ignore_ascii_case(b"Image") {
274 image_end_start = Some(start);
275 }
276 }
277 Event::Eof => break,
278 _ => {}
279 }
280 }
281
282 let image = resolve_image(image_count, &locations, image_end_start);
283 Ok((
284 header,
285 XmlIndex {
286 keyword_spans,
287 property_spans,
288 image,
289 },
290 ))
291}
292
293fn record_location(
298 xml: &str,
299 start: usize,
300 end: usize,
301 tag: &[u8],
302 locations: &mut Vec<(bool, usize, usize, usize, usize)>,
303) {
304 let Some((value_start, value_end)) =
305 find_attr_value_span(&xml.as_bytes()[start..end], b"location")
306 else {
307 return;
308 };
309 let (abs_start, abs_end) = (start + value_start, start + value_end);
310 let Some((offset, size)) = xml
311 .get(abs_start..abs_end)
312 .and_then(parse_attachment_location)
313 else {
314 return;
315 };
316 locations.push((
317 tag.eq_ignore_ascii_case(b"Image"),
318 abs_start,
319 abs_end,
320 offset,
321 size,
322 ));
323}
324
325fn resolve_image(
329 image_count: usize,
330 locations: &[(bool, usize, usize, usize, usize)],
331 image_end_start: Option<usize>,
332) -> std::result::Result<ImageInfo, String> {
333 if image_count == 0 {
334 return Err("no <Image> element found".to_owned());
335 }
336 if image_count > 1 {
337 return Err(format!(
338 "found {image_count} <Image> elements; update_file supports exactly one"
339 ));
340 }
341 if locations.len() != 1 {
342 return Err(format!(
343 "found {} attachment location(s); update_file supports exactly one",
344 locations.len()
345 ));
346 }
347 let (is_image, value_start, value_end, offset, size) = locations[0];
348 if !is_image {
349 return Err("the attachment location is not on the <Image> element".to_owned());
350 }
351 Ok(ImageInfo {
352 location_span: (value_start, value_end),
353 offset,
354 size,
355 insertion_point: image_end_start,
356 })
357}
358
359fn parse_attachment_location(value: &str) -> Option<(usize, usize)> {
361 let rest = value.strip_prefix("attachment:")?;
362 let (offset, size) = rest.split_once(':')?;
363 Some((offset.parse().ok()?, size.parse().ok()?))
364}
365
366fn find_attr_value_span(tag: &[u8], attr_name: &[u8]) -> Option<(usize, usize)> {
371 let mut i = 0;
372 while i + attr_name.len() <= tag.len() {
373 if !tag[i..i + attr_name.len()].eq_ignore_ascii_case(attr_name) {
374 i += 1;
375 continue;
376 }
377 let before_ok = i == 0 || tag[i - 1].is_ascii_whitespace();
378 let mut j = i + attr_name.len();
379 if before_ok {
380 while j < tag.len() && tag[j].is_ascii_whitespace() {
381 j += 1;
382 }
383 if j < tag.len() && tag[j] == b'=' {
384 j += 1;
385 while j < tag.len() && tag[j].is_ascii_whitespace() {
386 j += 1;
387 }
388 if let Some("e) = tag.get(j).filter(|&&b| b == b'"' || b == b'\'') {
389 let value_start = j + 1;
390 if let Some(rel_end) = tag[value_start..].iter().position(|&b| b == quote) {
391 return Some((value_start, value_start + rel_end));
392 }
393 }
394 }
395 }
396 i += attr_name.len();
397 }
398 None
399}
400
401fn parse_keyword(e: &BytesStart) -> Result<Option<FitsKeyword>> {
405 let mut kw = FitsKeyword::default();
406 for attr in e.attributes() {
407 let attr = attr?;
408 let value = attr.normalized_value(XmlVersion::Implicit1_0)?;
409 match attr.key.as_ref() {
410 k if k.eq_ignore_ascii_case(b"name") => kw.name = value.into_owned(),
411 k if k.eq_ignore_ascii_case(b"value") => {
412 kw.value = classify_value(&value);
413 }
414 k if k.eq_ignore_ascii_case(b"comment") => kw.comment = value.into_owned(),
415 _ => {}
416 }
417 }
418 Ok((!kw.name.is_empty()).then_some(kw))
419}
420
421fn parse_property(e: &BytesStart) -> Result<(Option<String>, Property, bool)> {
426 let mut id = None;
427 let mut prop = Property::default();
428 let mut has_value_attr = false;
429 for attr in e.attributes() {
430 let attr = attr?;
431 let raw = attr.normalized_value(XmlVersion::Implicit1_0)?;
432 match attr.key.as_ref() {
433 k if k.eq_ignore_ascii_case(b"id") => id = Some(raw.into_owned()),
434 k if k.eq_ignore_ascii_case(b"type") => prop.type_ = raw.into_owned(),
435 k if k.eq_ignore_ascii_case(b"value") => {
436 prop.value = raw.into_owned();
437 has_value_attr = true;
438 }
439 k if k.eq_ignore_ascii_case(b"comment") => prop.comment = raw.into_owned(),
440 k if k.eq_ignore_ascii_case(b"format") => prop.format = raw.into_owned(),
441 _ => {}
442 }
443 }
444 Ok((id, prop, has_value_attr))
445}
446
447fn classify_value(text: &str) -> Value {
450 let bytes = text.as_bytes();
451 if bytes.len() >= 2 && bytes[0] == b'\'' && bytes[bytes.len() - 1] == b'\'' {
452 Value::Str(text[1..text.len() - 1].to_owned())
453 } else {
454 Value::Literal(text.to_owned())
455 }
456}
457
458#[cfg(test)]
459mod tests {
460 use super::*;
461
462 fn container(xml: &str, reserved: [u8; 4]) -> Vec<u8> {
464 let mut out = Vec::new();
465 out.extend_from_slice(SIGNATURE);
466 out.extend_from_slice(&(u32::try_from(xml.len()).unwrap()).to_le_bytes());
467 out.extend_from_slice(&reserved);
468 out.extend_from_slice(xml.as_bytes());
469 out
470 }
471
472 #[test]
473 fn classify_quoted_and_bare() {
474 assert_eq!(classify_value("'M31'"), Value::Str("M31".to_owned()));
475 assert_eq!(classify_value("''"), Value::Str(String::new()));
476 assert_eq!(classify_value("'''"), Value::Str("'".to_owned()));
478 assert_eq!(classify_value("300"), Value::Literal("300".to_owned()));
479 assert_eq!(classify_value("'"), Value::Literal("'".to_owned()));
480 assert_eq!(classify_value(""), Value::Literal(String::new()));
481 }
482
483 #[test]
484 fn too_small_preamble() {
485 assert!(matches!(
486 Header::parse(b"XISF01"),
487 Err(Error::TooSmall { needed: 16, got: 6 })
488 ));
489 }
490
491 #[test]
492 fn too_small_for_declared_length() {
493 let mut bytes = container("<xisf/>", [0; 4]);
494 bytes[8..12].copy_from_slice(&100_u32.to_le_bytes()); assert!(matches!(
496 Header::parse(&bytes),
497 Err(Error::TooSmall { needed: 116, .. })
498 ));
499 }
500
501 #[test]
502 fn header_too_large_is_rejected() {
503 let mut bytes = container("<xisf/>", [0; 4]);
504 let over = u32::try_from(MAX_HEADER_LEN + 1).unwrap();
505 bytes[8..12].copy_from_slice(&over.to_le_bytes());
506 assert!(matches!(
507 Header::parse(&bytes),
508 Err(Error::HeaderTooLarge { max, .. }) if max == MAX_HEADER_LEN
509 ));
510 }
511
512 #[test]
513 fn invalid_utf8_is_rejected() {
514 let mut bytes = container("<xisf></xisf>", [0; 4]);
515 bytes[20] = 0xFF;
516 assert!(matches!(Header::parse(&bytes), Err(Error::Utf8(_))));
517 }
518
519 #[test]
520 fn malformed_xml_is_rejected() {
521 let bytes = container("<xisf><Image></xisf>", [0; 4]);
522 assert!(matches!(Header::parse(&bytes), Err(Error::Xml(_))));
523 }
524
525 #[test]
526 fn reserved_bytes_are_ignored() {
527 let bytes = container("<xisf/>", [0xDE, 0xAD, 0xBE, 0xEF]);
528 assert!(Header::parse(&bytes).is_ok());
529 }
530
531 #[test]
532 fn attribute_names_are_case_insensitive() {
533 let xml = r#"<xisf><FITSKeyword NAME="GAIN" VALUE="100" COMMENT="c"/></xisf>"#;
534 let h = Header::parse(&container(xml, [0; 4])).unwrap();
535 assert_eq!(h.get_i64("GAIN").unwrap(), Some(100));
536 assert_eq!(h.keywords()[0].comment, "c");
537 }
538
539 #[test]
540 fn nameless_keywords_are_skipped() {
541 let xml = r#"<xisf>
542 <FITSKeyword value="'orphan'" comment="no name"/>
543 <FITSKeyword name="GAIN" value="100" comment=""/>
544 </xisf>"#;
545 let h = Header::parse(&container(xml, [0; 4])).unwrap();
546 assert_eq!(h.keywords().len(), 1);
547 assert_eq!(h.get_i64("GAIN").unwrap(), Some(100));
548 }
549
550 #[test]
551 fn unknown_attributes_and_elements_are_skipped() {
552 let xml = r#"<xisf>
553 <Metadata><Property id="XISF:CreatorApplication" type="String" value="PixInsight"/></Metadata>
554 <Image geometry="256:256:1" sampleFormat="UInt16" colorSpace="Gray" location="attachment:4096:131072">
555 <FITSKeyword name="GAIN" value="100" comment="" unknown="x"/>
556 <Resolution horizontal="72" vertical="72"/>
557 </Image>
558 </xisf>"#;
559 let h = Header::parse(&container(xml, [0; 4])).unwrap();
560 assert_eq!(h.get_i64("GAIN").unwrap(), Some(100));
561 assert_eq!(h.property("XISF:CreatorApplication"), Some("PixInsight"));
563 }
564}