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}