Skip to main content

fits_header/
header.rs

1//! The ordered header of records and its keyword access.
2
3use crate::error::{FitsError, Result};
4use crate::key::Key;
5use crate::record::{is_commentary_keyword, validate_keyword, validate_keyword_raw, Record, Value};
6use crate::value::{FromCard, IntoValue};
7use crate::write;
8use crate::{BLOCK_LEN, CARD_LEN};
9use std::fs;
10use std::path::Path;
11
12/// An ordered FITS header unit: [`Record`]s in appearance order, with strict keyword
13/// access (via [`Key`]) and CRUD.
14///
15/// Equality is semantic (records compare by content, not by retained bytes).
16#[derive(Clone, Debug, Default, PartialEq)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18pub struct Header {
19    records: Vec<Record>,
20}
21
22impl Header {
23    /// An empty header.
24    ///
25    /// # Examples
26    ///
27    /// ```
28    /// # use fits_header::Header;
29    /// let h = Header::new();
30    /// assert_eq!(h.count("OBJECT"), 0);
31    /// ```
32    pub fn new() -> Self {
33        Header::default()
34    }
35
36    /// Construct from records (used by the parser).
37    pub(crate) fn from_records(records: Vec<Record>) -> Self {
38        Header { records }
39    }
40
41    /// Parse one FITS header unit from raw bytes.
42    ///
43    /// Reads 80-byte cards in order, stops at `END`, and retains every card (including
44    /// commentary, `HIERARCH`, and unrecognized cards) so untouched cards serialize verbatim.
45    /// `CONTINUE` runs are reassembled into a single logical value. Bytes after `END` (the data
46    /// unit, later HDUs) are ignored — this crate is header-only and never inspects them.
47    ///
48    /// # Examples
49    ///
50    /// ```
51    /// use fits_header::Header;
52    ///
53    /// let mut bytes = Vec::new();
54    /// for card in ["OBJECT  = 'M31     '", "EXPTIME =                120.0", "END"] {
55    ///     let mut c = card.as_bytes().to_vec();
56    ///     c.resize(80, b' ');
57    ///     bytes.extend(c);
58    /// }
59    ///
60    /// let header = Header::parse(&bytes).unwrap();
61    /// assert_eq!(header.get_str("OBJECT").unwrap(), Some("M31"));
62    /// assert_eq!(header.get::<f64>("EXPTIME").unwrap(), Some(120.0));
63    /// ```
64    pub fn parse(bytes: &[u8]) -> Result<Header> {
65        crate::parse::parse_header(bytes)
66    }
67
68    /// Read a FITS header from a file on disk.
69    ///
70    /// Reads the whole file and [`parse`](Self::parse)s it; parsing already stops at `END`, so
71    /// the data unit and any later HDUs are read but never interpreted.
72    ///
73    /// # Examples
74    ///
75    /// ```
76    /// use fits_header::Header;
77    ///
78    /// let mut bytes = Vec::new();
79    /// for card in ["OBJECT  = 'M31     '", "END"] {
80    ///     let mut c = card.as_bytes().to_vec();
81    ///     c.resize(80, b' ');
82    ///     bytes.extend(c);
83    /// }
84    /// while bytes.len() % fits_header::BLOCK_LEN != 0 {
85    ///     bytes.push(b' ');
86    /// }
87    /// bytes.extend_from_slice(&[0u8; 4]); // stand-in pixel data
88    ///
89    /// let path = std::env::temp_dir().join("fits-header-doctest-read_from_file.fits");
90    /// std::fs::write(&path, &bytes).unwrap();
91    ///
92    /// let header = Header::read_from_file(&path).unwrap();
93    /// assert_eq!(header.get_str("OBJECT").unwrap(), Some("M31"));
94    ///
95    /// std::fs::remove_file(&path).ok();
96    /// ```
97    pub fn read_from_file<P: AsRef<Path>>(path: P) -> Result<Header> {
98        let bytes = fs::read(path)?;
99        Header::parse(&bytes)
100    }
101
102    /// Edit a FITS file's header in place, preserving its data unit (and any later HDUs)
103    /// byte-for-byte.
104    ///
105    /// Reads the whole file, locates the header region by scanning for the `END` card, parses
106    /// only that region, runs `edit` on it, then writes the re-serialized header back followed
107    /// by every byte that came after the original header — untouched, regardless of what `edit`
108    /// did.
109    ///
110    /// The write is atomic and edits the real file in place: it writes a temp file in the
111    /// target's directory and renames it over the target. It follows symlinks (a symlinked
112    /// `path` stays a symlink; its target is edited) and, on Unix, preserves the target's file
113    /// mode. A crash or interruption cannot leave a truncated file.
114    ///
115    /// Errors with [`FitsError::MissingEnd`] if the file has no `END` card, or
116    /// [`FitsError::TruncatedHeader`] if it has one but ends before the header's 2880-byte
117    /// block is complete.
118    ///
119    /// # Examples
120    ///
121    /// ```
122    /// use fits_header::Header;
123    ///
124    /// let mut bytes = Vec::new();
125    /// for card in ["OBJECT  = 'M31     '", "END"] {
126    ///     let mut c = card.as_bytes().to_vec();
127    ///     c.resize(80, b' ');
128    ///     bytes.extend(c);
129    /// }
130    /// while bytes.len() % fits_header::BLOCK_LEN != 0 {
131    ///     bytes.push(b' ');
132    /// }
133    /// let data = [1u8, 2, 3, 4]; // stand-in pixel data
134    /// bytes.extend_from_slice(&data);
135    ///
136    /// let path = std::env::temp_dir().join("fits-header-doctest-update_file.fits");
137    /// std::fs::write(&path, &bytes).unwrap();
138    ///
139    /// Header::update_file(&path, |h| {
140    ///     h.set("OBJECT", "NGC 7000")?;
141    ///     Ok(())
142    /// })
143    /// .unwrap();
144    ///
145    /// let after = std::fs::read(&path).unwrap();
146    /// let header = Header::parse(&after).unwrap();
147    /// assert_eq!(header.get_str("OBJECT").unwrap(), Some("NGC 7000"));
148    /// assert_eq!(&after[after.len() - data.len()..], &data, "data unit preserved");
149    ///
150    /// std::fs::remove_file(&path).ok();
151    /// ```
152    pub fn update_file<P: AsRef<Path>>(
153        path: P,
154        edit: impl FnOnce(&mut Header) -> Result<()>,
155    ) -> Result<()> {
156        let path = path.as_ref();
157        let bytes = fs::read(path)?;
158        let header_len = header_region_len(&bytes)?;
159        if header_len > bytes.len() {
160            // END found, but the file ends before the header block is padded out.
161            return Err(FitsError::TruncatedHeader);
162        }
163        let tail = &bytes[header_len..];
164
165        let mut header = Header::parse(&bytes[..header_len])?;
166        edit(&mut header)?;
167
168        let mut out = header.to_header_bytes();
169        out.extend_from_slice(tail);
170        write_atomic(path, &out)?;
171        Ok(())
172    }
173
174    /// The records in order (read-only escape hatch).
175    ///
176    /// # Examples
177    ///
178    /// ```
179    /// # use fits_header::Header;
180    /// let mut h = Header::new();
181    /// h.set("OBJECT", "M31").unwrap();
182    /// assert_eq!(h.cards()[0].keyword(), Some("OBJECT"));
183    /// ```
184    pub fn cards(&self) -> &[Record] {
185        &self.records
186    }
187
188    /// Iterate the records in order.
189    ///
190    /// # Examples
191    ///
192    /// ```
193    /// # use fits_header::Header;
194    /// let mut h = Header::new();
195    /// h.set("OBJECT", "M31").unwrap();
196    /// h.set("EXPTIME", 120.0).unwrap();
197    /// let names: Vec<&str> = h.iter().filter_map(|r| r.keyword()).collect();
198    /// assert_eq!(names, vec!["OBJECT", "EXPTIME"]);
199    /// ```
200    pub fn iter(&self) -> impl Iterator<Item = &Record> {
201        self.records.iter()
202    }
203
204    /// How many records carry this keyword.
205    ///
206    /// # Examples
207    ///
208    /// ```
209    /// # use fits_header::Header;
210    /// let mut h = Header::new();
211    /// h.append("HISTORY", "dark subtracted").unwrap();
212    /// h.append("HISTORY", "flat fielded").unwrap();
213    /// assert_eq!(h.count("HISTORY"), 2);
214    /// assert_eq!(h.count("OBJECT"), 0);
215    /// ```
216    pub fn count(&self, name: &str) -> usize {
217        self.records
218            .iter()
219            .filter(|r| r.keyword() == Some(name))
220            .count()
221    }
222
223    /// Resolve a key to a record index. A bare name is strict.
224    fn resolve(&self, key: &Key) -> Result<Option<usize>> {
225        let name = key.name();
226        let indices: Vec<usize> = self
227            .records
228            .iter()
229            .enumerate()
230            .filter(|(_, r)| r.keyword() == Some(name))
231            .map(|(i, _)| i)
232            .collect();
233        match key.occurrence() {
234            Some(n) => Ok(indices.get(n).copied()),
235            None => match indices.len() {
236                0 => Ok(None),
237                1 => Ok(Some(indices[0])),
238                count => Err(FitsError::AmbiguousKeyword {
239                    keyword: name.to_string(),
240                    count,
241                }),
242            },
243        }
244    }
245
246    /// Read a keyword as `T`. `Err` only on an ambiguous bare name; `Ok(None)` when absent or the
247    /// value does not convert; never panics.
248    ///
249    /// # Examples
250    ///
251    /// ```
252    /// # use fits_header::Header;
253    /// let mut h = Header::new();
254    /// h.set("EXPTIME", 120.0).unwrap();
255    /// assert_eq!(h.get::<f64>("EXPTIME").unwrap(), Some(120.0));
256    /// assert_eq!(h.get::<i64>("MISSING").unwrap(), None);
257    /// ```
258    pub fn get<T: FromCard>(&self, key: impl Into<Key>) -> Result<Option<T>> {
259        Ok(self
260            .resolve(&key.into())?
261            .and_then(|i| T::from_card(&self.records[i])))
262    }
263
264    /// Borrow a keyword's string value (`Str` content, non-empty); `None` for empty or a literal.
265    ///
266    /// # Examples
267    ///
268    /// ```
269    /// # use fits_header::Header;
270    /// let mut h = Header::new();
271    /// h.set("OBJECT", "M31").unwrap();
272    /// h.set("EXPTIME", 120.0).unwrap(); // a Literal, not Str content
273    /// assert_eq!(h.get_str("OBJECT").unwrap(), Some("M31"));
274    /// assert_eq!(h.get_str("EXPTIME").unwrap(), None);
275    /// ```
276    pub fn get_str(&self, key: impl Into<Key>) -> Result<Option<&str>> {
277        Ok(self
278            .resolve(&key.into())?
279            .and_then(|i| self.records[i].str_content()))
280    }
281
282    /// Every value for a keyword, in order. Unlike [`get`](Self::get), never errors on a
283    /// duplicated keyword — that is the point of calling it.
284    ///
285    /// # Examples
286    ///
287    /// ```
288    /// # use fits_header::Header;
289    /// let mut h = Header::new();
290    /// h.append("HISTORY", "dark subtracted").unwrap();
291    /// h.append("HISTORY", "flat fielded").unwrap();
292    /// assert_eq!(
293    ///     h.get_all::<String>("HISTORY"),
294    ///     vec!["dark subtracted".to_string(), "flat fielded".to_string()]
295    /// );
296    /// ```
297    pub fn get_all<T: FromCard>(&self, name: &str) -> Vec<T> {
298        self.records
299            .iter()
300            .filter(|r| r.keyword() == Some(name))
301            .filter_map(T::from_card)
302            .collect()
303    }
304
305    fn make_record(name: &str, value: Value) -> Record {
306        if is_commentary_keyword(name) {
307            let text = match value {
308                Value::Str(s) | Value::Literal(s) => s,
309            };
310            Record::commentary(name, text)
311        } else {
312            Record::value(name, value, None)
313        }
314    }
315
316    fn set_inner(&mut self, key: Key, value: Value, raw: bool) -> Result<()> {
317        let name = key.name().to_string();
318        if raw {
319            validate_keyword_raw(&name)?;
320        } else {
321            validate_keyword(&name)?;
322        }
323        match self.resolve(&key)? {
324            Some(i) => {
325                self.records[i].replace_value(value);
326                Ok(())
327            }
328            None => match key.occurrence() {
329                Some(n) => Err(FitsError::OccurrenceOutOfRange {
330                    keyword: name.clone(),
331                    occurrence: n,
332                    count: self.count(&name),
333                }),
334                None => {
335                    self.records.push(Self::make_record(&name, value));
336                    Ok(())
337                }
338            },
339        }
340    }
341
342    /// Update the addressed record in place, or append when the (unique) name is absent.
343    /// The keyword must be FITS-standard (`≤8`, `A-Z 0-9 - _`); use [`set_raw`](Self::set_raw)
344    /// for vendor keys.
345    ///
346    /// # Examples
347    ///
348    /// ```
349    /// # use fits_header::{FitsError, Header};
350    /// let mut h = Header::new();
351    /// h.set("OBJECT", "M31").unwrap(); // appends
352    /// h.set("OBJECT", "NGC 7000").unwrap(); // updates in place
353    /// assert_eq!(h.count("OBJECT"), 1);
354    ///
355    /// let err = h.set("object", 1); // lowercase is not FITS-standard
356    /// assert!(matches!(err, Err(FitsError::InvalidKeyword { .. })));
357    /// ```
358    pub fn set(&mut self, key: impl Into<Key>, value: impl IntoValue) -> Result<()> {
359        self.set_inner(key.into(), value.into_value(), false)
360    }
361
362    /// Like [`set`](Self::set) but accepts any ≤8-char printable-ASCII keyword (vendor escape hatch).
363    ///
364    /// # Examples
365    ///
366    /// ```
367    /// # use fits_header::Header;
368    /// let mut h = Header::new();
369    /// h.set_raw("pi.name", "Jane Doe").unwrap(); // lowercase, not FITS-standard
370    /// assert_eq!(h.get_str("pi.name").unwrap(), Some("Jane Doe"));
371    /// ```
372    pub fn set_raw(&mut self, keyword: &str, value: impl IntoValue) -> Result<()> {
373        self.set_inner(Key::Name(keyword.to_string()), value.into_value(), true)
374    }
375
376    /// Always add a record (a value card, or a commentary card for `COMMENT`/`HISTORY`/blank).
377    ///
378    /// # Examples
379    ///
380    /// ```
381    /// # use fits_header::Header;
382    /// let mut h = Header::new();
383    /// h.append("HISTORY", "dark subtracted").unwrap();
384    /// h.append("HISTORY", "flat fielded").unwrap();
385    /// assert_eq!(h.get_all::<String>("HISTORY").len(), 2);
386    /// ```
387    pub fn append(&mut self, name: &str, value: impl IntoValue) -> Result<()> {
388        validate_keyword(name)?;
389        self.records
390            .push(Self::make_record(name, value.into_value()));
391        Ok(())
392    }
393
394    /// Set or replace the addressed value card's inline comment. No-op if the keyword is absent
395    /// or not a value card.
396    ///
397    /// # Examples
398    ///
399    /// ```
400    /// # use fits_header::Header;
401    /// let mut h = Header::new();
402    /// h.set("EXPTIME", 120.0).unwrap();
403    /// h.set_comment("EXPTIME", "seconds").unwrap();
404    /// assert_eq!(h.cards()[0].comment(), Some("seconds"));
405    /// ```
406    pub fn set_comment(&mut self, key: impl Into<Key>, comment: impl Into<String>) -> Result<()> {
407        if let Some(i) = self.resolve(&key.into())? {
408            self.records[i].set_comment(Some(comment.into()));
409        }
410        Ok(())
411    }
412
413    /// Remove the addressed record. Returns whether anything was removed.
414    ///
415    /// # Examples
416    ///
417    /// ```
418    /// # use fits_header::Header;
419    /// let mut h = Header::new();
420    /// h.set("AIRMASS", 1.2).unwrap();
421    /// assert!(h.remove("AIRMASS").unwrap());
422    /// assert!(!h.remove("AIRMASS").unwrap());
423    /// ```
424    pub fn remove(&mut self, key: impl Into<Key>) -> Result<bool> {
425        match self.resolve(&key.into())? {
426            Some(i) => {
427                self.records.remove(i);
428                Ok(true)
429            }
430            None => Ok(false),
431        }
432    }
433
434    /// Apply several mutations atomically: validate every entry first, then apply all or none.
435    ///
436    /// # Examples
437    ///
438    /// ```
439    /// # use fits_header::Header;
440    /// let mut h = Header::new();
441    /// h.set_many([("FILTER", "Ha"), ("TELESCOP", "EdgeHD 8")]).unwrap();
442    ///
443    /// // A rejected batch leaves the header untouched.
444    /// assert!(h.set_many([("GAIN", "1"), ("TOOLONGKEY", "2")]).is_err());
445    /// assert_eq!(h.count("GAIN"), 0);
446    /// ```
447    pub fn set_many<K, V>(&mut self, entries: impl IntoIterator<Item = (K, V)>) -> Result<()>
448    where
449        K: Into<Key>,
450        V: IntoValue,
451    {
452        let items: Vec<(Key, Value)> = entries
453            .into_iter()
454            .map(|(k, v)| (k.into(), v.into_value()))
455            .collect();
456        // Validate keywords and resolvability against the current state before mutating.
457        for (k, _) in &items {
458            validate_keyword(k.name())?;
459            if let Some(n) = k.occurrence() {
460                if self.resolve(k)?.is_none() {
461                    return Err(FitsError::OccurrenceOutOfRange {
462                        keyword: k.name().to_string(),
463                        occurrence: n,
464                        count: self.count(k.name()),
465                    });
466                }
467            } else {
468                // Surfaces AmbiguousKeyword before any change.
469                self.resolve(k)?;
470            }
471        }
472        for (k, v) in items {
473            self.set_inner(k, v, false)?;
474        }
475        Ok(())
476    }
477
478    /// Remove several keys atomically (validation only guards ambiguity). Returns the count removed.
479    ///
480    /// # Examples
481    ///
482    /// ```
483    /// # use fits_header::Header;
484    /// let mut h = Header::new();
485    /// h.set("OBJECT", "M31").unwrap();
486    /// h.set("EXPTIME", 120.0).unwrap();
487    /// assert_eq!(h.remove_many(["OBJECT", "MISSING"]).unwrap(), 1);
488    /// assert_eq!(h.count("OBJECT"), 0);
489    /// ```
490    pub fn remove_many<K: Into<Key>>(
491        &mut self,
492        keys: impl IntoIterator<Item = K>,
493    ) -> Result<usize> {
494        let keys: Vec<Key> = keys.into_iter().map(Into::into).collect();
495        for k in &keys {
496            self.resolve(k)?;
497        }
498        let mut removed = 0;
499        for k in keys {
500            if self.remove(k)? {
501                removed += 1;
502            }
503        }
504        Ok(removed)
505    }
506
507    /// Serialize the header block only (cards, `END`, padded to a 2880 multiple) for splicing onto
508    /// an existing file's data.
509    ///
510    /// # Examples
511    ///
512    /// ```
513    /// # use fits_header::Header;
514    /// let mut h = Header::new();
515    /// h.set("OBJECT", "M31").unwrap();
516    /// let bytes = h.to_header_bytes();
517    /// assert_eq!(bytes.len() % fits_header::BLOCK_LEN, 0);
518    /// ```
519    pub fn to_header_bytes(&self) -> Vec<u8> {
520        write::to_header_bytes(self)
521    }
522}
523
524/// The byte length of the header region at the start of `bytes`: cards up to and including
525/// `END`, rounded up to a [`BLOCK_LEN`] multiple. Scans the same way [`Header::parse`] does, so
526/// this always agrees with what parsing would consume.
527fn header_region_len(bytes: &[u8]) -> Result<usize> {
528    for (i, card) in bytes.chunks_exact(CARD_LEN).enumerate() {
529        let keyword = String::from_utf8_lossy(&card[..8]).trim().to_string();
530        if keyword == "END" {
531            let raw_len = (i + 1) * CARD_LEN;
532            return Ok(raw_len.div_ceil(BLOCK_LEN) * BLOCK_LEN);
533        }
534    }
535    Err(FitsError::MissingEnd)
536}
537
538/// Write `bytes` to `path` atomically, editing the real file in place.
539///
540/// Follows symlinks: if `path` is a symlink, its canonical target is resolved first and both
541/// the temp file and the rename land in the target's directory, so the link is preserved and
542/// the file it points at is the one edited. The temp file is renamed over the target (an atomic
543/// replace), so a crash mid-write leaves the original file intact, never a truncated one. On
544/// Unix the target's file mode is copied onto the temp file before the rename, so a 0600 file
545/// stays 0600 instead of dropping to the umask default. Any failure after the temp file is
546/// created removes it.
547fn write_atomic(path: &Path, bytes: &[u8]) -> Result<()> {
548    // Resolve symlinks so we edit the real file in place; fall back to `path` if it does not
549    // yet exist (update_file always reads first, so it does).
550    let target = fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
551    let dir = target
552        .parent()
553        .filter(|p| !p.as_os_str().is_empty())
554        .unwrap_or_else(|| Path::new("."));
555    let file_name = target.file_name().map(|n| n.to_string_lossy().into_owned());
556    let tmp_name = match file_name {
557        Some(name) => format!(".{name}.tmp-{}", std::process::id()),
558        None => format!(".fits-header.tmp-{}", std::process::id()),
559    };
560    let tmp_path = dir.join(tmp_name);
561
562    let result = (|| {
563        fs::write(&tmp_path, bytes)?;
564        copy_mode(&target, &tmp_path)?;
565        fs::rename(&tmp_path, &target)?;
566        Ok(())
567    })();
568    if result.is_err() {
569        let _ = fs::remove_file(&tmp_path);
570    }
571    result
572}
573
574/// Copy `src`'s file permissions onto `dst`. No-op off Unix, and silently ignores a missing
575/// `src` (a freshly created file has no prior mode to preserve).
576#[cfg(unix)]
577fn copy_mode(src: &Path, dst: &Path) -> Result<()> {
578    match fs::metadata(src) {
579        Ok(meta) => fs::set_permissions(dst, meta.permissions())?,
580        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
581        Err(e) => return Err(e.into()),
582    }
583    Ok(())
584}
585
586#[cfg(not(unix))]
587fn copy_mode(_src: &Path, _dst: &Path) -> Result<()> {
588    Ok(())
589}
590
591#[cfg(test)]
592mod tests {
593    use super::*;
594
595    #[test]
596    fn set_routes_commentary_keywords_to_commentary_records() {
597        let mut h = Header::new();
598        h.set("COMMENT", "a note").unwrap();
599        h.set("HISTORY", "step 1").unwrap();
600        assert!(matches!(
601            h.cards()[0].kind,
602            crate::record::RecordKind::Commentary { .. }
603        ));
604        assert_eq!(h.get_all::<String>("HISTORY"), vec!["step 1".to_string()]);
605    }
606
607    #[test]
608    fn get_all_skips_unconvertible_values() {
609        let mut h = Header::new();
610        h.append("GAIN", 100).unwrap();
611        h.append("GAIN", "not a number").unwrap();
612        h.append("GAIN", 200).unwrap();
613        assert_eq!(h.get_all::<i64>("GAIN"), vec![100, 200]);
614        assert_eq!(h.count("GAIN"), 3);
615    }
616
617    #[test]
618    fn set_comment_on_absent_key_is_noop() {
619        let mut h = Header::new();
620        h.set_comment("NOPE", "x").unwrap();
621        assert!(h.cards().is_empty());
622    }
623
624    #[test]
625    fn remove_returns_false_when_absent() {
626        let mut h = Header::new();
627        assert!(!h.remove("NOPE").unwrap());
628    }
629
630    #[test]
631    fn remove_many_aborts_on_ambiguity_before_removing() {
632        let mut h = Header::new();
633        h.set("A", 1).unwrap();
634        h.append("DUP", 1).unwrap();
635        h.append("DUP", 2).unwrap();
636        let before = h.clone();
637        assert!(matches!(
638            h.remove_many(["A", "DUP"]),
639            Err(FitsError::AmbiguousKeyword { .. })
640        ));
641        assert_eq!(h, before, "nothing may be removed on a rejected batch");
642
643        assert_eq!(h.remove_many(["A", "MISSING"]).unwrap(), 1);
644    }
645
646    #[test]
647    fn set_many_accepts_occurrence_keys() {
648        let mut h = Header::new();
649        h.append("GAIN", 1).unwrap();
650        h.append("GAIN", 2).unwrap();
651        h.set_many([(("GAIN", 0), 10), (("GAIN", 1), 20)]).unwrap();
652        assert_eq!(h.get_all::<i64>("GAIN"), vec![10, 20]);
653    }
654
655    #[test]
656    fn iter_matches_cards() {
657        let mut h = Header::new();
658        h.set("A", 1).unwrap();
659        h.set("B", 2).unwrap();
660        assert_eq!(h.iter().count(), 2);
661        let names: Vec<_> = h.iter().filter_map(|r| r.keyword()).collect();
662        assert_eq!(names, vec!["A", "B"]);
663    }
664
665    #[test]
666    fn get_on_missing_key_is_ok_none() {
667        let h = Header::new();
668        assert_eq!(h.get::<i64>("NOPE").unwrap(), None);
669        assert_eq!(h.get_str("NOPE").unwrap(), None);
670        assert_eq!(h.get::<i64>(("NOPE", 3)).unwrap(), None);
671    }
672}