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}