Skip to main content

sidereon_core/astro/
spk.rs

1//! JPL SPK/DAF binary-kernel parsing.
2//!
3//! This module starts with the DAF container layer used by SPK kernels. It can
4//! read segment descriptors from an in-memory byte slice without requiring any
5//! file I/O.
6//!
7//! ```
8//! use sidereon_core::astro::spk::Spk;
9//!
10//! const RECORD_BYTES: usize = 1024;
11//! const START_ADDRESS: usize = 513;
12//! const END_ADDRESS: usize = 524;
13//!
14//! fn put_i32(bytes: &mut [u8], offset: usize, value: i32) {
15//!     bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
16//! }
17//!
18//! fn put_f64(bytes: &mut [u8], address: usize, value: f64) {
19//!     let offset = (address - 1) * 8;
20//!     bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
21//! }
22//!
23//! fn put_ascii(bytes: &mut [u8], offset: usize, len: usize, text: &str) {
24//!     bytes[offset..offset + len].fill(b' ');
25//!     bytes[offset..offset + text.len()].copy_from_slice(text.as_bytes());
26//! }
27//!
28//! fn put_summary(bytes: &mut [u8], offset: usize) {
29//!     bytes[offset..offset + 8].copy_from_slice(&0.0f64.to_le_bytes());
30//!     bytes[offset + 8..offset + 16].copy_from_slice(&10.0f64.to_le_bytes());
31//!     for (index, value) in [10, 0, 1, 3, START_ADDRESS as i32, END_ADDRESS as i32]
32//!         .into_iter()
33//!         .enumerate()
34//!     {
35//!         put_i32(bytes, offset + 16 + index * 4, value);
36//!     }
37//! }
38//!
39//! let mut bytes = vec![0u8; END_ADDRESS * 8];
40//! bytes[0..8].copy_from_slice(b"DAF/SPK ");
41//! put_i32(&mut bytes, 8, 2);
42//! put_i32(&mut bytes, 12, 6);
43//! put_ascii(&mut bytes, 16, 60, "DOC SPK");
44//! put_i32(&mut bytes, 76, 3);
45//! put_i32(&mut bytes, 80, 3);
46//! put_i32(&mut bytes, 84, (END_ADDRESS + 1) as i32);
47//! bytes[88..96].copy_from_slice(b"LTL-IEEE");
48//!
49//! let summary_record = RECORD_BYTES * 2;
50//! bytes[summary_record..summary_record + 8].copy_from_slice(&0.0f64.to_le_bytes());
51//! bytes[summary_record + 8..summary_record + 16].copy_from_slice(&0.0f64.to_le_bytes());
52//! bytes[summary_record + 16..summary_record + 24].copy_from_slice(&1.0f64.to_le_bytes());
53//! put_summary(&mut bytes, summary_record + 24);
54//! put_ascii(&mut bytes, RECORD_BYTES * 3, 40, "BODY 10 TO SSB");
55//!
56//! put_f64(&mut bytes, START_ADDRESS, 5.0);
57//! put_f64(&mut bytes, START_ADDRESS + 1, 5.0);
58//! put_f64(&mut bytes, START_ADDRESS + 2, 100.0);
59//! put_f64(&mut bytes, START_ADDRESS + 3, 10.0);
60//! put_f64(&mut bytes, START_ADDRESS + 4, 1.0);
61//! put_f64(&mut bytes, START_ADDRESS + 5, 1.0);
62//! put_f64(&mut bytes, START_ADDRESS + 6, 0.0);
63//! put_f64(&mut bytes, START_ADDRESS + 7, 0.1);
64//! put_f64(&mut bytes, START_ADDRESS + 8, 0.0);
65//! put_f64(&mut bytes, START_ADDRESS + 9, 10.0);
66//! put_f64(&mut bytes, START_ADDRESS + 10, 8.0);
67//! put_f64(&mut bytes, START_ADDRESS + 11, 1.0);
68//!
69//! let spk = Spk::from_bytes(&bytes)?;
70//! let state = spk.spk_state(10, 0, 5.0)?;
71//! assert_eq!(state.target, 10);
72//! assert_eq!(state.center, 0);
73//! assert_eq!(state.frame, 1);
74//! assert_eq!(state.position_km, [100.0, 10.0, 1.0]);
75//! assert_eq!(state.velocity_km_s, Some([1.0, 0.0, 0.1]));
76//! # Ok::<(), sidereon_core::astro::spk::SpkError>(())
77//! ```
78
79extern crate alloc;
80
81use alloc::string::{String, ToString};
82use alloc::vec::Vec;
83use core::fmt;
84
85const DAF_RECORD_BYTES: usize = 1024;
86const DAF_ID_BYTES: usize = 8;
87const DAF_INTERNAL_NAME_BYTES: usize = 60;
88const DAF_BINARY_FORMAT_OFFSET: usize = 88;
89const DAF_BINARY_FORMAT_BYTES: usize = 8;
90const DAF_FILE_RECORD_BYTES: usize = DAF_RECORD_BYTES;
91const SUMMARY_CONTROL_WORDS: usize = 3;
92const SPK_ND: i32 = 2;
93const SPK_NI: i32 = 6;
94const SPK_TYPE_2: i32 = 2;
95const SPK_TYPE_3: i32 = 3;
96const SPK_TYPE_21: i32 = 21;
97/// Largest difference-table dimension (`MAXDIM`) a type-21 record may declare.
98/// Matches CSPICE `MAXTRM` from `spk21.inc`; records above this are rejected.
99const SPK_TYPE_21_MAX_TABLE_DIM: usize = 25;
100/// Number of record epochs covered by each directory epoch in a type-21 segment.
101const SPK_TYPE_21_DIRECTORY_STRIDE: usize = 100;
102
103/// Endianness declared by the DAF binary-format identifier.
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum DafByteOrder {
106    /// Little-endian IEEE floating-point and integer words (`LTL-IEEE`).
107    LittleEndian,
108    /// Big-endian IEEE floating-point and integer words (`BIG-IEEE`).
109    BigEndian,
110}
111
112impl DafByteOrder {
113    fn read_i32(self, bytes: &[u8], offset: usize, field: &'static str) -> Result<i32, SpkError> {
114        let data = bytes.get(offset..offset + 4).ok_or(SpkError::Truncated {
115            context: field,
116            needed: offset + 4,
117            actual: bytes.len(),
118        })?;
119        let mut word = [0u8; 4];
120        word.copy_from_slice(data);
121        Ok(match self {
122            DafByteOrder::LittleEndian => i32::from_le_bytes(word),
123            DafByteOrder::BigEndian => i32::from_be_bytes(word),
124        })
125    }
126
127    fn read_f64(self, bytes: &[u8], offset: usize, field: &'static str) -> Result<f64, SpkError> {
128        let data = bytes.get(offset..offset + 8).ok_or(SpkError::Truncated {
129            context: field,
130            needed: offset + 8,
131            actual: bytes.len(),
132        })?;
133        let mut word = [0u8; 8];
134        word.copy_from_slice(data);
135        Ok(match self {
136            DafByteOrder::LittleEndian => f64::from_le_bytes(word),
137            DafByteOrder::BigEndian => f64::from_be_bytes(word),
138        })
139    }
140}
141
142/// Parsed metadata from the first DAF record.
143#[derive(Debug, Clone, PartialEq)]
144pub struct DafFileRecord {
145    /// The eight-byte DAF identification word, such as `DAF/SPK`.
146    pub id_word: String,
147    /// The DAF array type from the identification word, such as `SPK`.
148    pub file_type: String,
149    /// Number of double-precision components in each array summary.
150    pub double_components: i32,
151    /// Number of integer components in each array summary.
152    pub integer_components: i32,
153    /// DAF internal file name with trailing padding removed.
154    pub internal_name: String,
155    /// One-based record number of the first summary record.
156    pub forward_record: i32,
157    /// One-based record number of the final summary record.
158    pub backward_record: i32,
159    /// First free DAF address.
160    pub free_address: i32,
161    /// Declared byte order for numeric DAF words.
162    pub byte_order: DafByteOrder,
163    /// Raw eight-byte binary-format identifier with trailing padding removed.
164    pub binary_format: String,
165}
166
167/// Descriptor for one SPK segment advertised by the DAF summary records.
168#[derive(Debug, Clone, PartialEq)]
169pub struct SpkSegmentDescriptor {
170    /// Segment name from the paired DAF name record.
171    pub name: String,
172    /// Coverage start ET/TDB seconds past J2000.
173    pub start_et: f64,
174    /// Coverage stop ET/TDB seconds past J2000.
175    pub stop_et: f64,
176    /// NAIF target body identifier.
177    pub target: i32,
178    /// NAIF center body identifier.
179    pub center: i32,
180    /// NAIF reference-frame identifier.
181    pub frame: i32,
182    /// SPK segment data type.
183    pub data_type: i32,
184    /// One-based DAF address of the first segment data word.
185    pub start_address: i32,
186    /// One-based DAF address of the last segment data word.
187    pub end_address: i32,
188}
189
190/// A parsed DAF/SPK directory containing the file record and segment list.
191#[derive(Debug, Clone, PartialEq)]
192pub struct DafSpk {
193    /// File-level DAF metadata.
194    pub file_record: DafFileRecord,
195    /// SPK segment descriptors in summary-record order.
196    pub segments: Vec<SpkSegmentDescriptor>,
197}
198
199/// In-memory SPK kernel with parsed segment descriptors.
200#[derive(Debug, Clone, PartialEq)]
201pub struct Spk {
202    bytes: Vec<u8>,
203    directory: DafSpk,
204}
205
206/// Position and velocity evaluated from an SPK segment.
207#[derive(Debug, Clone, Copy, PartialEq)]
208pub struct SpkStateVector {
209    /// Position vector in kilometers.
210    pub position_km: [f64; 3],
211    /// Velocity vector in kilometers per second.
212    pub velocity_km_s: [f64; 3],
213}
214
215/// State returned by an SPK body-to-center query.
216#[derive(Debug, Clone, Copy, PartialEq)]
217pub struct SpkState {
218    /// NAIF target body identifier for the returned relative state.
219    pub target: i32,
220    /// NAIF center body identifier for the returned relative state.
221    pub center: i32,
222    /// Position of the target relative to the requested center, in kilometers.
223    pub position_km: [f64; 3],
224    /// Velocity of the target relative to the requested center, in kilometers per second.
225    ///
226    /// Type-3 segments provide velocity directly. Queries that use any type-2
227    /// segment return `None` because type 2 stores position only.
228    pub velocity_km_s: Option<[f64; 3]>,
229    /// NAIF reference-frame identifier shared by all segments in the resolved path.
230    pub frame: i32,
231}
232
233/// Error returned while reading an SPK/DAF byte slice.
234#[derive(Debug, Clone, PartialEq)]
235pub enum SpkError {
236    /// File loading failed while reading an SPK kernel from disk.
237    Io {
238        /// Path passed to [`Spk::load`].
239        path: String,
240        /// Display string from the underlying I/O error.
241        message: String,
242    },
243    /// The input ended before a required DAF field could be read.
244    Truncated {
245        /// Name of the field or record being read.
246        context: &'static str,
247        /// Minimum number of bytes needed to read the field.
248        needed: usize,
249        /// Number of bytes available in the input.
250        actual: usize,
251    },
252    /// The file record did not identify a DAF/SPK kernel.
253    UnsupportedDafId {
254        /// Identification word found in the file record.
255        id_word: String,
256    },
257    /// The file record named a binary format this parser does not implement.
258    UnsupportedBinaryFormat {
259        /// Binary-format identifier found in the file record.
260        binary_format: String,
261    },
262    /// The DAF summary shape was not the SPK shape `ND=2, NI=6`.
263    UnsupportedSummaryShape {
264        /// Number of double components declared by the file record.
265        nd: i32,
266        /// Number of integer components declared by the file record.
267        ni: i32,
268    },
269    /// A numeric DAF field was present but outside the supported range.
270    InvalidField {
271        /// Name of the invalid DAF field.
272        field: &'static str,
273        /// Value decoded from the field.
274        value: i32,
275    },
276    /// A floating-point SPK field was present but outside the supported range.
277    InvalidDoubleField {
278        /// Name of the invalid SPK field.
279        field: &'static str,
280        /// Value decoded from the field.
281        value: f64,
282    },
283    /// The requested ET is not covered by the segment.
284    OutOfCoverage {
285        /// Requested ET/TDB seconds past J2000.
286        et: f64,
287        /// Segment coverage start ET/TDB seconds past J2000.
288        start_et: f64,
289        /// Segment coverage stop ET/TDB seconds past J2000.
290        stop_et: f64,
291    },
292    /// The segment data type is not supported by the requested evaluator.
293    UnsupportedSegmentType {
294        /// SPK segment type expected by the evaluator.
295        expected: i32,
296        /// SPK segment type found in the descriptor.
297        actual: i32,
298    },
299    /// The segment addresses or directory do not describe a valid SPK layout.
300    InvalidSegmentLayout {
301        /// Name of the malformed segment component.
302        context: &'static str,
303    },
304    /// No segment in the kernel names the requested NAIF body id.
305    UnknownBody {
306        /// NAIF body identifier that was not present in any segment.
307        body: i32,
308    },
309    /// The kernel has both bodies but no segment chain connecting them.
310    NoSegmentPath {
311        /// Requested target NAIF body identifier.
312        target: i32,
313        /// Requested center NAIF body identifier.
314        center: i32,
315    },
316    /// A segment chain exists, but no complete chain covers the requested ET.
317    CoverageGap {
318        /// Requested target NAIF body identifier.
319        target: i32,
320        /// Requested center NAIF body identifier.
321        center: i32,
322        /// Requested ET/TDB seconds past J2000.
323        et: f64,
324    },
325    /// A public state query reached an SPK segment type it cannot evaluate.
326    UnsupportedStateSegmentType {
327        /// SPK segment type found in the descriptor.
328        data_type: i32,
329    },
330    /// A chained query would combine states expressed in different frames.
331    FrameMismatch {
332        /// Frame identifier from the accumulated path.
333        first: i32,
334        /// Frame identifier from the next segment.
335        second: i32,
336    },
337}
338
339impl fmt::Display for SpkError {
340    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
341        match self {
342            SpkError::Io { path, message } => {
343                write!(f, "failed to read SPK kernel {path}: {message}")
344            }
345            SpkError::Truncated {
346                context,
347                needed,
348                actual,
349            } => write!(
350                f,
351                "truncated SPK/DAF input while reading {context}: need {needed} bytes, have {actual}"
352            ),
353            SpkError::UnsupportedDafId { id_word } => {
354                write!(f, "unsupported DAF identification word {id_word:?}")
355            }
356            SpkError::UnsupportedBinaryFormat { binary_format } => {
357                write!(f, "unsupported DAF binary format {binary_format:?}")
358            }
359            SpkError::UnsupportedSummaryShape { nd, ni } => {
360                write!(f, "unsupported SPK summary shape ND={nd}, NI={ni}")
361            }
362            SpkError::InvalidField { field, value } => {
363                write!(f, "invalid SPK/DAF field {field}: {value}")
364            }
365            SpkError::InvalidDoubleField { field, value } => {
366                write!(f, "invalid SPK field {field}: {value}")
367            }
368            SpkError::OutOfCoverage {
369                et,
370                start_et,
371                stop_et,
372            } => write!(
373                f,
374                "ET {et} is outside SPK segment coverage [{start_et}, {stop_et}]"
375            ),
376            SpkError::UnsupportedSegmentType { expected, actual } => write!(
377                f,
378                "unsupported SPK segment type {actual}; expected type {expected}"
379            ),
380            SpkError::InvalidSegmentLayout { context } => {
381                write!(f, "invalid SPK segment layout: {context}")
382            }
383            SpkError::UnknownBody { body } => {
384                write!(f, "unknown SPK body {body}")
385            }
386            SpkError::NoSegmentPath { target, center } => {
387                write!(f, "no SPK segment path from target {target} to center {center}")
388            }
389            SpkError::CoverageGap { target, center, et } => write!(
390                f,
391                "no SPK segment path from target {target} to center {center} covers ET {et}"
392            ),
393            SpkError::UnsupportedStateSegmentType { data_type } => {
394                write!(f, "unsupported SPK state segment type {data_type}")
395            }
396            SpkError::FrameMismatch { first, second } => write!(
397                f,
398                "cannot chain SPK states across frame ids {first} and {second}"
399            ),
400        }
401    }
402}
403
404impl std::error::Error for SpkError {}
405
406impl Spk {
407    /// Parse an in-memory SPK kernel from a byte slice.
408    pub fn from_bytes(bytes: &[u8]) -> Result<Self, SpkError> {
409        Self::from_vec(bytes.to_vec())
410    }
411
412    /// Read and parse an SPK kernel from a filesystem path.
413    pub fn load(path: impl AsRef<std::path::Path>) -> Result<Self, SpkError> {
414        let path = path.as_ref();
415        let bytes = std::fs::read(path).map_err(|error| SpkError::Io {
416            path: path.display().to_string(),
417            message: error.to_string(),
418        })?;
419        Self::from_vec(bytes)
420    }
421
422    /// Return parsed file-record metadata.
423    pub fn file_record(&self) -> &DafFileRecord {
424        &self.directory.file_record
425    }
426
427    /// Return parsed segment descriptors in DAF summary order.
428    pub fn segments(&self) -> &[SpkSegmentDescriptor] {
429        &self.directory.segments
430    }
431
432    /// Query the state of `target` relative to `center` at ET/TDB seconds past J2000.
433    pub fn spk_state(&self, target: i32, center: i32, et: f64) -> Result<SpkState, SpkError> {
434        if !et.is_finite() {
435            return Err(SpkError::InvalidDoubleField {
436                field: "ET",
437                value: et,
438            });
439        }
440
441        if !self.body_is_known(target) {
442            return Err(SpkError::UnknownBody { body: target });
443        }
444        if !self.body_is_known(center) {
445            return Err(SpkError::UnknownBody { body: center });
446        }
447        if target == center {
448            if !self.body_has_coverage_at(target, et) {
449                return Err(SpkError::CoverageGap { target, center, et });
450            }
451            return Ok(SpkState {
452                target,
453                center,
454                position_km: [0.0; 3],
455                velocity_km_s: Some([0.0; 3]),
456                frame: 0,
457            });
458        }
459        if !self.has_segment_path(target, center) {
460            return Err(SpkError::NoSegmentPath { target, center });
461        }
462
463        self.covering_state_path(target, center, et)
464            .ok_or(SpkError::CoverageGap { target, center, et })?
465    }
466
467    fn from_vec(bytes: Vec<u8>) -> Result<Self, SpkError> {
468        let directory = parse_daf_spk(&bytes)?;
469        Ok(Self { bytes, directory })
470    }
471
472    fn body_is_known(&self, body: i32) -> bool {
473        self.directory
474            .segments
475            .iter()
476            .any(|segment| segment.target == body || segment.center == body)
477    }
478
479    fn body_has_coverage_at(&self, body: i32, et: f64) -> bool {
480        self.directory.segments.iter().any(|segment| {
481            (segment.target == body || segment.center == body)
482                && et >= segment.start_et
483                && et <= segment.stop_et
484        })
485    }
486
487    fn has_segment_path(&self, target: i32, center: i32) -> bool {
488        let mut visited = Vec::new();
489        let mut queue = Vec::new();
490        visited.push(target);
491        queue.push(target);
492
493        let mut cursor = 0;
494        while cursor < queue.len() {
495            let body = queue[cursor];
496            cursor += 1;
497
498            if body == center {
499                return true;
500            }
501
502            for segment in self.directory.segments.iter().rev() {
503                let next = if segment.target == body {
504                    segment.center
505                } else if segment.center == body {
506                    segment.target
507                } else {
508                    continue;
509                };
510
511                if visited.contains(&next) {
512                    continue;
513                }
514                if next == center {
515                    return true;
516                }
517                visited.push(next);
518                queue.push(next);
519            }
520        }
521
522        false
523    }
524
525    fn covering_state_path(
526        &self,
527        target: i32,
528        center: i32,
529        et: f64,
530    ) -> Option<Result<SpkState, SpkError>> {
531        let root = StateSearchNode {
532            body: target,
533            state: AccumulatedSpkState {
534                position_km: [0.0; 3],
535                velocity_km_s: Some([0.0; 3]),
536                frame: None,
537            },
538        };
539
540        let mut search = StatePathSearch::new(target);
541
542        self.covering_state_path_from(root, center, et, &mut search)
543            .or_else(|| search.fallback())
544    }
545
546    fn covering_state_path_from(
547        &self,
548        node: StateSearchNode,
549        center: i32,
550        et: f64,
551        search: &mut StatePathSearch,
552    ) -> Option<Result<SpkState, SpkError>> {
553        for segment in self.directory.segments.iter().rev() {
554            let (next, sign) = if segment.target == node.body {
555                (segment.center, 1.0)
556            } else if segment.center == node.body {
557                (segment.target, -1.0)
558            } else {
559                continue;
560            };
561
562            if et < segment.start_et || et > segment.stop_et {
563                continue;
564            }
565
566            let leg = match self.evaluate_segment_state(segment, et) {
567                Ok(leg) => leg,
568                Err(SpkError::OutOfCoverage { .. }) => continue,
569                Err(error @ SpkError::UnsupportedStateSegmentType { .. }) => {
570                    if next == center && search.is_root() {
571                        return Some(Err(error));
572                    }
573                    if search.first_unsupported.is_none() {
574                        search.first_unsupported = Some(error);
575                    }
576                    continue;
577                }
578                Err(error) => return Some(Err(error)),
579            };
580            let state = match node.state.extend(leg, sign) {
581                Ok(state) => state,
582                Err(error @ SpkError::FrameMismatch { .. }) => {
583                    if search.first_frame_mismatch.is_none() {
584                        search.first_frame_mismatch = Some(error);
585                    }
586                    continue;
587                }
588                Err(error) => return Some(Err(error)),
589            };
590
591            if next == center {
592                let state = state.into_state(search.target, center);
593                if state.velocity_km_s.is_some() || search.is_root() {
594                    return Some(Ok(state));
595                }
596                if search.first_position_only_state.is_none() {
597                    search.first_position_only_state = Some(state);
598                }
599                continue;
600            }
601
602            if search.visited.contains(&next) {
603                continue;
604            }
605            search.visited.push(next);
606            if let Some(result) = self.covering_state_path_from(
607                StateSearchNode { body: next, state },
608                center,
609                et,
610                search,
611            ) {
612                return Some(result);
613            }
614            search.visited.pop();
615        }
616
617        None
618    }
619
620    fn evaluate_segment_state(
621        &self,
622        segment: &SpkSegmentDescriptor,
623        et: f64,
624    ) -> Result<SpkState, SpkError> {
625        match segment.data_type {
626            SPK_TYPE_2 => Ok(SpkState {
627                target: segment.target,
628                center: segment.center,
629                position_km: evaluate_type2_position(
630                    &self.bytes,
631                    self.directory.file_record.byte_order,
632                    segment,
633                    et,
634                )?,
635                velocity_km_s: None,
636                frame: segment.frame,
637            }),
638            SPK_TYPE_3 => {
639                let state = evaluate_type3_state(
640                    &self.bytes,
641                    self.directory.file_record.byte_order,
642                    segment,
643                    et,
644                )?;
645                Ok(SpkState {
646                    target: segment.target,
647                    center: segment.center,
648                    position_km: state.position_km,
649                    velocity_km_s: Some(state.velocity_km_s),
650                    frame: segment.frame,
651                })
652            }
653            SPK_TYPE_21 => {
654                let state = evaluate_type21_state(
655                    &self.bytes,
656                    self.directory.file_record.byte_order,
657                    segment,
658                    et,
659                )?;
660                Ok(SpkState {
661                    target: segment.target,
662                    center: segment.center,
663                    position_km: state.position_km,
664                    velocity_km_s: Some(state.velocity_km_s),
665                    frame: segment.frame,
666                })
667            }
668            data_type => Err(SpkError::UnsupportedStateSegmentType { data_type }),
669        }
670    }
671}
672
673#[derive(Debug, Clone, Copy)]
674struct StateSearchNode {
675    body: i32,
676    state: AccumulatedSpkState,
677}
678
679#[derive(Debug)]
680struct StatePathSearch {
681    target: i32,
682    visited: Vec<i32>,
683    first_unsupported: Option<SpkError>,
684    first_frame_mismatch: Option<SpkError>,
685    first_position_only_state: Option<SpkState>,
686}
687
688impl StatePathSearch {
689    fn new(target: i32) -> Self {
690        Self {
691            target,
692            visited: vec![target],
693            first_unsupported: None,
694            first_frame_mismatch: None,
695            first_position_only_state: None,
696        }
697    }
698
699    fn fallback(self) -> Option<Result<SpkState, SpkError>> {
700        self.first_position_only_state.map(Ok).or_else(|| {
701            self.first_frame_mismatch
702                .map(Err)
703                .or_else(|| self.first_unsupported.map(Err))
704        })
705    }
706
707    fn is_root(&self) -> bool {
708        self.visited.len() == 1
709    }
710}
711
712#[derive(Debug, Clone, Copy)]
713struct AccumulatedSpkState {
714    position_km: [f64; 3],
715    velocity_km_s: Option<[f64; 3]>,
716    frame: Option<i32>,
717}
718
719impl AccumulatedSpkState {
720    fn extend(self, leg: SpkState, sign: f64) -> Result<Self, SpkError> {
721        let frame = match self.frame {
722            Some(frame) if frame != leg.frame => {
723                return Err(SpkError::FrameMismatch {
724                    first: frame,
725                    second: leg.frame,
726                });
727            }
728            Some(frame) => Some(frame),
729            None => Some(leg.frame),
730        };
731
732        Ok(Self {
733            position_km: add_scaled(self.position_km, leg.position_km, sign),
734            velocity_km_s: match (self.velocity_km_s, leg.velocity_km_s) {
735                (Some(accumulated), Some(leg)) => Some(add_scaled(accumulated, leg, sign)),
736                _ => None,
737            },
738            frame,
739        })
740    }
741
742    fn into_state(self, target: i32, center: i32) -> SpkState {
743        SpkState {
744            target,
745            center,
746            position_km: self.position_km,
747            velocity_km_s: self.velocity_km_s,
748            frame: self.frame.unwrap_or(0),
749        }
750    }
751}
752
753/// Query target state relative to center from a loaded SPK kernel.
754pub fn spk_state(spk: &Spk, target: i32, center: i32, et: f64) -> Result<SpkState, SpkError> {
755    spk.spk_state(target, center, et)
756}
757
758fn add_scaled(lhs: [f64; 3], rhs: [f64; 3], sign: f64) -> [f64; 3] {
759    [
760        lhs[0] + sign * rhs[0],
761        lhs[1] + sign * rhs[1],
762        lhs[2] + sign * rhs[2],
763    ]
764}
765
766/// Parse the DAF/SPK directory from an in-memory kernel byte slice.
767pub fn parse_daf_spk(bytes: &[u8]) -> Result<DafSpk, SpkError> {
768    let file_record = parse_file_record(bytes)?;
769    if file_record.double_components != SPK_ND || file_record.integer_components != SPK_NI {
770        return Err(SpkError::UnsupportedSummaryShape {
771            nd: file_record.double_components,
772            ni: file_record.integer_components,
773        });
774    }
775
776    let mut segments = Vec::new();
777    let summary_words = summary_word_count(
778        file_record.double_components,
779        file_record.integer_components,
780    )?;
781    let summary_bytes = summary_words * 8;
782    let name_bytes = summary_words * 8;
783
784    let mut record = file_record.forward_record;
785    validate_summary_record_pointer(record, "FWARD")?;
786    let mut visited_records = Vec::new();
787    while record != 0 {
788        if visited_records.contains(&record) {
789            return Err(SpkError::InvalidField {
790                field: "summary record chain",
791                value: record,
792            });
793        }
794        visited_records.push(record);
795
796        let summary_offset = record_offset(record, bytes.len(), "summary record")?;
797        let name_record = paired_name_record(record, "name record")?;
798        let name_offset = record_offset(name_record, bytes.len(), "name record")?;
799
800        let next = read_summary_control_i32(
801            file_record.byte_order,
802            bytes,
803            summary_offset,
804            0,
805            "next summary record",
806        )?;
807        validate_summary_record_pointer(next, "next summary record")?;
808        let count = read_summary_control_i32(
809            file_record.byte_order,
810            bytes,
811            summary_offset,
812            2,
813            "summary count",
814        )?;
815        if count < 0 {
816            return Err(SpkError::InvalidField {
817                field: "summary count",
818                value: count,
819            });
820        }
821
822        let count = usize::try_from(count).map_err(|_| SpkError::InvalidField {
823            field: "summary count",
824            value: count,
825        })?;
826        let summaries_start = summary_offset + SUMMARY_CONTROL_WORDS * 8;
827        let summaries_end = summaries_start + count * summary_bytes;
828        if summaries_end > summary_offset + DAF_RECORD_BYTES {
829            return Err(SpkError::Truncated {
830                context: "summary record entries",
831                needed: summaries_end,
832                actual: summary_offset + DAF_RECORD_BYTES,
833            });
834        }
835        let names_end = name_offset + count * name_bytes;
836        if names_end > name_offset + DAF_RECORD_BYTES {
837            return Err(SpkError::Truncated {
838                context: "name record entries",
839                needed: names_end,
840                actual: name_offset + DAF_RECORD_BYTES,
841            });
842        }
843
844        for index in 0..count {
845            let entry_offset = summaries_start + index * summary_bytes;
846            let name_start = name_offset + index * name_bytes;
847            let name = trim_ascii(&bytes[name_start..name_start + name_bytes]);
848
849            let start_et =
850                file_record
851                    .byte_order
852                    .read_f64(bytes, entry_offset, "segment start ET")?;
853            let stop_et =
854                file_record
855                    .byte_order
856                    .read_f64(bytes, entry_offset + 8, "segment stop ET")?;
857            let ints_offset = entry_offset + 16;
858            let target = file_record
859                .byte_order
860                .read_i32(bytes, ints_offset, "segment target")?;
861            let center =
862                file_record
863                    .byte_order
864                    .read_i32(bytes, ints_offset + 4, "segment center")?;
865            let frame = file_record
866                .byte_order
867                .read_i32(bytes, ints_offset + 8, "segment frame")?;
868            let data_type =
869                file_record
870                    .byte_order
871                    .read_i32(bytes, ints_offset + 12, "segment data type")?;
872            let start_address = file_record.byte_order.read_i32(
873                bytes,
874                ints_offset + 16,
875                "segment start address",
876            )?;
877            let end_address =
878                file_record
879                    .byte_order
880                    .read_i32(bytes, ints_offset + 20, "segment end address")?;
881
882            segments.push(SpkSegmentDescriptor {
883                name,
884                start_et,
885                stop_et,
886                target,
887                center,
888                frame,
889                data_type,
890                start_address,
891                end_address,
892            });
893        }
894
895        record = next;
896    }
897
898    Ok(DafSpk {
899        file_record,
900        segments,
901    })
902}
903
904/// Evaluate a type-2 SPK segment and return position in kilometers.
905pub fn evaluate_type2_position(
906    bytes: &[u8],
907    byte_order: DafByteOrder,
908    segment: &SpkSegmentDescriptor,
909    et: f64,
910) -> Result<[f64; 3], SpkError> {
911    if segment.data_type != SPK_TYPE_2 {
912        return Err(SpkError::UnsupportedSegmentType {
913            expected: SPK_TYPE_2,
914            actual: segment.data_type,
915        });
916    }
917
918    let directory = read_type2_directory(bytes, byte_order, segment)?;
919    let record_index =
920        chebyshev_record_index(segment, directory.init, directory.intlen, directory.n, et)?;
921    let record_start = checked_address_add(
922        segment_start_address(segment)?,
923        record_index
924            .checked_mul(directory.rsize)
925            .ok_or(SpkError::InvalidSegmentLayout {
926                context: "record offset overflow",
927            })?,
928        "type-2 record address",
929    )?;
930
931    let mid = read_daf_f64(bytes, byte_order, record_start, "type-2 record midpoint")?;
932    let radius = read_daf_f64(bytes, byte_order, record_start + 1, "type-2 record radius")?;
933    if !radius.is_finite() || radius <= 0.0 {
934        return Err(SpkError::InvalidDoubleField {
935            field: "type-2 record radius",
936            value: radius,
937        });
938    }
939
940    let tau = (et - mid) / radius;
941    let coeff_count = (directory.rsize - 2) / 3;
942    let coeff_start = record_start + 2;
943    let x = evaluate_chebyshev_component(bytes, byte_order, coeff_start, coeff_count, tau)?;
944    let y = evaluate_chebyshev_component(
945        bytes,
946        byte_order,
947        coeff_start + coeff_count,
948        coeff_count,
949        tau,
950    )?;
951    let z = evaluate_chebyshev_component(
952        bytes,
953        byte_order,
954        coeff_start + 2 * coeff_count,
955        coeff_count,
956        tau,
957    )?;
958    Ok([x, y, z])
959}
960
961/// Evaluate a type-3 SPK segment and return position and velocity.
962pub fn evaluate_type3_state(
963    bytes: &[u8],
964    byte_order: DafByteOrder,
965    segment: &SpkSegmentDescriptor,
966    et: f64,
967) -> Result<SpkStateVector, SpkError> {
968    if segment.data_type != SPK_TYPE_3 {
969        return Err(SpkError::UnsupportedSegmentType {
970            expected: SPK_TYPE_3,
971            actual: segment.data_type,
972        });
973    }
974
975    let directory = read_type3_directory(bytes, byte_order, segment)?;
976    let record_index =
977        chebyshev_record_index(segment, directory.init, directory.intlen, directory.n, et)?;
978    let record_start = checked_address_add(
979        segment_start_address(segment)?,
980        record_index
981            .checked_mul(directory.rsize)
982            .ok_or(SpkError::InvalidSegmentLayout {
983                context: "record offset overflow",
984            })?,
985        "type-3 record address",
986    )?;
987
988    let mid = read_daf_f64(bytes, byte_order, record_start, "type-3 record midpoint")?;
989    let radius = read_daf_f64(bytes, byte_order, record_start + 1, "type-3 record radius")?;
990    if !radius.is_finite() || radius <= 0.0 {
991        return Err(SpkError::InvalidDoubleField {
992            field: "type-3 record radius",
993            value: radius,
994        });
995    }
996
997    let tau = (et - mid) / radius;
998    let coeff_count = (directory.rsize - 2) / 6;
999    let coeff_start = record_start + 2;
1000    let x = evaluate_chebyshev_component(bytes, byte_order, coeff_start, coeff_count, tau)?;
1001    let y = evaluate_chebyshev_component(
1002        bytes,
1003        byte_order,
1004        coeff_start + coeff_count,
1005        coeff_count,
1006        tau,
1007    )?;
1008    let z = evaluate_chebyshev_component(
1009        bytes,
1010        byte_order,
1011        coeff_start + 2 * coeff_count,
1012        coeff_count,
1013        tau,
1014    )?;
1015    let vx = evaluate_chebyshev_component(
1016        bytes,
1017        byte_order,
1018        coeff_start + 3 * coeff_count,
1019        coeff_count,
1020        tau,
1021    )?;
1022    let vy = evaluate_chebyshev_component(
1023        bytes,
1024        byte_order,
1025        coeff_start + 4 * coeff_count,
1026        coeff_count,
1027        tau,
1028    )?;
1029    let vz = evaluate_chebyshev_component(
1030        bytes,
1031        byte_order,
1032        coeff_start + 5 * coeff_count,
1033        coeff_count,
1034        tau,
1035    )?;
1036
1037    Ok(SpkStateVector {
1038        position_km: [x, y, z],
1039        velocity_km_s: [vx, vy, vz],
1040    })
1041}
1042
1043/// Evaluate a type-21 SPK segment (Extended Modified Difference Arrays) and
1044/// return position and velocity in kilometers and km/s.
1045///
1046/// Type 21 generalizes type 1: each logical record ("difference line") carries a
1047/// reference epoch, a stepsize vector, a reference state, and per-component
1048/// modified divided difference arrays whose table dimension `MAXDIM` is stored
1049/// in the segment rather than fixed at 15. Records are selected by the segment's
1050/// trailing epoch list (and directory), then evaluated with the Krogh MDA
1051/// recurrence. This mirrors CSPICE `SPKR21`/`SPKE21`.
1052pub fn evaluate_type21_state(
1053    bytes: &[u8],
1054    byte_order: DafByteOrder,
1055    segment: &SpkSegmentDescriptor,
1056    et: f64,
1057) -> Result<SpkStateVector, SpkError> {
1058    if segment.data_type != SPK_TYPE_21 {
1059        return Err(SpkError::UnsupportedSegmentType {
1060            expected: SPK_TYPE_21,
1061            actual: segment.data_type,
1062        });
1063    }
1064
1065    let directory = read_type21_directory(bytes, byte_order, segment)?;
1066    let record_address = type21_record_address(bytes, byte_order, segment, &directory, et)?;
1067
1068    // Assemble the SPKE21-style record: index 0 holds MAXDIM, indices
1069    // 1..=dlsize hold the difference line, matching CSPICE addressing exactly.
1070    let dlsize = 4 * directory.maxdim + 11;
1071    let mut record = [0.0f64; 4 * SPK_TYPE_21_MAX_TABLE_DIM + 12];
1072    record[0] = directory.maxdim as f64;
1073    for (offset, slot) in record[1..=dlsize].iter_mut().enumerate() {
1074        *slot = read_daf_f64(
1075            bytes,
1076            byte_order,
1077            record_address + offset,
1078            "type-21 difference line",
1079        )?;
1080    }
1081
1082    evaluate_type21_record(&record, directory.maxdim, et)
1083}
1084
1085#[derive(Debug, Clone, Copy)]
1086struct Type21Directory {
1087    /// One-based DAF address of the first record (difference line) word.
1088    begin_address: usize,
1089    /// One-based DAF address of the segment's final word.
1090    end_address: usize,
1091    /// Difference-table dimension per Cartesian component (`MAXDIM`).
1092    maxdim: usize,
1093    /// Number of logical records / epochs in the segment.
1094    record_count: usize,
1095    /// Number of directory epochs (`record_count / 100`).
1096    directory_count: usize,
1097}
1098
1099fn read_type21_directory(
1100    bytes: &[u8],
1101    byte_order: DafByteOrder,
1102    segment: &SpkSegmentDescriptor,
1103) -> Result<Type21Directory, SpkError> {
1104    let begin = segment_start_address(segment)?;
1105    let end = segment_end_address(segment)?;
1106    if end < begin + 1 {
1107        return Err(SpkError::InvalidSegmentLayout {
1108            context: "segment is shorter than the type-21 trailer",
1109        });
1110    }
1111
1112    // The last two segment words are MAXDIM (end - 1) and the record count N (end).
1113    let maxdim = read_daf_f64(bytes, byte_order, end - 1, "type-21 MAXDIM")?;
1114    let record_count = read_daf_f64(bytes, byte_order, end, "type-21 record count")?;
1115    let maxdim = f64_to_usize(maxdim, "type-21 MAXDIM")?;
1116    let record_count = f64_to_usize(record_count, "type-21 record count")?;
1117
1118    if maxdim == 0 || maxdim > SPK_TYPE_21_MAX_TABLE_DIM {
1119        return Err(SpkError::InvalidSegmentLayout {
1120            context: "type-21 difference table dimension is out of range",
1121        });
1122    }
1123    if record_count == 0 {
1124        return Err(SpkError::InvalidSegmentLayout {
1125            context: "type-21 segment has zero records",
1126        });
1127    }
1128
1129    let dlsize = 4 * maxdim + 11;
1130    let directory_count = record_count / SPK_TYPE_21_DIRECTORY_STRIDE;
1131
1132    // Layout: N difference lines, then N epochs, then the directory epochs, then
1133    // the two trailer words (MAXDIM, N).
1134    let required_words = record_count
1135        .checked_mul(dlsize)
1136        .and_then(|words| words.checked_add(record_count))
1137        .and_then(|words| words.checked_add(directory_count))
1138        .and_then(|words| words.checked_add(2))
1139        .ok_or(SpkError::InvalidSegmentLayout {
1140            context: "type-21 segment word count overflow",
1141        })?;
1142    let segment_words = end - begin + 1;
1143    if required_words > segment_words {
1144        return Err(SpkError::InvalidSegmentLayout {
1145            context: "type-21 records exceed segment address range",
1146        });
1147    }
1148
1149    Ok(Type21Directory {
1150        begin_address: begin,
1151        end_address: end,
1152        maxdim,
1153        record_count,
1154        directory_count,
1155    })
1156}
1157
1158/// Resolve the one-based DAF address of the difference line covering `et`.
1159fn type21_record_address(
1160    bytes: &[u8],
1161    byte_order: DafByteOrder,
1162    segment: &SpkSegmentDescriptor,
1163    directory: &Type21Directory,
1164    et: f64,
1165) -> Result<usize, SpkError> {
1166    if !et.is_finite() {
1167        return Err(SpkError::InvalidDoubleField {
1168            field: "type-21 ET",
1169            value: et,
1170        });
1171    }
1172    if et < segment.start_et || et > segment.stop_et {
1173        return Err(SpkError::OutOfCoverage {
1174            et,
1175            start_et: segment.start_et,
1176            stop_et: segment.stop_et,
1177        });
1178    }
1179
1180    // The epoch list ends just before the directory epochs and the two trailer
1181    // words, anchored at the segment end exactly as CSPICE SPKR21 computes it.
1182    let first_epoch_address = directory
1183        .end_address
1184        .checked_sub(directory.directory_count + 2 + directory.record_count)
1185        .ok_or(SpkError::InvalidSegmentLayout {
1186            context: "type-21 epoch list underflows segment",
1187        })?
1188        + 1;
1189
1190    // Find the first record whose epoch is >= et (CSPICE LSTLTD + 1), clamped to
1191    // the final record. Reading the full epoch list is equivalent to the
1192    // directory-guided search and selects the identical record.
1193    let mut earlier = 0usize;
1194    for index in 0..directory.record_count {
1195        let epoch = read_daf_f64(
1196            bytes,
1197            byte_order,
1198            first_epoch_address + index,
1199            "type-21 epoch",
1200        )?;
1201        if epoch < et {
1202            earlier += 1;
1203        } else {
1204            break;
1205        }
1206    }
1207    let record_index = earlier.min(directory.record_count - 1);
1208
1209    let dlsize = 4 * directory.maxdim + 11;
1210    checked_address_add(
1211        directory.begin_address,
1212        record_index
1213            .checked_mul(dlsize)
1214            .ok_or(SpkError::InvalidSegmentLayout {
1215                context: "type-21 record offset overflow",
1216            })?,
1217        "type-21 record address",
1218    )
1219}
1220
1221/// Evaluate one assembled type-21 record at `et`. `record[0]` is `MAXDIM`;
1222/// `record[1..]` is the difference line. Ported line-for-line from CSPICE
1223/// `SPKE21` (Krogh's modified divided difference recurrence).
1224fn evaluate_type21_record(
1225    record: &[f64],
1226    maxdim: usize,
1227    et: f64,
1228) -> Result<SpkStateVector, SpkError> {
1229    const DIM: usize = SPK_TYPE_21_MAX_TABLE_DIM;
1230    let mut fc = [0.0f64; DIM];
1231    fc[0] = 1.0;
1232    let mut wc = [0.0f64; DIM - 1];
1233    let mut w = [0.0f64; DIM + 2];
1234    let mut g = [0.0f64; DIM];
1235
1236    let tl = record[1];
1237    g[..maxdim].copy_from_slice(&record[2..2 + maxdim]);
1238
1239    let refpos = [record[maxdim + 2], record[maxdim + 4], record[maxdim + 6]];
1240    let refvel = [record[maxdim + 3], record[maxdim + 5], record[maxdim + 7]];
1241
1242    // Modified divided difference arrays, DT(MAXDIM, 3), stored column-major.
1243    let mut dt = [[0.0f64; 3]; DIM];
1244    for component in 0..3 {
1245        let base = (component + 1) * maxdim + 8;
1246        for (row, dt_row) in dt.iter_mut().enumerate().take(maxdim) {
1247            dt_row[component] = record[base + row];
1248        }
1249    }
1250
1251    // KQMAX1 and the per-axis KQ orders are file-controlled. Validate them as
1252    // integers within the difference-table bounds before they index FC/WC/W/DT,
1253    // so a malformed kernel returns a typed error instead of panicking. SPICE
1254    // invariant: KQMAX1 = max(KQ) + 1, so 1 <= KQMAX1 <= MAXDIM+1 and each
1255    // 0 <= KQ < KQMAX1.
1256    let kqmax1_u = f64_to_usize(record[4 * maxdim + 8], "type-21 KQMAX1")?;
1257    if kqmax1_u < 1 || kqmax1_u > maxdim + 1 {
1258        return Err(SpkError::InvalidSegmentLayout {
1259            context: "type-21 KQMAX1 is out of range",
1260        });
1261    }
1262    let kq_u = [
1263        f64_to_usize(record[4 * maxdim + 9], "type-21 KQ(1)")?,
1264        f64_to_usize(record[4 * maxdim + 10], "type-21 KQ(2)")?,
1265        f64_to_usize(record[4 * maxdim + 11], "type-21 KQ(3)")?,
1266    ];
1267    for &kqq in &kq_u {
1268        if kqq >= kqmax1_u {
1269            return Err(SpkError::InvalidSegmentLayout {
1270                context: "type-21 KQ order exceeds KQMAX1",
1271            });
1272        }
1273    }
1274    let kqmax1 = kqmax1_u as i64;
1275    let kq = [kq_u[0] as i64, kq_u[1] as i64, kq_u[2] as i64];
1276
1277    let delta = et - tl;
1278    let mut tp = delta;
1279    let mq2 = kqmax1 - 2;
1280    let mut ks = kqmax1 - 1;
1281
1282    // Build the stepsize-derived coefficients FC and WC.
1283    let mut j = 1i64;
1284    while j <= mq2 {
1285        let gj = g[(j - 1) as usize];
1286        if gj == 0.0 {
1287            return Err(SpkError::InvalidDoubleField {
1288                field: "type-21 stepsize vector",
1289                value: 0.0,
1290            });
1291        }
1292        fc[j as usize] = tp / gj;
1293        wc[(j - 1) as usize] = delta / gj;
1294        tp = delta + gj;
1295        j += 1;
1296    }
1297
1298    // Seed the W array with reciprocals.
1299    let mut j = 1i64;
1300    while j <= kqmax1 {
1301        w[(j - 1) as usize] = 1.0 / j as f64;
1302        j += 1;
1303    }
1304
1305    // Compute the W(K) terms used for position interpolation.
1306    let mut jx = 0i64;
1307    let mut ks1 = ks - 1;
1308    while ks >= 2 {
1309        jx += 1;
1310        let mut j = 1i64;
1311        while j <= jx {
1312            let term = fc[j as usize] * w[(j + ks1 - 1) as usize]
1313                - wc[(j - 1) as usize] * w[(j + ks - 1) as usize];
1314            w[(j + ks - 1) as usize] = term;
1315            j += 1;
1316        }
1317        ks = ks1;
1318        ks1 -= 1;
1319    }
1320
1321    let mut state = [0.0f64; 6];
1322    for component in 0..3 {
1323        let kqq = kq[component];
1324        let mut sum = 0.0;
1325        let mut j = kqq;
1326        while j >= 1 {
1327            sum += dt[(j - 1) as usize][component] * w[(j + ks - 1) as usize];
1328            j -= 1;
1329        }
1330        state[component] = refpos[component] + delta * (refvel[component] + delta * sum);
1331    }
1332
1333    // Recompute the W(K) terms for velocity interpolation.
1334    let mut j = 1i64;
1335    while j <= jx {
1336        let term = fc[j as usize] * w[(j + ks1 - 1) as usize]
1337            - wc[(j - 1) as usize] * w[(j + ks - 1) as usize];
1338        w[(j + ks - 1) as usize] = term;
1339        j += 1;
1340    }
1341    ks -= 1;
1342
1343    for component in 0..3 {
1344        let kqq = kq[component];
1345        let mut sum = 0.0;
1346        let mut j = kqq;
1347        while j >= 1 {
1348            sum += dt[(j - 1) as usize][component] * w[(j + ks - 1) as usize];
1349            j -= 1;
1350        }
1351        state[component + 3] = refvel[component] + delta * sum;
1352    }
1353
1354    Ok(SpkStateVector {
1355        position_km: [state[0], state[1], state[2]],
1356        velocity_km_s: [state[3], state[4], state[5]],
1357    })
1358}
1359
1360fn parse_file_record(bytes: &[u8]) -> Result<DafFileRecord, SpkError> {
1361    if bytes.len() < DAF_FILE_RECORD_BYTES {
1362        return Err(SpkError::Truncated {
1363            context: "DAF file record",
1364            needed: DAF_FILE_RECORD_BYTES,
1365            actual: bytes.len(),
1366        });
1367    }
1368
1369    let id_word = trim_ascii(&bytes[0..DAF_ID_BYTES]);
1370    let file_type = id_word
1371        .split_once('/')
1372        .map(|(_, file_type)| file_type.trim().to_string())
1373        .unwrap_or_default();
1374    if id_word != "DAF/SPK" {
1375        return Err(SpkError::UnsupportedDafId { id_word });
1376    }
1377
1378    let binary_format = trim_ascii(
1379        &bytes[DAF_BINARY_FORMAT_OFFSET..DAF_BINARY_FORMAT_OFFSET + DAF_BINARY_FORMAT_BYTES],
1380    );
1381    let byte_order = match binary_format.as_str() {
1382        "LTL-IEEE" => DafByteOrder::LittleEndian,
1383        "BIG-IEEE" => DafByteOrder::BigEndian,
1384        _ => {
1385            return Err(SpkError::UnsupportedBinaryFormat { binary_format });
1386        }
1387    };
1388
1389    let double_components = byte_order.read_i32(bytes, 8, "ND")?;
1390    let integer_components = byte_order.read_i32(bytes, 12, "NI")?;
1391    let internal_name = trim_ascii(&bytes[16..16 + DAF_INTERNAL_NAME_BYTES]);
1392    let forward_record = byte_order.read_i32(bytes, 76, "FWARD")?;
1393    let backward_record = byte_order.read_i32(bytes, 80, "BWARD")?;
1394    let free_address = byte_order.read_i32(bytes, 84, "FREE")?;
1395
1396    if forward_record < 0 {
1397        return Err(SpkError::InvalidField {
1398            field: "FWARD",
1399            value: forward_record,
1400        });
1401    }
1402    if backward_record < 0 {
1403        return Err(SpkError::InvalidField {
1404            field: "BWARD",
1405            value: backward_record,
1406        });
1407    }
1408
1409    Ok(DafFileRecord {
1410        id_word,
1411        file_type,
1412        double_components,
1413        integer_components,
1414        internal_name,
1415        forward_record,
1416        backward_record,
1417        free_address,
1418        byte_order,
1419        binary_format,
1420    })
1421}
1422
1423#[derive(Debug, Clone, Copy)]
1424struct ChebyshevDirectory {
1425    init: f64,
1426    intlen: f64,
1427    rsize: usize,
1428    n: usize,
1429}
1430
1431fn read_type2_directory(
1432    bytes: &[u8],
1433    byte_order: DafByteOrder,
1434    segment: &SpkSegmentDescriptor,
1435) -> Result<ChebyshevDirectory, SpkError> {
1436    let start = segment_start_address(segment)?;
1437    let end = segment_end_address(segment)?;
1438    if end < start + 3 {
1439        return Err(SpkError::InvalidSegmentLayout {
1440            context: "segment is shorter than the type-2 directory",
1441        });
1442    }
1443
1444    let init = read_daf_f64(bytes, byte_order, end - 3, "type-2 INIT")?;
1445    let intlen = read_daf_f64(bytes, byte_order, end - 2, "type-2 INTLEN")?;
1446    let rsize = read_daf_f64(bytes, byte_order, end - 1, "type-2 RSIZE")?;
1447    let n = read_daf_f64(bytes, byte_order, end, "type-2 N")?;
1448
1449    if !init.is_finite() {
1450        return Err(SpkError::InvalidDoubleField {
1451            field: "type-2 INIT",
1452            value: init,
1453        });
1454    }
1455    if !intlen.is_finite() || intlen <= 0.0 {
1456        return Err(SpkError::InvalidDoubleField {
1457            field: "type-2 INTLEN",
1458            value: intlen,
1459        });
1460    }
1461
1462    let rsize = f64_to_usize(rsize, "type-2 RSIZE")?;
1463    let n = f64_to_usize(n, "type-2 N")?;
1464    if n == 0 {
1465        return Err(SpkError::InvalidSegmentLayout {
1466            context: "type-2 segment has zero records",
1467        });
1468    }
1469    if rsize < 5 || (rsize - 2) % 3 != 0 {
1470        return Err(SpkError::InvalidSegmentLayout {
1471            context: "type-2 RSIZE does not match three position components",
1472        });
1473    }
1474
1475    let segment_words = end - start + 1;
1476    let required_words = n
1477        .checked_mul(rsize)
1478        .and_then(|words| words.checked_add(4))
1479        .ok_or(SpkError::InvalidSegmentLayout {
1480            context: "type-2 segment word count overflow",
1481        })?;
1482    if required_words > segment_words {
1483        return Err(SpkError::InvalidSegmentLayout {
1484            context: "type-2 records exceed segment address range",
1485        });
1486    }
1487
1488    Ok(ChebyshevDirectory {
1489        init,
1490        intlen,
1491        rsize,
1492        n,
1493    })
1494}
1495
1496fn read_type3_directory(
1497    bytes: &[u8],
1498    byte_order: DafByteOrder,
1499    segment: &SpkSegmentDescriptor,
1500) -> Result<ChebyshevDirectory, SpkError> {
1501    let start = segment_start_address(segment)?;
1502    let end = segment_end_address(segment)?;
1503    if end < start + 3 {
1504        return Err(SpkError::InvalidSegmentLayout {
1505            context: "segment is shorter than the type-3 directory",
1506        });
1507    }
1508
1509    let init = read_daf_f64(bytes, byte_order, end - 3, "type-3 INIT")?;
1510    let intlen = read_daf_f64(bytes, byte_order, end - 2, "type-3 INTLEN")?;
1511    let rsize = read_daf_f64(bytes, byte_order, end - 1, "type-3 RSIZE")?;
1512    let n = read_daf_f64(bytes, byte_order, end, "type-3 N")?;
1513
1514    if !init.is_finite() {
1515        return Err(SpkError::InvalidDoubleField {
1516            field: "type-3 INIT",
1517            value: init,
1518        });
1519    }
1520    if !intlen.is_finite() || intlen <= 0.0 {
1521        return Err(SpkError::InvalidDoubleField {
1522            field: "type-3 INTLEN",
1523            value: intlen,
1524        });
1525    }
1526
1527    let rsize = f64_to_usize(rsize, "type-3 RSIZE")?;
1528    let n = f64_to_usize(n, "type-3 N")?;
1529    if n == 0 {
1530        return Err(SpkError::InvalidSegmentLayout {
1531            context: "type-3 segment has zero records",
1532        });
1533    }
1534    if rsize < 8 || (rsize - 2) % 6 != 0 {
1535        return Err(SpkError::InvalidSegmentLayout {
1536            context: "type-3 RSIZE does not match six state components",
1537        });
1538    }
1539
1540    let segment_words = end - start + 1;
1541    let required_words = n
1542        .checked_mul(rsize)
1543        .and_then(|words| words.checked_add(4))
1544        .ok_or(SpkError::InvalidSegmentLayout {
1545            context: "type-3 segment word count overflow",
1546        })?;
1547    if required_words > segment_words {
1548        return Err(SpkError::InvalidSegmentLayout {
1549            context: "type-3 records exceed segment address range",
1550        });
1551    }
1552
1553    Ok(ChebyshevDirectory {
1554        init,
1555        intlen,
1556        rsize,
1557        n,
1558    })
1559}
1560
1561fn chebyshev_record_index(
1562    segment: &SpkSegmentDescriptor,
1563    init: f64,
1564    intlen: f64,
1565    n: usize,
1566    et: f64,
1567) -> Result<usize, SpkError> {
1568    if et < segment.start_et || et > segment.stop_et {
1569        return Err(SpkError::OutOfCoverage {
1570            et,
1571            start_et: segment.start_et,
1572            stop_et: segment.stop_et,
1573        });
1574    }
1575
1576    let directory_stop = init + intlen * n as f64;
1577    if et < init || et > directory_stop {
1578        return Err(SpkError::OutOfCoverage {
1579            et,
1580            start_et: init,
1581            stop_et: directory_stop,
1582        });
1583    }
1584
1585    let record = ((et - init) / intlen).floor();
1586    if !record.is_finite() || record < 0.0 {
1587        return Err(SpkError::OutOfCoverage {
1588            et,
1589            start_et: init,
1590            stop_et: directory_stop,
1591        });
1592    }
1593
1594    let record = record as usize;
1595    if record < n {
1596        Ok(record)
1597    } else if record == n && et <= directory_stop {
1598        Ok(n - 1)
1599    } else {
1600        Err(SpkError::OutOfCoverage {
1601            et,
1602            start_et: init,
1603            stop_et: directory_stop,
1604        })
1605    }
1606}
1607
1608fn evaluate_chebyshev_component(
1609    bytes: &[u8],
1610    byte_order: DafByteOrder,
1611    coeff_start: usize,
1612    coeff_count: usize,
1613    tau: f64,
1614) -> Result<f64, SpkError> {
1615    let mut sum = read_daf_f64(bytes, byte_order, coeff_start, "Chebyshev coefficient")?;
1616    if coeff_count == 1 {
1617        return Ok(sum);
1618    }
1619
1620    let mut previous = 1.0;
1621    let mut current = tau;
1622    sum += read_daf_f64(bytes, byte_order, coeff_start + 1, "Chebyshev coefficient")? * current;
1623
1624    for index in 2..coeff_count {
1625        let next = 2.0 * tau * current - previous;
1626        sum += read_daf_f64(
1627            bytes,
1628            byte_order,
1629            coeff_start + index,
1630            "Chebyshev coefficient",
1631        )? * next;
1632        previous = current;
1633        current = next;
1634    }
1635
1636    Ok(sum)
1637}
1638
1639fn f64_to_usize(value: f64, field: &'static str) -> Result<usize, SpkError> {
1640    if !value.is_finite() || value.fract() != 0.0 || value < 0.0 || value > usize::MAX as f64 {
1641        return Err(SpkError::InvalidDoubleField { field, value });
1642    }
1643    Ok(value as usize)
1644}
1645
1646fn segment_start_address(segment: &SpkSegmentDescriptor) -> Result<usize, SpkError> {
1647    segment_address(segment.start_address, "segment start address")
1648}
1649
1650fn segment_end_address(segment: &SpkSegmentDescriptor) -> Result<usize, SpkError> {
1651    let start = segment_start_address(segment)?;
1652    let end = segment_address(segment.end_address, "segment end address")?;
1653    if end < start {
1654        return Err(SpkError::InvalidSegmentLayout {
1655            context: "segment end address precedes start address",
1656        });
1657    }
1658    Ok(end)
1659}
1660
1661fn segment_address(address: i32, field: &'static str) -> Result<usize, SpkError> {
1662    if address <= 0 {
1663        return Err(SpkError::InvalidField {
1664            field,
1665            value: address,
1666        });
1667    }
1668    usize::try_from(address).map_err(|_| SpkError::InvalidField {
1669        field,
1670        value: address,
1671    })
1672}
1673
1674fn checked_address_add(
1675    address: usize,
1676    offset_words: usize,
1677    context: &'static str,
1678) -> Result<usize, SpkError> {
1679    address
1680        .checked_add(offset_words)
1681        .ok_or(SpkError::InvalidSegmentLayout { context })
1682}
1683
1684fn read_daf_f64(
1685    bytes: &[u8],
1686    byte_order: DafByteOrder,
1687    address: usize,
1688    field: &'static str,
1689) -> Result<f64, SpkError> {
1690    if address == 0 {
1691        return Err(SpkError::InvalidSegmentLayout {
1692            context: "DAF addresses are one-based",
1693        });
1694    }
1695    let offset = (address - 1)
1696        .checked_mul(8)
1697        .ok_or(SpkError::InvalidSegmentLayout {
1698            context: "DAF address byte offset overflow",
1699        })?;
1700    byte_order.read_f64(bytes, offset, field)
1701}
1702
1703fn read_summary_control_i32(
1704    byte_order: DafByteOrder,
1705    bytes: &[u8],
1706    summary_offset: usize,
1707    word_index: usize,
1708    field: &'static str,
1709) -> Result<i32, SpkError> {
1710    let value = byte_order.read_f64(bytes, summary_offset + word_index * 8, field)?;
1711    if !value.is_finite()
1712        || value.fract() != 0.0
1713        || value < i32::MIN as f64
1714        || value > i32::MAX as f64
1715    {
1716        return Err(SpkError::InvalidField { field, value: 0 });
1717    }
1718    Ok(value as i32)
1719}
1720
1721fn summary_word_count(nd: i32, ni: i32) -> Result<usize, SpkError> {
1722    if nd < 0 {
1723        return Err(SpkError::InvalidField {
1724            field: "ND",
1725            value: nd,
1726        });
1727    }
1728    if ni < 0 {
1729        return Err(SpkError::InvalidField {
1730            field: "NI",
1731            value: ni,
1732        });
1733    }
1734
1735    let nd = usize::try_from(nd).map_err(|_| SpkError::InvalidField {
1736        field: "ND",
1737        value: nd,
1738    })?;
1739    let ni = usize::try_from(ni).map_err(|_| SpkError::InvalidField {
1740        field: "NI",
1741        value: ni,
1742    })?;
1743    Ok(nd + ni.div_ceil(2))
1744}
1745
1746fn validate_summary_record_pointer(record: i32, field: &'static str) -> Result<(), SpkError> {
1747    paired_name_record(record, field).map(|_| ())
1748}
1749
1750fn paired_name_record(record: i32, field: &'static str) -> Result<i32, SpkError> {
1751    record.checked_add(1).ok_or(SpkError::InvalidField {
1752        field,
1753        value: record,
1754    })
1755}
1756
1757fn record_offset(record: i32, len: usize, context: &'static str) -> Result<usize, SpkError> {
1758    if record <= 0 {
1759        return Err(SpkError::InvalidField {
1760            field: context,
1761            value: record,
1762        });
1763    }
1764    let record = usize::try_from(record).map_err(|_| SpkError::InvalidField {
1765        field: context,
1766        value: record,
1767    })?;
1768    let offset = (record - 1)
1769        .checked_mul(DAF_RECORD_BYTES)
1770        .ok_or(SpkError::InvalidField {
1771            field: context,
1772            value: i32::MAX,
1773        })?;
1774    let needed = offset + DAF_RECORD_BYTES;
1775    if needed > len {
1776        return Err(SpkError::Truncated {
1777            context,
1778            needed,
1779            actual: len,
1780        });
1781    }
1782    Ok(offset)
1783}
1784
1785fn trim_ascii(bytes: &[u8]) -> String {
1786    let end = bytes
1787        .iter()
1788        .rposition(|byte| *byte != b' ' && *byte != 0)
1789        .map(|index| index + 1)
1790        .unwrap_or(0);
1791    let trimmed = &bytes[..end];
1792    String::from_utf8_lossy(trimmed).trim().to_string()
1793}
1794
1795#[cfg(test)]
1796mod tests {
1797    use super::*;
1798
1799    #[test]
1800    fn decodes_little_endian_daf_summaries() {
1801        let bytes = build_daf(DafByteOrder::LittleEndian);
1802
1803        let parsed = parse_daf_spk(&bytes).unwrap();
1804
1805        assert_eq!(parsed.file_record.id_word, "DAF/SPK");
1806        assert_eq!(parsed.file_record.file_type, "SPK");
1807        assert_eq!(parsed.file_record.double_components, 2);
1808        assert_eq!(parsed.file_record.integer_components, 6);
1809        assert_eq!(parsed.file_record.internal_name, "SYNTHETIC SPK");
1810        assert_eq!(parsed.file_record.forward_record, 3);
1811        assert_eq!(parsed.file_record.backward_record, 3);
1812        assert_eq!(parsed.file_record.free_address, 901);
1813        assert_eq!(parsed.file_record.byte_order, DafByteOrder::LittleEndian);
1814        assert_eq!(parsed.file_record.binary_format, "LTL-IEEE");
1815        assert_eq!(parsed.segments, expected_segments());
1816    }
1817
1818    #[test]
1819    fn decodes_big_endian_daf_summaries() {
1820        let bytes = build_daf(DafByteOrder::BigEndian);
1821
1822        let parsed = parse_daf_spk(&bytes).unwrap();
1823
1824        assert_eq!(parsed.file_record.byte_order, DafByteOrder::BigEndian);
1825        assert_eq!(parsed.file_record.binary_format, "BIG-IEEE");
1826        assert_eq!(parsed.segments, expected_segments());
1827    }
1828
1829    #[test]
1830    fn decodes_linear_daf_summary_record_chain() {
1831        let bytes = build_chained_daf(DafByteOrder::LittleEndian, 0.0);
1832
1833        let parsed = parse_daf_spk(&bytes).unwrap();
1834
1835        assert_eq!(parsed.file_record.forward_record, 3);
1836        assert_eq!(parsed.file_record.backward_record, 5);
1837        assert_eq!(parsed.segments, expected_segments());
1838    }
1839
1840    #[test]
1841    fn cyclic_daf_summary_record_chain_returns_typed_error() {
1842        let bytes = build_chained_daf(DafByteOrder::LittleEndian, 3.0);
1843
1844        let err = parse_daf_spk(&bytes).unwrap_err();
1845
1846        assert_eq!(
1847            err,
1848            SpkError::InvalidField {
1849                field: "summary record chain",
1850                value: 3,
1851            }
1852        );
1853    }
1854
1855    #[test]
1856    fn max_forward_summary_record_returns_typed_error() {
1857        let byte_order = DafByteOrder::LittleEndian;
1858        let mut bytes = build_daf(byte_order);
1859        write_i32(byte_order, &mut bytes, 76, i32::MAX);
1860
1861        let err = parse_daf_spk(&bytes).unwrap_err();
1862
1863        assert_eq!(
1864            err,
1865            SpkError::InvalidField {
1866                field: "FWARD",
1867                value: i32::MAX,
1868            }
1869        );
1870    }
1871
1872    #[test]
1873    fn max_next_summary_record_returns_typed_error() {
1874        let byte_order = DafByteOrder::LittleEndian;
1875        let mut bytes = build_daf(byte_order);
1876        write_f64(
1877            byte_order,
1878            &mut bytes,
1879            DAF_RECORD_BYTES * 2,
1880            f64::from(i32::MAX),
1881        );
1882
1883        let err = parse_daf_spk(&bytes).unwrap_err();
1884
1885        assert_eq!(
1886            err,
1887            SpkError::InvalidField {
1888                field: "next summary record",
1889                value: i32::MAX,
1890            }
1891        );
1892    }
1893
1894    #[test]
1895    fn truncated_header_returns_typed_error() {
1896        let err = parse_daf_spk(&[0u8; 16]).unwrap_err();
1897
1898        assert_eq!(
1899            err,
1900            SpkError::Truncated {
1901                context: "DAF file record",
1902                needed: 1024,
1903                actual: 16,
1904            }
1905        );
1906    }
1907
1908    #[test]
1909    fn evaluates_type2_position_records_and_boundaries() {
1910        let (bytes, segment) = build_type2_segment(DafByteOrder::LittleEndian);
1911
1912        assert_position_close(
1913            evaluate_type2_position(&bytes, DafByteOrder::LittleEndian, &segment, 0.0).unwrap(),
1914            [2.0, -5.5, 7.0],
1915        );
1916        assert_position_close(
1917            evaluate_type2_position(&bytes, DafByteOrder::LittleEndian, &segment, 5.0).unwrap(),
1918            [-2.0, -3.0, 7.0],
1919        );
1920        assert_position_close(
1921            evaluate_type2_position(&bytes, DafByteOrder::LittleEndian, &segment, 10.0).unwrap(),
1922            [11.0, 19.0, -9.0],
1923        );
1924        assert_position_close(
1925            evaluate_type2_position(&bytes, DafByteOrder::LittleEndian, &segment, 20.0).unwrap(),
1926            [9.0, 23.0, -1.0],
1927        );
1928    }
1929
1930    #[test]
1931    fn evaluates_big_endian_type2_position() {
1932        let (bytes, segment) = build_type2_segment(DafByteOrder::BigEndian);
1933
1934        assert_position_close(
1935            evaluate_type2_position(&bytes, DafByteOrder::BigEndian, &segment, 15.0).unwrap(),
1936            [10.0, 19.0, -1.0],
1937        );
1938    }
1939
1940    #[test]
1941    fn type2_out_of_coverage_returns_typed_error() {
1942        let (bytes, segment) = build_type2_segment(DafByteOrder::LittleEndian);
1943
1944        let err = evaluate_type2_position(&bytes, DafByteOrder::LittleEndian, &segment, -0.001)
1945            .unwrap_err();
1946
1947        assert_eq!(
1948            err,
1949            SpkError::OutOfCoverage {
1950                et: -0.001,
1951                start_et: 0.0,
1952                stop_et: 20.0,
1953            }
1954        );
1955    }
1956
1957    #[test]
1958    fn evaluates_type3_state_records_and_boundaries() {
1959        let (bytes, segment) = build_type3_segment(DafByteOrder::LittleEndian);
1960
1961        assert_state_close(
1962            evaluate_type3_state(&bytes, DafByteOrder::LittleEndian, &segment, 5.0).unwrap(),
1963            [1.0, 3.0, 5.0],
1964            [0.1, -0.3, 1.0],
1965        );
1966        assert_state_close(
1967            evaluate_type3_state(&bytes, DafByteOrder::LittleEndian, &segment, 10.0).unwrap(),
1968            [9.0, 22.0, -8.0],
1969            [0.0, 5.0, 3.75],
1970        );
1971        assert_state_close(
1972            evaluate_type3_state(&bytes, DafByteOrder::LittleEndian, &segment, 20.0).unwrap(),
1973            [11.0, 18.0, -2.0],
1974            [2.0, -1.0, 4.25],
1975        );
1976    }
1977
1978    #[test]
1979    fn type3_out_of_coverage_returns_typed_error() {
1980        let (bytes, segment) = build_type3_segment(DafByteOrder::LittleEndian);
1981
1982        let err =
1983            evaluate_type3_state(&bytes, DafByteOrder::LittleEndian, &segment, 20.001).unwrap_err();
1984
1985        assert_eq!(
1986            err,
1987            SpkError::OutOfCoverage {
1988                et: 20.001,
1989                start_et: 0.0,
1990                stop_et: 20.0,
1991            }
1992        );
1993    }
1994
1995    #[test]
1996    fn evaluates_type21_synthetic_records_and_boundaries() {
1997        let (bytes, segment) = build_type21_segment(DafByteOrder::LittleEndian);
1998
1999        // Difference table is zero, so each record reduces to linear motion
2000        // refpos + delta * refvel about its own reference epoch. This makes the
2001        // selected-record/position/velocity result exactly checkable.
2002        assert_state_close(
2003            evaluate_type21_state(&bytes, DafByteOrder::LittleEndian, &segment, 0.0).unwrap(),
2004            [100.0, 200.0, 300.0],
2005            [1.0, 2.0, 3.0],
2006        );
2007        assert_state_close(
2008            evaluate_type21_state(&bytes, DafByteOrder::LittleEndian, &segment, 5.0).unwrap(),
2009            [105.0, 210.0, 315.0],
2010            [1.0, 2.0, 3.0],
2011        );
2012        // et == first epoch still resolves to the first record.
2013        assert_state_close(
2014            evaluate_type21_state(&bytes, DafByteOrder::LittleEndian, &segment, 10.0).unwrap(),
2015            [110.0, 220.0, 330.0],
2016            [1.0, 2.0, 3.0],
2017        );
2018        // Past the first epoch we cross into the second record.
2019        assert_state_close(
2020            evaluate_type21_state(&bytes, DafByteOrder::LittleEndian, &segment, 15.0).unwrap(),
2021            [1050.0, 2100.0, 3150.0],
2022            [10.0, 20.0, 30.0],
2023        );
2024        assert_state_close(
2025            evaluate_type21_state(&bytes, DafByteOrder::LittleEndian, &segment, 20.0).unwrap(),
2026            [1100.0, 2200.0, 3300.0],
2027            [10.0, 20.0, 30.0],
2028        );
2029    }
2030
2031    #[test]
2032    fn evaluates_big_endian_type21_state() {
2033        let (bytes, segment) = build_type21_segment(DafByteOrder::BigEndian);
2034
2035        assert_state_close(
2036            evaluate_type21_state(&bytes, DafByteOrder::BigEndian, &segment, 5.0).unwrap(),
2037            [105.0, 210.0, 315.0],
2038            [1.0, 2.0, 3.0],
2039        );
2040    }
2041
2042    #[test]
2043    fn type21_out_of_coverage_returns_typed_error() {
2044        let (bytes, segment) = build_type21_segment(DafByteOrder::LittleEndian);
2045
2046        let err = evaluate_type21_state(&bytes, DafByteOrder::LittleEndian, &segment, 20.001)
2047            .unwrap_err();
2048
2049        assert_eq!(
2050            err,
2051            SpkError::OutOfCoverage {
2052                et: 20.001,
2053                start_et: 0.0,
2054                stop_et: 20.0,
2055            }
2056        );
2057    }
2058
2059    #[test]
2060    fn type21_nonfinite_et_returns_typed_error() {
2061        let (bytes, segment) = build_type21_segment(DafByteOrder::LittleEndian);
2062
2063        for et in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
2064            let err = evaluate_type21_state(&bytes, DafByteOrder::LittleEndian, &segment, et)
2065                .unwrap_err();
2066            assert!(
2067                matches!(
2068                    err,
2069                    SpkError::InvalidDoubleField {
2070                        field: "type-21 ET",
2071                        ..
2072                    }
2073                ),
2074                "non-finite et {et} should be rejected, got {err:?}"
2075            );
2076        }
2077    }
2078
2079    #[test]
2080    fn type21_malformed_order_words_return_typed_error() {
2081        // KQMAX1 lives at one-based address `start + 4*maxdim + 8`; for record 0
2082        // (start_address 1, maxdim 3) that is address 20. A hostile out-of-range
2083        // value must be rejected, not panic on an out-of-bounds array index.
2084        let maxdim = 3usize;
2085        let kqmax1_addr = 1 + 4 * maxdim + 8;
2086        let kq1_addr = 1 + 4 * maxdim + 9;
2087
2088        // KQMAX1 far larger than MAXDIM+1.
2089        let (mut bytes, segment) = build_type21_segment(DafByteOrder::LittleEndian);
2090        write_f64_address(DafByteOrder::LittleEndian, &mut bytes, kqmax1_addr, 99.0);
2091        let err =
2092            evaluate_type21_state(&bytes, DafByteOrder::LittleEndian, &segment, 5.0).unwrap_err();
2093        assert!(
2094            matches!(err, SpkError::InvalidSegmentLayout { .. }),
2095            "oversized KQMAX1 should be rejected, got {err:?}"
2096        );
2097
2098        // KQ order >= KQMAX1 (here KQMAX1 = 2, KQ(1) = 5).
2099        let (mut bytes, segment) = build_type21_segment(DafByteOrder::LittleEndian);
2100        write_f64_address(DafByteOrder::LittleEndian, &mut bytes, kq1_addr, 5.0);
2101        let err =
2102            evaluate_type21_state(&bytes, DafByteOrder::LittleEndian, &segment, 5.0).unwrap_err();
2103        assert!(
2104            matches!(err, SpkError::InvalidSegmentLayout { .. }),
2105            "out-of-range KQ should be rejected, got {err:?}"
2106        );
2107
2108        // Non-integer KQMAX1 must also be rejected (f64_to_usize).
2109        let (mut bytes, segment) = build_type21_segment(DafByteOrder::LittleEndian);
2110        write_f64_address(
2111            DafByteOrder::LittleEndian,
2112            &mut bytes,
2113            kqmax1_addr,
2114            f64::NAN,
2115        );
2116        let err =
2117            evaluate_type21_state(&bytes, DafByteOrder::LittleEndian, &segment, 5.0).unwrap_err();
2118        assert!(
2119            matches!(
2120                err,
2121                SpkError::InvalidDoubleField { .. } | SpkError::InvalidSegmentLayout { .. }
2122            ),
2123            "non-integer KQMAX1 should be rejected, got {err:?}"
2124        );
2125    }
2126
2127    /// Bit-exact parity against CSPICE on a real type-21 (Extended Modified
2128    /// Difference Array) kernel: a 433 Eros SPK fetched from JPL Horizons. The
2129    /// reference vectors are CSPICE `spkgeo(20000433, et, "J2000", 10)` output.
2130    /// See `tests/fixtures/spk/gen_eros_type21.py`. Agreement is within one ULP
2131    /// of the ~1e8 km / ~20 km/s magnitudes (observed max position residual
2132    /// 7.5e-9 km, velocity 4.4e-16 km/s).
2133    #[test]
2134    fn real_type21_kernel_matches_cspice_reference() {
2135        const KERNEL: &[u8] = include_bytes!("../../tests/fixtures/spk/horizons_eros_type21.bsp");
2136
2137        // (et seconds past J2000 TDB, [x, y, z, vx, vy, vz]) km, km/s.
2138        const REFERENCE: &[(f64, [f64; 6])] = &[
2139            (
2140                757339200.0,
2141                [
2142                    198083634.33689928,
2143                    56306354.00566181,
2144                    67761020.0290685,
2145                    -14.136880898003753,
2146                    18.729945253375007,
2147                    8.080580941541488,
2148                ],
2149            ),
2150            (
2151                757655424.0,
2152                [
2153                    193484166.17007136,
2154                    62190955.161292516,
2155                    70271250.61392583,
2156                    -14.95317475425177,
2157                    18.482891309963502,
2158                    7.792787505001918,
2159                ],
2160            ),
2161            (
2162                760501440.0,
2163                [
2164                    140599517.39824444,
2165                    110142414.48840125,
2166                    87942357.2561364,
2167                    -22.110500498220798,
2168                    14.728648269072185,
2169                    4.367688325339683,
2170                ],
2171            ),
2172            (
2173                765244800.0,
2174                [
2175                    14324682.473833444,
2176                    151855494.96957216,
2177                    88809564.6055465,
2178                    -29.543141519840074,
2179                    1.384349579197926,
2180                    -4.552338928064369,
2181                ],
2182            ),
2183            (
2184                767879989.4592,
2185                [
2186                    -62463976.26374265,
2187                    142278295.29334122,
2188                    69496198.60194506,
2189                    -27.83899206786184,
2190                    -8.728214407471189,
2191                    -9.98623557339431,
2192                ],
2193            ),
2194            (
2195                773150400.0,
2196                [
2197                    -170058326.9714746,
2198                    51931259.239147335,
2199                    -1243746.8623651236,
2200                    -10.894277182103927,
2201                    -23.170448141059893,
2202                    -15.1244115495765,
2203                ],
2204            ),
2205            (
2206                778420810.5408,
2207                [
2208                    -175392526.76953015,
2209                    -73836504.48334283,
2210                    -73616410.40702733,
2211                    7.685028379798697,
2212                    -22.45642797493469,
2213                    -11.361635361252944,
2214                ],
2215            ),
2216            (
2217                781056000.0,
2218                [
2219                    -146671840.75331673,
2220                    -128352102.87455015,
2221                    -99379387.41255096,
2222                    13.724782826077085,
2223                    -18.71213758130164,
2224                    -8.14425619120024,
2225                ],
2226            ),
2227            (
2228                785799360.0,
2229                [
2230                    -65781054.32577276,
2231                    -197470134.64271438,
2232                    -124005727.09542452,
2233                    19.398793883808853,
2234                    -10.268325580683918,
2235                    -2.324610823885138,
2236                ],
2237            ),
2238            (
2239                788645376.0,
2240                [
2241                    -8859755.122267516,
2242                    -219270459.75263178,
2243                    -126097226.6160046,
2244                    20.34548822205983,
2245                    -5.074720269573395,
2246                    0.7953511794172388,
2247                ],
2248            ),
2249            (
2250                788961600.0,
2251                [
2252                    -2423286.488811064,
2253                    -220785626.12491044,
2254                    -125794359.14041424,
2255                    20.360009383792537,
2256                    -4.508637229520069,
2257                    1.1193915696949732,
2258                ],
2259            ),
2260        ];
2261
2262        let spk = Spk::from_bytes(KERNEL).unwrap();
2263
2264        let segments = spk.segments();
2265        assert_eq!(segments.len(), 1);
2266        assert_eq!(segments[0].data_type, SPK_TYPE_21);
2267        assert_eq!(segments[0].target, 20000433);
2268        assert_eq!(segments[0].center, 10);
2269
2270        let mut max_position_error = 0.0f64;
2271        let mut max_velocity_error = 0.0f64;
2272        for &(et, expected) in REFERENCE {
2273            let state = spk.spk_state(20000433, 10, et).unwrap();
2274            let velocity = state.velocity_km_s.expect("type-21 yields velocity");
2275            for axis in 0..3 {
2276                max_position_error =
2277                    max_position_error.max((state.position_km[axis] - expected[axis]).abs());
2278                max_velocity_error =
2279                    max_velocity_error.max((velocity[axis] - expected[axis + 3]).abs());
2280            }
2281        }
2282
2283        // ~1-ULP gates at these magnitudes (|pos| ~2.2e8 km -> 1 ULP ~4.9e-8 km;
2284        // |vel| ~20 km/s -> 1 ULP ~4.4e-15 km/s). Measured agreement is sub-ULP
2285        // (~7.5e-9 km, ~4.4e-16 km/s); these gates assert the bit-exact claim
2286        // rather than a loose tolerance, and will catch any real regression.
2287        assert!(
2288            max_position_error < 5e-8,
2289            "type-21 position drift {max_position_error:e} km exceeds CSPICE parity gate"
2290        );
2291        assert!(
2292            max_velocity_error < 1e-14,
2293            "type-21 velocity drift {max_velocity_error:e} km/s exceeds CSPICE parity gate"
2294        );
2295    }
2296
2297    #[test]
2298    fn spk_state_returns_direct_segment_state() {
2299        let bytes = build_query_spk();
2300        let spk = Spk::from_bytes(&bytes).unwrap();
2301
2302        let state = spk.spk_state(301, 3, 5.0).unwrap();
2303
2304        assert_eq!(spk.file_record().internal_name, "QUERY SPK");
2305        assert_eq!(spk.segments().len(), 4);
2306        assert_eq!(state.target, 301);
2307        assert_eq!(state.center, 3);
2308        assert_query_state_close(state, [100.0, 200.0, 300.0], Some([1.0, 2.0, 3.0]), 1);
2309    }
2310
2311    #[test]
2312    fn spk_state_chains_through_common_center() {
2313        let bytes = build_query_spk();
2314        let spk = Spk::from_bytes(&bytes).unwrap();
2315
2316        let state = spk_state(&spk, 399, 3, 5.0).unwrap();
2317
2318        assert_eq!(state.target, 399);
2319        assert_eq!(state.center, 3);
2320        assert_query_state_close(state, [950.0, -5.0, 5.0], Some([9.5, -0.25, 0.5]), 1);
2321    }
2322
2323    #[test]
2324    fn spk_state_prefers_later_overlapping_segments() {
2325        let bytes = build_priority_spk();
2326        let spk = Spk::from_bytes(&bytes).unwrap();
2327
2328        let direct = spk.spk_state(301, 3, 5.0).unwrap();
2329        let chained = spk.spk_state(399, 3, 5.0).unwrap();
2330
2331        assert_eq!(spk.segments().len(), 5);
2332        assert_query_state_close(direct, [900.0, 800.0, 700.0], Some([9.0, 8.0, 7.0]), 1);
2333        assert_query_state_close(chained, [1950.0, 5.0, 25.0], Some([19.5, 0.25, 1.5]), 1);
2334    }
2335
2336    #[test]
2337    fn spk_state_prefers_later_position_only_segment() {
2338        let bytes = build_position_only_priority_spk();
2339        let spk = Spk::from_bytes(&bytes).unwrap();
2340
2341        let state = spk.spk_state(301, 3, 5.0).unwrap();
2342
2343        assert_eq!(spk.segments().len(), 2);
2344        assert_query_state_close(state, [700.0, 800.0, 900.0], None, 1);
2345    }
2346
2347    #[test]
2348    fn spk_state_errors_on_later_unsupported_segment() {
2349        let bytes = build_unsupported_priority_spk();
2350        let spk = Spk::from_bytes(&bytes).unwrap();
2351
2352        let err = spk.spk_state(301, 3, 5.0).unwrap_err();
2353        let supported = spk.spk_state(302, 3, 5.0).unwrap();
2354
2355        assert_eq!(err, SpkError::UnsupportedStateSegmentType { data_type: 99 });
2356        assert_query_state_close(supported, [400.0, 500.0, 600.0], Some([4.0, 5.0, 6.0]), 1);
2357    }
2358
2359    #[test]
2360    fn spk_state_prefers_later_segment_when_center_changes() {
2361        let bytes = build_changed_center_priority_spk();
2362        let spk = Spk::from_bytes(&bytes).unwrap();
2363
2364        let state = spk.spk_state(301, 3, 5.0).unwrap();
2365
2366        assert_eq!(spk.segments().len(), 3);
2367        assert_query_state_close(state, [1000.0, 80.0, 12.0], Some([100.0, 8.0, 1.2]), 1);
2368    }
2369
2370    #[test]
2371    fn spk_state_prefers_later_reversed_segment() {
2372        let bytes = build_reversed_priority_spk();
2373        let spk = Spk::from_bytes(&bytes).unwrap();
2374
2375        let state = spk.spk_state(301, 3, 5.0).unwrap();
2376
2377        assert_eq!(spk.segments().len(), 3);
2378        assert_query_state_close(state, [-800.0, -80.0, -2.0], Some([-80.0, -8.0, -0.2]), 1);
2379    }
2380
2381    #[test]
2382    fn spk_state_preserves_velocity_bearing_chain() {
2383        let bytes = build_velocity_retention_spk();
2384        let spk = Spk::from_bytes(&bytes).unwrap();
2385
2386        let state = spk.spk_state(800, 3, 5.0).unwrap();
2387
2388        assert_query_state_close(state, [120.0, 3.0, 4.0], Some([12.0, 0.3, 0.4]), 1);
2389    }
2390
2391    #[test]
2392    fn spk_state_tries_alternate_chain_after_frame_mismatch() {
2393        let bytes = build_frame_mismatch_spk();
2394        let spk = Spk::from_bytes(&bytes).unwrap();
2395
2396        let state = spk.spk_state(700, 3, 5.0).unwrap();
2397
2398        assert_query_state_close(state, [120.0, 3.0, 4.0], Some([12.0, 0.3, 0.4]), 1);
2399    }
2400
2401    #[test]
2402    fn spk_state_returns_frame_mismatch_when_no_chain_is_compatible() {
2403        let bytes = build_frame_mismatch_spk();
2404        let spk = Spk::from_bytes(&bytes).unwrap();
2405
2406        let err = spk.spk_state(701, 3, 5.0).unwrap_err();
2407
2408        assert_eq!(
2409            err,
2410            SpkError::FrameMismatch {
2411                first: 1,
2412                second: 2,
2413            }
2414        );
2415    }
2416
2417    #[test]
2418    fn spk_state_returns_none_velocity_for_type2_segment() {
2419        let bytes = build_query_spk();
2420        let spk = Spk::from_bytes(&bytes).unwrap();
2421
2422        let state = spk.spk_state(302, 3, 5.0).unwrap();
2423
2424        assert_query_state_close(state, [7.0, 8.0, 9.0], None, 1);
2425    }
2426
2427    #[test]
2428    fn spk_state_known_self_query_returns_zero_state() {
2429        let bytes = build_query_spk();
2430        let spk = Spk::from_bytes(&bytes).unwrap();
2431
2432        let state = spk.spk_state(301, 301, 5.0).unwrap();
2433
2434        assert_query_state_close(state, [0.0; 3], Some([0.0; 3]), 0);
2435    }
2436
2437    #[test]
2438    fn spk_state_unknown_self_query_returns_typed_error() {
2439        let bytes = build_query_spk();
2440        let spk = Spk::from_bytes(&bytes).unwrap();
2441
2442        let err = spk.spk_state(999, 999, 5.0).unwrap_err();
2443
2444        assert_eq!(err, SpkError::UnknownBody { body: 999 });
2445    }
2446
2447    #[test]
2448    fn spk_state_self_query_out_of_coverage_returns_typed_error() {
2449        let bytes = build_query_spk();
2450        let spk = Spk::from_bytes(&bytes).unwrap();
2451
2452        let err = spk.spk_state(301, 301, 20.0).unwrap_err();
2453
2454        assert_eq!(
2455            err,
2456            SpkError::CoverageGap {
2457                target: 301,
2458                center: 301,
2459                et: 20.0,
2460            }
2461        );
2462    }
2463
2464    #[test]
2465    fn spk_state_unknown_target_returns_typed_error() {
2466        let bytes = build_query_spk();
2467        let spk = Spk::from_bytes(&bytes).unwrap();
2468
2469        let err = spk.spk_state(999, 0, 5.0).unwrap_err();
2470
2471        assert_eq!(err, SpkError::UnknownBody { body: 999 });
2472    }
2473
2474    #[test]
2475    fn spk_state_out_of_coverage_returns_typed_error() {
2476        let bytes = build_query_spk();
2477        let spk = Spk::from_bytes(&bytes).unwrap();
2478
2479        let err = spk.spk_state(301, 3, 20.0).unwrap_err();
2480
2481        assert_eq!(
2482            err,
2483            SpkError::CoverageGap {
2484                target: 301,
2485                center: 3,
2486                et: 20.0,
2487            }
2488        );
2489    }
2490
2491    #[test]
2492    fn std_load_reads_spk_file() {
2493        let bytes = build_query_spk();
2494        let mut path = std::env::temp_dir();
2495        let nonce = std::time::SystemTime::now()
2496            .duration_since(std::time::UNIX_EPOCH)
2497            .unwrap()
2498            .as_nanos();
2499        path.push(format!(
2500            "sidereon-spk-load-{}-{nonce}.bsp",
2501            std::process::id()
2502        ));
2503
2504        std::fs::write(&path, &bytes).unwrap();
2505
2506        let spk = Spk::load(&path).unwrap();
2507        std::fs::remove_file(&path).unwrap();
2508
2509        let state = spk.spk_state(301, 3, 5.0).unwrap();
2510        assert_query_state_close(state, [100.0, 200.0, 300.0], Some([1.0, 2.0, 3.0]), 1);
2511
2512        let err = Spk::load(&path).unwrap_err();
2513        match err {
2514            SpkError::Io {
2515                path: err_path,
2516                message,
2517            } => {
2518                assert_eq!(err_path, path.display().to_string());
2519                assert!(!message.is_empty());
2520            }
2521            other => panic!("expected IO error from missing SPK path, got {other:?}"),
2522        }
2523    }
2524
2525    fn expected_segments() -> Vec<SpkSegmentDescriptor> {
2526        vec![
2527            SpkSegmentDescriptor {
2528                name: "MERCURY BARYCENTER".to_string(),
2529                start_et: -100.0,
2530                stop_et: 100.0,
2531                target: 1,
2532                center: 0,
2533                frame: 1,
2534                data_type: 2,
2535                start_address: 513,
2536                end_address: 700,
2537            },
2538            SpkSegmentDescriptor {
2539                name: "EARTH".to_string(),
2540                start_et: 200.0,
2541                stop_et: 400.0,
2542                target: 399,
2543                center: 3,
2544                frame: 17,
2545                data_type: 3,
2546                start_address: 701,
2547                end_address: 900,
2548            },
2549        ]
2550    }
2551
2552    fn build_daf(byte_order: DafByteOrder) -> Vec<u8> {
2553        let mut bytes = vec![0u8; DAF_RECORD_BYTES * 4];
2554
2555        bytes[0..8].copy_from_slice(b"DAF/SPK ");
2556        write_i32(byte_order, &mut bytes, 8, 2);
2557        write_i32(byte_order, &mut bytes, 12, 6);
2558        write_ascii(&mut bytes, 16, DAF_INTERNAL_NAME_BYTES, "SYNTHETIC SPK");
2559        write_i32(byte_order, &mut bytes, 76, 3);
2560        write_i32(byte_order, &mut bytes, 80, 3);
2561        write_i32(byte_order, &mut bytes, 84, 901);
2562        match byte_order {
2563            DafByteOrder::LittleEndian => bytes
2564                [DAF_BINARY_FORMAT_OFFSET..DAF_BINARY_FORMAT_OFFSET + 8]
2565                .copy_from_slice(b"LTL-IEEE"),
2566            DafByteOrder::BigEndian => bytes
2567                [DAF_BINARY_FORMAT_OFFSET..DAF_BINARY_FORMAT_OFFSET + 8]
2568                .copy_from_slice(b"BIG-IEEE"),
2569        }
2570
2571        let summary_offset = DAF_RECORD_BYTES * 2;
2572        let name_offset = DAF_RECORD_BYTES * 3;
2573        write_f64(byte_order, &mut bytes, summary_offset, 0.0);
2574        write_f64(byte_order, &mut bytes, summary_offset + 8, 0.0);
2575        write_f64(byte_order, &mut bytes, summary_offset + 16, 2.0);
2576
2577        write_summary(
2578            byte_order,
2579            &mut bytes,
2580            summary_offset + 24,
2581            -100.0,
2582            100.0,
2583            [1, 0, 1, 2, 513, 700],
2584        );
2585        write_summary(
2586            byte_order,
2587            &mut bytes,
2588            summary_offset + 64,
2589            200.0,
2590            400.0,
2591            [399, 3, 17, 3, 701, 900],
2592        );
2593        write_ascii(&mut bytes, name_offset, 40, "MERCURY BARYCENTER");
2594        write_ascii(&mut bytes, name_offset + 40, 40, "EARTH");
2595
2596        bytes
2597    }
2598
2599    fn build_chained_daf(byte_order: DafByteOrder, final_next_record: f64) -> Vec<u8> {
2600        let mut bytes = vec![0u8; DAF_RECORD_BYTES * 6];
2601
2602        bytes[0..8].copy_from_slice(b"DAF/SPK ");
2603        write_i32(byte_order, &mut bytes, 8, 2);
2604        write_i32(byte_order, &mut bytes, 12, 6);
2605        write_ascii(&mut bytes, 16, DAF_INTERNAL_NAME_BYTES, "CHAINED SPK");
2606        write_i32(byte_order, &mut bytes, 76, 3);
2607        write_i32(byte_order, &mut bytes, 80, 5);
2608        write_i32(byte_order, &mut bytes, 84, 901);
2609        match byte_order {
2610            DafByteOrder::LittleEndian => bytes
2611                [DAF_BINARY_FORMAT_OFFSET..DAF_BINARY_FORMAT_OFFSET + 8]
2612                .copy_from_slice(b"LTL-IEEE"),
2613            DafByteOrder::BigEndian => bytes
2614                [DAF_BINARY_FORMAT_OFFSET..DAF_BINARY_FORMAT_OFFSET + 8]
2615                .copy_from_slice(b"BIG-IEEE"),
2616        }
2617
2618        let first_summary_offset = DAF_RECORD_BYTES * 2;
2619        let first_name_offset = DAF_RECORD_BYTES * 3;
2620        write_f64(byte_order, &mut bytes, first_summary_offset, 5.0);
2621        write_f64(byte_order, &mut bytes, first_summary_offset + 8, 0.0);
2622        write_f64(byte_order, &mut bytes, first_summary_offset + 16, 1.0);
2623        write_summary(
2624            byte_order,
2625            &mut bytes,
2626            first_summary_offset + 24,
2627            -100.0,
2628            100.0,
2629            [1, 0, 1, 2, 513, 700],
2630        );
2631        write_ascii(&mut bytes, first_name_offset, 40, "MERCURY BARYCENTER");
2632
2633        let second_summary_offset = DAF_RECORD_BYTES * 4;
2634        let second_name_offset = DAF_RECORD_BYTES * 5;
2635        write_f64(
2636            byte_order,
2637            &mut bytes,
2638            second_summary_offset,
2639            final_next_record,
2640        );
2641        write_f64(byte_order, &mut bytes, second_summary_offset + 8, 3.0);
2642        write_f64(byte_order, &mut bytes, second_summary_offset + 16, 1.0);
2643        write_summary(
2644            byte_order,
2645            &mut bytes,
2646            second_summary_offset + 24,
2647            200.0,
2648            400.0,
2649            [399, 3, 17, 3, 701, 900],
2650        );
2651        write_ascii(&mut bytes, second_name_offset, 40, "EARTH");
2652
2653        bytes
2654    }
2655
2656    fn build_query_spk() -> Vec<u8> {
2657        let byte_order = DafByteOrder::LittleEndian;
2658        let direct_start = 513usize;
2659        let target_to_ssb_start = 525usize;
2660        let center_to_ssb_start = 537usize;
2661        let type2_start = 549usize;
2662        let type2_end = type2_start + 8;
2663        let mut bytes = vec![0u8; type2_end * 8];
2664
2665        bytes[0..8].copy_from_slice(b"DAF/SPK ");
2666        write_i32(byte_order, &mut bytes, 8, 2);
2667        write_i32(byte_order, &mut bytes, 12, 6);
2668        write_ascii(&mut bytes, 16, DAF_INTERNAL_NAME_BYTES, "QUERY SPK");
2669        write_i32(byte_order, &mut bytes, 76, 3);
2670        write_i32(byte_order, &mut bytes, 80, 3);
2671        write_i32(byte_order, &mut bytes, 84, (type2_end + 1) as i32);
2672        bytes[DAF_BINARY_FORMAT_OFFSET..DAF_BINARY_FORMAT_OFFSET + 8].copy_from_slice(b"LTL-IEEE");
2673
2674        let summary_offset = DAF_RECORD_BYTES * 2;
2675        let name_offset = DAF_RECORD_BYTES * 3;
2676        write_f64(byte_order, &mut bytes, summary_offset, 0.0);
2677        write_f64(byte_order, &mut bytes, summary_offset + 8, 0.0);
2678        write_f64(byte_order, &mut bytes, summary_offset + 16, 4.0);
2679
2680        write_summary(
2681            byte_order,
2682            &mut bytes,
2683            summary_offset + 24,
2684            0.0,
2685            10.0,
2686            [301, 3, 1, 3, direct_start as i32, direct_start as i32 + 11],
2687        );
2688        write_summary(
2689            byte_order,
2690            &mut bytes,
2691            summary_offset + 64,
2692            0.0,
2693            10.0,
2694            [
2695                399,
2696                0,
2697                1,
2698                3,
2699                target_to_ssb_start as i32,
2700                target_to_ssb_start as i32 + 11,
2701            ],
2702        );
2703        write_summary(
2704            byte_order,
2705            &mut bytes,
2706            summary_offset + 104,
2707            0.0,
2708            10.0,
2709            [
2710                3,
2711                0,
2712                1,
2713                3,
2714                center_to_ssb_start as i32,
2715                center_to_ssb_start as i32 + 11,
2716            ],
2717        );
2718        write_summary(
2719            byte_order,
2720            &mut bytes,
2721            summary_offset + 144,
2722            0.0,
2723            10.0,
2724            [302, 3, 1, 2, type2_start as i32, type2_end as i32],
2725        );
2726
2727        write_ascii(&mut bytes, name_offset, 40, "BODY 301 TO CENTER 3");
2728        write_ascii(&mut bytes, name_offset + 40, 40, "BODY 399 TO SSB");
2729        write_ascii(&mut bytes, name_offset + 80, 40, "CENTER 3 TO SSB");
2730        write_ascii(
2731            &mut bytes,
2732            name_offset + 120,
2733            40,
2734            "TYPE2 BODY 302 TO CENTER 3",
2735        );
2736
2737        write_type3_constant_segment(
2738            byte_order,
2739            &mut bytes,
2740            direct_start,
2741            [100.0, 200.0, 300.0],
2742            [1.0, 2.0, 3.0],
2743        );
2744        write_type3_constant_segment(
2745            byte_order,
2746            &mut bytes,
2747            target_to_ssb_start,
2748            [1000.0, 0.0, 0.0],
2749            [10.0, 0.0, 0.0],
2750        );
2751        write_type3_constant_segment(
2752            byte_order,
2753            &mut bytes,
2754            center_to_ssb_start,
2755            [50.0, 5.0, -5.0],
2756            [0.5, 0.25, -0.5],
2757        );
2758        write_type2_constant_segment(byte_order, &mut bytes, type2_start, [7.0, 8.0, 9.0]);
2759
2760        bytes
2761    }
2762
2763    fn build_priority_spk() -> Vec<u8> {
2764        let byte_order = DafByteOrder::LittleEndian;
2765        let direct_early_start = 513usize;
2766        let direct_priority_start = 525usize;
2767        let target_early_start = 537usize;
2768        let center_start = 549usize;
2769        let target_priority_start = 561usize;
2770        let target_priority_end = target_priority_start + 11;
2771        let mut bytes = vec![0u8; target_priority_end * 8];
2772
2773        bytes[0..8].copy_from_slice(b"DAF/SPK ");
2774        write_i32(byte_order, &mut bytes, 8, 2);
2775        write_i32(byte_order, &mut bytes, 12, 6);
2776        write_ascii(&mut bytes, 16, DAF_INTERNAL_NAME_BYTES, "PRIORITY SPK");
2777        write_i32(byte_order, &mut bytes, 76, 3);
2778        write_i32(byte_order, &mut bytes, 80, 3);
2779        write_i32(byte_order, &mut bytes, 84, (target_priority_end + 1) as i32);
2780        bytes[DAF_BINARY_FORMAT_OFFSET..DAF_BINARY_FORMAT_OFFSET + 8].copy_from_slice(b"LTL-IEEE");
2781
2782        let summary_offset = DAF_RECORD_BYTES * 2;
2783        let name_offset = DAF_RECORD_BYTES * 3;
2784        write_f64(byte_order, &mut bytes, summary_offset, 0.0);
2785        write_f64(byte_order, &mut bytes, summary_offset + 8, 0.0);
2786        write_f64(byte_order, &mut bytes, summary_offset + 16, 5.0);
2787
2788        write_summary(
2789            byte_order,
2790            &mut bytes,
2791            summary_offset + 24,
2792            0.0,
2793            10.0,
2794            [
2795                301,
2796                3,
2797                1,
2798                3,
2799                direct_early_start as i32,
2800                direct_early_start as i32 + 11,
2801            ],
2802        );
2803        write_summary(
2804            byte_order,
2805            &mut bytes,
2806            summary_offset + 64,
2807            0.0,
2808            10.0,
2809            [
2810                301,
2811                3,
2812                1,
2813                3,
2814                direct_priority_start as i32,
2815                direct_priority_start as i32 + 11,
2816            ],
2817        );
2818        write_summary(
2819            byte_order,
2820            &mut bytes,
2821            summary_offset + 104,
2822            0.0,
2823            10.0,
2824            [
2825                399,
2826                0,
2827                1,
2828                3,
2829                target_early_start as i32,
2830                target_early_start as i32 + 11,
2831            ],
2832        );
2833        write_summary(
2834            byte_order,
2835            &mut bytes,
2836            summary_offset + 144,
2837            0.0,
2838            10.0,
2839            [3, 0, 1, 3, center_start as i32, center_start as i32 + 11],
2840        );
2841        write_summary(
2842            byte_order,
2843            &mut bytes,
2844            summary_offset + 184,
2845            0.0,
2846            10.0,
2847            [
2848                399,
2849                0,
2850                1,
2851                3,
2852                target_priority_start as i32,
2853                target_priority_start as i32 + 11,
2854            ],
2855        );
2856
2857        write_ascii(&mut bytes, name_offset, 40, "EARLY BODY 301 TO CENTER 3");
2858        write_ascii(
2859            &mut bytes,
2860            name_offset + 40,
2861            40,
2862            "PRIORITY BODY 301 TO CENTER 3",
2863        );
2864        write_ascii(&mut bytes, name_offset + 80, 40, "EARLY BODY 399 TO SSB");
2865        write_ascii(&mut bytes, name_offset + 120, 40, "CENTER 3 TO SSB");
2866        write_ascii(
2867            &mut bytes,
2868            name_offset + 160,
2869            40,
2870            "PRIORITY BODY 399 TO SSB",
2871        );
2872
2873        write_type3_constant_segment(
2874            byte_order,
2875            &mut bytes,
2876            direct_early_start,
2877            [100.0, 200.0, 300.0],
2878            [1.0, 2.0, 3.0],
2879        );
2880        write_type3_constant_segment(
2881            byte_order,
2882            &mut bytes,
2883            direct_priority_start,
2884            [900.0, 800.0, 700.0],
2885            [9.0, 8.0, 7.0],
2886        );
2887        write_type3_constant_segment(
2888            byte_order,
2889            &mut bytes,
2890            target_early_start,
2891            [1000.0, 0.0, 0.0],
2892            [10.0, 0.0, 0.0],
2893        );
2894        write_type3_constant_segment(
2895            byte_order,
2896            &mut bytes,
2897            center_start,
2898            [50.0, 5.0, -5.0],
2899            [0.5, 0.25, -0.5],
2900        );
2901        write_type3_constant_segment(
2902            byte_order,
2903            &mut bytes,
2904            target_priority_start,
2905            [2000.0, 10.0, 20.0],
2906            [20.0, 0.5, 1.0],
2907        );
2908
2909        bytes
2910    }
2911
2912    fn build_position_only_priority_spk() -> Vec<u8> {
2913        let byte_order = DafByteOrder::LittleEndian;
2914        let direct_early_start = 513usize;
2915        let direct_priority_start = 525usize;
2916        let direct_priority_end = direct_priority_start + 8;
2917        let mut bytes = vec![0u8; direct_priority_end * 8];
2918
2919        bytes[0..8].copy_from_slice(b"DAF/SPK ");
2920        write_i32(byte_order, &mut bytes, 8, 2);
2921        write_i32(byte_order, &mut bytes, 12, 6);
2922        write_ascii(
2923            &mut bytes,
2924            16,
2925            DAF_INTERNAL_NAME_BYTES,
2926            "POSITION ONLY PRIORITY SPK",
2927        );
2928        write_i32(byte_order, &mut bytes, 76, 3);
2929        write_i32(byte_order, &mut bytes, 80, 3);
2930        write_i32(byte_order, &mut bytes, 84, (direct_priority_end + 1) as i32);
2931        bytes[DAF_BINARY_FORMAT_OFFSET..DAF_BINARY_FORMAT_OFFSET + 8].copy_from_slice(b"LTL-IEEE");
2932
2933        let summary_offset = DAF_RECORD_BYTES * 2;
2934        let name_offset = DAF_RECORD_BYTES * 3;
2935        write_f64(byte_order, &mut bytes, summary_offset, 0.0);
2936        write_f64(byte_order, &mut bytes, summary_offset + 8, 0.0);
2937        write_f64(byte_order, &mut bytes, summary_offset + 16, 2.0);
2938
2939        write_summary(
2940            byte_order,
2941            &mut bytes,
2942            summary_offset + 24,
2943            0.0,
2944            10.0,
2945            [
2946                301,
2947                3,
2948                1,
2949                3,
2950                direct_early_start as i32,
2951                direct_early_start as i32 + 11,
2952            ],
2953        );
2954        write_summary(
2955            byte_order,
2956            &mut bytes,
2957            summary_offset + 64,
2958            0.0,
2959            10.0,
2960            [
2961                301,
2962                3,
2963                1,
2964                2,
2965                direct_priority_start as i32,
2966                direct_priority_end as i32,
2967            ],
2968        );
2969
2970        write_ascii(
2971            &mut bytes,
2972            name_offset,
2973            40,
2974            "EARLY TYPE3 BODY 301 TO CENTER 3",
2975        );
2976        write_ascii(
2977            &mut bytes,
2978            name_offset + 40,
2979            40,
2980            "LATE TYPE2 BODY 301 TO CENTER 3",
2981        );
2982
2983        write_type3_constant_segment(
2984            byte_order,
2985            &mut bytes,
2986            direct_early_start,
2987            [100.0, 200.0, 300.0],
2988            [1.0, 2.0, 3.0],
2989        );
2990        write_type2_constant_segment(
2991            byte_order,
2992            &mut bytes,
2993            direct_priority_start,
2994            [700.0, 800.0, 900.0],
2995        );
2996
2997        bytes
2998    }
2999
3000    fn build_unsupported_priority_spk() -> Vec<u8> {
3001        let byte_order = DafByteOrder::LittleEndian;
3002        let direct_early_start = 513usize;
3003        let unsupported_start = 525usize;
3004        let supported_start = 537usize;
3005        let supported_end = supported_start + 11;
3006        let mut bytes = vec![0u8; supported_end * 8];
3007
3008        bytes[0..8].copy_from_slice(b"DAF/SPK ");
3009        write_i32(byte_order, &mut bytes, 8, 2);
3010        write_i32(byte_order, &mut bytes, 12, 6);
3011        write_ascii(
3012            &mut bytes,
3013            16,
3014            DAF_INTERNAL_NAME_BYTES,
3015            "UNSUPPORTED PRIORITY SPK",
3016        );
3017        write_i32(byte_order, &mut bytes, 76, 3);
3018        write_i32(byte_order, &mut bytes, 80, 3);
3019        write_i32(byte_order, &mut bytes, 84, (supported_end + 1) as i32);
3020        bytes[DAF_BINARY_FORMAT_OFFSET..DAF_BINARY_FORMAT_OFFSET + 8].copy_from_slice(b"LTL-IEEE");
3021
3022        let summary_offset = DAF_RECORD_BYTES * 2;
3023        let name_offset = DAF_RECORD_BYTES * 3;
3024        write_f64(byte_order, &mut bytes, summary_offset, 0.0);
3025        write_f64(byte_order, &mut bytes, summary_offset + 8, 0.0);
3026        write_f64(byte_order, &mut bytes, summary_offset + 16, 3.0);
3027
3028        write_summary(
3029            byte_order,
3030            &mut bytes,
3031            summary_offset + 24,
3032            0.0,
3033            10.0,
3034            [
3035                301,
3036                3,
3037                1,
3038                3,
3039                direct_early_start as i32,
3040                direct_early_start as i32 + 11,
3041            ],
3042        );
3043        write_summary(
3044            byte_order,
3045            &mut bytes,
3046            summary_offset + 64,
3047            0.0,
3048            10.0,
3049            [
3050                301,
3051                3,
3052                1,
3053                99,
3054                unsupported_start as i32,
3055                unsupported_start as i32 + 11,
3056            ],
3057        );
3058        write_summary(
3059            byte_order,
3060            &mut bytes,
3061            summary_offset + 104,
3062            0.0,
3063            10.0,
3064            [
3065                302,
3066                3,
3067                1,
3068                3,
3069                supported_start as i32,
3070                supported_start as i32 + 11,
3071            ],
3072        );
3073
3074        write_ascii(
3075            &mut bytes,
3076            name_offset,
3077            40,
3078            "EARLY TYPE3 BODY 301 TO CENTER 3",
3079        );
3080        write_ascii(
3081            &mut bytes,
3082            name_offset + 40,
3083            40,
3084            "LATE UNSUPPORTED BODY 301 TO 3",
3085        );
3086        write_ascii(
3087            &mut bytes,
3088            name_offset + 80,
3089            40,
3090            "SUPPORTED BODY 302 TO CENTER 3",
3091        );
3092
3093        write_type3_constant_segment(
3094            byte_order,
3095            &mut bytes,
3096            direct_early_start,
3097            [100.0, 200.0, 300.0],
3098            [1.0, 2.0, 3.0],
3099        );
3100        write_type3_constant_segment(
3101            byte_order,
3102            &mut bytes,
3103            unsupported_start,
3104            [900.0, 900.0, 900.0],
3105            [9.0, 9.0, 9.0],
3106        );
3107        write_type3_constant_segment(
3108            byte_order,
3109            &mut bytes,
3110            supported_start,
3111            [400.0, 500.0, 600.0],
3112            [4.0, 5.0, 6.0],
3113        );
3114
3115        bytes
3116    }
3117
3118    fn build_changed_center_priority_spk() -> Vec<u8> {
3119        let byte_order = DafByteOrder::LittleEndian;
3120        let direct_early_start = 513usize;
3121        let center_start = 525usize;
3122        let target_priority_start = 537usize;
3123        let target_priority_end = target_priority_start + 11;
3124        let mut bytes = vec![0u8; target_priority_end * 8];
3125
3126        bytes[0..8].copy_from_slice(b"DAF/SPK ");
3127        write_i32(byte_order, &mut bytes, 8, 2);
3128        write_i32(byte_order, &mut bytes, 12, 6);
3129        write_ascii(
3130            &mut bytes,
3131            16,
3132            DAF_INTERNAL_NAME_BYTES,
3133            "CHANGED CENTER PRIORITY SPK",
3134        );
3135        write_i32(byte_order, &mut bytes, 76, 3);
3136        write_i32(byte_order, &mut bytes, 80, 3);
3137        write_i32(byte_order, &mut bytes, 84, (target_priority_end + 1) as i32);
3138        bytes[DAF_BINARY_FORMAT_OFFSET..DAF_BINARY_FORMAT_OFFSET + 8].copy_from_slice(b"LTL-IEEE");
3139
3140        let summary_offset = DAF_RECORD_BYTES * 2;
3141        let name_offset = DAF_RECORD_BYTES * 3;
3142        write_f64(byte_order, &mut bytes, summary_offset, 0.0);
3143        write_f64(byte_order, &mut bytes, summary_offset + 8, 0.0);
3144        write_f64(byte_order, &mut bytes, summary_offset + 16, 3.0);
3145
3146        write_summary(
3147            byte_order,
3148            &mut bytes,
3149            summary_offset + 24,
3150            0.0,
3151            10.0,
3152            [
3153                301,
3154                3,
3155                1,
3156                3,
3157                direct_early_start as i32,
3158                direct_early_start as i32 + 11,
3159            ],
3160        );
3161        write_summary(
3162            byte_order,
3163            &mut bytes,
3164            summary_offset + 64,
3165            0.0,
3166            10.0,
3167            [20, 3, 1, 3, center_start as i32, center_start as i32 + 11],
3168        );
3169        write_summary(
3170            byte_order,
3171            &mut bytes,
3172            summary_offset + 104,
3173            0.0,
3174            10.0,
3175            [
3176                301,
3177                20,
3178                1,
3179                3,
3180                target_priority_start as i32,
3181                target_priority_start as i32 + 11,
3182            ],
3183        );
3184
3185        write_ascii(&mut bytes, name_offset, 40, "EARLY BODY 301 TO CENTER 3");
3186        write_ascii(&mut bytes, name_offset + 40, 40, "CENTER 20 TO 3");
3187        write_ascii(
3188            &mut bytes,
3189            name_offset + 80,
3190            40,
3191            "PRIORITY BODY 301 TO CENTER 20",
3192        );
3193
3194        write_type3_constant_segment(
3195            byte_order,
3196            &mut bytes,
3197            direct_early_start,
3198            [10.0, 20.0, 30.0],
3199            [1.0, 2.0, 3.0],
3200        );
3201        write_type3_constant_segment(
3202            byte_order,
3203            &mut bytes,
3204            center_start,
3205            [100.0, 0.0, 5.0],
3206            [10.0, 0.0, 0.5],
3207        );
3208        write_type3_constant_segment(
3209            byte_order,
3210            &mut bytes,
3211            target_priority_start,
3212            [900.0, 80.0, 7.0],
3213            [90.0, 8.0, 0.7],
3214        );
3215
3216        bytes
3217    }
3218
3219    fn build_reversed_priority_spk() -> Vec<u8> {
3220        let byte_order = DafByteOrder::LittleEndian;
3221        let forward_start = 513usize;
3222        let center_start = 525usize;
3223        let reversed_priority_start = 537usize;
3224        let reversed_priority_end = reversed_priority_start + 11;
3225        let mut bytes = vec![0u8; reversed_priority_end * 8];
3226
3227        bytes[0..8].copy_from_slice(b"DAF/SPK ");
3228        write_i32(byte_order, &mut bytes, 8, 2);
3229        write_i32(byte_order, &mut bytes, 12, 6);
3230        write_ascii(
3231            &mut bytes,
3232            16,
3233            DAF_INTERNAL_NAME_BYTES,
3234            "REVERSED PRIORITY SPK",
3235        );
3236        write_i32(byte_order, &mut bytes, 76, 3);
3237        write_i32(byte_order, &mut bytes, 80, 3);
3238        write_i32(
3239            byte_order,
3240            &mut bytes,
3241            84,
3242            (reversed_priority_end + 1) as i32,
3243        );
3244        bytes[DAF_BINARY_FORMAT_OFFSET..DAF_BINARY_FORMAT_OFFSET + 8].copy_from_slice(b"LTL-IEEE");
3245
3246        let summary_offset = DAF_RECORD_BYTES * 2;
3247        let name_offset = DAF_RECORD_BYTES * 3;
3248        write_f64(byte_order, &mut bytes, summary_offset, 0.0);
3249        write_f64(byte_order, &mut bytes, summary_offset + 8, 0.0);
3250        write_f64(byte_order, &mut bytes, summary_offset + 16, 3.0);
3251
3252        write_summary(
3253            byte_order,
3254            &mut bytes,
3255            summary_offset + 24,
3256            0.0,
3257            10.0,
3258            [
3259                301,
3260                20,
3261                1,
3262                3,
3263                forward_start as i32,
3264                forward_start as i32 + 11,
3265            ],
3266        );
3267        write_summary(
3268            byte_order,
3269            &mut bytes,
3270            summary_offset + 64,
3271            0.0,
3272            10.0,
3273            [20, 3, 1, 3, center_start as i32, center_start as i32 + 11],
3274        );
3275        write_summary(
3276            byte_order,
3277            &mut bytes,
3278            summary_offset + 104,
3279            0.0,
3280            10.0,
3281            [
3282                20,
3283                301,
3284                1,
3285                3,
3286                reversed_priority_start as i32,
3287                reversed_priority_start as i32 + 11,
3288            ],
3289        );
3290
3291        write_ascii(&mut bytes, name_offset, 40, "EARLY BODY 301 TO CENTER 20");
3292        write_ascii(&mut bytes, name_offset + 40, 40, "CENTER 20 TO 3");
3293        write_ascii(&mut bytes, name_offset + 80, 40, "PRIORITY BODY 20 TO 301");
3294
3295        write_type3_constant_segment(
3296            byte_order,
3297            &mut bytes,
3298            forward_start,
3299            [10.0, 20.0, 30.0],
3300            [1.0, 2.0, 3.0],
3301        );
3302        write_type3_constant_segment(
3303            byte_order,
3304            &mut bytes,
3305            center_start,
3306            [100.0, 0.0, 5.0],
3307            [10.0, 0.0, 0.5],
3308        );
3309        write_type3_constant_segment(
3310            byte_order,
3311            &mut bytes,
3312            reversed_priority_start,
3313            [900.0, 80.0, 7.0],
3314            [90.0, 8.0, 0.7],
3315        );
3316
3317        bytes
3318    }
3319
3320    fn build_velocity_retention_spk() -> Vec<u8> {
3321        let byte_order = DafByteOrder::LittleEndian;
3322        let center_start = 513usize;
3323        let velocity_target_start = 525usize;
3324        let position_target_start = 537usize;
3325        let position_target_end = position_target_start + 8;
3326        let mut bytes = vec![0u8; position_target_end * 8];
3327
3328        bytes[0..8].copy_from_slice(b"DAF/SPK ");
3329        write_i32(byte_order, &mut bytes, 8, 2);
3330        write_i32(byte_order, &mut bytes, 12, 6);
3331        write_ascii(
3332            &mut bytes,
3333            16,
3334            DAF_INTERNAL_NAME_BYTES,
3335            "VELOCITY RETENTION SPK",
3336        );
3337        write_i32(byte_order, &mut bytes, 76, 3);
3338        write_i32(byte_order, &mut bytes, 80, 3);
3339        write_i32(byte_order, &mut bytes, 84, (position_target_end + 1) as i32);
3340        bytes[DAF_BINARY_FORMAT_OFFSET..DAF_BINARY_FORMAT_OFFSET + 8].copy_from_slice(b"LTL-IEEE");
3341
3342        let summary_offset = DAF_RECORD_BYTES * 2;
3343        let name_offset = DAF_RECORD_BYTES * 3;
3344        write_f64(byte_order, &mut bytes, summary_offset, 0.0);
3345        write_f64(byte_order, &mut bytes, summary_offset + 8, 0.0);
3346        write_f64(byte_order, &mut bytes, summary_offset + 16, 3.0);
3347
3348        write_summary(
3349            byte_order,
3350            &mut bytes,
3351            summary_offset + 24,
3352            0.0,
3353            10.0,
3354            [20, 3, 1, 3, center_start as i32, center_start as i32 + 11],
3355        );
3356        write_summary(
3357            byte_order,
3358            &mut bytes,
3359            summary_offset + 64,
3360            0.0,
3361            10.0,
3362            [
3363                800,
3364                20,
3365                1,
3366                3,
3367                velocity_target_start as i32,
3368                velocity_target_start as i32 + 11,
3369            ],
3370        );
3371        write_summary(
3372            byte_order,
3373            &mut bytes,
3374            summary_offset + 104,
3375            0.0,
3376            10.0,
3377            [
3378                800,
3379                20,
3380                1,
3381                2,
3382                position_target_start as i32,
3383                position_target_end as i32,
3384            ],
3385        );
3386
3387        write_ascii(&mut bytes, name_offset, 40, "CENTER 20 TO 3");
3388        write_ascii(
3389            &mut bytes,
3390            name_offset + 40,
3391            40,
3392            "VELOCITY TARGET 800 TO 20",
3393        );
3394        write_ascii(
3395            &mut bytes,
3396            name_offset + 80,
3397            40,
3398            "POSITION TARGET 800 TO 20",
3399        );
3400
3401        write_type3_constant_segment(
3402            byte_order,
3403            &mut bytes,
3404            center_start,
3405            [20.0, 3.0, 4.0],
3406            [2.0, 0.3, 0.4],
3407        );
3408        write_type3_constant_segment(
3409            byte_order,
3410            &mut bytes,
3411            velocity_target_start,
3412            [100.0, 0.0, 0.0],
3413            [10.0, 0.0, 0.0],
3414        );
3415        write_type2_constant_segment(
3416            byte_order,
3417            &mut bytes,
3418            position_target_start,
3419            [900.0, 0.0, 0.0],
3420        );
3421
3422        bytes
3423    }
3424
3425    fn build_frame_mismatch_spk() -> Vec<u8> {
3426        let byte_order = DafByteOrder::LittleEndian;
3427        let good_center_start = 513usize;
3428        let good_target_start = 525usize;
3429        let bad_center_start = 537usize;
3430        let bad_target_start = 549usize;
3431        let unresolvable_center_start = 561usize;
3432        let unresolvable_target_start = 573usize;
3433        let unresolvable_target_end = unresolvable_target_start + 11;
3434        let mut bytes = vec![0u8; unresolvable_target_end * 8];
3435
3436        bytes[0..8].copy_from_slice(b"DAF/SPK ");
3437        write_i32(byte_order, &mut bytes, 8, 2);
3438        write_i32(byte_order, &mut bytes, 12, 6);
3439        write_ascii(
3440            &mut bytes,
3441            16,
3442            DAF_INTERNAL_NAME_BYTES,
3443            "FRAME MISMATCH SPK",
3444        );
3445        write_i32(byte_order, &mut bytes, 76, 3);
3446        write_i32(byte_order, &mut bytes, 80, 3);
3447        write_i32(
3448            byte_order,
3449            &mut bytes,
3450            84,
3451            (unresolvable_target_end + 1) as i32,
3452        );
3453        bytes[DAF_BINARY_FORMAT_OFFSET..DAF_BINARY_FORMAT_OFFSET + 8].copy_from_slice(b"LTL-IEEE");
3454
3455        let summary_offset = DAF_RECORD_BYTES * 2;
3456        let name_offset = DAF_RECORD_BYTES * 3;
3457        write_f64(byte_order, &mut bytes, summary_offset, 0.0);
3458        write_f64(byte_order, &mut bytes, summary_offset + 8, 0.0);
3459        write_f64(byte_order, &mut bytes, summary_offset + 16, 6.0);
3460
3461        write_summary(
3462            byte_order,
3463            &mut bytes,
3464            summary_offset + 24,
3465            0.0,
3466            10.0,
3467            [
3468                20,
3469                3,
3470                1,
3471                3,
3472                good_center_start as i32,
3473                good_center_start as i32 + 11,
3474            ],
3475        );
3476        write_summary(
3477            byte_order,
3478            &mut bytes,
3479            summary_offset + 64,
3480            0.0,
3481            10.0,
3482            [
3483                700,
3484                20,
3485                1,
3486                3,
3487                good_target_start as i32,
3488                good_target_start as i32 + 11,
3489            ],
3490        );
3491        write_summary(
3492            byte_order,
3493            &mut bytes,
3494            summary_offset + 104,
3495            0.0,
3496            10.0,
3497            [
3498                10,
3499                3,
3500                2,
3501                3,
3502                bad_center_start as i32,
3503                bad_center_start as i32 + 11,
3504            ],
3505        );
3506        write_summary(
3507            byte_order,
3508            &mut bytes,
3509            summary_offset + 144,
3510            0.0,
3511            10.0,
3512            [
3513                700,
3514                10,
3515                1,
3516                3,
3517                bad_target_start as i32,
3518                bad_target_start as i32 + 11,
3519            ],
3520        );
3521        write_summary(
3522            byte_order,
3523            &mut bytes,
3524            summary_offset + 184,
3525            0.0,
3526            10.0,
3527            [
3528                30,
3529                3,
3530                2,
3531                3,
3532                unresolvable_center_start as i32,
3533                unresolvable_center_start as i32 + 11,
3534            ],
3535        );
3536        write_summary(
3537            byte_order,
3538            &mut bytes,
3539            summary_offset + 224,
3540            0.0,
3541            10.0,
3542            [
3543                701,
3544                30,
3545                1,
3546                3,
3547                unresolvable_target_start as i32,
3548                unresolvable_target_start as i32 + 11,
3549            ],
3550        );
3551
3552        write_ascii(&mut bytes, name_offset, 40, "GOOD CENTER 20 TO 3");
3553        write_ascii(&mut bytes, name_offset + 40, 40, "GOOD TARGET 700 TO 20");
3554        write_ascii(&mut bytes, name_offset + 80, 40, "BAD CENTER 10 TO 3");
3555        write_ascii(&mut bytes, name_offset + 120, 40, "BAD TARGET 700 TO 10");
3556        write_ascii(&mut bytes, name_offset + 160, 40, "BAD CENTER 30 TO 3");
3557        write_ascii(
3558            &mut bytes,
3559            name_offset + 200,
3560            40,
3561            "UNRESOLVABLE TARGET 701 TO 30",
3562        );
3563
3564        write_type3_constant_segment(
3565            byte_order,
3566            &mut bytes,
3567            good_center_start,
3568            [20.0, 3.0, 4.0],
3569            [2.0, 0.3, 0.4],
3570        );
3571        write_type3_constant_segment(
3572            byte_order,
3573            &mut bytes,
3574            good_target_start,
3575            [100.0, 0.0, 0.0],
3576            [10.0, 0.0, 0.0],
3577        );
3578        write_type3_constant_segment(
3579            byte_order,
3580            &mut bytes,
3581            bad_center_start,
3582            [900.0, 0.0, 0.0],
3583            [90.0, 0.0, 0.0],
3584        );
3585        write_type3_constant_segment(
3586            byte_order,
3587            &mut bytes,
3588            bad_target_start,
3589            [1.0, 0.0, 0.0],
3590            [0.1, 0.0, 0.0],
3591        );
3592        write_type3_constant_segment(
3593            byte_order,
3594            &mut bytes,
3595            unresolvable_center_start,
3596            [30.0, 0.0, 0.0],
3597            [3.0, 0.0, 0.0],
3598        );
3599        write_type3_constant_segment(
3600            byte_order,
3601            &mut bytes,
3602            unresolvable_target_start,
3603            [701.0, 0.0, 0.0],
3604            [70.1, 0.0, 0.0],
3605        );
3606
3607        bytes
3608    }
3609
3610    fn build_type2_segment(byte_order: DafByteOrder) -> (Vec<u8>, SpkSegmentDescriptor) {
3611        let rsize = 11usize;
3612        let n = 2usize;
3613        let end_address = rsize * n + 4;
3614        let mut bytes = vec![0u8; end_address * 8];
3615
3616        write_type2_record(
3617            byte_order,
3618            &mut bytes,
3619            1,
3620            5.0,
3621            5.0,
3622            [[1.0, 2.0, 3.0], [-4.0, 0.5, -1.0], [7.0, 0.0, 0.0]],
3623        );
3624        write_type2_record(
3625            byte_order,
3626            &mut bytes,
3627            12,
3628            15.0,
3629            5.0,
3630            [[10.0, -1.0, 0.0], [20.0, 2.0, 1.0], [-3.0, 4.0, -2.0]],
3631        );
3632        write_f64_address(byte_order, &mut bytes, end_address - 3, 0.0);
3633        write_f64_address(byte_order, &mut bytes, end_address - 2, 10.0);
3634        write_f64_address(byte_order, &mut bytes, end_address - 1, rsize as f64);
3635        write_f64_address(byte_order, &mut bytes, end_address, n as f64);
3636
3637        (
3638            bytes,
3639            SpkSegmentDescriptor {
3640                name: "TYPE 2 TEST".to_string(),
3641                start_et: 0.0,
3642                stop_et: 20.0,
3643                target: 301,
3644                center: 3,
3645                frame: 1,
3646                data_type: 2,
3647                start_address: 1,
3648                end_address: end_address as i32,
3649            },
3650        )
3651    }
3652
3653    fn build_type3_segment(byte_order: DafByteOrder) -> (Vec<u8>, SpkSegmentDescriptor) {
3654        let rsize = 14usize;
3655        let n = 2usize;
3656        let end_address = rsize * n + 4;
3657        let mut bytes = vec![0u8; end_address * 8];
3658
3659        write_type3_record(
3660            byte_order,
3661            &mut bytes,
3662            1,
3663            5.0,
3664            5.0,
3665            [
3666                [1.0, 2.0],
3667                [3.0, -4.0],
3668                [5.0, 0.5],
3669                [0.1, 0.2],
3670                [-0.3, 0.0],
3671                [1.0, -1.0],
3672            ],
3673        );
3674        write_type3_record(
3675            byte_order,
3676            &mut bytes,
3677            15,
3678            15.0,
3679            5.0,
3680            [
3681                [10.0, 1.0],
3682                [20.0, -2.0],
3683                [-5.0, 3.0],
3684                [1.0, 1.0],
3685                [2.0, -3.0],
3686                [4.0, 0.25],
3687            ],
3688        );
3689        write_f64_address(byte_order, &mut bytes, end_address - 3, 0.0);
3690        write_f64_address(byte_order, &mut bytes, end_address - 2, 10.0);
3691        write_f64_address(byte_order, &mut bytes, end_address - 1, rsize as f64);
3692        write_f64_address(byte_order, &mut bytes, end_address, n as f64);
3693
3694        (
3695            bytes,
3696            SpkSegmentDescriptor {
3697                name: "TYPE 3 TEST".to_string(),
3698                start_et: 0.0,
3699                stop_et: 20.0,
3700                target: 499,
3701                center: 4,
3702                frame: 1,
3703                data_type: 3,
3704                start_address: 1,
3705                end_address: end_address as i32,
3706            },
3707        )
3708    }
3709
3710    fn build_type21_segment(byte_order: DafByteOrder) -> (Vec<u8>, SpkSegmentDescriptor) {
3711        let maxdim = 3usize;
3712        let n = 2usize;
3713        let dlsize = 4 * maxdim + 11;
3714        // N difference lines, then N epochs, then 0 directory epochs, then the
3715        // two trailer words MAXDIM and N.
3716        let end_address = n * dlsize + n + 2;
3717        let mut bytes = vec![0u8; end_address * 8];
3718
3719        // Record 0 (one-based start address 1): reference epoch 0.
3720        write_type21_record(
3721            byte_order,
3722            &mut bytes,
3723            1,
3724            maxdim,
3725            0.0,
3726            [100.0, 200.0, 300.0],
3727            [1.0, 2.0, 3.0],
3728        );
3729        // Record 1: reference epoch 10.
3730        write_type21_record(
3731            byte_order,
3732            &mut bytes,
3733            1 + dlsize,
3734            maxdim,
3735            10.0,
3736            [1000.0, 2000.0, 3000.0],
3737            [10.0, 20.0, 30.0],
3738        );
3739
3740        // Per-record epochs follow the difference lines.
3741        let first_epoch = n * dlsize + 1;
3742        write_f64_address(byte_order, &mut bytes, first_epoch, 10.0);
3743        write_f64_address(byte_order, &mut bytes, first_epoch + 1, 20.0);
3744
3745        // Trailer: MAXDIM then N.
3746        write_f64_address(byte_order, &mut bytes, end_address - 1, maxdim as f64);
3747        write_f64_address(byte_order, &mut bytes, end_address, n as f64);
3748
3749        (
3750            bytes,
3751            SpkSegmentDescriptor {
3752                name: "TYPE 21 TEST".to_string(),
3753                start_et: 0.0,
3754                stop_et: 20.0,
3755                target: 2000001,
3756                center: 10,
3757                frame: 1,
3758                data_type: 21,
3759                start_address: 1,
3760                end_address: end_address as i32,
3761            },
3762        )
3763    }
3764
3765    /// Write one type-21 difference line with a zero difference table, reducing
3766    /// evaluation to linear motion `refpos + delta * refvel`.
3767    fn write_type21_record(
3768        byte_order: DafByteOrder,
3769        bytes: &mut [u8],
3770        start_address: usize,
3771        maxdim: usize,
3772        tl: f64,
3773        refpos: [f64; 3],
3774        refvel: [f64; 3],
3775    ) {
3776        // The difference line maps to CSPICE RECORD(2..1+DLSIZE); start_address
3777        // is the one-based address of its first word (the reference epoch TL).
3778        write_f64_address(byte_order, bytes, start_address, tl);
3779        for offset in 0..maxdim {
3780            // Stepsize vector G: nonzero to avoid a divide-by-zero guard trip.
3781            write_f64_address(byte_order, bytes, start_address + 1 + offset, 1.0);
3782        }
3783        // Reference position/velocity, interleaved x, x', y, y', z, z'.
3784        write_f64_address(byte_order, bytes, start_address + maxdim + 1, refpos[0]);
3785        write_f64_address(byte_order, bytes, start_address + maxdim + 2, refvel[0]);
3786        write_f64_address(byte_order, bytes, start_address + maxdim + 3, refpos[1]);
3787        write_f64_address(byte_order, bytes, start_address + maxdim + 4, refvel[1]);
3788        write_f64_address(byte_order, bytes, start_address + maxdim + 5, refpos[2]);
3789        write_f64_address(byte_order, bytes, start_address + maxdim + 6, refvel[2]);
3790        // Modified divided difference arrays (MAXDIM * 3) left zero.
3791        // KQMAX1 then the per-component integration order array KQ.
3792        write_f64_address(byte_order, bytes, start_address + 4 * maxdim + 7, 2.0);
3793        write_f64_address(byte_order, bytes, start_address + 4 * maxdim + 8, 1.0);
3794        write_f64_address(byte_order, bytes, start_address + 4 * maxdim + 9, 1.0);
3795        write_f64_address(byte_order, bytes, start_address + 4 * maxdim + 10, 1.0);
3796    }
3797
3798    fn write_type3_constant_segment(
3799        byte_order: DafByteOrder,
3800        bytes: &mut [u8],
3801        start_address: usize,
3802        position_km: [f64; 3],
3803        velocity_km_s: [f64; 3],
3804    ) {
3805        let rsize = 8usize;
3806        let end_address = start_address + rsize + 3;
3807        write_f64_address(byte_order, bytes, start_address, 5.0);
3808        write_f64_address(byte_order, bytes, start_address + 1, 5.0);
3809        write_f64_address(byte_order, bytes, start_address + 2, position_km[0]);
3810        write_f64_address(byte_order, bytes, start_address + 3, position_km[1]);
3811        write_f64_address(byte_order, bytes, start_address + 4, position_km[2]);
3812        write_f64_address(byte_order, bytes, start_address + 5, velocity_km_s[0]);
3813        write_f64_address(byte_order, bytes, start_address + 6, velocity_km_s[1]);
3814        write_f64_address(byte_order, bytes, start_address + 7, velocity_km_s[2]);
3815        write_f64_address(byte_order, bytes, end_address - 3, 0.0);
3816        write_f64_address(byte_order, bytes, end_address - 2, 10.0);
3817        write_f64_address(byte_order, bytes, end_address - 1, rsize as f64);
3818        write_f64_address(byte_order, bytes, end_address, 1.0);
3819    }
3820
3821    fn write_type2_constant_segment(
3822        byte_order: DafByteOrder,
3823        bytes: &mut [u8],
3824        start_address: usize,
3825        position_km: [f64; 3],
3826    ) {
3827        let rsize = 5usize;
3828        let end_address = start_address + rsize + 3;
3829        write_f64_address(byte_order, bytes, start_address, 5.0);
3830        write_f64_address(byte_order, bytes, start_address + 1, 5.0);
3831        write_f64_address(byte_order, bytes, start_address + 2, position_km[0]);
3832        write_f64_address(byte_order, bytes, start_address + 3, position_km[1]);
3833        write_f64_address(byte_order, bytes, start_address + 4, position_km[2]);
3834        write_f64_address(byte_order, bytes, end_address - 3, 0.0);
3835        write_f64_address(byte_order, bytes, end_address - 2, 10.0);
3836        write_f64_address(byte_order, bytes, end_address - 1, rsize as f64);
3837        write_f64_address(byte_order, bytes, end_address, 1.0);
3838    }
3839
3840    fn write_type3_record(
3841        byte_order: DafByteOrder,
3842        bytes: &mut [u8],
3843        start_address: usize,
3844        mid: f64,
3845        radius: f64,
3846        coeffs: [[f64; 2]; 6],
3847    ) {
3848        write_f64_address(byte_order, bytes, start_address, mid);
3849        write_f64_address(byte_order, bytes, start_address + 1, radius);
3850        let mut address = start_address + 2;
3851        for component in coeffs {
3852            for coeff in component {
3853                write_f64_address(byte_order, bytes, address, coeff);
3854                address += 1;
3855            }
3856        }
3857    }
3858
3859    fn write_type2_record(
3860        byte_order: DafByteOrder,
3861        bytes: &mut [u8],
3862        start_address: usize,
3863        mid: f64,
3864        radius: f64,
3865        coeffs: [[f64; 3]; 3],
3866    ) {
3867        write_f64_address(byte_order, bytes, start_address, mid);
3868        write_f64_address(byte_order, bytes, start_address + 1, radius);
3869        let mut address = start_address + 2;
3870        for component in coeffs {
3871            for coeff in component {
3872                write_f64_address(byte_order, bytes, address, coeff);
3873                address += 1;
3874            }
3875        }
3876    }
3877
3878    fn write_f64_address(byte_order: DafByteOrder, bytes: &mut [u8], address: usize, value: f64) {
3879        write_f64(byte_order, bytes, (address - 1) * 8, value);
3880    }
3881
3882    fn assert_position_close(actual: [f64; 3], expected: [f64; 3]) {
3883        for axis in 0..3 {
3884            assert!(
3885                (actual[axis] - expected[axis]).abs() < 1e-12,
3886                "axis {axis}: actual {:?}, expected {:?}",
3887                actual,
3888                expected
3889            );
3890        }
3891    }
3892
3893    fn assert_state_close(
3894        actual: SpkStateVector,
3895        expected_position: [f64; 3],
3896        expected_velocity: [f64; 3],
3897    ) {
3898        assert_position_close(actual.position_km, expected_position);
3899        assert_position_close(actual.velocity_km_s, expected_velocity);
3900    }
3901
3902    fn assert_query_state_close(
3903        actual: SpkState,
3904        expected_position: [f64; 3],
3905        expected_velocity: Option<[f64; 3]>,
3906        expected_frame: i32,
3907    ) {
3908        assert_position_close(actual.position_km, expected_position);
3909        match (actual.velocity_km_s, expected_velocity) {
3910            (Some(actual), Some(expected)) => assert_position_close(actual, expected),
3911            (None, None) => {}
3912            _ => panic!(
3913                "velocity mismatch: actual {:?}, expected {:?}",
3914                actual.velocity_km_s, expected_velocity
3915            ),
3916        }
3917        assert_eq!(actual.frame, expected_frame);
3918    }
3919
3920    fn write_summary(
3921        byte_order: DafByteOrder,
3922        bytes: &mut [u8],
3923        offset: usize,
3924        start_et: f64,
3925        stop_et: f64,
3926        ints: [i32; 6],
3927    ) {
3928        write_f64(byte_order, bytes, offset, start_et);
3929        write_f64(byte_order, bytes, offset + 8, stop_et);
3930        for (index, value) in ints.into_iter().enumerate() {
3931            write_i32(byte_order, bytes, offset + 16 + index * 4, value);
3932        }
3933    }
3934
3935    fn write_ascii(bytes: &mut [u8], offset: usize, len: usize, text: &str) {
3936        bytes[offset..offset + len].fill(b' ');
3937        bytes[offset..offset + text.len()].copy_from_slice(text.as_bytes());
3938    }
3939
3940    fn write_i32(byte_order: DafByteOrder, bytes: &mut [u8], offset: usize, value: i32) {
3941        let word = match byte_order {
3942            DafByteOrder::LittleEndian => value.to_le_bytes(),
3943            DafByteOrder::BigEndian => value.to_be_bytes(),
3944        };
3945        bytes[offset..offset + 4].copy_from_slice(&word);
3946    }
3947
3948    fn write_f64(byte_order: DafByteOrder, bytes: &mut [u8], offset: usize, value: f64) {
3949        let word = match byte_order {
3950            DafByteOrder::LittleEndian => value.to_le_bytes(),
3951            DafByteOrder::BigEndian => value.to_be_bytes(),
3952        };
3953        bytes[offset..offset + 8].copy_from_slice(&word);
3954    }
3955}