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