astro_rs/fits/header.rs
1//! Provides tools to construct, serialize, and deserialize FITS files.
2
3use super::header_value::*;
4
5use std::fmt::Debug;
6use std::rc::Rc;
7
8use thiserror::Error;
9
10/// The expected keyword for the first header card of the primary HDU.
11pub const SIMPLE_KEYWORD: [u8; 8] = *b"SIMPLE ";
12/// The header keyword indicating the size of each value in the HDU data section.
13pub const BITPIX_KEYWORD: [u8; 8] = *b"BITPIX ";
14/// The header keyword indicating how many axes are present in the HDU data section.
15pub const NAXIS_KEYWORD: [u8; 8] = *b"NAXIS ";
16/// The header keyword indicating the end of the header section.
17pub const END_KEYWORD: [u8; 8] = *b"END ";
18/// The expected keyword for the first header card of each HDU following the primary.
19pub const XTENSION_KEYWORD: [u8; 8] = *b"XTENSION";
20
21pub(crate) const FITS_RECORD_LEN: usize = 2880;
22pub(crate) const HEADER_CARD_LEN: usize = 80;
23pub(crate) const HEADER_KEYWORD_LEN: usize = 8;
24
25/// An enumeration of errors that could occur when processing a FITS header element.
26#[derive(Debug, Error)]
27pub enum FitsHeaderError {
28 /// Indicates an unexpected length of bytes was encountered during processing.
29 #[error("unexpected byte count - expected {expected} bytes for {intent}, found {found}")]
30 InvalidLength {
31 /// The number of bytes expected by the operation.
32 expected: usize,
33 /// The number of bytes found by the operation.
34 found: usize,
35 /// The objective of the operation.
36 intent: String,
37 },
38 /// Indicates invalid bytes were encountered during processing.
39 #[error("expected valid string for {intent}, found {found:?}")]
40 DeserializationError {
41 /// The bytes that were found by the operation.
42 found: Vec<u8>,
43 /// The objective of the operation.
44 intent: String,
45 },
46 /// Indicates the expected type does not match the cached value type.
47 #[error("expected type does not match cached value type")]
48 InvalidType,
49}
50
51/// The header portion of an HDU.
52#[derive(Debug, Default, Clone)]
53pub struct FitsHeader {
54 /// The card images contained in the header.
55 pub cards: Vec<FitsHeaderCard>,
56}
57
58impl FitsHeader {
59 /// Constructs an empty header.
60 pub fn new() -> Self {
61 Self::default()
62 }
63
64 /// Constructs a FitsHeader from the given bytes.
65 ///
66 /// # Examples
67 ///
68 /// ```
69 /// use astro_rs::fits::*;
70 /// use std::rc::Rc;
71 ///
72 /// // default primary HDU header bytes
73 /// let bytes = *b"SIMPLE = T BITPIX = 8 NAXIS = 0 END ";
74 /// let mut header = FitsHeader::from_bytes(bytes.to_vec());
75 ///
76 /// assert!(*header
77 /// .get_card(SIMPLE_KEYWORD)
78 /// .and_then(|card| card.get_value::<bool>().ok())
79 /// .unwrap_or_default());
80 /// assert_eq!(
81 /// header
82 /// .get_card(BITPIX_KEYWORD)
83 /// .and_then(|card| card.get_value::<Bitpix>().ok()),
84 /// Some(Rc::new(Bitpix::U8))
85 /// );
86 /// assert_eq!(
87 /// header
88 /// .get_card(NAXIS_KEYWORD)
89 /// .and_then(|card| card.get_value::<u16>().ok()),
90 /// Some(Rc::new(0))
91 /// );
92 /// assert!(header.get_card(END_KEYWORD).is_some());
93 /// ```
94 pub fn from_bytes(raw: Vec<u8>) -> FitsHeader {
95 let raw_len = raw.len();
96 let num_cards = raw_len / HEADER_CARD_LEN;
97
98 let mut cards = Vec::with_capacity(num_cards);
99 for i in 0..num_cards {
100 let index = i * HEADER_CARD_LEN;
101 let card_slice: [u8; 80] = raw[index..index + HEADER_CARD_LEN].try_into().unwrap();
102 cards.push(FitsHeaderCard::from(card_slice));
103 }
104
105 FitsHeader { cards }
106 }
107
108 /// Serializes the header into bytes.
109 ///
110 /// # Examples
111 ///
112 /// ```
113 /// use astro_rs::fits::*;
114 ///
115 /// let hdu = primary_hdu::default();
116 /// let mut bytes = b"SIMPLE = T BITPIX = 8 NAXIS = 0 END ".to_vec();
117 /// bytes.resize(2880, b' ');
118 ///
119 /// assert_eq!(hdu.header.to_bytes(), bytes);
120 /// ```
121 pub fn to_bytes(self) -> Vec<u8> {
122 let mut result = Vec::with_capacity(FITS_RECORD_LEN);
123 let filled_cards = self.cards.len();
124 for card in self.cards {
125 let card_raw: [u8; HEADER_CARD_LEN] = card.into();
126 result.extend_from_slice(&card_raw);
127 }
128 if filled_cards < 36 {
129 result.resize(FITS_RECORD_LEN, b' ');
130 }
131 result
132 }
133
134 /// Searches the header cards for a match with the given keyword.
135 ///
136 /// # Examples
137 ///
138 /// ```
139 /// use astro_rs::fits::*;
140 ///
141 /// let mut hdu = primary_hdu::default();
142 /// assert!(hdu.header.get_card(SIMPLE_KEYWORD).is_some());
143 /// assert!(hdu.header.get_card(EXTNAME_KEYWORD).is_none());
144 /// ```
145 pub fn get_card<K: PartialEq<FitsHeaderKeyword>>(
146 &mut self,
147 keyword: K,
148 ) -> Option<&mut FitsHeaderCard> {
149 self.cards.iter_mut().find(|card| keyword == card.keyword)
150 }
151
152 /// Sets the value and comment of the card with the given keyword.
153 /// If a card already exists, the data is overwritten.
154 /// If a card does not exist, one is created.
155 ///
156 /// # Examples
157 ///
158 /// ```
159 /// use astro_rs::fits::*;
160 /// use std::rc::Rc;
161 ///
162 /// let mut header = FitsHeader::new();
163 /// header.set_card(SIMPLE_KEYWORD, true, None);
164 /// assert!(*header
165 /// .get_card(SIMPLE_KEYWORD)
166 /// .and_then(|card| card.get_value::<bool>().ok())
167 /// .unwrap_or_default());
168 ///
169 /// header.set_card(SIMPLE_KEYWORD, false, Some(String::from("FITS STANDARD")));
170 /// let mut card = header.get_card(SIMPLE_KEYWORD).unwrap();
171 /// assert!(!*card.get_value::<bool>()?);
172 /// assert_eq!(card.get_comment()?, Rc::new(String::from("FITS STANDARD")));
173 /// # Ok::<(), astro_rs::fits::FitsHeaderError>(())
174 /// ```
175 pub fn set_card<
176 K: PartialEq<FitsHeaderKeyword> + Into<FitsHeaderKeyword>,
177 T: FitsHeaderValue + 'static,
178 >(
179 &mut self,
180 keyword: K,
181 value: T,
182 comment: Option<String>,
183 ) -> Result<(), FitsHeaderError> {
184 let fits_keyword = keyword.into();
185 let new_card = FitsHeaderCard {
186 keyword: fits_keyword,
187 value: FitsHeaderValueContainer::new(value, comment)?,
188 };
189 if let Some(card) = self.get_card(fits_keyword) {
190 *card = new_card;
191 } else {
192 let index = if self
193 .cards
194 .last()
195 .map(|card| card.keyword == END_KEYWORD)
196 .unwrap_or_default()
197 {
198 self.cards.len() - 1
199 } else {
200 self.cards.len()
201 };
202 self.cards.insert(index, new_card);
203 }
204 Ok(())
205 }
206
207 /// Sets the value of the card with the given keyword.
208 /// If a card already exists, the value is overwritten, and the comment is retained.
209 /// If a card does not exist, one is created.
210 ///
211 /// # Examples
212 ///
213 /// ```
214 /// use astro_rs::fits::*;
215 /// use std::rc::Rc;
216 ///
217 /// let bytes = *b"SIMPLE = T / FITS STANDARD ";
218 /// let mut header = FitsHeader::from_bytes(bytes.to_vec());
219 /// header.set_value(SIMPLE_KEYWORD, false)?;
220 /// let mut card = header.get_card(SIMPLE_KEYWORD).unwrap();
221 /// assert!(!*card.get_value::<bool>()?);
222 /// assert_eq!(card.get_comment()?, Rc::new(String::from("FITS STANDARD")));
223 ///
224 /// header.set_value(BITPIX_KEYWORD, Bitpix::U8)?;
225 /// assert_eq!(
226 /// header
227 /// .get_card(BITPIX_KEYWORD)
228 /// .and_then(|card| card.get_value::<Bitpix>().ok()),
229 /// Some(Rc::new(Bitpix::U8))
230 /// );
231 /// # Ok::<(), astro_rs::fits::FitsHeaderError>(())
232 /// ```
233 pub fn set_value<K, T: FitsHeaderValue + 'static>(
234 &mut self,
235 keyword: K,
236 value: T,
237 ) -> Result<(), FitsHeaderError>
238 where
239 K: PartialEq<FitsHeaderKeyword> + Into<FitsHeaderKeyword>,
240 {
241 let fits_keyword = keyword.into();
242 if let Some(card) = self.get_card(fits_keyword) {
243 card.value.set_value(value)?;
244 } else {
245 let new_card = FitsHeaderCard {
246 keyword: fits_keyword,
247 value: FitsHeaderValueContainer::new(value, None)?,
248 };
249 let index = if self
250 .cards
251 .last()
252 .map(|card| card.keyword == END_KEYWORD)
253 .unwrap_or_default()
254 {
255 self.cards.len() - 1
256 } else {
257 self.cards.len()
258 };
259 self.cards.insert(index, new_card);
260 }
261 Ok(())
262 }
263
264 /// Sets the comment of the card with the given keyword.
265 /// If a card already exists, the comment is overwritten, and the value is retained.
266 /// If a card does not exist, this function has no effect.
267 ///
268 /// # Examples
269 ///
270 /// ```
271 /// use astro_rs::fits::*;
272 /// use std::rc::Rc;
273 ///
274 /// let mut hdu = primary_hdu::default();
275 /// hdu.header.set_comment(SIMPLE_KEYWORD, Some(String::from("FITS STANDARD")));
276 /// let mut card = hdu.header.get_card(SIMPLE_KEYWORD).unwrap();
277 /// assert!(*card.get_value::<bool>()?);
278 /// assert_eq!(card.get_comment()?, Rc::new(String::from("FITS STANDARD")));
279 ///
280 /// hdu.header.set_comment(EXTNAME_KEYWORD, Some(String::from("Error 404")));
281 /// assert!(hdu.header.get_card(EXTNAME_KEYWORD).is_none());
282 /// # Ok::<(), astro_rs::fits::FitsHeaderError>(())
283 /// ```
284 pub fn set_comment<K: PartialEq<FitsHeaderKeyword>>(
285 &mut self,
286 keyword: K,
287 comment: Option<String>,
288 ) -> Result<(), FitsHeaderError> {
289 if let Some(card) = self.get_card(keyword) {
290 card.value.set_comment(comment)?;
291 }
292 Ok(())
293 }
294}
295
296/// A card within an HDU header section.
297///
298/// # Examples
299///
300/// ```
301/// use astro_rs::fits::FitsHeaderCard;
302///
303/// let card_raw = *b"SIMPLE = T / FITS STANDARD ";
304/// let mut card = FitsHeaderCard::from(card_raw);
305///
306/// assert_eq!(*card.keyword(), "SIMPLE");
307/// // deserializes value and comment, discarding padding
308/// assert_eq!(*card.get_value::<bool>()?, true);
309/// assert_eq!(*card.get_comment()?, String::from("FITS STANDARD"));
310///
311/// // re-serialize the header card
312/// let comparison: [u8; 80] = card.into();
313/// assert_eq!(comparison, card_raw);
314/// # Ok::<(), astro_rs::fits::FitsHeaderError>(())
315/// ```
316#[derive(Debug, Clone)]
317pub struct FitsHeaderCard {
318 keyword: FitsHeaderKeyword,
319 value: FitsHeaderValueContainer,
320}
321
322impl FitsHeaderCard {
323 /// Gets the keyword of the header card.
324 pub fn keyword(&self) -> &FitsHeaderKeyword {
325 &self.keyword
326 }
327
328 /// Gets the value of the header card.
329 /// If the value has not yet been deserialized, the deserialization process is attempted.
330 /// If the process succeeds, the deserialized value is cached.
331 ///
332 /// # Examples
333 ///
334 /// ```
335 /// use astro_rs::fits::*;
336 ///
337 /// let mut card = FitsHeaderCard::from(
338 /// *b"SIMPLE = T ",
339 /// );
340 /// assert!(card.get_value::<Bitpix>().is_err());
341 /// assert!(card.get_value::<bool>().map(|value| *value).unwrap_or_default());
342 /// // value is now cached, deserialization is not attempted, but types differ
343 /// assert!(card.get_value::<u32>().is_err());
344 /// assert!(card.get_value::<bool>().map(|value| *value).unwrap_or_default());
345 /// ```
346 pub fn get_value<T: FitsHeaderValue + 'static>(&mut self) -> Result<Rc<T>, FitsHeaderError> {
347 self.value.get_value()
348 }
349
350 /// Gets the comment section of the header card.
351 ///
352 /// # Examples
353 ///
354 /// ```
355 /// use astro_rs::fits::FitsHeaderCard;
356 ///
357 /// let mut card = FitsHeaderCard::from(*b"SIMPLE = T / FITS STANDARD ");
358 /// assert_eq!(*card.get_comment()?, String::from("FITS STANDARD"));
359 /// # Ok::<(), astro_rs::fits::FitsHeaderError>(())
360 /// ```
361 pub fn get_comment(&mut self) -> Result<Rc<String>, FitsHeaderError> {
362 self.value.get_comment()
363 }
364}
365
366impl From<[u8; 80]> for FitsHeaderCard {
367 fn from(raw: [u8; 80]) -> Self {
368 let keyword_bytes: [u8; 8] = raw[0..HEADER_KEYWORD_LEN].try_into().unwrap();
369 let keyword = FitsHeaderKeyword::from(keyword_bytes);
370 let value_bytes: [u8; 72] = raw[HEADER_KEYWORD_LEN..HEADER_CARD_LEN].try_into().unwrap();
371 let value = FitsHeaderValueContainer::from(value_bytes);
372 FitsHeaderCard { keyword, value }
373 }
374}
375
376impl From<FitsHeaderCard> for [u8; 80] {
377 fn from(card: FitsHeaderCard) -> Self {
378 let mut result = [0; HEADER_CARD_LEN];
379 let keyword_raw: [u8; HEADER_KEYWORD_LEN] = card.keyword.into();
380 result[0..HEADER_KEYWORD_LEN].copy_from_slice(&keyword_raw);
381 let value_raw: [u8; 72] = card.value.into();
382 result[HEADER_KEYWORD_LEN..HEADER_CARD_LEN].copy_from_slice(&value_raw);
383
384 result
385 }
386}
387
388/// A FITS header keyword.
389/// This wrapper provides functions to interact with both raw arrays and strings.
390///
391/// # Examples
392///
393/// ```
394/// use astro_rs::fits::FitsHeaderKeyword;
395///
396/// let simple_keyword = FitsHeaderKeyword::from(*b"SIMPLE ");
397/// assert!(simple_keyword == "SIMPLE");
398/// assert!(simple_keyword == *b"SIMPLE ");
399///
400/// assert!(simple_keyword != "BITPIX");
401/// assert!(simple_keyword != *b"BITPIX ");
402/// ```
403#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
404pub struct FitsHeaderKeyword {
405 raw: [u8; 8],
406}
407
408impl FitsHeaderKeyword {
409 /// Appends the given number to the keyword.
410 /// If a number is already appended, it is replaced by the given number.
411 ///
412 /// # Examples
413 ///
414 /// ```
415 /// use astro_rs::fits::*;
416 ///
417 /// let mut naxis_keyword = FitsHeaderKeyword::from(NAXIS_KEYWORD);
418 /// naxis_keyword.append_number(1);
419 /// assert_eq!(naxis_keyword, "NAXIS1");
420 /// naxis_keyword.append_number(2);
421 /// assert_eq!(naxis_keyword, "NAXIS2");
422 ///
423 /// let mut tform_keyword = FitsHeaderKeyword::from(TFORM_KEYWORD);
424 /// tform_keyword.append_number(100);
425 /// assert_eq!(tform_keyword, "TFORM100");
426 /// tform_keyword.append_number(10);
427 /// assert_eq!(tform_keyword, "TFORM10");
428 /// ```
429 pub fn append_number(&mut self, number: u16) {
430 let mut i = 0;
431 while i < 8 {
432 let c = self.raw[i];
433 if c == b' ' || c.is_ascii_digit() {
434 break;
435 }
436 i += 1;
437 }
438 if number > 99 {
439 self.raw[i] = (number / 100 + 48) as u8;
440 i += 1;
441 }
442 if number > 9 {
443 self.raw[i] = (number % 100 / 10 + 48) as u8;
444 i += 1;
445 }
446 self.raw[i] = (number % 10 + 48) as u8;
447 i += 1;
448 while i < 8 {
449 self.raw[i] = b' ';
450 i += 1;
451 }
452 }
453}
454
455impl From<[u8; 8]> for FitsHeaderKeyword {
456 fn from(raw: [u8; 8]) -> Self {
457 FitsHeaderKeyword { raw }
458 }
459}
460
461impl From<FitsHeaderKeyword> for [u8; 8] {
462 fn from(keyword: FitsHeaderKeyword) -> Self {
463 keyword.raw
464 }
465}
466
467impl PartialEq<&str> for FitsHeaderKeyword {
468 fn eq(&self, other: &&str) -> bool {
469 if other.len() > HEADER_KEYWORD_LEN {
470 return false;
471 }
472 let other_bytes = other.as_bytes();
473 for (index, b) in self.raw.iter().enumerate() {
474 if b != other_bytes.get(index).unwrap_or(&b' ') {
475 return false;
476 }
477 }
478
479 true
480 }
481}
482
483impl PartialEq<str> for FitsHeaderKeyword {
484 fn eq(&self, other: &str) -> bool {
485 if other.len() > HEADER_KEYWORD_LEN {
486 return false;
487 }
488 let other_bytes = other.as_bytes();
489 for (index, b) in self.raw.iter().enumerate() {
490 if b != other_bytes.get(index).unwrap_or(&b' ') {
491 return false;
492 }
493 }
494
495 true
496 }
497}
498
499impl PartialEq<[u8; 8]> for FitsHeaderKeyword {
500 fn eq(&self, other: &[u8; 8]) -> bool {
501 self.raw == *other
502 }
503}
504
505impl PartialEq<FitsHeaderKeyword> for [u8; 8] {
506 fn eq(&self, other: &FitsHeaderKeyword) -> bool {
507 *self == other.raw
508 }
509}
510
511/// A representation of the combined header card value and comment.
512/// This wrapper ensures that the total number of bytes between the value and comment will not exceed 72.
513#[derive(Debug, Clone)]
514pub struct FitsHeaderValueContainer {
515 raw: Vec<u8>,
516 value: Option<Rc<dyn FitsHeaderValue>>,
517 comment: Option<Rc<String>>,
518}
519
520impl FitsHeaderValueContainer {
521 /// Constructs a new FitsHeaderValueContainer with the given value and comment.
522 pub fn new<T: FitsHeaderValue + 'static>(
523 value: T,
524 comment: Option<String>,
525 ) -> Result<Self, FitsHeaderError> {
526 Self::check_comment_length(value.to_bytes(), comment.as_ref())?;
527 Ok(FitsHeaderValueContainer {
528 raw: Vec::new(),
529 value: Some(Rc::new(value)),
530 comment: comment.map(Rc::new),
531 })
532 }
533
534 /// Gets the value of the header card.
535 /// If the value has not yet been deserialized, the deserialization process is attempted.
536 /// If the process succeeds, the deserialized value is cached.
537 ///
538 /// # Examples
539 ///
540 /// ```
541 /// use astro_rs::fits::*;
542 ///
543 /// let mut card_value = FitsHeaderValueContainer::from(
544 /// *b"= T ",
545 /// );
546 /// assert!(card_value.get_value::<Bitpix>().is_err());
547 /// assert!(card_value.get_value::<bool>().map(|value| *value).unwrap_or_default());
548 /// // value is now cached, deserialization is not attempted, but types differ
549 /// assert!(card_value.get_value::<u32>().is_err());
550 /// assert!(card_value.get_value::<bool>().map(|value| *value).unwrap_or_default());
551 /// ```
552 pub fn get_value<T: FitsHeaderValue + 'static>(&mut self) -> Result<Rc<T>, FitsHeaderError> {
553 if let Some(data) = &self.value {
554 if !data.is::<T>() {
555 return Err(FitsHeaderError::InvalidType);
556 }
557 // safety: type is checked above
558 unsafe {
559 let ptr = Rc::into_raw(Rc::clone(data));
560 let new_ptr: *const T = ptr.cast();
561 Ok(Rc::from_raw(new_ptr))
562 }
563 } else {
564 let comment_start_index = self
565 .raw
566 .iter()
567 .position(|b| *b == b'/')
568 .unwrap_or(self.raw.len());
569 let mut value_bytes = self.raw[0..comment_start_index].to_vec();
570 // discard '=' prefix
571 if value_bytes.first() == Some(&b'=') {
572 value_bytes.remove(0);
573 }
574
575 let data = Rc::new(T::from_bytes(Self::trim_value(value_bytes))?);
576 // only remove bytes from raw if deserialization is successful
577 self.raw = self.raw.split_off(comment_start_index);
578 let ret = Rc::clone(&data);
579 self.value = Some(data);
580 Ok(ret)
581 }
582 }
583
584 /// Sets the value of the header card.
585 pub fn set_value<T: FitsHeaderValue + 'static>(
586 &mut self,
587 value: T,
588 ) -> Result<(), FitsHeaderError> {
589 let comment = match (self.value.as_ref(), self.comment.as_ref()) {
590 (None, None) => {
591 let comment = self.get_comment()?.to_string();
592 self.raw.clear();
593 comment
594 }
595 (None, Some(comment)) => {
596 self.raw.clear();
597 comment.to_string()
598 }
599 (Some(_), None) => self.get_comment()?.to_string(),
600 (Some(_), Some(comment)) => comment.to_string(),
601 };
602 Self::check_comment_length(value.to_bytes(), Some(&comment))?;
603 self.value = Some(Rc::new(value));
604 Ok(())
605 }
606
607 /// Gets the comment section of the header card.
608 ///
609 /// # Examples
610 ///
611 /// ```
612 /// use astro_rs::fits::FitsHeaderValueContainer;
613 ///
614 /// let mut card_value = FitsHeaderValueContainer::from(*b"= T / FITS STANDARD ");
615 /// assert_eq!(*card_value.get_comment()?, String::from("FITS STANDARD"));
616 /// # Ok::<(), astro_rs::fits::FitsHeaderError>(())
617 /// ```
618 pub fn get_comment(&mut self) -> Result<Rc<String>, FitsHeaderError> {
619 if let Some(data) = &self.comment {
620 Ok(Rc::clone(data))
621 } else if let Some(comment_start_index) = self
622 .raw
623 .iter()
624 .position(|b| *b == b'/')
625 .or_else(|| self.raw.iter().rposition(|b| *b != b' ').map(|idx| idx + 1))
626 {
627 let mut value_bytes: Vec<u8> = self.raw.drain(comment_start_index..).collect();
628 // discard '/' prefix
629 value_bytes.remove(0);
630 let value_string = String::from_utf8(Self::trim_value(value_bytes)).map_err(|er| {
631 FitsHeaderError::DeserializationError {
632 found: er.into_bytes(),
633 intent: String::from("header card comment"),
634 }
635 })?;
636 let value = Rc::new(value_string);
637 let ret = Rc::clone(&value);
638 self.comment = Some(value);
639
640 Ok(ret)
641 } else {
642 Ok(Default::default())
643 }
644 }
645
646 /// Sets the comment section of the header card.
647 pub fn set_comment(&mut self, comment: Option<String>) -> Result<(), FitsHeaderError> {
648 let value_raw = match (self.value.as_ref(), self.comment.as_ref()) {
649 (Some(value), Some(_comment)) => value.to_bytes(),
650 (Some(value), None) => {
651 self.raw.clear();
652 value.to_bytes()
653 }
654 (None, Some(_comment)) => {
655 let mut value_raw = [b' '; 70];
656 let idx_diff = if self.raw.len() > 70 {
657 self.raw.len() - 70
658 } else {
659 0
660 };
661 value_raw[0..(self.raw.len() - idx_diff)].copy_from_slice(&self.raw[idx_diff..]);
662 value_raw
663 }
664 (None, None) => {
665 self.get_comment()?;
666 let mut value_raw = [b' '; 70];
667 let idx_diff = if self.raw.len() > 70 {
668 self.raw.len() - 70
669 } else {
670 0
671 };
672 value_raw[0..(self.raw.len() - idx_diff)].copy_from_slice(&self.raw[idx_diff..]);
673 value_raw
674 }
675 };
676 Self::check_comment_length(value_raw, comment.as_ref())?;
677 self.comment = comment.map(Rc::new);
678 Ok(())
679 }
680
681 fn check_comment_length(
682 value_raw: [u8; 70],
683 comment: Option<&String>,
684 ) -> Result<(), FitsHeaderError> {
685 if let Some(comment_str) = comment {
686 let comment_start = value_raw
687 .iter()
688 .rposition(|b| *b != b' ')
689 .unwrap_or_default();
690 let diff = 68_usize.checked_sub(comment_start).unwrap_or_default(); // minus an additional 2 for the delimiter
691 if diff < comment_str.len() {
692 return Err(FitsHeaderError::InvalidLength {
693 expected: diff,
694 found: comment_str.len(),
695 intent: String::from("header card comment"),
696 });
697 }
698 }
699 Ok(())
700 }
701
702 fn trim_value(value: Vec<u8>) -> Vec<u8> {
703 value
704 .iter()
705 .position(|b| *b != b' ')
706 .map(|index1| {
707 let index2 = value
708 .iter()
709 .rposition(|b| *b != b' ')
710 .unwrap_or(value.len())
711 + 1;
712 value[index1..index2].to_vec()
713 })
714 .unwrap_or_default()
715 }
716}
717
718impl From<[u8; 72]> for FitsHeaderValueContainer {
719 fn from(raw: [u8; 72]) -> Self {
720 FitsHeaderValueContainer {
721 raw: raw.to_vec(),
722 value: None,
723 comment: None,
724 }
725 }
726}
727
728impl From<FitsHeaderValueContainer> for [u8; 72] {
729 fn from(container: FitsHeaderValueContainer) -> Self {
730 match (container.value, container.comment) {
731 (Some(value), Some(comment)) => {
732 let mut result = [b' '; 72];
733 result[0] = b'=';
734 result[2..72].copy_from_slice(&value.to_bytes());
735 let mut comment_start =
736 result.iter().rposition(|b| *b != b' ').unwrap_or_default() + 2;
737 result[comment_start] = b'/';
738 comment_start += 2;
739 let comment_raw = comment.as_bytes();
740 result[comment_start..comment_start + comment_raw.len()]
741 .copy_from_slice(comment_raw);
742 result
743 }
744 (Some(value), None) => {
745 let mut result = [b' '; 72];
746 result[0] = b'=';
747 result[2..72].copy_from_slice(&value.to_bytes());
748 let comment_start = result.iter().rposition(|b| *b != b' ').unwrap_or_default() + 2;
749 let comment_raw = container.raw.as_slice();
750 result[comment_start..comment_start + comment_raw.len()]
751 .copy_from_slice(comment_raw);
752 result
753 }
754 (None, Some(comment)) => {
755 let mut result = [b' '; 72];
756 let value_raw = container.raw.as_slice();
757 let mut comment_start = value_raw.len();
758 result[0..comment_start].copy_from_slice(value_raw);
759 comment_start += 1;
760 result[comment_start] = b'/';
761 comment_start += 2;
762 let comment_raw = comment.as_bytes();
763 result[comment_start..comment_start + comment_raw.len()]
764 .copy_from_slice(comment_raw);
765 result
766 }
767 (None, None) => {
768 let result: [u8; 72] = container.raw[0..72].try_into().unwrap();
769 result
770 }
771 }
772 }
773}