limnifs_write/file_categorizer/
fits.rs1use std::path::Path;
22
23use super::{Categorization, FileCategorizer};
24use limnifs_core::codec::CODEC_RICEPP;
25
26const RICEPP_ENABLED: bool = true;
30
31const FITS_MAGIC: &[u8] = b"SIMPLE =";
33
34const FITS_RECORD_LEN: usize = 80;
36
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub struct FitsParams {
40 pub bitpix: i32,
42 pub naxis: u32,
44 pub naxis1: u32,
46 pub naxis2: u32,
48}
49
50impl FitsParams {
51 #[must_use]
53 pub fn encode(&self) -> [u8; 16] {
54 let mut out = [0u8; 16];
55 out[0..4].copy_from_slice(&self.bitpix.to_le_bytes());
56 out[4..8].copy_from_slice(&self.naxis.to_le_bytes());
57 out[8..12].copy_from_slice(&self.naxis1.to_le_bytes());
58 out[12..16].copy_from_slice(&self.naxis2.to_le_bytes());
59 out
60 }
61}
62
63pub struct FitsCategorizer;
64
65impl FileCategorizer for FitsCategorizer {
66 fn name(&self) -> &'static str {
67 "fits"
68 }
69
70 fn categories(&self) -> &'static [&'static str] {
71 &["fits/image"]
72 }
73
74 fn first_byte_hint(&self) -> Option<&'static [u8]> {
75 Some(b"S")
76 }
77
78 fn categorize(&self, _path: &Path, data: &[u8]) -> Option<Categorization> {
79 if !RICEPP_ENABLED {
80 return None;
81 }
82 let params = parse_fits(data)?;
83 Some(Categorization {
84 codec_id: CODEC_RICEPP,
85 codec_params: params.encode().to_vec(),
86 category: "fits/image",
87 })
88 }
89}
90
91#[must_use]
94fn parse_fits(data: &[u8]) -> Option<FitsParams> {
95 if data.len() < FITS_MAGIC.len() + FITS_RECORD_LEN {
96 return None;
97 }
98 if &data[0..FITS_MAGIC.len()] != FITS_MAGIC {
99 return None;
100 }
101 let mut bitpix: Option<i32> = None;
102 let mut naxis: Option<u32> = None;
103 let mut naxis1: Option<u32> = None;
104 let mut naxis2: Option<u32> = None;
105
106 let header_block = data.len().min(2880);
108 let mut off = 0;
109 while off + FITS_RECORD_LEN <= header_block {
110 let record = &data[off..off + FITS_RECORD_LEN];
111 let key = &record[0..8];
112 let value = &record[10..30];
113 if key.starts_with(b"END ") {
114 break;
115 }
116 match key {
117 b"BITPIX " => bitpix = parse_int(value),
118 b"NAXIS " => naxis = parse_int(value).map(|i| i as u32),
119 b"NAXIS1 " => naxis1 = parse_int(value).map(|i| i as u32),
120 b"NAXIS2 " => naxis2 = parse_int(value).map(|i| i as u32),
121 _ => {}
122 }
123 off += FITS_RECORD_LEN;
124 }
125 Some(FitsParams {
126 bitpix: bitpix?,
127 naxis: naxis?,
128 naxis1: naxis1.unwrap_or(0),
129 naxis2: naxis2.unwrap_or(0),
130 })
131}
132
133fn parse_int(value: &[u8]) -> Option<i32> {
136 let s = std::str::from_utf8(value).ok()?;
137 s.trim().trim_end_matches('/').trim().parse().ok()
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 fn make_fits_record(key: &str, value: &str) -> [u8; FITS_RECORD_LEN] {
145 let mut rec = [b' '; FITS_RECORD_LEN];
146 let key_bytes = key.as_bytes();
147 let copy_len = key_bytes.len().min(8);
148 rec[..copy_len].copy_from_slice(&key_bytes[..copy_len]);
149 rec[8] = b'=';
150 rec[9] = b' ';
151 let value_bytes = value.as_bytes();
152 let copy_len = value_bytes.len().min(20);
153 rec[10..10 + copy_len].copy_from_slice(&value_bytes[..copy_len]);
154 rec
155 }
156
157 fn make_fits_header() -> Vec<u8> {
158 let mut buf = Vec::new();
159 buf.extend_from_slice(&make_fits_record("SIMPLE", "T"));
160 buf.extend_from_slice(&make_fits_record("BITPIX", "16"));
161 buf.extend_from_slice(&make_fits_record("NAXIS", "2"));
162 buf.extend_from_slice(&make_fits_record("NAXIS1", "512"));
163 buf.extend_from_slice(&make_fits_record("NAXIS2", "512"));
164 buf.extend_from_slice(&make_fits_record("END", ""));
165 while buf.len() < 2880 {
167 buf.push(0);
168 }
169 buf
170 }
171
172 #[test]
173 fn fits_routes_to_ricepp_when_enabled() {
174 let c = FitsCategorizer;
175 let fits = make_fits_header();
176 let cat = c
177 .categorize(Path::new("/x.fits"), &fits)
178 .expect("fits claims");
179 assert_eq!(cat.codec_id, limnifs_core::codec::CODEC_RICEPP);
180 }
181
182 #[test]
183 fn fits_header_parsed_correctly() {
184 let fits = make_fits_header();
185 let params = parse_fits(&fits).expect("fits parses");
186 assert_eq!(params.bitpix, 16);
187 assert_eq!(params.naxis, 2);
188 assert_eq!(params.naxis1, 512);
189 assert_eq!(params.naxis2, 512);
190 }
191
192 #[test]
193 fn rejects_non_fits_magic() {
194 let junk = vec![0u8; 4096];
195 assert!(parse_fits(&junk).is_none());
196 }
197}