starfield 0.12.6

Astronomical data reduction toolkit with star catalogs, coordinate systems, and star finding algorithms (inspired by skyfield)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
//! Double Array File (DAF) format reader for SPICE files
//!
//! Handles reading NAIF's DAF binary format, used for SPK and PCK files.

use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use std::sync::Mutex;

use byteorder::{BigEndian, ByteOrder, LittleEndian};
use memmap2::{Mmap, MmapOptions};

use super::errors::{io_err, JplephemError, Result};

const RECORD_SIZE: usize = 1024;
const DOUBLE_SIZE: usize = 8;

/// DAF file endianness
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Endian {
    Big,
    Little,
}

/// Double Array File (DAF) reader
pub struct DAF {
    pub path: PathBuf,
    file: Option<Mutex<File>>,
    /// File ID word (e.g. "DAF/SPK", "DAF/PCK")
    pub locidw: String,
    /// Number of double-precision components per summary
    pub nd: u32,
    /// Number of integer components per summary
    pub ni: u32,
    /// Forward pointer to first summary record
    pub fward: u32,
    /// Backward pointer to last summary record
    pub bward: u32,
    /// First free address
    pub free: u32,
    /// Internal file name
    pub ifname: String,
    /// Byte order
    pub endian: Endian,
    /// Memory map for efficient access
    map: Option<Mmap>,
    /// In-memory byte buffer (used by `from_bytes`)
    bytes: Option<Vec<u8>>,
    /// Size of each summary entry in bytes
    summary_step: usize,
    /// Size of each summary entry in double-words
    summary_length: usize,
}

impl DAF {
    /// Open a DAF file at the given path
    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
        let path_buf = path.as_ref().to_path_buf();
        let file = File::open(&path_buf).map_err(|e| io_err(&path_buf, e))?;

        let mut daf = DAF {
            path: path_buf,
            file: Some(Mutex::new(file)),
            locidw: String::new(),
            nd: 0,
            ni: 0,
            fward: 0,
            bward: 0,
            free: 0,
            ifname: String::new(),
            endian: Endian::Little,
            map: None,
            bytes: None,
            summary_step: 0,
            summary_length: 0,
        };

        daf.read_header()?;
        daf.setup_memory_map()?;

        daf.summary_length = daf.nd as usize + (daf.ni as usize).div_ceil(2);
        daf.summary_step = 8 * daf.summary_length;

