1use crate::types::{DataCodeResult, InstanceCodeResult, SumCodeResult};
10use crate::{IsccResult, cdc, codec, gen_iscc_code_v0, minhash};
11
12pub struct InstanceHasher {
17 hasher: blake3::Hasher,
18 filesize: u64,
19}
20
21impl InstanceHasher {
22 pub fn new() -> Self {
24 Self {
25 hasher: blake3::Hasher::new(),
26 filesize: 0,
27 }
28 }
29
30 pub fn update(&mut self, data: &[u8]) {
32 self.filesize += data.len() as u64;
33 self.hasher.update(data);
34 }
35
36 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 fn default() -> Self {
61 Self::new()
62 }
63}
64
65pub struct DataHasher {
72 chunk_features: Vec<u32>,
73 buf: Vec<u8>,
74}
75
76impl DataHasher {
77 pub fn new() -> Self {
79 Self {
80 chunk_features: Vec::new(),
81 buf: Vec::new(),
82 }
83 }
84
85 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 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 let tail_len = prev_chunk.map_or(0, |c| c.len());
108 drop(chunks);
109
110 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 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 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 fn default() -> Self {
147 Self::new()
148 }
149}
150
151pub struct SumHasher {
158 data_hasher: DataHasher,
159 instance_hasher: InstanceHasher,
160}
161
162impl SumHasher {
163 pub fn new() -> Self {
165 Self {
166 data_hasher: DataHasher::new(),
167 instance_hasher: InstanceHasher::new(),
168 }
169 }
170
171 pub fn update(&mut self, data: &[u8]) {
173 self.data_hasher.update(data);
174 self.instance_hasher.update(data);
175 }
176
177 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 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 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 #[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 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 assert_ne!(narrow_result.iscc, wide_result.iscc);
603 assert_eq!(narrow_result.datahash, wide_result.datahash);
605 assert_eq!(narrow_result.filesize, wide_result.filesize);
606
607 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 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 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}