Skip to main content

holos_tda/coverage_geometry/
wire.rs

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/// Decoder limits for a geometry-bound coverage artifact.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12#[non_exhaustive]
13pub struct GeometryBoundCoverageDecodeLimits {
14    /// Largest accepted outer envelope.
15    pub max_bytes: usize,
16}
17
18impl Default for GeometryBoundCoverageDecodeLimits {
19    fn default() -> Self {
20        Self { max_bytes: 1 << 30 }
21    }
22}
23
24/// Coverage synthesis proof bound to checked finite planar geometry.
25#[derive(Debug, Clone)]
26pub struct GeometryBoundCoverageArtifact {
27    geometry: CoverageGeometry,
28    coverage: CoverageSynthesisArtifact,
29    digest: [u8; 32],
30}
31
32impl GeometryBoundCoverageArtifact {
33    /// Bind an existing coverage proof to checked state coordinates.
34    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    /// Checked coordinates in state order.
52    pub fn geometry(&self) -> &CoverageGeometry {
53        &self.geometry
54    }
55
56    /// Nested [`CoverageSynthesisArtifact`].
57    pub fn coverage(&self) -> &CoverageSynthesisArtifact {
58        &self.coverage
59    }
60
61    /// Content digest of the outer payload.
62    pub fn digest(&self) -> &[u8; 32] {
63        &self.digest
64    }
65
66    /// Recheck geometry, coverage, optimality, and the outer binding.
67    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    /// Encode canonical `HOLOSGEO` version 1 bytes.
84    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    /// Decode and verify canonical `HOLOSGEO` version 1 bytes.
102    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}