use crate::error::{FitsError, Result};
use crate::key::Key;
use crate::record::{is_commentary_keyword, validate_keyword, validate_keyword_raw, Record, Value};
use crate::value::{FromCard, IntoValue};
use crate::write;
use crate::{BLOCK_LEN, CARD_LEN};
use std::fs;
use std::path::Path;
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Header {
records: Vec<Record>,
}
impl Header {
pub fn new() -> Self {
Header::default()
}
pub(crate) fn from_records(records: Vec<Record>) -> Self {
Header { records }
}
pub fn parse(bytes: &[u8]) -> Result<Header> {
crate::parse::parse_header(bytes)
}
pub fn read_from_file<P: AsRef<Path>>(path: P) -> Result<Header> {
let bytes = fs::read(path)?;
Header::parse(&bytes)
}
pub fn update_file<P: AsRef<Path>>(
path: P,
edit: impl FnOnce(&mut Header) -> Result<()>,
) -> Result<()> {
let path = path.as_ref();
let bytes = fs::read(path)?;
let header_len = header_region_len(&bytes)?;
if header_len > bytes.len() {
return Err(FitsError::TruncatedHeader);
}
let tail = &bytes[header_len..];
let mut header = Header::parse(&bytes[..header_len])?;
edit(&mut header)?;
let mut out = header.to_header_bytes();
out.extend_from_slice(tail);
write_atomic(path, &out)?;
Ok(())
}
pub fn cards(&self) -> &[Record] {
&self.records
}
pub fn iter(&self) -> impl Iterator<Item = &Record> {
self.records.iter()
}
pub fn count(&self, name: &str) -> usize {
self.records
.iter()
.filter(|r| r.keyword() == Some(name))
.count()
}
fn resolve(&self, key: &Key) -> Result<Option<usize>> {
let name = key.name();
let indices: Vec<usize> = self
.records
.iter()
.enumerate()
.filter(|(_, r)| r.keyword() == Some(name))
.map(|(i, _)| i)
.collect();
match key.occurrence() {
Some(n) => Ok(indices.get(n).copied()),
None => match indices.len() {
0 => Ok(None),
1 => Ok(Some(indices[0])),
count => Err(FitsError::AmbiguousKeyword {
keyword: name.to_string(),
count,
}),
},
}
}
pub fn get<T: FromCard>(&self, key: impl Into<Key>) -> Result<Option<T>> {
Ok(self
.resolve(&key.into())?
.and_then(|i| T::from_card(&self.records[i])))
}
pub fn get_str(&self, key: impl Into<Key>) -> Result<Option<&str>> {
Ok(self
.resolve(&key.into())?
.and_then(|i| self.records[i].str_content()))
}
pub fn get_all<T: FromCard>(&self, name: &str) -> Vec<T> {
self.records
.iter()
.filter(|r| r.keyword() == Some(name))
.filter_map(T::from_card)
.collect()
}
fn make_record(name: &str, value: Value) -> Record {
if is_commentary_keyword(name) {
let text = match value {
Value::Str(s) | Value::Literal(s) => s,
};
Record::commentary(name, text)
} else {
Record::value(name, value, None)
}
}
fn set_inner(&mut self, key: Key, value: Value, raw: bool) -> Result<()> {
let name = key.name().to_string();
if raw {
validate_keyword_raw(&name)?;
} else {
validate_keyword(&name)?;
}
match self.resolve(&key)? {
Some(i) => {
self.records[i].replace_value(value);
Ok(())
}
None => match key.occurrence() {
Some(n) => Err(FitsError::OccurrenceOutOfRange {
keyword: name.clone(),
occurrence: n,
count: self.count(&name),
}),
None => {
self.records.push(Self::make_record(&name, value));
Ok(())
}
},
}
}
pub fn set(&mut self, key: impl Into<Key>, value: impl IntoValue) -> Result<()> {
self.set_inner(key.into(), value.into_value(), false)
}
pub fn set_raw(&mut self, keyword: &str, value: impl IntoValue) -> Result<()> {
self.set_inner(Key::Name(keyword.to_string()), value.into_value(), true)
}
pub fn append(&mut self, name: &str, value: impl IntoValue) -> Result<()> {
validate_keyword(name)?;
self.records
.push(Self::make_record(name, value.into_value()));
Ok(())
}
pub fn set_comment(&mut self, key: impl Into<Key>, comment: impl Into<String>) -> Result<()> {
if let Some(i) = self.resolve(&key.into())? {
self.records[i].set_comment(Some(comment.into()));
}
Ok(())
}
pub fn remove(&mut self, key: impl Into<Key>) -> Result<bool> {
match self.resolve(&key.into())? {
Some(i) => {
self.records.remove(i);
Ok(true)
}
None => Ok(false),
}
}
pub fn set_many<K, V>(&mut self, entries: impl IntoIterator<Item = (K, V)>) -> Result<()>
where
K: Into<Key>,
V: IntoValue,
{
let items: Vec<(Key, Value)> = entries
.into_iter()
.map(|(k, v)| (k.into(), v.into_value()))
.collect();
for (k, _) in &items {
validate_keyword(k.name())?;
if let Some(n) = k.occurrence() {
if self.resolve(k)?.is_none() {
return Err(FitsError::OccurrenceOutOfRange {
keyword: k.name().to_string(),
occurrence: n,
count: self.count(k.name()),
});
}
} else {
self.resolve(k)?;
}
}
for (k, v) in items {
self.set_inner(k, v, false)?;
}
Ok(())
}
pub fn remove_many<K: Into<Key>>(
&mut self,
keys: impl IntoIterator<Item = K>,
) -> Result<usize> {
let keys: Vec<Key> = keys.into_iter().map(Into::into).collect();
for k in &keys {
self.resolve(k)?;
}
let mut removed = 0;
for k in keys {
if self.remove(k)? {
removed += 1;
}
}
Ok(removed)
}
pub fn to_header_bytes(&self) -> Vec<u8> {
write::to_header_bytes(self)
}
}
fn header_region_len(bytes: &[u8]) -> Result<usize> {
for (i, card) in bytes.chunks_exact(CARD_LEN).enumerate() {
let keyword = String::from_utf8_lossy(&card[..8]).trim().to_string();
if keyword == "END" {
let raw_len = (i + 1) * CARD_LEN;
return Ok(raw_len.div_ceil(BLOCK_LEN) * BLOCK_LEN);
}
}
Err(FitsError::MissingEnd)
}
fn write_atomic(path: &Path, bytes: &[u8]) -> Result<()> {
let target = fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
let dir = target
.parent()
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
let file_name = target.file_name().map(|n| n.to_string_lossy().into_owned());
let tmp_name = match file_name {
Some(name) => format!(".{name}.tmp-{}", std::process::id()),
None => format!(".fits-header.tmp-{}", std::process::id()),
};
let tmp_path = dir.join(tmp_name);
let result = (|| {
fs::write(&tmp_path, bytes)?;
copy_mode(&target, &tmp_path)?;
fs::rename(&tmp_path, &target)?;
Ok(())
})();
if result.is_err() {
let _ = fs::remove_file(&tmp_path);
}
result
}
#[cfg(unix)]
fn copy_mode(src: &Path, dst: &Path) -> Result<()> {
match fs::metadata(src) {
Ok(meta) => fs::set_permissions(dst, meta.permissions())?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(e.into()),
}
Ok(())
}
#[cfg(not(unix))]
fn copy_mode(_src: &Path, _dst: &Path) -> Result<()> {
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn set_routes_commentary_keywords_to_commentary_records() {
let mut h = Header::new();
h.set("COMMENT", "a note").unwrap();
h.set("HISTORY", "step 1").unwrap();
assert!(matches!(
h.cards()[0].kind,
crate::record::RecordKind::Commentary { .. }
));
assert_eq!(h.get_all::<String>("HISTORY"), vec!["step 1".to_string()]);
}
#[test]
fn get_all_skips_unconvertible_values() {
let mut h = Header::new();
h.append("GAIN", 100).unwrap();
h.append("GAIN", "not a number").unwrap();
h.append("GAIN", 200).unwrap();
assert_eq!(h.get_all::<i64>("GAIN"), vec![100, 200]);
assert_eq!(h.count("GAIN"), 3);
}
#[test]
fn set_comment_on_absent_key_is_noop() {
let mut h = Header::new();
h.set_comment("NOPE", "x").unwrap();
assert!(h.cards().is_empty());
}
#[test]
fn remove_returns_false_when_absent() {
let mut h = Header::new();
assert!(!h.remove("NOPE").unwrap());
}
#[test]
fn remove_many_aborts_on_ambiguity_before_removing() {
let mut h = Header::new();
h.set("A", 1).unwrap();
h.append("DUP", 1).unwrap();
h.append("DUP", 2).unwrap();
let before = h.clone();
assert!(matches!(
h.remove_many(["A", "DUP"]),
Err(FitsError::AmbiguousKeyword { .. })
));
assert_eq!(h, before, "nothing may be removed on a rejected batch");
assert_eq!(h.remove_many(["A", "MISSING"]).unwrap(), 1);
}
#[test]
fn set_many_accepts_occurrence_keys() {
let mut h = Header::new();
h.append("GAIN", 1).unwrap();
h.append("GAIN", 2).unwrap();
h.set_many([(("GAIN", 0), 10), (("GAIN", 1), 20)]).unwrap();
assert_eq!(h.get_all::<i64>("GAIN"), vec![10, 20]);
}
#[test]
fn iter_matches_cards() {
let mut h = Header::new();
h.set("A", 1).unwrap();
h.set("B", 2).unwrap();
assert_eq!(h.iter().count(), 2);
let names: Vec<_> = h.iter().filter_map(|r| r.keyword()).collect();
assert_eq!(names, vec!["A", "B"]);
}
#[test]
fn get_on_missing_key_is_ok_none() {
let h = Header::new();
assert_eq!(h.get::<i64>("NOPE").unwrap(), None);
assert_eq!(h.get_str("NOPE").unwrap(), None);
assert_eq!(h.get::<i64>(("NOPE", 3)).unwrap(), None);
}
}