Skip to main content

iscc_lib/
streaming.rs

1//! Streaming hash types for incremental ISCC code generation.
2//!
3//! Provides `DataHasher`, `InstanceHasher`, and `SumHasher` — streaming
4//! counterparts to `gen_data_code_v0`, `gen_instance_code_v0`, and
5//! `gen_sum_code_v0`. All follow the
6//! `new() → update(&[u8]) → finalize()` pattern for incremental processing
7//! of large files without loading entire contents into memory.
8
9use crate::types::{DataCodeResult, InstanceCodeResult, SumCodeResult};
10use crate::{IsccResult, cdc, codec, gen_iscc_code_v0, minhash};
11
12/// Streaming Instance-Code generator.
13///
14/// Incrementally hashes data with BLAKE3 to produce an ISCC Instance-Code
15/// identical to `gen_instance_code_v0` for the same byte stream.
16pub struct InstanceHasher {
17    hasher: blake3::Hasher,
18    filesize: u64,
19}
20
21impl InstanceHasher {
22    /// Create a new `InstanceHasher`.
23    pub fn new() -> Self {
24        Self {
25            hasher: blake3::Hasher::new(),
26            filesize: 0,
27        }
28    }
29
30    /// Push data into the hasher.
31    pub fn update(&mut self, data: &[u8]) {
32        self.filesize += data.len() as u64;
33        self.hasher.update(data);
34    }
35
36    /// Consume the hasher and produce an Instance-Code result.
37    ///
38    /// Equivalent to calling `gen_instance_code_v0` with the concatenation
39    /// of all data passed to `update`.
40    pub fn finalize(self, bits: u32) -> IsccResult<InstanceCodeResult> {
41        let digest = self.hasher.finalize();
42        let datahash = format!("1e20{}", hex::encode(digest.as_bytes()));
43        let component = codec::encode_component(
44            codec::MainType::Instance,
45            codec::SubType::None,
46            codec::Version::V0,
47            bits,
48            digest.as_bytes(),
49        )?;
50        Ok(InstanceCodeResult {
51            iscc: format!("ISCC:{component}"),
52            datahash,
53            filesize: self.filesize,
54        })
55    }
56}
57
58impl Default for InstanceHasher {
59    /// Create a new `InstanceHasher` (delegates to `new()`).
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65/// Streaming Data-Code generator.
66///
67/// Incrementally processes data with content-defined chunking (CDC) and
68/// MinHash to produce an ISCC Data-Code identical to `gen_data_code_v0`
69/// for the same byte stream. Uses a persistent internal buffer to avoid
70/// per-call heap allocations.
71pub struct DataHasher {
72    chunk_features: Vec<u32>,
73    buf: Vec<u8>,
74}
75
76impl DataHasher {
77    /// Create a new `DataHasher`.
78    pub fn new() -> Self {
79        Self {
80            chunk_features: Vec::new(),
81            buf: Vec::new(),
82        }
83    }
84
85    /// Push data into the hasher.
86    ///
87    /// Appends data to the internal buffer (which starts with the retained
88    /// tail from the previous call), runs CDC, hashes all complete chunks,
89    /// and shifts the last chunk (tail) to the front of the buffer for the
90    /// next call. The buffer is reused across calls to avoid allocations.
91    pub fn update(&mut self, data: &[u8]) {
92        self.buf.extend_from_slice(data);
93
94        let chunks = cdc::alg_cdc_chunks_unchecked(&self.buf, false, cdc::DATA_AVG_CHUNK_SIZE);
95
96        // Process all chunks except the last (which becomes the new tail).
97        // This mirrors the Python `push()` method's `prev_chunk` pattern.
98        let mut prev_chunk: Option<&[u8]> = None;
99        for chunk in &chunks {
100            if let Some(pc) = prev_chunk {
101                self.chunk_features.push(xxhash_rust::xxh32::xxh32(pc, 0));
102            }
103            prev_chunk = Some(chunk);
104        }
105
106        // Extract tail length before dropping borrows on self.buf
107        let tail_len = prev_chunk.map_or(0, |c| c.len());
108        drop(chunks);
109
110        // Shift tail to front of buffer, reusing existing capacity
111        let tail_start = self.buf.len() - tail_len;
112        self.buf.copy_within(tail_start.., 0);
113        self.buf.truncate(tail_len);
114    }
115
116    /// Consume the hasher and produce a Data-Code result.
117    ///
118    /// Equivalent to calling `gen_data_code_v0` with the concatenation
119    /// of all data passed to `update`.
120    pub fn finalize(mut self, bits: u32) -> IsccResult<DataCodeResult> {
121        if !self.buf.is_empty() {
122            self.chunk_features
123                .push(xxhash_rust::xxh32::xxh32(&self.buf, 0));
124        } else if self.chunk_features.is_empty() {
125            // Empty input: ensure at least one feature
126            self.chunk_features.push(xxhash_rust::xxh32::xxh32(b"", 0));
127        }
128
129        let digest = minhash::alg_minhash_256(&self.chunk_features);
130        let component = codec::encode_component(
131            codec::MainType::Data,
132            codec::SubType::None,
133            codec::Version::V0,
134            bits,
135            &digest,
136        )?;
137
138        Ok(DataCodeResult {
139            iscc: format!("ISCC:{component}"),
140        })
141    }
142}
143
144impl Default for DataHasher {
145    /// Create a new `DataHasher` (delegates to `new()`).
146    fn default() -> Self {
147        Self::new()
148    }
149}
150
151/// Streaming composite ISCC-CODE (Sum) generator.
152///
153/// Runs the Data-Code (CDC/MinHash) and Instance-Code (BLAKE3) algorithms in a
154/// single pass over the input by feeding the same bytes to an inner
155/// `DataHasher` and `InstanceHasher`, then composes the final ISCC-CODE.
156/// Produces output identical to `gen_sum_code_v0` for the same byte stream.
157pub struct SumHasher {
158    data_hasher: DataHasher,
159    instance_hasher: InstanceHasher,
160}
161
162impl SumHasher {
163    /// Create a new `SumHasher`.
164    pub fn new() -> Self {
165        Self {
166            data_hasher: DataHasher::new(),
167            instance_hasher: InstanceHasher::new(),
168        }
169    }
170
171    /// Push data into both inner hashers in a single pass.
172    pub fn update(&mut self, data: &[u8]) {
173        self.data_hasher.update(data);
174        self.instance_hasher.update(data);
175    }
176
177    /// Consume the hasher and produce a composite ISCC-CODE result.
178    ///
179    /// Finalizes the inner Data-Code and Instance-Code, then composes them via
180    /// `gen_iscc_code_v0`. When `add_units` is `true`, the result includes the
181    /// individual Data-Code and Instance-Code ISCC strings at the requested
182    /// `bits` precision. Equivalent to calling `gen_sum_code_v0` on a file
183    /// containing the concatenation of all data passed to `update`.
184    pub fn finalize(self, bits: u32, wide: bool, add_units: bool) -> IsccResult<SumCodeResult> {
185        let data_result = self.data_hasher.finalize(bits)?;
186        let instance_result = self.instance_hasher.finalize(bits)?;
187
188        // Borrow strings for gen_iscc_code_v0 before potentially moving them into units.
189        let iscc_result = gen_iscc_code_v0(&[&data_result.iscc, &instance_result.iscc], wide)?;
190
191        let units = if add_units {
192            Some(vec![data_result.iscc, instance_result.iscc])
193        } else {
194            None
195        };
196
197        Ok(SumCodeResult {
198            iscc: iscc_result.iscc,
199            datahash: instance_result.datahash,
200            filesize: instance_result.filesize,
201            units,
202        })
203    }
204}
205
206impl Default for SumHasher {
207    /// Create a new `SumHasher` (delegates to `new()`).
208    fn default() -> Self {
209        Self::new()
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use crate::{gen_data_code_v0, gen_instance_code_v0, gen_iscc_code_v0, gen_sum_code_v0};
217
218    // ---- InstanceHasher tests ----
219
220    #[test]
221    fn test_instance_hasher_empty() {
222        let ih = InstanceHasher::new();
223        let streaming = ih.finalize(64).unwrap();
224        let oneshot = gen_instance_code_v0(b"", 64).unwrap();
225        assert_eq!(streaming.iscc, oneshot.iscc);
226        assert_eq!(streaming.datahash, oneshot.datahash);
227        assert_eq!(streaming.filesize, oneshot.filesize);
228        assert_eq!(streaming.filesize, 0);
229    }
230
231    #[test]
232    fn test_instance_hasher_small_data() {
233        let data = b"Hello, ISCC World!";
234        let mut ih = InstanceHasher::new();
235        ih.update(data);
236        let streaming = ih.finalize(64).unwrap();
237        let oneshot = gen_instance_code_v0(data, 64).unwrap();
238        assert_eq!(streaming.iscc, oneshot.iscc);
239        assert_eq!(streaming.datahash, oneshot.datahash);
240        assert_eq!(streaming.filesize, oneshot.filesize);
241    }
242
243    #[test]
244    fn test_instance_hasher_multi_chunk() {
245        let data = b"The quick brown fox jumps over the lazy dog";
246        let mut ih = InstanceHasher::new();
247        ih.update(&data[..10]);
248        ih.update(&data[10..25]);
249        ih.update(&data[25..]);
250        let streaming = ih.finalize(64).unwrap();
251        let oneshot = gen_instance_code_v0(data, 64).unwrap();
252        assert_eq!(streaming.iscc, oneshot.iscc);
253        assert_eq!(streaming.datahash, oneshot.datahash);
254        assert_eq!(streaming.filesize, oneshot.filesize);
255    }
256
257    #[test]
258    fn test_instance_hasher_byte_at_a_time() {
259        let data = b"streaming byte by byte";
260        let mut ih = InstanceHasher::new();
261        for &b in data.iter() {
262            ih.update(&[b]);
263        }
264        let streaming = ih.finalize(128).unwrap();
265        let oneshot = gen_instance_code_v0(data, 128).unwrap();
266        assert_eq!(streaming.iscc, oneshot.iscc);
267        assert_eq!(streaming.datahash, oneshot.datahash);
268        assert_eq!(streaming.filesize, oneshot.filesize);
269    }
270
271    #[test]
272    fn test_instance_hasher_default() {
273        let ih = InstanceHasher::default();
274        let streaming = ih.finalize(64).unwrap();
275        let oneshot = gen_instance_code_v0(b"", 64).unwrap();
276        assert_eq!(streaming.iscc, oneshot.iscc);
277    }
278
279    #[test]
280    fn test_instance_hasher_various_bits() {
281        let data = b"test various bit widths";
282        for bits in [64, 128, 256] {
283            let mut ih = InstanceHasher::new();
284            ih.update(data);
285            let streaming = ih.finalize(bits).unwrap();
286            let oneshot = gen_instance_code_v0(data, bits).unwrap();
287            assert_eq!(streaming.iscc, oneshot.iscc, "bits={bits}");
288            assert_eq!(streaming.datahash, oneshot.datahash, "bits={bits}");
289        }
290    }
291
292    #[test]
293    fn test_instance_hasher_conformance() {
294        let json_str = include_str!("../tests/data.json");
295        let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
296        let section = &data["gen_instance_code_v0"];
297        let cases = section.as_object().unwrap();
298
299        for (name, tc) in cases {
300            let inputs = tc["inputs"].as_array().unwrap();
301            let stream_str = inputs[0].as_str().unwrap();
302            let bits = inputs[1].as_u64().unwrap() as u32;
303
304            let hex_data = stream_str
305                .strip_prefix("stream:")
306                .unwrap_or_else(|| panic!("expected 'stream:' prefix in test case {name}"));
307            let input_bytes = hex::decode(hex_data)
308                .unwrap_or_else(|e| panic!("invalid hex in test case {name}: {e}"));
309
310            // One-shot reference
311            let oneshot = gen_instance_code_v0(&input_bytes, bits)
312                .unwrap_or_else(|e| panic!("gen_instance_code_v0 failed for {name}: {e}"));
313
314            // Streaming — single update
315            let mut ih = InstanceHasher::new();
316            ih.update(&input_bytes);
317            let streaming = ih
318                .finalize(bits)
319                .unwrap_or_else(|e| panic!("InstanceHasher failed for {name}: {e}"));
320
321            assert_eq!(
322                streaming.iscc, oneshot.iscc,
323                "ISCC mismatch in test case {name}"
324            );
325            assert_eq!(
326                streaming.datahash, oneshot.datahash,
327                "datahash mismatch in test case {name}"
328            );
329            assert_eq!(
330                streaming.filesize, oneshot.filesize,
331                "filesize mismatch in test case {name}"
332            );
333
334            // Streaming — multi-chunk (split into 256-byte chunks)
335            let mut ih2 = InstanceHasher::new();
336            for chunk in input_bytes.chunks(256) {
337                ih2.update(chunk);
338            }
339            let streaming2 = ih2
340                .finalize(bits)
341                .unwrap_or_else(|e| panic!("InstanceHasher multi-chunk failed for {name}: {e}"));
342
343            assert_eq!(
344                streaming2.iscc, oneshot.iscc,
345                "multi-chunk ISCC mismatch in test case {name}"
346            );
347            assert_eq!(
348                streaming2.datahash, oneshot.datahash,
349                "multi-chunk datahash mismatch in test case {name}"
350            );
351        }
352    }
353
354    // ---- DataHasher tests ----
355
356    #[test]
357    fn test_data_hasher_empty() {
358        let dh = DataHasher::new();
359        let streaming = dh.finalize(64).unwrap();
360        let oneshot = gen_data_code_v0(b"", 64).unwrap();
361        assert_eq!(streaming.iscc, oneshot.iscc);
362    }
363
364    #[test]
365    fn test_data_hasher_small_data() {
366        let data = b"Hello, ISCC World!";
367        let mut dh = DataHasher::new();
368        dh.update(data);
369        let streaming = dh.finalize(64).unwrap();
370        let oneshot = gen_data_code_v0(data, 64).unwrap();
371        assert_eq!(streaming.iscc, oneshot.iscc);
372    }
373
374    #[test]
375    fn test_data_hasher_multi_chunk_small() {
376        let data = b"The quick brown fox jumps over the lazy dog";
377        let mut dh = DataHasher::new();
378        dh.update(&data[..10]);
379        dh.update(&data[10..25]);
380        dh.update(&data[25..]);
381        let streaming = dh.finalize(64).unwrap();
382        let oneshot = gen_data_code_v0(data, 64).unwrap();
383        assert_eq!(streaming.iscc, oneshot.iscc);
384    }
385
386    #[test]
387    fn test_data_hasher_byte_at_a_time() {
388        // Small data that fits within a single CDC chunk
389        let data = b"streaming byte by byte";
390        let mut dh = DataHasher::new();
391        for &b in data.iter() {
392            dh.update(&[b]);
393        }
394        let streaming = dh.finalize(64).unwrap();
395        let oneshot = gen_data_code_v0(data, 64).unwrap();
396        assert_eq!(streaming.iscc, oneshot.iscc);
397    }
398
399    #[test]
400    fn test_data_hasher_large_data_multi_chunk() {
401        // Generate data large enough to produce multiple CDC chunks
402        let data: Vec<u8> = (0..10_000).map(|i| (i % 256) as u8).collect();
403        for chunk_size in [1, 256, 1024, 4096] {
404            let mut dh = DataHasher::new();
405            for chunk in data.chunks(chunk_size) {
406                dh.update(chunk);
407            }
408            let streaming = dh.finalize(64).unwrap();
409            let oneshot = gen_data_code_v0(&data, 64).unwrap();
410            assert_eq!(
411                streaming.iscc, oneshot.iscc,
412                "chunk_size={chunk_size} mismatch"
413            );
414        }
415    }
416
417    #[test]
418    fn test_data_hasher_default() {
419        let dh = DataHasher::default();
420        let streaming = dh.finalize(64).unwrap();
421        let oneshot = gen_data_code_v0(b"", 64).unwrap();
422        assert_eq!(streaming.iscc, oneshot.iscc);
423    }
424
425    #[test]
426    fn test_data_hasher_various_bits() {
427        let data = b"test various bit widths for data code";
428        for bits in [64, 128, 256] {
429            let mut dh = DataHasher::new();
430            dh.update(data);
431            let streaming = dh.finalize(bits).unwrap();
432            let oneshot = gen_data_code_v0(data, bits).unwrap();
433            assert_eq!(streaming.iscc, oneshot.iscc, "bits={bits}");
434        }
435    }
436
437    #[test]
438    fn test_data_hasher_conformance() {
439        let json_str = include_str!("../tests/data.json");
440        let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
441        let section = &data["gen_data_code_v0"];
442        let cases = section.as_object().unwrap();
443
444        for (name, tc) in cases {
445            let inputs = tc["inputs"].as_array().unwrap();
446            let stream_str = inputs[0].as_str().unwrap();
447            let bits = inputs[1].as_u64().unwrap() as u32;
448
449            let hex_data = stream_str
450                .strip_prefix("stream:")
451                .unwrap_or_else(|| panic!("expected 'stream:' prefix in test case {name}"));
452            let input_bytes = hex::decode(hex_data)
453                .unwrap_or_else(|e| panic!("invalid hex in test case {name}: {e}"));
454
455            // One-shot reference
456            let oneshot = gen_data_code_v0(&input_bytes, bits)
457                .unwrap_or_else(|e| panic!("gen_data_code_v0 failed for {name}: {e}"));
458
459            // Streaming — single update
460            let mut dh = DataHasher::new();
461            dh.update(&input_bytes);
462            let streaming = dh
463                .finalize(bits)
464                .unwrap_or_else(|e| panic!("DataHasher failed for {name}: {e}"));
465
466            assert_eq!(
467                streaming.iscc, oneshot.iscc,
468                "ISCC mismatch in test case {name}"
469            );
470
471            // Streaming — 256-byte chunks
472            let mut dh2 = DataHasher::new();
473            for chunk in input_bytes.chunks(256) {
474                dh2.update(chunk);
475            }
476            let streaming2 = dh2
477                .finalize(bits)
478                .unwrap_or_else(|e| panic!("DataHasher multi-chunk failed for {name}: {e}"));
479
480            assert_eq!(
481                streaming2.iscc, oneshot.iscc,
482                "multi-chunk ISCC mismatch in test case {name}"
483            );
484
485            // Streaming — 1-byte chunks (stress test)
486            let mut dh3 = DataHasher::new();
487            for &b in &input_bytes {
488                dh3.update(&[b]);
489            }
490            let streaming3 = dh3
491                .finalize(bits)
492                .unwrap_or_else(|e| panic!("DataHasher byte-at-a-time failed for {name}: {e}"));
493
494            assert_eq!(
495                streaming3.iscc, oneshot.iscc,
496                "byte-at-a-time ISCC mismatch in test case {name}"
497            );
498        }
499    }
500
501    // ---- SumHasher tests ----
502
503    /// Compose the expected composite ISCC-CODE for `data` from the one-shot
504    /// Data-Code and Instance-Code functions (the streaming source of truth).
505    fn expected_sum(data: &[u8], bits: u32, wide: bool) -> SumCodeResult {
506        let data_result = gen_data_code_v0(data, bits).unwrap();
507        let instance_result = gen_instance_code_v0(data, bits).unwrap();
508        let iscc_result =
509            gen_iscc_code_v0(&[&data_result.iscc, &instance_result.iscc], wide).unwrap();
510        SumCodeResult {
511            iscc: iscc_result.iscc,
512            datahash: instance_result.datahash,
513            filesize: instance_result.filesize,
514            units: None,
515        }
516    }
517
518    #[test]
519    fn test_sum_hasher_empty() {
520        let streaming = SumHasher::new().finalize(64, false, false).unwrap();
521        let expected = expected_sum(b"", 64, false);
522
523        assert_eq!(streaming.iscc, expected.iscc);
524        assert_eq!(streaming.datahash, expected.datahash);
525        assert_eq!(streaming.filesize, expected.filesize);
526        assert_eq!(streaming.filesize, 0);
527        assert_eq!(streaming.units, None);
528    }
529
530    #[test]
531    fn test_sum_hasher_small_data() {
532        let data = b"Hello, ISCC World!";
533        let mut sh = SumHasher::new();
534        sh.update(data);
535        let streaming = sh.finalize(64, false, false).unwrap();
536        let expected = expected_sum(data, 64, false);
537
538        assert_eq!(streaming.iscc, expected.iscc);
539        assert_eq!(streaming.datahash, expected.datahash);
540        assert_eq!(streaming.filesize, expected.filesize);
541    }
542
543    #[test]
544    fn test_sum_hasher_multi_update_invariance() {
545        // Large enough to span multiple CDC chunks.
546        let data: Vec<u8> = (0..20_000).map(|i| (i % 256) as u8).collect();
547
548        let mut single = SumHasher::new();
549        single.update(&data);
550        let single_result = single.finalize(64, false, false).unwrap();
551
552        // Split the same bytes across three update calls.
553        let mut multi = SumHasher::new();
554        multi.update(&data[..3000]);
555        multi.update(&data[3000..12_000]);
556        multi.update(&data[12_000..]);
557        let multi_result = multi.finalize(64, false, false).unwrap();
558
559        assert_eq!(single_result.iscc, multi_result.iscc);
560        assert_eq!(single_result.datahash, multi_result.datahash);
561        assert_eq!(single_result.filesize, multi_result.filesize);
562
563        // And both match the one-shot composition.
564        let expected = expected_sum(&data, 64, false);
565        assert_eq!(multi_result.iscc, expected.iscc);
566    }
567
568    #[test]
569    fn test_sum_hasher_units_toggle() {
570        let data = b"unit toggle test data";
571
572        let mut sh_on = SumHasher::new();
573        sh_on.update(data);
574        let with_units = sh_on.finalize(64, false, true).unwrap();
575
576        let data_result = gen_data_code_v0(data, 64).unwrap();
577        let instance_result = gen_instance_code_v0(data, 64).unwrap();
578        assert_eq!(
579            with_units.units,
580            Some(vec![data_result.iscc, instance_result.iscc])
581        );
582
583        let mut sh_off = SumHasher::new();
584        sh_off.update(data);
585        let without_units = sh_off.finalize(64, false, false).unwrap();
586        assert_eq!(without_units.units, None);
587    }
588
589    #[test]
590    fn test_sum_hasher_wide_mode() {
591        let data = b"wide mode comparison test data";
592
593        let mut narrow = SumHasher::new();
594        narrow.update(data);
595        let narrow_result = narrow.finalize(128, false, false).unwrap();
596
597        let mut wide = SumHasher::new();
598        wide.update(data);
599        let wide_result = wide.finalize(128, true, false).unwrap();
600
601        // Wide mode at 128 bits produces a different (longer) composite code.
602        assert_ne!(narrow_result.iscc, wide_result.iscc);
603        // datahash and filesize are unaffected by wide mode.
604        assert_eq!(narrow_result.datahash, wide_result.datahash);
605        assert_eq!(narrow_result.filesize, wide_result.filesize);
606
607        // Each matches its one-shot composition.
608        assert_eq!(narrow_result.iscc, expected_sum(data, 128, false).iscc);
609        assert_eq!(wide_result.iscc, expected_sum(data, 128, true).iscc);
610    }
611
612    #[test]
613    fn test_sum_hasher_datahash_filesize_match_instance() {
614        let data = b"datahash and filesize parity check";
615        let mut sh = SumHasher::new();
616        sh.update(data);
617        let streaming = sh.finalize(64, false, false).unwrap();
618
619        let instance_result = gen_instance_code_v0(data, 64).unwrap();
620        assert_eq!(streaming.datahash, instance_result.datahash);
621        assert_eq!(streaming.filesize, instance_result.filesize);
622        assert_eq!(streaming.filesize, data.len() as u64);
623    }
624
625    #[test]
626    fn test_sum_hasher_default() {
627        let from_default = SumHasher::default().finalize(64, false, false).unwrap();
628        let from_new = SumHasher::new().finalize(64, false, false).unwrap();
629        assert_eq!(from_default.iscc, from_new.iscc);
630    }
631
632    #[test]
633    fn test_sum_hasher_matches_gen_sum_code_v0() {
634        use std::io::Write;
635
636        // Large enough to span multiple CDC chunks.
637        let data: Vec<u8> = (0..20_000).map(|i| (i % 256) as u8).collect();
638
639        let mut tmp = tempfile::NamedTempFile::new().unwrap();
640        tmp.write_all(&data).unwrap();
641        tmp.flush().unwrap();
642
643        // Confirm SumHasher is the single source of truth across parameter
644        // combinations: same iscc/datahash/filesize/units as the file-based path.
645        for &(bits, wide, add_units) in &[
646            (64u32, false, false),
647            (64u32, false, true),
648            (128u32, true, true),
649        ] {
650            let mut sh = SumHasher::new();
651            sh.update(&data);
652            let streaming = sh.finalize(bits, wide, add_units).unwrap();
653
654            let file_based = gen_sum_code_v0(tmp.path(), bits, wide, add_units).unwrap();
655
656            assert_eq!(
657                streaming.iscc, file_based.iscc,
658                "iscc mismatch (bits={bits}, wide={wide}, add_units={add_units})"
659            );
660            assert_eq!(streaming.datahash, file_based.datahash);
661            assert_eq!(streaming.filesize, file_based.filesize);
662            assert_eq!(streaming.units, file_based.units);
663        }
664    }
665}