1use sha2::{Digest, Sha256};
2
3use super::{CoverageGeometry, CoverageGeometryLimits, PlanarPoint, geometry_error};
4use crate::{CoverageSynthesisArtifact, CoverageSynthesisLimits, Result};
5
6const MAGIC: &[u8; 8] = b"HOLOSGEO";
7const VERSION: u16 = 1;
8const F64_BITS_CODEC: u8 = 1;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12#[non_exhaustive]
13pub struct GeometryBoundCoverageDecodeLimits {
14 pub max_bytes: usize,
16}
17
18impl Default for GeometryBoundCoverageDecodeLimits {
19 fn default() -> Self {
20 Self { max_bytes: 1 << 30 }
21 }
22}
23
24#[derive(Debug, Clone)]
26pub struct GeometryBoundCoverageArtifact {
27 geometry: CoverageGeometry,
28 coverage: CoverageSynthesisArtifact,
29 digest: [u8; 32],
30}
31
32impl GeometryBoundCoverageArtifact {
33 pub fn build(
35 coverage: CoverageSynthesisArtifact,
36 geometry: CoverageGeometry,
37 coverage_limits: CoverageSynthesisLimits,
38 geometry_limits: CoverageGeometryLimits,
39 ) -> Result<Self> {
40 coverage.verify(coverage_limits)?;
41 geometry.verify(coverage.specification(), geometry_limits)?;
42 let mut artifact = Self {
43 geometry,
44 coverage,
45 digest: [0; 32],
46 };
47 artifact.digest = artifact.compute_digest(coverage_limits)?;
48 Ok(artifact)
49 }
50
51 pub fn geometry(&self) -> &CoverageGeometry {
53 &self.geometry
54 }
55
56 pub fn coverage(&self) -> &CoverageSynthesisArtifact {
58 &self.coverage
59 }
60
61 pub fn digest(&self) -> &[u8; 32] {
63 &self.digest
64 }
65
66 pub fn verify(
68 &self,
69 coverage_limits: CoverageSynthesisLimits,
70 geometry_limits: CoverageGeometryLimits,
71 ) -> Result<()> {
72 self.coverage.verify(coverage_limits)?;
73 self.geometry
74 .verify(self.coverage.specification(), geometry_limits)?;
75 if self.compute_digest(coverage_limits)? != self.digest {
76 return Err(geometry_error(
77 "geometry-bound artifact digest does not match",
78 ));
79 }
80 Ok(())
81 }
82
83 pub fn encode(
85 &self,
86 coverage_limits: CoverageSynthesisLimits,
87 geometry_limits: CoverageGeometryLimits,
88 decode_limits: GeometryBoundCoverageDecodeLimits,
89 ) -> Result<Vec<u8>> {
90 self.verify(coverage_limits, geometry_limits)?;
91 let mut output = self.encode_payload(coverage_limits)?;
92 output.extend_from_slice(&self.digest);
93 if output.len() > decode_limits.max_bytes {
94 return Err(geometry_error(
95 "geometry-bound artifact exceeds its byte limit",
96 ));
97 }
98 Ok(output)
99 }
100
101 pub fn decode(
103 bytes: &[u8],
104 coverage_limits: CoverageSynthesisLimits,
105 geometry_limits: CoverageGeometryLimits,
106 decode_limits: GeometryBoundCoverageDecodeLimits,
107 ) -> Result<Self> {
108 let (payload, digest) = decode_digest(bytes, decode_limits.max_bytes)?;
109 let artifact = decode_payload(payload, digest, coverage_limits, geometry_limits)?;
110 artifact.verify(coverage_limits, geometry_limits)?;
111 Ok(artifact)
112 }
113
114 fn encode_payload(&self, coverage_limits: CoverageSynthesisLimits) -> Result<Vec<u8>> {
115 let mut output = Vec::new();
116 output.extend_from_slice(MAGIC);
117 put_u16(&mut output, VERSION);
118 output.push(F64_BITS_CODEC);
119 encode_coordinates(&mut output, self.geometry.coordinates())?;
120 let coverage = self.coverage.encode(coverage_limits)?;
121 put_usize(&mut output, coverage.len(), "coverage artifact byte count")?;
122 output.extend_from_slice(&coverage);
123 Ok(output)
124 }
125
126 fn compute_digest(&self, coverage_limits: CoverageSynthesisLimits) -> Result<[u8; 32]> {
127 Ok(Sha256::digest(self.encode_payload(coverage_limits)?).into())
128 }
129}
130
131fn encode_coordinates(output: &mut Vec<u8>, states: &[Vec<PlanarPoint>]) -> Result<()> {
132 put_usize(output, states.len(), "geometry state count")?;
133 for coordinates in states {
134 put_usize(output, coordinates.len(), "geometry vertex count")?;
135 for point in coordinates {
136 put_u64(output, point.x().to_bits());
137 put_u64(output, point.y().to_bits());
138 }
139 }
140 Ok(())
141}
142
143fn decode_digest(bytes: &[u8], maximum: usize) -> Result<(&[u8], [u8; 32])> {
144 if bytes.len() < 32 || bytes.len() > maximum {
145 return Err(geometry_error(
146 "geometry-bound artifact is truncated or exceeds its byte limit",
147 ));
148 }
149 let payload_length = bytes.len() - 32;
150 let expected: [u8; 32] = Sha256::digest(&bytes[..payload_length]).into();
151 let digest: [u8; 32] = bytes[payload_length..]
152 .try_into()
153 .expect("32-byte geometry-bound digest");
154 if digest != expected {
155 return Err(geometry_error(
156 "geometry-bound artifact digest does not match its bytes",
157 ));
158 }
159 Ok((&bytes[..payload_length], digest))
160}
161
162fn decode_payload(
163 payload: &[u8],
164 digest: [u8; 32],
165 coverage_limits: CoverageSynthesisLimits,
166 geometry_limits: CoverageGeometryLimits,
167) -> Result<GeometryBoundCoverageArtifact> {
168 let mut reader = Reader::new(payload);
169 decode_prefix(&mut reader)?;
170 let coordinates = decode_coordinates(&mut reader, geometry_limits)?;
171 let coverage_bytes =
172 reader.bounded_usize("coverage artifact byte count", coverage_limits.max_bytes)?;
173 let coverage =
174 CoverageSynthesisArtifact::decode(reader.take(coverage_bytes)?, coverage_limits)?;
175 reader.finish()?;
176 let geometry = CoverageGeometry::new(coverage.specification(), coordinates, geometry_limits)?;
177 Ok(GeometryBoundCoverageArtifact {
178 geometry,
179 coverage,
180 digest,
181 })
182}
183
184fn decode_prefix(reader: &mut Reader<'_>) -> Result<()> {
185 if reader.take(8)? != MAGIC || reader.u16()? != VERSION || reader.u8()? != F64_BITS_CODEC {
186 return Err(geometry_error(
187 "geometry-bound artifact envelope version is unsupported",
188 ));
189 }
190 Ok(())
191}
192
193fn decode_coordinates(
194 reader: &mut Reader<'_>,
195 limits: CoverageGeometryLimits,
196) -> Result<Vec<Vec<PlanarPoint>>> {
197 let state_count = reader.bounded_usize("geometry state count", limits.max_states)?;
198 let mut total = 0usize;
199 let mut states = Vec::with_capacity(state_count);
200 for _ in 0..state_count {
201 let count = reader.bounded_usize("geometry vertex count", limits.max_vertices)?;
202 total = total
203 .checked_add(count)
204 .ok_or_else(|| geometry_error("geometry coordinate count overflows"))?;
205 if total > limits.max_coordinates {
206 return Err(geometry_error(
207 "geometry coordinates exceed their total limit",
208 ));
209 }
210 states.push(decode_state_coordinates(reader, count)?);
211 }
212 Ok(states)
213}
214
215fn decode_state_coordinates(reader: &mut Reader<'_>, count: usize) -> Result<Vec<PlanarPoint>> {
216 let mut points = Vec::with_capacity(count);
217 for _ in 0..count {
218 let x = f64::from_bits(reader.u64()?);
219 let y = f64::from_bits(reader.u64()?);
220 let point = PlanarPoint::new(x, y)?;
221 if point.x().to_bits() != x.to_bits() || point.y().to_bits() != y.to_bits() {
222 return Err(geometry_error(
223 "geometry coordinate encoding is not canonical",
224 ));
225 }
226 points.push(point);
227 }
228 Ok(points)
229}
230
231fn put_u16(output: &mut Vec<u8>, value: u16) {
232 output.extend_from_slice(&value.to_be_bytes());
233}
234
235fn put_u64(output: &mut Vec<u8>, value: u64) {
236 output.extend_from_slice(&value.to_be_bytes());
237}
238
239fn put_usize(output: &mut Vec<u8>, value: usize, label: &str) -> Result<()> {
240 let value = u64::try_from(value)
241 .map_err(|_| geometry_error(format!("{label} does not fit the wire integer")))?;
242 put_u64(output, value);
243 Ok(())
244}
245
246struct Reader<'a> {
247 bytes: &'a [u8],
248 position: usize,
249}
250
251impl<'a> Reader<'a> {
252 fn new(bytes: &'a [u8]) -> Self {
253 Self { bytes, position: 0 }
254 }
255
256 fn take(&mut self, count: usize) -> Result<&'a [u8]> {
257 let end = self
258 .position
259 .checked_add(count)
260 .ok_or_else(|| geometry_error("geometry read position overflows"))?;
261 let bytes = self
262 .bytes
263 .get(self.position..end)
264 .ok_or_else(|| geometry_error("geometry-bound artifact is truncated"))?;
265 self.position = end;
266 Ok(bytes)
267 }
268
269 fn u8(&mut self) -> Result<u8> {
270 Ok(self.take(1)?[0])
271 }
272
273 fn u16(&mut self) -> Result<u16> {
274 Ok(u16::from_be_bytes(
275 self.take(2)?.try_into().expect("two-byte geometry slice"),
276 ))
277 }
278
279 fn u64(&mut self) -> Result<u64> {
280 Ok(u64::from_be_bytes(
281 self.take(8)?.try_into().expect("eight-byte geometry slice"),
282 ))
283 }
284
285 fn usize(&mut self) -> Result<usize> {
286 usize::try_from(self.u64()?)
287 .map_err(|_| geometry_error("geometry integer does not fit usize"))
288 }
289
290 fn bounded_usize(&mut self, label: &str, maximum: usize) -> Result<usize> {
291 let value = self.usize()?;
292 if value > maximum {
293 return Err(geometry_error(format!(
294 "{label} {value} exceeds its limit {maximum}"
295 )));
296 }
297 Ok(value)
298 }
299
300 fn finish(&self) -> Result<()> {
301 if self.position != self.bytes.len() {
302 return Err(geometry_error(
303 "geometry-bound artifact has trailing payload bytes",
304 ));
305 }
306 Ok(())
307 }
308}
309
310#[cfg(all(test, holos_repository_tests))]
311mod tests {
312 use super::*;
313 use crate::{
314 CoverageAction, CoverageFence, CoverageLimits, CoverageState, CoverageSynthesisStatus,
315 PlanarCoverageModel, SparseDistanceMatrix,
316 };
317
318 fn specimen() -> (CoverageSynthesisArtifact, CoverageGeometry) {
319 let points = [(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0), (1.0, 1.0)]
320 .into_iter()
321 .map(|(x, y)| PlanarPoint::new(x, y).unwrap())
322 .collect::<Vec<_>>();
323 let graph = SparseDistanceMatrix::from_triplets(
324 5,
325 &[
326 (0, 1, 1.0),
327 (1, 2, 1.0),
328 (2, 3, 1.0),
329 (0, 3, 1.0),
330 (0, 4, 1.0),
331 (1, 4, 1.0),
332 (2, 4, 1.0),
333 (3, 4, 1.0),
334 ],
335 )
336 .unwrap();
337 let specification = crate::CoverageSpecification::new(
338 5,
339 PlanarCoverageModel::new(2.0, 2.0).unwrap(),
340 2,
341 CoverageFence::new(vec![0, 1, 2, 3]).unwrap(),
342 Vec::new(),
343 0,
344 vec![CoverageState::new(0, 0, &graph, (0..4).collect(), 2.0).unwrap()],
345 CoverageLimits::default(),
346 )
347 .unwrap();
348 let action = CoverageAction::throughout(4, 1, &specification);
349 let geometry = CoverageGeometry::new(
350 &specification,
351 vec![points],
352 CoverageGeometryLimits::default(),
353 )
354 .unwrap();
355 let coverage = CoverageSynthesisArtifact::build(
356 specification,
357 vec![action],
358 1,
359 CoverageSynthesisLimits::default(),
360 )
361 .unwrap();
362 assert_eq!(coverage.status(), CoverageSynthesisStatus::Optimal);
363 (coverage, geometry)
364 }
365
366 #[test]
367 fn geometry_bound_artifact_round_trips_and_rejects_mutation() {
368 let coverage_limits = CoverageSynthesisLimits::default();
369 let geometry_limits = CoverageGeometryLimits::default();
370 let decode_limits = GeometryBoundCoverageDecodeLimits::default();
371 let (coverage, geometry) = specimen();
372 let artifact = GeometryBoundCoverageArtifact::build(
373 coverage,
374 geometry,
375 coverage_limits,
376 geometry_limits,
377 )
378 .unwrap();
379 let bytes = artifact
380 .encode(coverage_limits, geometry_limits, decode_limits)
381 .unwrap();
382 let decoded = GeometryBoundCoverageArtifact::decode(
383 &bytes,
384 coverage_limits,
385 geometry_limits,
386 decode_limits,
387 )
388 .unwrap();
389 assert_eq!(decoded.geometry().coordinates().len(), 1);
390 let independently_checked = holos_tda_check::verify_geometry_bound_coverage(
391 &bytes,
392 holos_tda_check::ProofLimits::default(),
393 )
394 .unwrap();
395 assert_eq!(independently_checked.vertices, 5);
396 assert_eq!(independently_checked.pair_checks, 10);
397
398 let mut changed = bytes;
399 changed[20] ^= 1;
400 assert!(
401 GeometryBoundCoverageArtifact::decode(
402 &changed,
403 coverage_limits,
404 geometry_limits,
405 decode_limits,
406 )
407 .is_err()
408 );
409 }
410}