Skip to main content

copc_core/
info.rs

1use crate::{Error, Result, HIERARCHY_ENTRY_BYTES};
2
3pub const COPC_INFO_BYTES: usize = 160;
4
5/// COPC info VLR payload.
6#[derive(Clone, Copy, Debug, PartialEq)]
7pub struct CopcInfo {
8    pub center: (f64, f64, f64),
9    pub halfsize: f64,
10    pub spacing: f64,
11    pub root_hier_offset: u64,
12    pub root_hier_size: u64,
13    pub gpstime_min: f64,
14    pub gpstime_max: f64,
15}
16
17impl CopcInfo {
18    pub fn from_le_bytes(bytes: &[u8]) -> Result<Self> {
19        if bytes.len() != COPC_INFO_BYTES {
20            return Err(Error::InvalidData(format!(
21                "COPC info payload is {} bytes, expected exactly {}",
22                bytes.len(),
23                COPC_INFO_BYTES
24            )));
25        }
26        if bytes[72..].iter().any(|byte| *byte != 0) {
27            return Err(Error::InvalidData(
28                "COPC info reserved bytes must all be zero".into(),
29            ));
30        }
31        let info = Self {
32            center: (read_f64(bytes, 0), read_f64(bytes, 8), read_f64(bytes, 16)),
33            halfsize: read_f64(bytes, 24),
34            spacing: read_f64(bytes, 32),
35            root_hier_offset: read_u64(bytes, 40),
36            root_hier_size: read_u64(bytes, 48),
37            gpstime_min: read_f64(bytes, 56),
38            gpstime_max: read_f64(bytes, 64),
39        };
40        info.validate()?;
41        Ok(info)
42    }
43
44    /// Validate and serialize the exact 160-byte COPC info payload.
45    pub fn write_le_bytes(self) -> Result<[u8; COPC_INFO_BYTES]> {
46        self.validate()?;
47        let mut out = [0u8; COPC_INFO_BYTES];
48        write_f64(&mut out, 0, self.center.0);
49        write_f64(&mut out, 8, self.center.1);
50        write_f64(&mut out, 16, self.center.2);
51        write_f64(&mut out, 24, self.halfsize);
52        write_f64(&mut out, 32, self.spacing);
53        write_u64(&mut out, 40, self.root_hier_offset);
54        write_u64(&mut out, 48, self.root_hier_size);
55        write_f64(&mut out, 56, self.gpstime_min);
56        write_f64(&mut out, 64, self.gpstime_max);
57        Ok(out)
58    }
59
60    pub fn validate(self) -> Result<()> {
61        for (name, value) in [
62            ("center x", self.center.0),
63            ("center y", self.center.1),
64            ("center z", self.center.2),
65        ] {
66            if !value.is_finite() {
67                return Err(Error::InvalidData(format!(
68                    "COPC info {name} must be finite, got {value}"
69                )));
70            }
71        }
72        for (name, value) in [("halfsize", self.halfsize), ("spacing", self.spacing)] {
73            if !value.is_finite() || value <= 0.0 {
74                return Err(Error::InvalidData(format!(
75                    "COPC info {name} must be finite and positive, got {value}"
76                )));
77            }
78        }
79        if self.root_hier_offset == 0 {
80            return Err(Error::InvalidData(
81                "COPC root hierarchy offset must be non-zero".into(),
82            ));
83        }
84        if self.root_hier_size == 0 {
85            return Err(Error::InvalidData(
86                "COPC root hierarchy page is empty".into(),
87            ));
88        }
89        if !self
90            .root_hier_size
91            .is_multiple_of(HIERARCHY_ENTRY_BYTES as u64)
92        {
93            return Err(Error::InvalidData(format!(
94                "COPC root hierarchy size {} is not a multiple of {HIERARCHY_ENTRY_BYTES}",
95                self.root_hier_size
96            )));
97        }
98        if !self.gpstime_min.is_finite() || !self.gpstime_max.is_finite() {
99            return Err(Error::InvalidData(format!(
100                "COPC GPS time range must be finite, got {}..{}",
101                self.gpstime_min, self.gpstime_max
102            )));
103        }
104        if self.gpstime_min > self.gpstime_max {
105            return Err(Error::InvalidData(format!(
106                "COPC GPS time minimum {} exceeds maximum {}",
107                self.gpstime_min, self.gpstime_max
108            )));
109        }
110        Ok(())
111    }
112}
113
114fn read_u64(bytes: &[u8], offset: usize) -> u64 {
115    u64::from_le_bytes(
116        bytes[offset..offset + 8]
117            .try_into()
118            .expect("u64 width checked by caller"),
119    )
120}
121
122fn read_f64(bytes: &[u8], offset: usize) -> f64 {
123    f64::from_le_bytes(
124        bytes[offset..offset + 8]
125            .try_into()
126            .expect("f64 width checked by caller"),
127    )
128}
129
130fn write_u64(bytes: &mut [u8], offset: usize, value: u64) {
131    bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
132}
133
134fn write_f64(bytes: &mut [u8], offset: usize, value: f64) {
135    bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn copc_info_round_trips() {
144        let info = CopcInfo {
145            center: (1.0, 2.0, 3.0),
146            halfsize: 4.0,
147            spacing: 0.25,
148            root_hier_offset: 100,
149            root_hier_size: 320,
150            gpstime_min: 10.0,
151            gpstime_max: 20.0,
152        };
153        assert_eq!(
154            CopcInfo::from_le_bytes(&info.write_le_bytes().unwrap()).unwrap(),
155            info
156        );
157    }
158
159    #[test]
160    fn copc_info_rejects_nonzero_reserved_bytes_and_invalid_numbers() {
161        let info = CopcInfo {
162            center: (1.0, 2.0, 3.0),
163            halfsize: 4.0,
164            spacing: 0.25,
165            root_hier_offset: 100,
166            root_hier_size: 32,
167            gpstime_min: 10.0,
168            gpstime_max: 20.0,
169        };
170        let mut bytes = info.write_le_bytes().unwrap();
171        bytes[159] = 1;
172        assert!(CopcInfo::from_le_bytes(&bytes).is_err());
173
174        let mut bytes = info.write_le_bytes().unwrap();
175        write_f64(&mut bytes, 24, f64::NAN);
176        assert!(CopcInfo::from_le_bytes(&bytes).is_err());
177
178        let mut bytes = info.write_le_bytes().unwrap();
179        write_f64(&mut bytes, 56, 21.0);
180        assert!(CopcInfo::from_le_bytes(&bytes).is_err());
181    }
182}