Skip to main content

appcore_filemaker/
fingerprint.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: fingerprint.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/08/30 05:00:00 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/08/30 05:00:00 by dnettoRaw
8//      ###########      S: 1.0.2-rc
9// =============================================================================
10
11//! Defines bounded fingerprint contracts and behavior for this crate.
12
13use std::collections::BTreeSet;
14use std::io::{self, Write};
15
16use serde::{Deserialize, Serialize};
17use sha2::{Digest, Sha256};
18
19use crate::{
20    AssetResolver, DataValue, ElementIr, ErrorCode, FileMakerError, FontManager, Patch,
21    PatchOperation, ResourceLimits, Result, TemplateIr, ENGINE_VERSION, FILEMAKER_SCHEMA_V1,
22};
23
24const DEFAULT_MAX_FINGERPRINT_BYTES: usize = 512 * 1024 * 1024;
25
26/// SHA-256 identity of every input affecting a bound document.
27#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
28#[serde(transparent)]
29pub struct DocumentFingerprint([u8; 32]);
30
31impl DocumentFingerprint {
32    /// Computes a fingerprint from canonical IR/data plus explicit assets and fonts.
33    pub fn compute(
34        template: &TemplateIr,
35        data: &DataValue,
36        assets: Option<&dyn AssetResolver>,
37        fonts: &FontManager,
38        limits: &ResourceLimits,
39    ) -> Result<Self> {
40        Self::compute_with_patches(template, data, &[], assets, fonts, limits)
41    }
42
43    /// Computes the cache identity for the complete bind input, including patches.
44    pub fn compute_with_patches(
45        template: &TemplateIr,
46        data: &DataValue,
47        patches: &[Patch],
48        assets: Option<&dyn AssetResolver>,
49        fonts: &FontManager,
50        limits: &ResourceLimits,
51    ) -> Result<Self> {
52        let mut builder = FingerprintBuilder::with_max_bytes(limits.max_output_bytes)?;
53        builder.field("schema", FILEMAKER_SCHEMA_V1.as_bytes())?;
54        builder.field("engine", ENGINE_VERSION.as_bytes())?;
55        builder.serialized("template", template)?;
56        builder.serialized("data", data)?;
57        builder.serialized("patches", &patches)?;
58        for (name, digest) in fonts.digests() {
59            builder.field("font-name", name.as_bytes())?;
60            builder.field("font-digest", digest)?;
61        }
62        for name in fonts.fallback_names() {
63            builder.field("font-fallback", name.as_bytes())?;
64        }
65        let mut names = asset_names(&template.elements);
66        for patch in patches {
67            for operation in &patch.operations {
68                if let PatchOperation::Add { element, .. }
69                | PatchOperation::Replace { element, .. } = operation
70                {
71                    names.extend(asset_names(std::slice::from_ref(element)));
72                }
73            }
74        }
75        if !names.is_empty() && assets.is_none() {
76            return Err(fingerprint_error(
77                "fingerprint requires a resolver for referenced assets",
78            ));
79        }
80        if let Some(resolver) = assets {
81            for name in names {
82                let asset = resolver.resolve_asset(name, limits.max_asset_bytes)?;
83                builder.field("asset-name", name.as_bytes())?;
84                builder.field("asset-media-type", asset.media_type.as_bytes())?;
85                builder.field("asset-digest", &asset.digest)?;
86            }
87        }
88        Ok(builder.finish())
89    }
90
91    /// Returns lowercase hexadecimal without allocating intermediary data.
92    #[must_use]
93    pub fn to_hex(self) -> String {
94        const DIGITS: &[u8; 16] = b"0123456789abcdef";
95        let mut result = String::with_capacity(64);
96        for byte in self.0 {
97            result.push(char::from(DIGITS[usize::from(byte >> 4)]));
98            result.push(char::from(DIGITS[usize::from(byte & 0x0f)]));
99        }
100        result
101    }
102
103    /// Returns the raw digest.
104    #[must_use]
105    pub const fn as_bytes(&self) -> &[u8; 32] {
106        &self.0
107    }
108}
109
110/// Length-delimited deterministic SHA-256 input builder.
111pub struct FingerprintBuilder {
112    hasher: Sha256,
113    remaining: usize,
114}
115
116impl FingerprintBuilder {
117    /// Starts a fingerprint with a domain-separation marker and 512 MiB field budget.
118    #[must_use]
119    pub fn new() -> Self {
120        Self::with_max_bytes_unchecked(DEFAULT_MAX_FINGERPRINT_BYTES)
121    }
122
123    /// Starts a fingerprint with an aggregate budget for all framed fields.
124    pub fn with_max_bytes(max_bytes: usize) -> Result<Self> {
125        if max_bytes == 0 {
126            return Err(fingerprint_limit_error(
127                "fingerprint aggregate byte limit must be non-zero",
128            ));
129        }
130        Ok(Self::with_max_bytes_unchecked(max_bytes))
131    }
132
133    fn with_max_bytes_unchecked(max_bytes: usize) -> Self {
134        let mut hasher = Sha256::new();
135        hasher.update(b"appcore-filemaker-fingerprint-v1\0");
136        Self {
137            hasher,
138            remaining: max_bytes,
139        }
140    }
141
142    /// Adds one named byte field with unambiguous length boundaries.
143    pub fn field(&mut self, name: &str, bytes: &[u8]) -> Result<()> {
144        let framed_bytes = framed_field_size(name, bytes.len())?;
145        self.require_remaining(framed_bytes)?;
146        let byte_len = u64::try_from(bytes.len())
147            .map_err(|_| fingerprint_error("fingerprint field is too long"))?;
148        frame_field(&mut self.hasher, name, byte_len)?;
149        self.hasher.update(bytes);
150        self.remaining -= framed_bytes;
151        Ok(())
152    }
153
154    /// Serializes canonical field-order JSON directly into the digest.
155    ///
156    /// Serialization runs once for bounded sizing and once for hashing so the
157    /// length prefix can precede the payload without retaining a JSON buffer.
158    /// Custom `Serialize` implementations must produce the same bytes in both
159    /// passes; a size change fails without mutating the builder.
160    pub fn serialized<T: Serialize>(&mut self, name: &str, value: &T) -> Result<()> {
161        let framing_bytes = framed_field_size(name, 0)?;
162        self.require_remaining(framing_bytes)?;
163        let mut counter = JsonLengthWriter::new(self.remaining - framing_bytes);
164        if let Err(error) = serde_json::to_writer(&mut counter, value) {
165            if counter.exceeded {
166                return Err(fingerprint_limit_error(
167                    "serialized fingerprint fields exceed the aggregate byte limit",
168                ));
169            }
170            return Err(serialization_error(error));
171        }
172        let framed_bytes = framing_bytes
173            .checked_add(counter.written)
174            .ok_or_else(|| fingerprint_limit_error("fingerprint field size overflow"))?;
175        let byte_len = u64::try_from(counter.written)
176            .map_err(|_| fingerprint_error("fingerprint field is too long"))?;
177        let mut candidate = self.hasher.clone();
178        frame_field(&mut candidate, name, byte_len)?;
179        let (remaining, exceeded) = {
180            let mut writer = JsonDigestWriter::new(&mut candidate, counter.written);
181            let result = serde_json::to_writer(&mut writer, value);
182            if let Err(error) = result {
183                if writer.exceeded {
184                    return Err(fingerprint_error(
185                        "fingerprint serialization changed while hashing",
186                    ));
187                }
188                return Err(serialization_error(error));
189            }
190            (writer.remaining, writer.exceeded)
191        };
192        if remaining != 0 || exceeded {
193            return Err(fingerprint_error(
194                "fingerprint serialization changed while hashing",
195            ));
196        }
197        self.hasher = candidate;
198        self.remaining -= framed_bytes;
199        Ok(())
200    }
201
202    fn require_remaining(&self, bytes: usize) -> Result<()> {
203        if bytes > self.remaining {
204            return Err(fingerprint_limit_error(
205                "fingerprint fields exceed the aggregate byte limit",
206            ));
207        }
208        Ok(())
209    }
210
211    /// Finishes the digest.
212    #[must_use]
213    pub fn finish(self) -> DocumentFingerprint {
214        DocumentFingerprint(self.hasher.finalize().into())
215    }
216}
217
218struct JsonLengthWriter {
219    written: usize,
220    limit: usize,
221    exceeded: bool,
222}
223
224impl JsonLengthWriter {
225    const fn new(limit: usize) -> Self {
226        Self {
227            written: 0,
228            limit,
229            exceeded: false,
230        }
231    }
232}
233
234impl Write for JsonLengthWriter {
235    fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
236        let Some(written) = self.written.checked_add(bytes.len()) else {
237            self.exceeded = true;
238            return Err(io::Error::other("fingerprint JSON length overflow"));
239        };
240        if written > self.limit {
241            self.exceeded = true;
242            return Err(io::Error::other("fingerprint JSON exceeded byte limit"));
243        }
244        self.written = written;
245        Ok(bytes.len())
246    }
247
248    fn flush(&mut self) -> io::Result<()> {
249        Ok(())
250    }
251}
252
253struct JsonDigestWriter<'a> {
254    hasher: &'a mut Sha256,
255    remaining: usize,
256    exceeded: bool,
257}
258
259impl<'a> JsonDigestWriter<'a> {
260    const fn new(hasher: &'a mut Sha256, expected: usize) -> Self {
261        Self {
262            hasher,
263            remaining: expected,
264            exceeded: false,
265        }
266    }
267}
268
269impl Write for JsonDigestWriter<'_> {
270    fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
271        if bytes.len() > self.remaining {
272            self.exceeded = true;
273            return Err(io::Error::other("fingerprint JSON exceeded measured size"));
274        }
275        self.hasher.update(bytes);
276        self.remaining -= bytes.len();
277        Ok(bytes.len())
278    }
279
280    fn flush(&mut self) -> io::Result<()> {
281        Ok(())
282    }
283}
284
285fn frame_field(hasher: &mut Sha256, name: &str, byte_len: u64) -> Result<()> {
286    let name_len = u64::try_from(name.len())
287        .map_err(|_| fingerprint_error("fingerprint field name is too long"))?;
288    hasher.update(name_len.to_be_bytes());
289    hasher.update(name.as_bytes());
290    hasher.update(byte_len.to_be_bytes());
291    Ok(())
292}
293
294fn framed_field_size(name: &str, payload_bytes: usize) -> Result<usize> {
295    16_usize
296        .checked_add(name.len())
297        .and_then(|size| size.checked_add(payload_bytes))
298        .ok_or_else(|| fingerprint_limit_error("fingerprint field size overflow"))
299}
300
301fn serialization_error(error: serde_json::Error) -> FileMakerError {
302    fingerprint_error(format!("cannot serialize fingerprint field: {error}"))
303}
304
305impl Default for FingerprintBuilder {
306    fn default() -> Self {
307        Self::new()
308    }
309}
310
311fn asset_names(elements: &[ElementIr]) -> BTreeSet<&str> {
312    let mut names = BTreeSet::new();
313    let mut stack = elements.iter().collect::<Vec<_>>();
314    while let Some(element) = stack.pop() {
315        if let Some(asset) = &element.asset {
316            names.insert(asset.as_str());
317        }
318        stack.extend(element.children.iter());
319    }
320    names
321}
322
323fn fingerprint_error(message: impl Into<String>) -> FileMakerError {
324    FileMakerError::new(ErrorCode::Validation, message)
325}
326
327fn fingerprint_limit_error(message: impl Into<String>) -> FileMakerError {
328    FileMakerError::new(ErrorCode::LimitExceeded, message)
329}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334    use std::cell::Cell;
335
336    struct ChangingSerialization(Cell<bool>);
337
338    impl Serialize for ChangingSerialization {
339        fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
340        where
341            S: serde::Serializer,
342        {
343            if self.0.replace(true) {
344                "longer".serialize(serializer)
345            } else {
346                "x".serialize(serializer)
347            }
348        }
349    }
350
351    #[test]
352    fn length_boundaries_prevent_ambiguous_hashes() {
353        let mut left = FingerprintBuilder::new();
354        left.field("a", b"bc").unwrap();
355        let mut right = FingerprintBuilder::new();
356        right.field("ab", b"c").unwrap();
357        assert_ne!(left.finish(), right.finish());
358    }
359
360    #[test]
361    fn streaming_serialization_preserves_the_buffered_v1_fingerprint() {
362        let value = serde_json::json!({
363            "alpha": [1, 2, 3],
364            "nested": {"stable": true},
365            "unicode": "日本語 العربية",
366        });
367        let mut streamed = FingerprintBuilder::new();
368        streamed.serialized("value", &value).unwrap();
369
370        let mut buffered = FingerprintBuilder::new();
371        buffered
372            .field("value", &serde_json::to_vec(&value).unwrap())
373            .unwrap();
374        assert_eq!(streamed.finish(), buffered.finish());
375    }
376
377    #[test]
378    fn aggregate_budget_counts_field_framing_and_serialized_bytes() {
379        let mut exact_field = FingerprintBuilder::with_max_bytes(18).unwrap();
380        exact_field.field("a", b"b").unwrap();
381        let mut short_field = FingerprintBuilder::with_max_bytes(17).unwrap();
382        assert!(short_field.field("a", b"b").is_err());
383
384        let mut exact_json = FingerprintBuilder::with_max_bytes(20).unwrap();
385        exact_json.serialized("v", &"x").unwrap();
386        let mut short_json = FingerprintBuilder::with_max_bytes(19).unwrap();
387        assert!(short_json.serialized("v", &"x").is_err());
388    }
389
390    #[test]
391    fn changing_serialization_does_not_mutate_the_builder() {
392        let mut actual = FingerprintBuilder::with_max_bytes(128).unwrap();
393        actual.field("before", b"stable").unwrap();
394        assert!(actual
395            .serialized("changing", &ChangingSerialization(Cell::new(false)))
396            .is_err());
397        actual.field("after", b"stable").unwrap();
398
399        let mut expected = FingerprintBuilder::with_max_bytes(128).unwrap();
400        expected.field("before", b"stable").unwrap();
401        expected.field("after", b"stable").unwrap();
402        assert_eq!(actual.finish(), expected.finish());
403    }
404}