        Ok(daf)
    }

    /// Create a DAF from an in-memory byte buffer
    ///
    /// Parses the same binary format as a file, but from `&[u8]`.
    /// Useful with `include_bytes!()` for compile-time embedded assets.
    pub fn from_bytes(data: &[u8]) -> Result<Self> {
        if data.len() < RECORD_SIZE {
            return Err(JplephemError::InvalidFormat(
                "Data too small for a DAF file".to_string(),
            ));
        }

        let mut daf = DAF {
            path: PathBuf::from("<memory>"),
            file: None,
            locidw: String::new(),
            nd: 0,
            ni: 0,
            fward: 0,
            bward: 0,
            free: 0,
            ifname: String::new(),
            endian: Endian::Little,
            map: None,
            bytes: Some(data.to_vec()),
            summary_step: 0,
            summary_length: 0,
        };

        daf.read_header()?;

        daf.summary_length = daf.nd as usize + (daf.ni as usize).div_ceil(2);
        daf.summary_step = 8 * daf.summary_length;

        Ok(daf)
    }

    fn read_header(&mut self) -> Result<()> {
        let header = self.read_record(1)?;

        let locidw = String::from_utf8_lossy(&header[0..8])
            .trim_end()
            .to_string();

        // Determine endianness: ND and NI should be small values (1-10)
        let nd_le = LittleEndian::read_u32(&header[8..12]);
        let ni_le = LittleEndian::read_u32(&header[12..16]);
        let nd_be = BigEndian::read_u32(&header[8..12]);
        let ni_be = BigEndian::read_u32(&header[12..16]);

        let endian = if nd_le > 0 && nd_le < 10 && ni_le > 0 && ni_le < 10 {
            Endian::Little
        } else if nd_be > 0 && nd_be < 10 && ni_be > 0 && ni_be < 10 {
            Endian::Big
        } else {
            return Err(JplephemError::InvalidFormat(format!(
                "Cannot determine endianness: LE({nd_le},{ni_le}) BE({nd_be},{ni_be})"
            )));
        };

        // DAF header layout:
        //   0..8    LOCIDW (8 bytes, ASCII)
        //   8..12   ND (u32)
        //   12..16  NI (u32)
        //   16..76  LOCIFN (60 bytes, internal filename)
        //   76..80  FWARD (u32, first summary record)
        //   80..84  BWARD (u32, last summary record)
        //   84..88  FREE (u32, first free address)
        let (nd, ni, fward, bward, free) = match endian {
            Endian::Little => (
                LittleEndian::read_u32(&header[8..12]),
                LittleEndian::read_u32(&header[12..16]),
                LittleEndian::read_u32(&header[76..80]),
                LittleEndian::read_u32(&header[80..84]),
                LittleEndian::read_u32(&header[84..88]),
            ),
            Endian::Big => (
                BigEndian::read_u32(&header[8..12]),
                BigEndian::read_u32(&header[12..16]),
                BigEndian::read_u32(&header[76..80]),
                BigEndian::read_u32(&header[80..84]),
                BigEndian::read_u32(&header[84..88]),
            ),
        };

        let ifname = String::from_utf8_lossy(&header[16..76])
            .trim_end()
            .to_string();

        self.locidw = locidw;
        self.nd = nd;
        self.ni = ni;
        self.fward = fward;
        self.bward = bward;
        self.free = free;
        self.ifname = ifname;
        self.endian = endian;

        if self.fward == 0 || self.bward == 0 {
            return Err(JplephemError::InvalidFormat(format!(
                "Invalid DAF header: nd={nd}, ni={ni}, fward={fward}, bward={bward}"
            )));
        }

        Ok(())
    }

    fn setup_memory_map(&mut self) -> Result<()> {
        if let Some(ref mut mutex) = self.file {
            let file = mutex.get_mut().unwrap();
            if let Ok(file_clone) = file.try_clone() {
                if let Ok(mmap) = unsafe { MmapOptions::new().map(&file_clone) } {
                    self.map = Some(mmap);
                }
            }
        }
        Ok(())
    }

    fn get_file(&self) -> Result<std::sync::MutexGuard<'_, std::fs::File>> {
        self.file
            .as_ref()
            .ok_or_else(|| JplephemError::Other("No file handle (in-memory DAF)".to_string()))?
            .lock()
            .map_err(|_| JplephemError::Other("Failed to lock file".to_string()))
    }

    /// Read a 1024-byte record at the given record number (1-indexed)
    pub fn read_record(&self, record_number: usize) -> Result<Vec<u8>> {
        if record_number < 1 {
            return Err(JplephemError::InvalidFormat(format!(
                "Invalid record number: {record_number}"
            )));
        }
        let offset = (record_number - 1) * RECORD_SIZE;

        // Try in-memory bytes first
        if let Some(ref bytes) = self.bytes {
            if offset + RECORD_SIZE <= bytes.len() {
                return Ok(bytes[offset..offset + RECORD_SIZE].to_vec());
            }
            return Err(JplephemError::InvalidFormat(format!(
                "Record {record_number} out of range for in-memory data ({} bytes)",
                bytes.len()
            )));
        }

        // Try memory map
        if let Some(ref map) = self.map {
            if offset + RECORD_SIZE <= map.len() {
                return Ok(map[offset..offset + RECORD_SIZE].to_vec());
            }
        }

        // Fall back to file I/O
        let mut file = self.get_file()?;
        let mut buffer = vec![0u8; RECORD_SIZE];
        file.seek(SeekFrom::Start(offset as u64))
            .map_err(|e| io_err(&self.path, e))?;
        file.read_exact(&mut buffer)
            .map_err(|e| io_err(&self.path, e))?;
        Ok(buffer)
    }

    /// Read comments from the comment area (records 2..fward)
    pub fn comments(&self) -> Result<String> {
        let fward = self.fward as usize;
        if fward <= 2 {
            return Ok(String::new());
        }

        let mut comments = String::new();
        for record_number in 2..fward {
            let record = self.read_record(record_number)?;
            let text = String::from_utf8_lossy(&record);
            comments.push_str(&text);
        }

        Ok(comments
            .trim_end_matches(|c: char| c == '\0' || c.is_whitespace())
            .to_string())
    }

    /// Read summary records and extract segment metadata
    ///
    /// Returns pairs of (name_bytes, summary_values) where summary_values
    /// contains ND doubles followed by NI integers (as f64).
    pub fn summaries(&self) -> Result<Vec<(Vec<u8>, Vec<f64>)>> {
        let mut result = Vec::new();
        let mut visited = std::collections::HashSet::new();

        if self.fward == 0 || self.fward > 10000 {
            return Err(JplephemError::InvalidFormat(format!(
                "Invalid forward pointer: {}",
                self.fward
            )));
        }

        let mut record_number = self.fward as usize;

        while record_number > 0 {
            if !visited.insert(record_number) {
                break; // Cycle detected
            }

            let summary_data = self.read_record(record_number)?;
            let name_data = self.read_record(record_number + 1)?;

            // First 24 bytes: NEXT (f64), PREV (f64), NSUM (f64)
            let next = self.read_f64_from_bytes(&summary_data[0..8]) as usize;
            let _prev = self.read_f64_from_bytes(&summary_data[8..16]) as usize;
            let n_summaries = self.read_f64_from_bytes(&summary_data[16..24]) as usize;

            let max_summaries = (RECORD_SIZE - 24) / self.summary_step.max(1);
            if n_summaries > max_summaries {
                return Err(JplephemError::InvalidFormat(format!(
                    "Too many summaries in record {record_number}: {n_summaries} > {max_summaries}"
                )));
            }

            for i in 0..n_summaries {
                let name_start = i * self.summary_step;
                let name_end = (name_start + self.summary_step).min(name_data.len());
                let name = name_data[name_start..name_end].to_vec();

                let summary_start = 24 + i * self.summary_step;

                let mut values = Vec::with_capacity(self.nd as usize + self.ni as usize);

                // Read ND double-precision values
                for j in 0..self.nd as usize {
                    let pos = summary_start + j * 8;
                    if pos + 8 <= summary_data.len() {
                        values.push(self.read_f64_from_bytes(&summary_data[pos..pos + 8]));
                    }
                }

                // Read NI integer values (packed as pairs of i32 into 8-byte slots)
                let int_start = summary_start + (self.nd as usize * 8);
                for j in 0..self.ni as usize {
                    let double_idx = j / 2;
                    let int_offset = j % 2;
                    let pos = int_start + double_idx * 8 + int_offset * 4;

                    if pos + 4 <= summary_data.len() {
                        let value = match self.endian {
                            Endian::Big => BigEndian::read_i32(&summary_data[pos..pos + 4]) as f64,
                            Endian::Little => {
                                LittleEndian::read_i32(&summary_data[pos..pos + 4]) as f64
                            }
                        };
                        values.push(value);
                    }
                }

                result.push((name, values));
            }

            if next == 0 || next == record_number {
                break;
            }
            record_number = next;
        }

        if result.is_empty() {
            return Err(JplephemError::InvalidFormat(
                "No summaries found in DAF file".to_string(),
            ));
        }

        Ok(result)
    }

    /// Read an array of f64 values from the file (1-indexed addresses)
    pub fn read_array(&self, start: usize, end: usize) -> Result<Vec<f64>> {
        if start < 1 || end < start {
            return Err(JplephemError::InvalidFormat(format!(
                "Invalid array bounds: start={start}, end={end}"
            )));
        }

        let length = end - start + 1;

        // Helper: decode f64 values from a byte slice
        let decode_slice = |slice: &[u8], count: usize| -> Vec<f64> {
            let mut result = Vec::with_capacity(count);
            for i in 0..count {
                let pos = i * DOUBLE_SIZE;
                let value = match self.endian {
                    Endian::Big => BigEndian::read_f64(&slice[pos..pos + DOUBLE_SIZE]),
                    Endian::Little => LittleEndian::read_f64(&slice[pos..pos + DOUBLE_SIZE]),
                };
                result.push(value);
            }
            result
        };

        let byte_start = (start - 1) * DOUBLE_SIZE;
        let byte_end = byte_start + length * DOUBLE_SIZE;

        // Try in-memory bytes first
        if let Some(ref bytes) = self.bytes {
            if byte_end <= bytes.len() {
                return Ok(decode_slice(&bytes[byte_start..byte_end], length));
            }
            return Err(JplephemError::InvalidFormat(format!(
                "Array bounds [{start}..{end}] out of range for in-memory data ({} bytes)",
                bytes.len()
            )));
        }

        // Try memory map
        if let Some(ref map) = self.map {
            if byte_end <= map.len() {
                return Ok(decode_slice(&map[byte_start..byte_end], length));
            }
        }

        // Fall back to file I/O
        let mut file = self.get_file()?;
        file.seek(SeekFrom::Start(byte_start as u64))
            .map_err(|e| io_err(&self.path, e))?;

        let mut buffer = vec![0u8; length * DOUBLE_SIZE];
        file.read_exact(&mut buffer)
            .map_err(|e| io_err(&self.path, e))?;

        Ok(decode_slice(&buffer, length))
    }

    /// Map an array of f64 values (alias for read_array, uses mmap when available)
    pub fn map_array(&self, start: usize, end: usize) -> Result<Vec<f64>> {
        self.read_array(start, end)
    }

    /// Read an f64 from a byte slice using the file's endianness
    fn read_f64_from_bytes(&self, bytes: &[u8]) -> f64 {
        match self.endian {
            Endian::Big => BigEndian::read_f64(bytes),
            Endian::Little => LittleEndian::read_f64(bytes),
        }
    }
}