1use crate::{Error, Result, HIERARCHY_ENTRY_BYTES};
2
3pub const COPC_INFO_BYTES: usize = 160;
4
5#[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 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}