1pub(crate) mod cdc;
8pub mod codec;
9pub(crate) mod conformance;
10pub(crate) mod dct;
11pub(crate) mod minhash;
12pub(crate) mod simhash;
13pub mod streaming;
14pub mod types;
15pub(crate) mod utils;
16pub(crate) mod wtahash;
17
18pub use cdc::alg_cdc_chunks;
19pub use codec::encode_base64;
20pub use codec::iscc_decompose;
21pub use conformance::conformance_selftest;
22pub use minhash::alg_minhash_256;
23pub use simhash::{alg_simhash, sliding_window};
24pub use streaming::{DataHasher, InstanceHasher};
25pub use types::*;
26#[cfg(feature = "text-processing")]
27pub use utils::{text_clean, text_collapse};
28pub use utils::{text_remove_newlines, text_trim};
29
30#[cfg(feature = "meta-code")]
32pub const META_TRIM_NAME: usize = 128;
33
34#[cfg(feature = "meta-code")]
36pub const META_TRIM_DESCRIPTION: usize = 4096;
37
38#[cfg(feature = "meta-code")]
40pub const META_TRIM_META: usize = 128_000;
41
42pub const IO_READ_SIZE: usize = 4_194_304;
44
45pub const TEXT_NGRAM_SIZE: usize = 13;
47
48#[derive(Debug, thiserror::Error)]
50pub enum IsccError {
51 #[error("invalid input: {0}")]
53 InvalidInput(String),
54}
55
56pub type IsccResult<T> = Result<T, IsccError>;
58
59#[cfg(feature = "meta-code")]
65fn interleave_digests(a: &[u8], b: &[u8]) -> Vec<u8> {
66 let mut result = vec![0u8; 32];
67 for chunk in 0..4 {
68 let src = chunk * 4;
69 let dst_a = chunk * 8;
70 let dst_b = chunk * 8 + 4;
71 result[dst_a..dst_a + 4].copy_from_slice(&a[src..src + 4]);
72 result[dst_b..dst_b + 4].copy_from_slice(&b[src..src + 4]);
73 }
74 result
75}
76
77#[cfg(feature = "meta-code")]
82fn meta_name_simhash(name: &str) -> Vec<u8> {
83 let collapsed_name = utils::text_collapse(name);
84 let name_ngrams = simhash::sliding_window_strs(&collapsed_name, 3);
85 let name_hashes: Vec<[u8; 32]> = name_ngrams
86 .iter()
87 .map(|ng| *blake3::hash(ng.as_bytes()).as_bytes())
88 .collect();
89 simhash::alg_simhash_inner(&name_hashes)
90}
91
92#[cfg(feature = "meta-code")]
97fn soft_hash_meta_v0(name: &str, extra: Option<&str>) -> Vec<u8> {
98 let name_simhash = meta_name_simhash(name);
99
100 match extra {
101 None | Some("") => name_simhash,
102 Some(extra_str) => {
103 let collapsed_extra = utils::text_collapse(extra_str);
104 let extra_ngrams = simhash::sliding_window_strs(&collapsed_extra, 3);
105 let extra_hashes: Vec<[u8; 32]> = extra_ngrams
106 .iter()
107 .map(|ng| *blake3::hash(ng.as_bytes()).as_bytes())
108 .collect();
109 let extra_simhash = simhash::alg_simhash_inner(&extra_hashes);
110
111 interleave_digests(&name_simhash, &extra_simhash)
112 }
113 }
114}
115
116#[cfg(feature = "meta-code")]
122fn soft_hash_meta_v0_with_bytes(name: &str, extra: &[u8]) -> Vec<u8> {
123 let name_simhash = meta_name_simhash(name);
124
125 if extra.is_empty() {
126 return name_simhash;
127 }
128
129 let byte_ngrams = simhash::sliding_window_bytes(extra, 4);
130 let byte_hashes: Vec<[u8; 32]> = byte_ngrams
131 .iter()
132 .map(|ng| *blake3::hash(ng).as_bytes())
133 .collect();
134 let byte_simhash = simhash::alg_simhash_inner(&byte_hashes);
135
136 interleave_digests(&name_simhash, &byte_simhash)
137}
138
139#[cfg(feature = "meta-code")]
145fn decode_data_url(data_url: &str) -> IsccResult<Vec<u8>> {
146 let payload_b64 = data_url
147 .split_once(',')
148 .map(|(_, b64)| b64)
149 .ok_or_else(|| IsccError::InvalidInput("Data-URL missing comma separator".into()))?;
150 data_encoding::BASE64
151 .decode(payload_b64.as_bytes())
152 .map_err(|e| IsccError::InvalidInput(format!("invalid base64 in Data-URL: {e}")))
153}
154
155#[cfg(feature = "meta-code")]
157fn parse_meta_json(meta_str: &str) -> IsccResult<Vec<u8>> {
158 let parsed: serde_json::Value = serde_json::from_str(meta_str)
159 .map_err(|e| IsccError::InvalidInput(format!("invalid JSON in meta: {e}")))?;
160 let mut buf = Vec::new();
161 serde_json_canonicalizer::to_writer(&parsed, &mut buf)
162 .map_err(|e| IsccError::InvalidInput(format!("JSON canonicalization failed: {e}")))?;
163 Ok(buf)
164}
165
166#[cfg(feature = "meta-code")]
171fn build_meta_data_url(json_bytes: &[u8], json_value: &serde_json::Value) -> String {
172 let media_type = if json_value.get("@context").is_some() {
173 "application/ld+json"
174 } else {
175 "application/json"
176 };
177 let b64 = data_encoding::BASE64.encode(json_bytes);
178 format!("data:{media_type};base64,{b64}")
179}
180
181pub fn encode_component(
191 mtype: u8,
192 stype: u8,
193 version: u8,
194 bit_length: u32,
195 digest: &[u8],
196) -> IsccResult<String> {
197 let mt = codec::MainType::try_from(mtype)?;
198 let st = codec::SubType::try_from(stype)?;
199 let vs = codec::Version::try_from(version)?;
200 let needed = (bit_length / 8) as usize;
201 if digest.len() < needed {
202 return Err(IsccError::InvalidInput(format!(
203 "digest length {} < bit_length/8 ({})",
204 digest.len(),
205 needed
206 )));
207 }
208 codec::encode_component(mt, st, vs, bit_length, digest)
209}
210
211pub fn iscc_decode(iscc: &str) -> IsccResult<(u8, u8, u8, u8, Vec<u8>)> {
226 let clean = iscc.strip_prefix("ISCC:").unwrap_or(iscc);
228 let clean = clean.replace('-', "");
230 let raw = codec::decode_base32(&clean)?;
231 let (mt, st, vs, length_index, tail) = codec::decode_header(&raw)?;
232 let bit_length = codec::decode_length(mt, length_index, st);
233 let nbytes = (bit_length / 8) as usize;
234 if tail.len() < nbytes {
235 return Err(IsccError::InvalidInput(format!(
236 "decoded body too short: expected {nbytes} digest bytes, got {}",
237 tail.len()
238 )));
239 }
240 Ok((
241 mt as u8,
242 st as u8,
243 vs as u8,
244 length_index as u8,
245 tail[..nbytes].to_vec(),
246 ))
247}
248
249#[cfg(feature = "meta-code")]
276pub fn json_to_data_url(json: &str) -> IsccResult<String> {
277 let parsed: serde_json::Value = serde_json::from_str(json)
278 .map_err(|e| IsccError::InvalidInput(format!("invalid JSON: {e}")))?;
279 let mut canonical_bytes = Vec::new();
280 serde_json_canonicalizer::to_writer(&parsed, &mut canonical_bytes)
281 .map_err(|e| IsccError::InvalidInput(format!("JSON canonicalization failed: {e}")))?;
282 Ok(build_meta_data_url(&canonical_bytes, &parsed))
283}
284
285#[cfg(feature = "meta-code")]
293pub fn gen_meta_code_v0(
294 name: &str,
295 description: Option<&str>,
296 meta: Option<&str>,
297 bits: u32,
298) -> IsccResult<MetaCodeResult> {
299 let name = utils::text_clean(name);
301 let name = utils::text_remove_newlines(&name);
302 let name = utils::text_trim(&name, META_TRIM_NAME);
303
304 if name.is_empty() {
305 return Err(IsccError::InvalidInput(
306 "name is empty after normalization".into(),
307 ));
308 }
309
310 let desc_str = description.unwrap_or("");
312 let desc_clean = utils::text_clean(desc_str);
313 let desc_clean = utils::text_trim(&desc_clean, META_TRIM_DESCRIPTION);
314
315 if let Some(meta_str) = meta {
317 const PRE_DECODE_LIMIT: usize = META_TRIM_META * 4 / 3 + 256;
318 if meta_str.len() > PRE_DECODE_LIMIT {
319 return Err(IsccError::InvalidInput(format!(
320 "meta string exceeds size limit ({} > {PRE_DECODE_LIMIT} bytes)",
321 meta_str.len()
322 )));
323 }
324 }
325
326 let meta_payload: Option<Vec<u8>> = match meta {
328 Some(meta_str) if meta_str.starts_with("data:") => Some(decode_data_url(meta_str)?),
329 Some(meta_str) => Some(parse_meta_json(meta_str)?),
330 None => None,
331 };
332
333 if let Some(ref payload) = meta_payload {
335 if payload.len() > META_TRIM_META {
336 return Err(IsccError::InvalidInput(format!(
337 "decoded meta payload exceeds size limit ({} > {META_TRIM_META} bytes)",
338 payload.len()
339 )));
340 }
341 }
342
343 if let Some(ref payload) = meta_payload {
345 let meta_code_digest = soft_hash_meta_v0_with_bytes(&name, payload);
346 let metahash = utils::multi_hash_blake3(payload);
347
348 let meta_code = codec::encode_component(
349 codec::MainType::Meta,
350 codec::SubType::None,
351 codec::Version::V0,
352 bits,
353 &meta_code_digest,
354 )?;
355
356 let meta_value = match meta {
358 Some(meta_str) if meta_str.starts_with("data:") => meta_str.to_string(),
359 Some(meta_str) => {
360 let parsed: serde_json::Value = serde_json::from_str(meta_str)
361 .map_err(|e| IsccError::InvalidInput(format!("invalid JSON: {e}")))?;
362 build_meta_data_url(payload, &parsed)
363 }
364 None => unreachable!(),
365 };
366
367 Ok(MetaCodeResult {
368 iscc: format!("ISCC:{meta_code}"),
369 name: name.clone(),
370 description: if desc_clean.is_empty() {
371 None
372 } else {
373 Some(desc_clean)
374 },
375 meta: Some(meta_value),
376 metahash,
377 })
378 } else {
379 let payload = if desc_clean.is_empty() {
381 name.clone()
382 } else {
383 format!("{name} {desc_clean}")
384 };
385 let payload = payload.trim().to_string();
386 let metahash = utils::multi_hash_blake3(payload.as_bytes());
387
388 let extra = if desc_clean.is_empty() {
390 None
391 } else {
392 Some(desc_clean.as_str())
393 };
394 let meta_code_digest = soft_hash_meta_v0(&name, extra);
395
396 let meta_code = codec::encode_component(
397 codec::MainType::Meta,
398 codec::SubType::None,
399 codec::Version::V0,
400 bits,
401 &meta_code_digest,
402 )?;
403
404 Ok(MetaCodeResult {
405 iscc: format!("ISCC:{meta_code}"),
406 name: name.clone(),
407 description: if desc_clean.is_empty() {
408 None
409 } else {
410 Some(desc_clean)
411 },
412 meta: None,
413 metahash,
414 })
415 }
416}
417
418#[cfg(feature = "text-processing")]
423fn soft_hash_text_v0(text: &str) -> Vec<u8> {
424 let ngrams = simhash::sliding_window_strs(text, TEXT_NGRAM_SIZE);
425 let features: Vec<u32> = ngrams
426 .iter()
427 .map(|ng| xxhash_rust::xxh32::xxh32(ng.as_bytes(), 0))
428 .collect();
429 minhash::alg_minhash_256(&features)
430}
431
432#[cfg(feature = "text-processing")]
438pub fn gen_text_code_v0(text: &str, bits: u32) -> IsccResult<TextCodeResult> {
439 let collapsed = utils::text_collapse(text);
440 let characters = collapsed.chars().count();
441 let hash_digest = soft_hash_text_v0(&collapsed);
442 let component = codec::encode_component(
443 codec::MainType::Content,
444 codec::SubType::TEXT,
445 codec::Version::V0,
446 bits,
447 &hash_digest,
448 )?;
449 Ok(TextCodeResult {
450 iscc: format!("ISCC:{component}"),
451 characters,
452 })
453}
454
455fn transpose_matrix(matrix: &[Vec<f64>]) -> Vec<Vec<f64>> {
457 let rows = matrix.len();
458 if rows == 0 {
459 return vec![];
460 }
461 let cols = matrix[0].len();
462 let mut result = vec![vec![0.0f64; rows]; cols];
463 for (r, row) in matrix.iter().enumerate() {
464 for (c, &val) in row.iter().enumerate() {
465 result[c][r] = val;
466 }
467 }
468 result
469}
470
471fn flatten_8x8(matrix: &[Vec<f64>], col: usize, row: usize) -> Vec<f64> {
476 let mut flat = Vec::with_capacity(64);
477 for matrix_row in matrix.iter().skip(row).take(8) {
478 for &val in matrix_row.iter().skip(col).take(8) {
479 flat.push(val);
480 }
481 }
482 flat
483}
484
485fn compute_median(values: &[f64]) -> f64 {
490 let mut sorted: Vec<f64> = values.to_vec();
491 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
492 let n = sorted.len();
493 if n % 2 == 1 {
494 sorted[n / 2]
495 } else {
496 (sorted[n / 2 - 1] + sorted[n / 2]) / 2.0
497 }
498}
499
500fn bits_to_bytes(bits: &[bool]) -> Vec<u8> {
502 bits.chunks(8)
503 .map(|chunk| {
504 let mut byte = 0u8;
505 for (i, &bit) in chunk.iter().enumerate() {
506 if bit {
507 byte |= 1 << (7 - i);
508 }
509 }
510 byte
511 })
512 .collect()
513}
514
515fn soft_hash_image_v0(pixels: &[u8], bits: u32) -> IsccResult<Vec<u8>> {
521 if pixels.len() != 1024 {
522 return Err(IsccError::InvalidInput(format!(
523 "expected 1024 pixels, got {}",
524 pixels.len()
525 )));
526 }
527 if bits > 256 {
528 return Err(IsccError::InvalidInput(format!(
529 "bits must be <= 256, got {bits}"
530 )));
531 }
532
533 let rows: Vec<Vec<f64>> = pixels
535 .chunks(32)
536 .map(|row| {
537 let row_f64: Vec<f64> = row.iter().map(|&p| p as f64).collect();
538 dct::alg_dct(&row_f64)
539 })
540 .collect::<IsccResult<Vec<Vec<f64>>>>()?;
541
542 let transposed = transpose_matrix(&rows);
544
545 let dct_cols: Vec<Vec<f64>> = transposed
547 .iter()
548 .map(|col| dct::alg_dct(col))
549 .collect::<IsccResult<Vec<Vec<f64>>>>()?;
550
551 let dct_matrix = transpose_matrix(&dct_cols);
553
554 let positions = [(0, 0), (1, 0), (0, 1), (1, 1)];
556 let mut bitstring = Vec::<bool>::with_capacity(256);
557
558 for (col, row) in positions {
559 let flat = flatten_8x8(&dct_matrix, col, row);
560 let median = compute_median(&flat);
561 for val in &flat {
562 bitstring.push(*val > median);
563 }
564 if bitstring.len() >= bits as usize {
565 break;
566 }
567 }
568
569 Ok(bits_to_bytes(&bitstring[..bits as usize]))
571}
572
573pub fn gen_image_code_v0(pixels: &[u8], bits: u32) -> IsccResult<ImageCodeResult> {
579 let hash_digest = soft_hash_image_v0(pixels, bits)?;
580 let component = codec::encode_component(
581 codec::MainType::Content,
582 codec::SubType::Image,
583 codec::Version::V0,
584 bits,
585 &hash_digest,
586 )?;
587 Ok(ImageCodeResult {
588 iscc: format!("ISCC:{component}"),
589 })
590}
591
592fn array_split<T>(slice: &[T], n: usize) -> Vec<&[T]> {
598 if n == 0 {
599 return vec![];
600 }
601 let len = slice.len();
602 let base = len / n;
603 let remainder = len % n;
604 let mut parts = Vec::with_capacity(n);
605 let mut offset = 0;
606 for i in 0..n {
607 let size = base + if i < remainder { 1 } else { 0 };
608 parts.push(&slice[offset..offset + size]);
609 offset += size;
610 }
611 parts
612}
613
614fn soft_hash_audio_v0(cv: &[i32]) -> Vec<u8> {
621 let digests: Vec<[u8; 4]> = cv.iter().map(|&v| v.to_be_bytes()).collect();
623
624 if digests.is_empty() {
625 return vec![0u8; 32];
626 }
627
628 let mut parts: Vec<u8> = simhash::alg_simhash_inner(&digests);
630
631 let quarters = array_split(&digests, 4);
633 for quarter in &quarters {
634 if quarter.is_empty() {
635 parts.extend_from_slice(&[0u8; 4]);
636 } else {
637 parts.extend_from_slice(&simhash::alg_simhash_inner(quarter));
638 }
639 }
640
641 let mut sorted_values: Vec<i32> = cv.to_vec();
643 sorted_values.sort();
644 let sorted_digests: Vec<[u8; 4]> = sorted_values.iter().map(|&v| v.to_be_bytes()).collect();
645 let thirds = array_split(&sorted_digests, 3);
646 for third in &thirds {
647 if third.is_empty() {
648 parts.extend_from_slice(&[0u8; 4]);
649 } else {
650 parts.extend_from_slice(&simhash::alg_simhash_inner(third));
651 }
652 }
653
654 parts
655}
656
657pub fn gen_audio_code_v0(cv: &[i32], bits: u32) -> IsccResult<AudioCodeResult> {
662 let hash_digest = soft_hash_audio_v0(cv);
663 let component = codec::encode_component(
664 codec::MainType::Content,
665 codec::SubType::Audio,
666 codec::Version::V0,
667 bits,
668 &hash_digest,
669 )?;
670 Ok(AudioCodeResult {
671 iscc: format!("ISCC:{component}"),
672 })
673}
674
675pub fn soft_hash_video_v0<S: AsRef<[i32]> + Ord>(
680 frame_sigs: &[S],
681 bits: u32,
682) -> IsccResult<Vec<u8>> {
683 if frame_sigs.is_empty() {
684 return Err(IsccError::InvalidInput(
685 "frame_sigs must not be empty".into(),
686 ));
687 }
688
689 let unique: std::collections::BTreeSet<&S> = frame_sigs.iter().collect();
691
692 let cols = frame_sigs[0].as_ref().len();
694 let mut vecsum = vec![0i64; cols];
695 for sig in &unique {
696 for (c, &val) in sig.as_ref().iter().enumerate() {
697 vecsum[c] += val as i64;
698 }
699 }
700
701 wtahash::alg_wtahash(&vecsum, bits)
702}
703
704pub fn gen_video_code_v0<S: AsRef<[i32]> + Ord>(
709 frame_sigs: &[S],
710 bits: u32,
711) -> IsccResult<VideoCodeResult> {
712 let digest = soft_hash_video_v0(frame_sigs, bits)?;
713 let component = codec::encode_component(
714 codec::MainType::Content,
715 codec::SubType::Video,
716 codec::Version::V0,
717 bits,
718 &digest,
719 )?;
720 Ok(VideoCodeResult {
721 iscc: format!("ISCC:{component}"),
722 })
723}
724
725fn soft_hash_codes_v0(cc_digests: &[Vec<u8>], bits: u32) -> IsccResult<Vec<u8>> {
732 if cc_digests.len() < 2 {
733 return Err(IsccError::InvalidInput(
734 "at least 2 Content-Codes required for mixing".into(),
735 ));
736 }
737
738 let nbytes = (bits / 8) as usize;
739 let mut prepared: Vec<Vec<u8>> = Vec::with_capacity(cc_digests.len());
740
741 for raw in cc_digests {
742 let (mtype, stype, _ver, blen, body) = codec::decode_header(raw)?;
743 if mtype != codec::MainType::Content {
744 return Err(IsccError::InvalidInput(
745 "all codes must be Content-Codes".into(),
746 ));
747 }
748 let unit_bits = codec::decode_length(mtype, blen, stype);
749 if unit_bits < bits {
750 return Err(IsccError::InvalidInput(format!(
751 "Content-Code too short for {bits}-bit length (has {unit_bits} bits)"
752 )));
753 }
754 let mut entry = Vec::with_capacity(nbytes);
755 entry.push(raw[0]); let take = std::cmp::min(nbytes - 1, body.len());
757 entry.extend_from_slice(&body[..take]);
758 while entry.len() < nbytes {
760 entry.push(0);
761 }
762 prepared.push(entry);
763 }
764
765 Ok(simhash::alg_simhash_inner(&prepared))
766}
767
768pub fn gen_mixed_code_v0(codes: &[&str], bits: u32) -> IsccResult<MixedCodeResult> {
774 let decoded: Vec<Vec<u8>> = codes
775 .iter()
776 .map(|code| {
777 let clean = code.strip_prefix("ISCC:").unwrap_or(code);
778 codec::decode_base32(clean)
779 })
780 .collect::<IsccResult<Vec<Vec<u8>>>>()?;
781
782 let digest = soft_hash_codes_v0(&decoded, bits)?;
783
784 let component = codec::encode_component(
785 codec::MainType::Content,
786 codec::SubType::Mixed,
787 codec::Version::V0,
788 bits,
789 &digest,
790 )?;
791
792 Ok(MixedCodeResult {
793 iscc: format!("ISCC:{component}"),
794 parts: codes.iter().map(|s| s.to_string()).collect(),
795 })
796}
797
798pub fn gen_data_code_v0(data: &[u8], bits: u32) -> IsccResult<DataCodeResult> {
804 let chunks = cdc::alg_cdc_chunks_unchecked(data, false, cdc::DATA_AVG_CHUNK_SIZE);
805 let mut features: Vec<u32> = chunks
806 .iter()
807 .map(|chunk| xxhash_rust::xxh32::xxh32(chunk, 0))
808 .collect();
809
810 if features.is_empty() {
812 features.push(xxhash_rust::xxh32::xxh32(b"", 0));
813 }
814
815 let digest = minhash::alg_minhash_256(&features);
816 let component = codec::encode_component(
817 codec::MainType::Data,
818 codec::SubType::None,
819 codec::Version::V0,
820 bits,
821 &digest,
822 )?;
823
824 Ok(DataCodeResult {
825 iscc: format!("ISCC:{component}"),
826 })
827}
828
829pub fn gen_instance_code_v0(data: &[u8], bits: u32) -> IsccResult<InstanceCodeResult> {
834 let digest = blake3::hash(data);
835 let datahash = utils::multi_hash_blake3(data);
836 let filesize = data.len() as u64;
837 let component = codec::encode_component(
838 codec::MainType::Instance,
839 codec::SubType::None,
840 codec::Version::V0,
841 bits,
842 digest.as_bytes(),
843 )?;
844 Ok(InstanceCodeResult {
845 iscc: format!("ISCC:{component}"),
846 datahash,
847 filesize,
848 })
849}
850
851pub fn gen_iscc_code_v0(codes: &[&str], wide: bool) -> IsccResult<IsccCodeResult> {
860 let cleaned: Vec<&str> = codes
862 .iter()
863 .map(|c| c.strip_prefix("ISCC:").unwrap_or(c))
864 .collect();
865
866 if cleaned.len() < 2 {
868 return Err(IsccError::InvalidInput(
869 "at least 2 ISCC unit codes required".into(),
870 ));
871 }
872
873 for code in &cleaned {
875 if code.len() < 16 {
876 return Err(IsccError::InvalidInput(format!(
877 "ISCC unit code too short (min 16 chars): {code}"
878 )));
879 }
880 }
881
882 let mut decoded: Vec<(
884 codec::MainType,
885 codec::SubType,
886 codec::Version,
887 u32,
888 Vec<u8>,
889 )> = Vec::with_capacity(cleaned.len());
890 for code in &cleaned {
891 let raw = codec::decode_base32(code)?;
892 let header = codec::decode_header(&raw)?;
893 decoded.push(header);
894 }
895
896 decoded.sort_by_key(|&(mt, ..)| mt);
898
899 let main_types: Vec<codec::MainType> = decoded.iter().map(|&(mt, ..)| mt).collect();
901
902 let n = main_types.len();
904 if main_types[n - 2] != codec::MainType::Data || main_types[n - 1] != codec::MainType::Instance
905 {
906 return Err(IsccError::InvalidInput(
907 "Data-Code and Instance-Code are mandatory".into(),
908 ));
909 }
910
911 let is_wide = wide
913 && decoded.len() == 2
914 && main_types == [codec::MainType::Data, codec::MainType::Instance]
915 && decoded
916 .iter()
917 .all(|&(mt, st, _, len, _)| codec::decode_length(mt, len, st) >= 128);
918
919 let st = if is_wide {
921 codec::SubType::Wide
922 } else {
923 let sc_subtypes: Vec<codec::SubType> = decoded
925 .iter()
926 .filter(|&&(mt, ..)| mt == codec::MainType::Semantic || mt == codec::MainType::Content)
927 .map(|&(_, st, ..)| st)
928 .collect();
929
930 if !sc_subtypes.is_empty() {
931 let first = sc_subtypes[0];
933 if sc_subtypes.iter().all(|&s| s == first) {
934 first
935 } else {
936 return Err(IsccError::InvalidInput(
937 "mixed SubTypes among Content/Semantic units".into(),
938 ));
939 }
940 } else if decoded.len() == 2 {
941 codec::SubType::Sum
942 } else {
943 codec::SubType::IsccNone
944 }
945 };
946
947 let optional_types = &main_types[..n - 2];
949 let encoded_length = codec::encode_units(optional_types)?;
950
951 let bytes_per_unit = if is_wide { 16 } else { 8 };
953 let mut digest = Vec::with_capacity(decoded.len() * bytes_per_unit);
954 for (_, _, _, _, tail) in &decoded {
955 let take = bytes_per_unit.min(tail.len());
956 digest.extend_from_slice(&tail[..take]);
957 }
958
959 let header = codec::encode_header(
961 codec::MainType::Iscc,
962 st,
963 codec::Version::V0,
964 encoded_length,
965 )?;
966 let mut code_bytes = header;
967 code_bytes.extend_from_slice(&digest);
968 let code = codec::encode_base32(&code_bytes);
969
970 Ok(IsccCodeResult {
972 iscc: format!("ISCC:{code}"),
973 })
974}
975
976pub fn gen_sum_code_v0(
987 path: &std::path::Path,
988 bits: u32,
989 wide: bool,
990 add_units: bool,
991) -> IsccResult<SumCodeResult> {
992 use std::io::Read;
993
994 let mut file = std::fs::File::open(path)
995 .map_err(|e| IsccError::InvalidInput(format!("Cannot open file: {e}")))?;
996
997 let mut hasher = streaming::SumHasher::new();
998
999 let mut buf = vec![0u8; IO_READ_SIZE];
1000 loop {
1001 let n = file
1002 .read(&mut buf)
1003 .map_err(|e| IsccError::InvalidInput(format!("Cannot read file: {e}")))?;
1004 if n == 0 {
1005 break;
1006 }
1007 hasher.update(&buf[..n]);
1008 }
1009
1010 hasher.finalize(bits, wide, add_units)
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015 use super::*;
1016
1017 #[cfg(feature = "meta-code")]
1018 #[test]
1019 fn test_gen_meta_code_v0_title_only() {
1020 let result = gen_meta_code_v0("Die Unendliche Geschichte", None, None, 64).unwrap();
1021 assert_eq!(result.iscc, "ISCC:AAAZXZ6OU74YAZIM");
1022 assert_eq!(result.name, "Die Unendliche Geschichte");
1023 assert_eq!(result.description, None);
1024 assert_eq!(result.meta, None);
1025 }
1026
1027 #[cfg(feature = "meta-code")]
1028 #[test]
1029 fn test_gen_meta_code_v0_title_description() {
1030 let result = gen_meta_code_v0(
1031 "Die Unendliche Geschichte",
1032 Some("Von Michael Ende"),
1033 None,
1034 64,
1035 )
1036 .unwrap();
1037 assert_eq!(result.iscc, "ISCC:AAAZXZ6OU4E45RB5");
1038 assert_eq!(result.name, "Die Unendliche Geschichte");
1039 assert_eq!(result.description, Some("Von Michael Ende".to_string()));
1040 assert_eq!(result.meta, None);
1041 }
1042
1043 #[cfg(feature = "meta-code")]
1044 #[test]
1045 fn test_gen_meta_code_v0_json_meta() {
1046 let result = gen_meta_code_v0("Hello", None, Some(r#"{"some":"object"}"#), 64).unwrap();
1047 assert_eq!(result.iscc, "ISCC:AAAWKLHFXN63LHL2");
1048 assert!(result.meta.is_some());
1049 assert!(
1050 result
1051 .meta
1052 .unwrap()
1053 .starts_with("data:application/json;base64,")
1054 );
1055 }
1056
1057 #[cfg(feature = "meta-code")]
1058 #[test]
1059 fn test_gen_meta_code_v0_data_url_meta() {
1060 let result = gen_meta_code_v0(
1061 "Hello",
1062 None,
1063 Some("data:application/json;charset=utf-8;base64,eyJzb21lIjogIm9iamVjdCJ9"),
1064 64,
1065 )
1066 .unwrap();
1067 assert_eq!(result.iscc, "ISCC:AAAWKLHFXN43ICP2");
1068 assert_eq!(
1070 result.meta,
1071 Some("data:application/json;charset=utf-8;base64,eyJzb21lIjogIm9iamVjdCJ9".to_string())
1072 );
1073 }
1074
1075 #[cfg(feature = "meta-code")]
1081 #[test]
1082 fn test_gen_meta_code_v0_jcs_float_canonicalization() {
1083 let result = gen_meta_code_v0("Test", None, Some(r#"{"value":1.0}"#), 64).unwrap();
1086
1087 assert_eq!(
1089 result.iscc, "ISCC:AAAX4GX3RZH2I6QZ",
1090 "ISCC mismatch: parse_meta_json must use RFC 8785 (JCS) canonicalization"
1091 );
1092 assert_eq!(
1093 result.meta,
1094 Some("data:application/json;base64,eyJ2YWx1ZSI6MX0=".to_string()),
1095 "meta Data-URL mismatch: JCS should serialize 1.0 as 1"
1096 );
1097 assert_eq!(
1098 result.metahash, "1e2010b291d392b6999ffe4aa4661fb343fc371fca3bfb5bb4e8d8226fdf85743232",
1099 "metahash mismatch: canonical bytes differ between JCS and serde_json"
1100 );
1101 }
1102
1103 #[cfg(feature = "meta-code")]
1108 #[test]
1109 fn test_gen_meta_code_v0_jcs_large_float_canonicalization() {
1110 let result = gen_meta_code_v0("Test", None, Some(r#"{"value":1e20}"#), 64).unwrap();
1111
1112 assert_eq!(
1113 result.iscc, "ISCC:AAAX4GX3R32YH5P7",
1114 "ISCC mismatch: JCS should expand 1e20 to 100000000000000000000"
1115 );
1116 assert_eq!(
1117 result.meta,
1118 Some(
1119 "data:application/json;base64,eyJ2YWx1ZSI6MTAwMDAwMDAwMDAwMDAwMDAwMDAwfQ=="
1120 .to_string()
1121 ),
1122 "meta Data-URL mismatch: JCS should expand large float to integer form"
1123 );
1124 assert_eq!(
1125 result.metahash, "1e201ff83c1822c348717658a0b4713739646da7c59832691b337a457416ddd1c73d",
1126 "metahash mismatch: canonical bytes differ for large float"
1127 );
1128 }
1129
1130 #[cfg(feature = "meta-code")]
1131 #[test]
1132 fn test_gen_meta_code_v0_invalid_json() {
1133 assert!(matches!(
1134 gen_meta_code_v0("test", None, Some("not json"), 64),
1135 Err(IsccError::InvalidInput(_))
1136 ));
1137 }
1138
1139 #[cfg(feature = "meta-code")]
1140 #[test]
1141 fn test_gen_meta_code_v0_invalid_data_url() {
1142 assert!(matches!(
1143 gen_meta_code_v0("test", None, Some("data:no-comma-here"), 64),
1144 Err(IsccError::InvalidInput(_))
1145 ));
1146 }
1147
1148 #[cfg(feature = "meta-code")]
1149 #[test]
1150 fn test_gen_meta_code_v0_conformance() {
1151 let json_str = include_str!("../tests/data.json");
1152 let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
1153 let section = &data["gen_meta_code_v0"];
1154 let cases = section.as_object().unwrap();
1155
1156 let mut tested = 0;
1157
1158 for (tc_name, tc) in cases {
1159 let inputs = tc["inputs"].as_array().unwrap();
1160 let input_name = inputs[0].as_str().unwrap();
1161 let input_desc = inputs[1].as_str().unwrap();
1162 let meta_val = &inputs[2];
1163 let bits = inputs[3].as_u64().unwrap() as u32;
1164
1165 let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();
1166 let expected_metahash = tc["outputs"]["metahash"].as_str().unwrap();
1167
1168 let meta_arg: Option<String> = match meta_val {
1170 serde_json::Value::Null => None,
1171 serde_json::Value::String(s) => Some(s.clone()),
1172 serde_json::Value::Object(_) => Some(serde_json::to_string(meta_val).unwrap()),
1173 other => panic!("unexpected meta type in {tc_name}: {other:?}"),
1174 };
1175
1176 let desc = if input_desc.is_empty() {
1177 None
1178 } else {
1179 Some(input_desc)
1180 };
1181
1182 let result = gen_meta_code_v0(input_name, desc, meta_arg.as_deref(), bits)
1184 .unwrap_or_else(|e| panic!("gen_meta_code_v0 failed for {tc_name}: {e}"));
1185 assert_eq!(
1186 result.iscc, expected_iscc,
1187 "ISCC mismatch in test case {tc_name}"
1188 );
1189
1190 assert_eq!(
1192 result.metahash, expected_metahash,
1193 "metahash mismatch in test case {tc_name}"
1194 );
1195
1196 if let Some(expected_name) = tc["outputs"].get("name") {
1198 let expected_name = expected_name.as_str().unwrap();
1199 assert_eq!(
1200 result.name, expected_name,
1201 "name mismatch in test case {tc_name}"
1202 );
1203 }
1204
1205 if let Some(expected_desc) = tc["outputs"].get("description") {
1207 let expected_desc = expected_desc.as_str().unwrap();
1208 assert_eq!(
1209 result.description.as_deref(),
1210 Some(expected_desc),
1211 "description mismatch in test case {tc_name}"
1212 );
1213 }
1214
1215 if meta_arg.is_some() {
1217 assert!(
1218 result.meta.is_some(),
1219 "meta should be present in test case {tc_name}"
1220 );
1221 } else {
1222 assert!(
1223 result.meta.is_none(),
1224 "meta should be absent in test case {tc_name}"
1225 );
1226 }
1227
1228 tested += 1;
1229 }
1230
1231 assert_eq!(tested, 20, "expected 20 conformance tests to run");
1232 }
1233
1234 #[cfg(feature = "text-processing")]
1235 #[test]
1236 fn test_gen_text_code_v0_empty() {
1237 let result = gen_text_code_v0("", 64).unwrap();
1238 assert_eq!(result.iscc, "ISCC:EAASL4F2WZY7KBXB");
1239 assert_eq!(result.characters, 0);
1240 }
1241
1242 #[cfg(feature = "text-processing")]
1243 #[test]
1244 fn test_gen_text_code_v0_hello_world() {
1245 let result = gen_text_code_v0("Hello World", 64).unwrap();
1246 assert_eq!(result.iscc, "ISCC:EAASKDNZNYGUUF5A");
1247 assert_eq!(result.characters, 10); }
1249
1250 #[cfg(feature = "text-processing")]
1251 #[test]
1252 fn test_gen_text_code_v0_conformance() {
1253 let json_str = include_str!("../tests/data.json");
1254 let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
1255 let section = &data["gen_text_code_v0"];
1256 let cases = section.as_object().unwrap();
1257
1258 let mut tested = 0;
1259
1260 for (tc_name, tc) in cases {
1261 let inputs = tc["inputs"].as_array().unwrap();
1262 let input_text = inputs[0].as_str().unwrap();
1263 let bits = inputs[1].as_u64().unwrap() as u32;
1264
1265 let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();
1266 let expected_chars = tc["outputs"]["characters"].as_u64().unwrap() as usize;
1267
1268 let result = gen_text_code_v0(input_text, bits)
1270 .unwrap_or_else(|e| panic!("gen_text_code_v0 failed for {tc_name}: {e}"));
1271 assert_eq!(
1272 result.iscc, expected_iscc,
1273 "ISCC mismatch in test case {tc_name}"
1274 );
1275
1276 assert_eq!(
1278 result.characters, expected_chars,
1279 "character count mismatch in test case {tc_name}"
1280 );
1281
1282 tested += 1;
1283 }
1284
1285 assert_eq!(tested, 5, "expected 5 conformance tests to run");
1286 }
1287
1288 #[test]
1289 fn test_gen_image_code_v0_all_black() {
1290 let pixels = vec![0u8; 1024];
1291 let result = gen_image_code_v0(&pixels, 64).unwrap();
1292 assert_eq!(result.iscc, "ISCC:EEAQAAAAAAAAAAAA");
1293 }
1294
1295 #[test]
1296 fn test_gen_image_code_v0_all_white() {
1297 let pixels = vec![255u8; 1024];
1298 let result = gen_image_code_v0(&pixels, 128).unwrap();
1299 assert_eq!(result.iscc, "ISCC:EEBYAAAAAAAAAAAAAAAAAAAAAAAAA");
1300 }
1301
1302 #[test]
1303 fn test_gen_image_code_v0_invalid_pixel_count() {
1304 assert!(gen_image_code_v0(&[0u8; 100], 64).is_err());
1305 }
1306
1307 #[test]
1308 fn test_gen_image_code_v0_conformance() {
1309 let json_str = include_str!("../tests/data.json");
1310 let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
1311 let section = &data["gen_image_code_v0"];
1312 let cases = section.as_object().unwrap();
1313
1314 let mut tested = 0;
1315
1316 for (tc_name, tc) in cases {
1317 let inputs = tc["inputs"].as_array().unwrap();
1318 let pixels_json = inputs[0].as_array().unwrap();
1319 let bits = inputs[1].as_u64().unwrap() as u32;
1320 let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();
1321
1322 let pixels: Vec<u8> = pixels_json
1323 .iter()
1324 .map(|v| v.as_u64().unwrap() as u8)
1325 .collect();
1326
1327 let result = gen_image_code_v0(&pixels, bits)
1328 .unwrap_or_else(|e| panic!("gen_image_code_v0 failed for {tc_name}: {e}"));
1329 assert_eq!(
1330 result.iscc, expected_iscc,
1331 "ISCC mismatch in test case {tc_name}"
1332 );
1333
1334 tested += 1;
1335 }
1336
1337 assert_eq!(tested, 3, "expected 3 conformance tests to run");
1338 }
1339
1340 #[test]
1341 fn test_gen_audio_code_v0_empty() {
1342 let result = gen_audio_code_v0(&[], 64).unwrap();
1343 assert_eq!(result.iscc, "ISCC:EIAQAAAAAAAAAAAA");
1344 }
1345
1346 #[test]
1347 fn test_gen_audio_code_v0_single() {
1348 let result = gen_audio_code_v0(&[1], 128).unwrap();
1349 assert_eq!(result.iscc, "ISCC:EIBQAAAAAEAAAAABAAAAAAAAAAAAA");
1350 }
1351
1352 #[test]
1353 fn test_gen_audio_code_v0_negative() {
1354 let result = gen_audio_code_v0(&[-1, 0, 1], 256).unwrap();
1355 assert_eq!(
1356 result.iscc,
1357 "ISCC:EIDQAAAAAH777777AAAAAAAAAAAACAAAAAAP777774AAAAAAAAAAAAI"
1358 );
1359 }
1360
1361 #[test]
1362 fn test_gen_audio_code_v0_conformance() {
1363 let json_str = include_str!("../tests/data.json");
1364 let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
1365 let section = &data["gen_audio_code_v0"];
1366 let cases = section.as_object().unwrap();
1367
1368 let mut tested = 0;
1369
1370 for (tc_name, tc) in cases {
1371 let inputs = tc["inputs"].as_array().unwrap();
1372 let cv_json = inputs[0].as_array().unwrap();
1373 let bits = inputs[1].as_u64().unwrap() as u32;
1374 let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();
1375
1376 let cv: Vec<i32> = cv_json.iter().map(|v| v.as_i64().unwrap() as i32).collect();
1377
1378 let result = gen_audio_code_v0(&cv, bits)
1379 .unwrap_or_else(|e| panic!("gen_audio_code_v0 failed for {tc_name}: {e}"));
1380 assert_eq!(
1381 result.iscc, expected_iscc,
1382 "ISCC mismatch in test case {tc_name}"
1383 );
1384
1385 tested += 1;
1386 }
1387
1388 assert_eq!(tested, 5, "expected 5 conformance tests to run");
1389 }
1390
1391 #[test]
1392 fn test_array_split_even() {
1393 let data = vec![1, 2, 3, 4];
1394 let parts = array_split(&data, 4);
1395 assert_eq!(parts, vec![&[1][..], &[2][..], &[3][..], &[4][..]]);
1396 }
1397
1398 #[test]
1399 fn test_array_split_remainder() {
1400 let data = vec![1, 2, 3, 4, 5];
1401 let parts = array_split(&data, 3);
1402 assert_eq!(parts, vec![&[1, 2][..], &[3, 4][..], &[5][..]]);
1403 }
1404
1405 #[test]
1406 fn test_array_split_more_parts_than_elements() {
1407 let data = vec![1, 2];
1408 let parts = array_split(&data, 4);
1409 assert_eq!(
1410 parts,
1411 vec![&[1][..], &[2][..], &[][..] as &[i32], &[][..] as &[i32]]
1412 );
1413 }
1414
1415 #[test]
1416 fn test_array_split_empty() {
1417 let data: Vec<i32> = vec![];
1418 let parts = array_split(&data, 3);
1419 assert_eq!(
1420 parts,
1421 vec![&[][..] as &[i32], &[][..] as &[i32], &[][..] as &[i32]]
1422 );
1423 }
1424
1425 #[test]
1426 fn test_gen_video_code_v0_empty_frames() {
1427 let frames: Vec<Vec<i32>> = vec![];
1428 assert!(matches!(
1429 gen_video_code_v0(&frames, 64),
1430 Err(IsccError::InvalidInput(_))
1431 ));
1432 }
1433
1434 #[test]
1435 fn test_gen_video_code_v0_conformance() {
1436 let json_str = include_str!("../tests/data.json");
1437 let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
1438 let section = &data["gen_video_code_v0"];
1439 let cases = section.as_object().unwrap();
1440
1441 let mut tested = 0;
1442
1443 for (tc_name, tc) in cases {
1444 let inputs = tc["inputs"].as_array().unwrap();
1445 let frames_json = inputs[0].as_array().unwrap();
1446 let bits = inputs[1].as_u64().unwrap() as u32;
1447 let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();
1448
1449 let frame_sigs: Vec<Vec<i32>> = frames_json
1450 .iter()
1451 .map(|frame| {
1452 frame
1453 .as_array()
1454 .unwrap()
1455 .iter()
1456 .map(|v| v.as_i64().unwrap() as i32)
1457 .collect()
1458 })
1459 .collect();
1460
1461 let result = gen_video_code_v0(&frame_sigs, bits)
1462 .unwrap_or_else(|e| panic!("gen_video_code_v0 failed for {tc_name}: {e}"));
1463 assert_eq!(
1464 result.iscc, expected_iscc,
1465 "ISCC mismatch in test case {tc_name}"
1466 );
1467
1468 tested += 1;
1469 }
1470
1471 assert_eq!(tested, 3, "expected 3 conformance tests to run");
1472 }
1473
1474 #[test]
1475 fn test_gen_mixed_code_v0_conformance() {
1476 let json_str = include_str!("../tests/data.json");
1477 let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
1478 let section = &data["gen_mixed_code_v0"];
1479 let cases = section.as_object().unwrap();
1480
1481 let mut tested = 0;
1482
1483 for (tc_name, tc) in cases {
1484 let inputs = tc["inputs"].as_array().unwrap();
1485 let codes_json = inputs[0].as_array().unwrap();
1486 let bits = inputs[1].as_u64().unwrap() as u32;
1487 let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();
1488 let expected_parts: Vec<&str> = tc["outputs"]["parts"]
1489 .as_array()
1490 .unwrap()
1491 .iter()
1492 .map(|v| v.as_str().unwrap())
1493 .collect();
1494
1495 let codes: Vec<&str> = codes_json.iter().map(|v| v.as_str().unwrap()).collect();
1496
1497 let result = gen_mixed_code_v0(&codes, bits)
1498 .unwrap_or_else(|e| panic!("gen_mixed_code_v0 failed for {tc_name}: {e}"));
1499 assert_eq!(
1500 result.iscc, expected_iscc,
1501 "ISCC mismatch in test case {tc_name}"
1502 );
1503
1504 let result_parts: Vec<&str> = result.parts.iter().map(|s| s.as_str()).collect();
1506 assert_eq!(
1507 result_parts, expected_parts,
1508 "parts mismatch in test case {tc_name}"
1509 );
1510
1511 tested += 1;
1512 }
1513
1514 assert_eq!(tested, 2, "expected 2 conformance tests to run");
1515 }
1516
1517 #[test]
1518 fn test_gen_mixed_code_v0_too_few_codes() {
1519 assert!(matches!(
1520 gen_mixed_code_v0(&["EUA6GIKXN42IQV3S"], 64),
1521 Err(IsccError::InvalidInput(_))
1522 ));
1523 }
1524
1525 fn make_content_code_raw(stype: codec::SubType, bit_length: u32) -> Vec<u8> {
1527 let nbytes = (bit_length / 8) as usize;
1528 let body: Vec<u8> = (0..nbytes).map(|i| (i & 0xFF) as u8).collect();
1529 let base32 = codec::encode_component(
1530 codec::MainType::Content,
1531 stype,
1532 codec::Version::V0,
1533 bit_length,
1534 &body,
1535 )
1536 .unwrap();
1537 codec::decode_base32(&base32).unwrap()
1538 }
1539
1540 #[test]
1541 fn test_soft_hash_codes_v0_rejects_short_code() {
1542 let code_64 = make_content_code_raw(codec::SubType::None, 64);
1544 let code_32 = make_content_code_raw(codec::SubType::Image, 32);
1545 let result = soft_hash_codes_v0(&[code_64, code_32], 64);
1546 assert!(
1547 matches!(&result, Err(IsccError::InvalidInput(msg)) if msg.contains("too short")),
1548 "expected InvalidInput with 'too short', got {result:?}"
1549 );
1550 }
1551
1552 #[test]
1553 fn test_soft_hash_codes_v0_accepts_exact_length() {
1554 let code_a = make_content_code_raw(codec::SubType::None, 64);
1556 let code_b = make_content_code_raw(codec::SubType::Image, 64);
1557 let result = soft_hash_codes_v0(&[code_a, code_b], 64);
1558 assert!(result.is_ok(), "expected Ok, got {result:?}");
1559 }
1560
1561 #[test]
1562 fn test_soft_hash_codes_v0_accepts_longer_codes() {
1563 let code_a = make_content_code_raw(codec::SubType::None, 128);
1565 let code_b = make_content_code_raw(codec::SubType::Audio, 128);
1566 let result = soft_hash_codes_v0(&[code_a, code_b], 64);
1567 assert!(result.is_ok(), "expected Ok, got {result:?}");
1568 }
1569
1570 #[test]
1571 fn test_gen_data_code_v0_conformance() {
1572 let json_str = include_str!("../tests/data.json");
1573 let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
1574 let section = &data["gen_data_code_v0"];
1575 let cases = section.as_object().unwrap();
1576
1577 let mut tested = 0;
1578
1579 for (tc_name, tc) in cases {
1580 let inputs = tc["inputs"].as_array().unwrap();
1581 let stream_str = inputs[0].as_str().unwrap();
1582 let bits = inputs[1].as_u64().unwrap() as u32;
1583 let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();
1584
1585 let hex_data = stream_str
1587 .strip_prefix("stream:")
1588 .unwrap_or_else(|| panic!("expected 'stream:' prefix in test case {tc_name}"));
1589 let input_bytes = hex::decode(hex_data)
1590 .unwrap_or_else(|e| panic!("invalid hex in test case {tc_name}: {e}"));
1591
1592 let result = gen_data_code_v0(&input_bytes, bits)
1593 .unwrap_or_else(|e| panic!("gen_data_code_v0 failed for {tc_name}: {e}"));
1594 assert_eq!(
1595 result.iscc, expected_iscc,
1596 "ISCC mismatch in test case {tc_name}"
1597 );
1598
1599 tested += 1;
1600 }
1601
1602 assert_eq!(tested, 4, "expected 4 conformance tests to run");
1603 }
1604
1605 #[test]
1606 fn test_gen_instance_code_v0_empty() {
1607 let result = gen_instance_code_v0(b"", 64).unwrap();
1608 assert_eq!(result.iscc, "ISCC:IAA26E2JXH27TING");
1609 assert_eq!(result.filesize, 0);
1610 assert_eq!(
1611 result.datahash,
1612 "1e20af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"
1613 );
1614 }
1615
1616 #[test]
1617 fn test_gen_instance_code_v0_conformance() {
1618 let json_str = include_str!("../tests/data.json");
1619 let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
1620 let section = &data["gen_instance_code_v0"];
1621 let cases = section.as_object().unwrap();
1622
1623 for (name, tc) in cases {
1624 let inputs = tc["inputs"].as_array().unwrap();
1625 let stream_str = inputs[0].as_str().unwrap();
1626 let bits = inputs[1].as_u64().unwrap() as u32;
1627 let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();
1628
1629 let hex_data = stream_str
1631 .strip_prefix("stream:")
1632 .unwrap_or_else(|| panic!("expected 'stream:' prefix in test case {name}"));
1633 let input_bytes = hex::decode(hex_data)
1634 .unwrap_or_else(|e| panic!("invalid hex in test case {name}: {e}"));
1635
1636 let result = gen_instance_code_v0(&input_bytes, bits)
1637 .unwrap_or_else(|e| panic!("gen_instance_code_v0 failed for {name}: {e}"));
1638 assert_eq!(
1639 result.iscc, expected_iscc,
1640 "ISCC mismatch in test case {name}"
1641 );
1642
1643 if let Some(expected_datahash) = tc["outputs"].get("datahash") {
1645 let expected_datahash = expected_datahash.as_str().unwrap();
1646 assert_eq!(
1647 result.datahash, expected_datahash,
1648 "datahash mismatch in test case {name}"
1649 );
1650 }
1651
1652 if let Some(expected_filesize) = tc["outputs"].get("filesize") {
1654 let expected_filesize = expected_filesize.as_u64().unwrap();
1655 assert_eq!(
1656 result.filesize, expected_filesize,
1657 "filesize mismatch in test case {name}"
1658 );
1659 }
1660
1661 assert_eq!(
1663 result.filesize,
1664 input_bytes.len() as u64,
1665 "filesize should match input length in test case {name}"
1666 );
1667 }
1668 }
1669
1670 #[test]
1671 fn test_gen_iscc_code_v0_conformance() {
1672 let json_str = include_str!("../tests/data.json");
1673 let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
1674 let section = &data["gen_iscc_code_v0"];
1675 let cases = section.as_object().unwrap();
1676
1677 let mut tested = 0;
1678
1679 for (tc_name, tc) in cases {
1680 let inputs = tc["inputs"].as_array().unwrap();
1681 let codes_json = inputs[0].as_array().unwrap();
1682 let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();
1683
1684 let codes: Vec<&str> = codes_json.iter().map(|v| v.as_str().unwrap()).collect();
1685
1686 let result = gen_iscc_code_v0(&codes, false)
1687 .unwrap_or_else(|e| panic!("gen_iscc_code_v0 failed for {tc_name}: {e}"));
1688 assert_eq!(
1689 result.iscc, expected_iscc,
1690 "ISCC mismatch in test case {tc_name}"
1691 );
1692
1693 tested += 1;
1694 }
1695
1696 assert_eq!(tested, 5, "expected 5 conformance tests to run");
1697 }
1698
1699 #[test]
1700 fn test_gen_iscc_code_v0_too_few_codes() {
1701 assert!(matches!(
1702 gen_iscc_code_v0(&["AAAWKLHFPV6OPKDG"], false),
1703 Err(IsccError::InvalidInput(_))
1704 ));
1705 }
1706
1707 #[test]
1708 fn test_gen_iscc_code_v0_missing_instance() {
1709 assert!(matches!(
1711 gen_iscc_code_v0(&["AAAWKLHFPV6OPKDG", "AAAWKLHFPV6OPKDG"], false),
1712 Err(IsccError::InvalidInput(_))
1713 ));
1714 }
1715
1716 #[test]
1717 fn test_gen_iscc_code_v0_short_code() {
1718 assert!(matches!(
1720 gen_iscc_code_v0(&["AAAWKLHFPV6", "AAAWKLHFPV6OPKDG"], false),
1721 Err(IsccError::InvalidInput(_))
1722 ));
1723 }
1724
1725 #[cfg(feature = "meta-code")]
1732 #[test]
1733 fn test_gen_meta_code_empty_data_url_enters_meta_branch() {
1734 let result =
1735 gen_meta_code_v0("Test", None, Some("data:application/json;base64,"), 64).unwrap();
1736
1737 assert_eq!(result.name, "Test");
1739
1740 assert_eq!(
1742 result.meta,
1743 Some("data:application/json;base64,".to_string()),
1744 "empty Data-URL payload should still enter meta branch"
1745 );
1746
1747 let expected_metahash = utils::multi_hash_blake3(&[]);
1749 assert_eq!(
1750 result.metahash, expected_metahash,
1751 "metahash should be BLAKE3 of empty bytes"
1752 );
1753 }
1754
1755 #[cfg(feature = "meta-code")]
1761 #[test]
1762 fn test_soft_hash_meta_v0_with_bytes_empty_equals_name_only() {
1763 let name_only = soft_hash_meta_v0("test", None);
1764 let empty_bytes = soft_hash_meta_v0_with_bytes("test", &[]);
1765 assert_eq!(
1766 name_only, empty_bytes,
1767 "empty bytes should produce same digest as name-only (no interleaving)"
1768 );
1769 }
1770
1771 #[cfg(feature = "meta-code")]
1774 #[test]
1775 fn test_meta_trim_name_value() {
1776 assert_eq!(META_TRIM_NAME, 128);
1777 }
1778
1779 #[cfg(feature = "meta-code")]
1780 #[test]
1781 fn test_meta_trim_description_value() {
1782 assert_eq!(META_TRIM_DESCRIPTION, 4096);
1783 }
1784
1785 #[test]
1786 fn test_io_read_size_value() {
1787 assert_eq!(IO_READ_SIZE, 4_194_304);
1788 }
1789
1790 #[test]
1791 fn test_text_ngram_size_value() {
1792 assert_eq!(TEXT_NGRAM_SIZE, 13);
1793 }
1794
1795 #[test]
1799 fn test_encode_component_matches_codec() {
1800 let digest = [0xABu8; 8];
1801 let tier1 = encode_component(3, 0, 0, 64, &digest).unwrap();
1802 let tier2 = codec::encode_component(
1803 codec::MainType::Data,
1804 codec::SubType::None,
1805 codec::Version::V0,
1806 64,
1807 &digest,
1808 )
1809 .unwrap();
1810 assert_eq!(tier1, tier2);
1811 }
1812
1813 #[test]
1815 fn test_encode_component_round_trip() {
1816 let digest = [0x42u8; 32];
1817 let result = encode_component(0, 0, 0, 64, &digest).unwrap();
1818 assert!(!result.is_empty());
1820 }
1821
1822 #[test]
1824 fn test_encode_component_rejects_iscc() {
1825 let result = encode_component(5, 0, 0, 64, &[0u8; 8]);
1826 assert!(result.is_err());
1827 }
1828
1829 #[test]
1831 fn test_encode_component_rejects_short_digest() {
1832 let result = encode_component(0, 0, 0, 64, &[0u8; 4]);
1833 assert!(result.is_err());
1834 let err = result.unwrap_err().to_string();
1835 assert!(
1836 err.contains("digest length 4 < bit_length/8 (8)"),
1837 "unexpected error: {err}"
1838 );
1839 }
1840
1841 #[test]
1843 fn test_encode_component_rejects_invalid_mtype() {
1844 let result = encode_component(99, 0, 0, 64, &[0u8; 8]);
1845 assert!(result.is_err());
1846 }
1847
1848 #[test]
1850 fn test_encode_component_rejects_invalid_stype() {
1851 let result = encode_component(0, 99, 0, 64, &[0u8; 8]);
1852 assert!(result.is_err());
1853 }
1854
1855 #[test]
1857 fn test_encode_component_rejects_invalid_version() {
1858 let result = encode_component(0, 0, 99, 64, &[0u8; 8]);
1859 assert!(result.is_err());
1860 }
1861
1862 #[test]
1866 fn test_iscc_decode_round_trip_meta() {
1867 let digest = [0xaa_u8; 8];
1868 let encoded = encode_component(0, 0, 0, 64, &digest).unwrap();
1869 let (mt, st, vs, li, decoded_digest) = iscc_decode(&encoded).unwrap();
1870 assert_eq!(mt, 0, "MainType::Meta");
1871 assert_eq!(st, 0, "SubType::None");
1872 assert_eq!(vs, 0, "Version::V0");
1873 assert_eq!(li, 1, "length_index");
1875 assert_eq!(decoded_digest, digest.to_vec());
1876 }
1877
1878 #[test]
1880 fn test_iscc_decode_round_trip_content() {
1881 let digest = [0xbb_u8; 8];
1882 let encoded = encode_component(2, 0, 0, 64, &digest).unwrap();
1883 let (mt, st, vs, _li, decoded_digest) = iscc_decode(&encoded).unwrap();
1884 assert_eq!(mt, 2, "MainType::Content");
1885 assert_eq!(st, 0, "SubType::TEXT");
1886 assert_eq!(vs, 0, "Version::V0");
1887 assert_eq!(decoded_digest, digest.to_vec());
1888 }
1889
1890 #[test]
1892 fn test_iscc_decode_round_trip_data() {
1893 let digest = [0xcc_u8; 8];
1894 let encoded = encode_component(3, 0, 0, 64, &digest).unwrap();
1895 let (mt, _st, _vs, _li, decoded_digest) = iscc_decode(&encoded).unwrap();
1896 assert_eq!(mt, 3, "MainType::Data");
1897 assert_eq!(decoded_digest, digest.to_vec());
1898 }
1899
1900 #[test]
1902 fn test_iscc_decode_round_trip_instance() {
1903 let digest = [0xdd_u8; 8];
1904 let encoded = encode_component(4, 0, 0, 64, &digest).unwrap();
1905 let (mt, _st, _vs, _li, decoded_digest) = iscc_decode(&encoded).unwrap();
1906 assert_eq!(mt, 4, "MainType::Instance");
1907 assert_eq!(decoded_digest, digest.to_vec());
1908 }
1909
1910 #[test]
1912 fn test_iscc_decode_with_prefix() {
1913 let digest = [0xaa_u8; 8];
1914 let encoded = encode_component(0, 0, 0, 64, &digest).unwrap();
1915 let with_prefix = format!("ISCC:{encoded}");
1916 let (mt, st, vs, li, decoded_digest) = iscc_decode(&with_prefix).unwrap();
1917 assert_eq!(mt, 0);
1918 assert_eq!(st, 0);
1919 assert_eq!(vs, 0);
1920 assert_eq!(li, 1);
1921 assert_eq!(decoded_digest, digest.to_vec());
1922 }
1923
1924 #[test]
1926 fn test_iscc_decode_with_dashes() {
1927 let digest = [0xaa_u8; 8];
1928 let encoded = encode_component(0, 0, 0, 64, &digest).unwrap();
1929 let with_dashes = format!("{}-{}-{}", &encoded[..4], &encoded[4..8], &encoded[8..]);
1931 let (mt, st, vs, li, decoded_digest) = iscc_decode(&with_dashes).unwrap();
1932 assert_eq!(mt, 0);
1933 assert_eq!(st, 0);
1934 assert_eq!(vs, 0);
1935 assert_eq!(li, 1);
1936 assert_eq!(decoded_digest, digest.to_vec());
1937 }
1938
1939 #[test]
1941 fn test_iscc_decode_invalid_base32() {
1942 let result = iscc_decode("!!!INVALID!!!");
1943 assert!(result.is_err());
1944 let err = result.unwrap_err().to_string();
1945 assert!(err.contains("base32"), "expected base32 error: {err}");
1946 }
1947
1948 #[test]
1951 fn test_iscc_decode_known_meta_code() {
1952 let (mt, st, vs, li, digest) = iscc_decode("ISCC:AAAZXZ6OU74YAZIM").unwrap();
1953 assert_eq!(mt, 0, "MainType::Meta");
1954 assert_eq!(st, 0, "SubType::None");
1955 assert_eq!(vs, 0, "Version::V0");
1956 assert_eq!(li, 1, "length_index for 64-bit");
1957 assert_eq!(digest.len(), 8, "64-bit = 8 bytes");
1958 }
1959
1960 #[test]
1963 fn test_iscc_decode_known_instance_code() {
1964 let (mt, st, vs, li, digest) = iscc_decode("ISCC:IAA26E2JXH27TING").unwrap();
1965 assert_eq!(mt, 4, "MainType::Instance");
1966 assert_eq!(st, 0, "SubType::None");
1967 assert_eq!(vs, 0, "Version::V0");
1968 assert_eq!(li, 1, "length_index for 64-bit");
1969 assert_eq!(digest.len(), 8, "64-bit = 8 bytes");
1970 }
1971
1972 #[test]
1975 fn test_iscc_decode_known_data_code() {
1976 let (mt, st, vs, _li, digest) = iscc_decode("ISCC:GAAXL2XYM5BQIAZ3").unwrap();
1977 assert_eq!(mt, 3, "MainType::Data");
1978 assert_eq!(st, 0, "SubType::None");
1979 assert_eq!(vs, 0, "Version::V0");
1980 assert_eq!(digest.len(), 8, "64-bit = 8 bytes");
1981 }
1982
1983 #[test]
1986 fn test_iscc_decode_verification_round_trip() {
1987 let digest = [0xaa_u8; 8];
1988 let encoded = encode_component(0, 0, 0, 64, &digest).unwrap();
1989 let result = iscc_decode(&encoded).unwrap();
1990 assert_eq!(result, (0, 0, 0, 1, vec![0xaa; 8]));
1991 }
1992
1993 #[test]
1995 fn test_iscc_decode_truncated_input() {
1996 let digest = [0xff_u8; 32];
1998 let encoded = encode_component(0, 0, 0, 256, &digest).unwrap();
1999 let truncated = &encoded[..6];
2001 let result = iscc_decode(truncated);
2002 assert!(result.is_err(), "should fail on truncated input");
2003 }
2004
2005 #[cfg(feature = "meta-code")]
2009 #[test]
2010 fn test_json_to_data_url_basic() {
2011 let url = json_to_data_url(r#"{"key": "value"}"#).unwrap();
2012 assert!(
2013 url.starts_with("data:application/json;base64,"),
2014 "expected application/json prefix, got: {url}"
2015 );
2016 }
2017
2018 #[cfg(feature = "meta-code")]
2020 #[test]
2021 fn test_json_to_data_url_ld_json() {
2022 let url = json_to_data_url(r#"{"@context": "https://schema.org"}"#).unwrap();
2023 assert!(
2024 url.starts_with("data:application/ld+json;base64,"),
2025 "expected application/ld+json prefix, got: {url}"
2026 );
2027 }
2028
2029 #[cfg(feature = "meta-code")]
2031 #[test]
2032 fn test_json_to_data_url_jcs_ordering() {
2033 let url = json_to_data_url(r#"{"b":1,"a":2}"#).unwrap();
2034 let b64 = url.split_once(',').unwrap().1;
2036 let decoded = data_encoding::BASE64.decode(b64.as_bytes()).unwrap();
2037 let canonical = std::str::from_utf8(&decoded).unwrap();
2038 assert_eq!(canonical, r#"{"a":2,"b":1}"#, "JCS should sort keys");
2039 }
2040
2041 #[cfg(feature = "meta-code")]
2044 #[test]
2045 fn test_json_to_data_url_round_trip() {
2046 let input = r#"{"hello": "world", "num": 42}"#;
2047 let url = json_to_data_url(input).unwrap();
2048 let decoded_bytes = decode_data_url(&url).unwrap();
2049 let canonical: serde_json::Value =
2051 serde_json::from_slice(&decoded_bytes).expect("decoded bytes should be valid JSON");
2052 let original: serde_json::Value = serde_json::from_str(input).unwrap();
2053 assert_eq!(canonical, original, "round-trip preserves JSON semantics");
2054 }
2055
2056 #[cfg(feature = "meta-code")]
2058 #[test]
2059 fn test_json_to_data_url_invalid_json() {
2060 let result = json_to_data_url("not json");
2061 assert!(result.is_err(), "should reject invalid JSON");
2062 let err = result.unwrap_err().to_string();
2063 assert!(
2064 err.contains("invalid JSON"),
2065 "expected 'invalid JSON' in error: {err}"
2066 );
2067 }
2068
2069 #[cfg(feature = "meta-code")]
2082 #[test]
2083 fn test_json_to_data_url_conformance_0016() {
2084 let url = json_to_data_url(r#"{"some": "object"}"#).unwrap();
2085 assert!(
2087 url.starts_with("data:application/json;base64,"),
2088 "expected application/json prefix"
2089 );
2090 let b64 = url.split_once(',').unwrap().1;
2092 let decoded = data_encoding::BASE64.decode(b64.as_bytes()).unwrap();
2093 let canonical = std::str::from_utf8(&decoded).unwrap();
2094 assert_eq!(
2095 canonical, r#"{"some":"object"}"#,
2096 "JCS removes whitespace from JSON"
2097 );
2098 }
2099
2100 #[cfg(feature = "meta-code")]
2101 #[test]
2102 fn test_meta_trim_meta_value() {
2103 assert_eq!(META_TRIM_META, 128_000);
2104 }
2105
2106 #[cfg(feature = "meta-code")]
2107 #[test]
2108 fn test_gen_meta_code_v0_meta_at_limit() {
2109 let padding = "a".repeat(128_000 - 8);
2113 let json_str = format!(r#"{{"x":"{padding}"}}"#);
2114 let result = gen_meta_code_v0("test", None, Some(&json_str), 64);
2115 assert!(
2116 result.is_ok(),
2117 "payload at exactly META_TRIM_META should succeed"
2118 );
2119 }
2120
2121 #[cfg(feature = "meta-code")]
2122 #[test]
2123 fn test_gen_meta_code_v0_meta_over_limit() {
2124 let padding = "a".repeat(128_000 - 8 + 1);
2126 let json_str = format!(r#"{{"x":"{padding}"}}"#);
2127 let result = gen_meta_code_v0("test", None, Some(&json_str), 64);
2128 assert!(
2129 matches!(result, Err(IsccError::InvalidInput(ref msg)) if msg.contains("size limit")),
2130 "payload exceeding META_TRIM_META should return InvalidInput"
2131 );
2132 }
2133
2134 #[cfg(feature = "meta-code")]
2135 #[test]
2136 fn test_gen_meta_code_v0_data_url_pre_decode_reject() {
2137 let pre_decode_limit = META_TRIM_META * 4 / 3 + 256;
2140 let padding = "A".repeat(pre_decode_limit + 1);
2141 let data_url = format!("data:application/octet-stream;base64,{padding}");
2142 let result = gen_meta_code_v0("test", None, Some(&data_url), 64);
2143 assert!(
2144 matches!(result, Err(IsccError::InvalidInput(ref msg)) if msg.contains("size limit")),
2145 "oversized Data-URL should be rejected before decoding"
2146 );
2147 }
2148
2149 fn write_temp_file(name: &str, data: &[u8]) -> std::path::PathBuf {
2153 let path = std::env::temp_dir().join(format!("iscc_test_{name}"));
2154 std::fs::write(&path, data).expect("failed to write temp file");
2155 path
2156 }
2157
2158 #[test]
2159 fn test_gen_sum_code_v0_equivalence() {
2160 let data = b"Hello, ISCC World! This is a test of gen_sum_code_v0.";
2161 let path = write_temp_file("sum_equiv", data);
2162
2163 let sum_result = gen_sum_code_v0(&path, 64, false, false).unwrap();
2164
2165 let data_result = gen_data_code_v0(data, 64).unwrap();
2167 let instance_result = gen_instance_code_v0(data, 64).unwrap();
2168 let iscc_result =
2169 gen_iscc_code_v0(&[&data_result.iscc, &instance_result.iscc], false).unwrap();
2170
2171 assert_eq!(sum_result.iscc, iscc_result.iscc);
2172 assert_eq!(sum_result.datahash, instance_result.datahash);
2173 assert_eq!(sum_result.filesize, instance_result.filesize);
2174 assert_eq!(sum_result.filesize, data.len() as u64);
2175 assert_eq!(sum_result.units, None);
2176
2177 std::fs::remove_file(&path).ok();
2178 }
2179
2180 #[test]
2181 fn test_gen_sum_code_v0_empty_file() {
2182 let path = write_temp_file("sum_empty", b"");
2183
2184 let sum_result = gen_sum_code_v0(&path, 64, false, false).unwrap();
2185
2186 let data_result = gen_data_code_v0(b"", 64).unwrap();
2187 let instance_result = gen_instance_code_v0(b"", 64).unwrap();
2188 let iscc_result =
2189 gen_iscc_code_v0(&[&data_result.iscc, &instance_result.iscc], false).unwrap();
2190
2191 assert_eq!(sum_result.iscc, iscc_result.iscc);
2192 assert_eq!(sum_result.datahash, instance_result.datahash);
2193 assert_eq!(sum_result.filesize, 0);
2194
2195 std::fs::remove_file(&path).ok();
2196 }
2197
2198 #[test]
2199 fn test_gen_sum_code_v0_file_not_found() {
2200 let path = std::env::temp_dir().join("iscc_test_nonexistent_file_xyz");
2201 let result = gen_sum_code_v0(&path, 64, false, false);
2202 assert!(result.is_err());
2203 let err_msg = result.unwrap_err().to_string();
2204 assert!(
2205 err_msg.contains("Cannot open file"),
2206 "error message should mention file open failure: {err_msg}"
2207 );
2208 }
2209
2210 #[test]
2211 fn test_gen_sum_code_v0_wide_mode() {
2212 let data = b"Testing wide mode for gen_sum_code_v0 function.";
2213 let path = write_temp_file("sum_wide", data);
2214
2215 let narrow = gen_sum_code_v0(&path, 64, false, false).unwrap();
2216 let wide = gen_sum_code_v0(&path, 64, true, false).unwrap();
2217
2218 assert_eq!(narrow.iscc, wide.iscc);
2220
2221 let narrow_128 = gen_sum_code_v0(&path, 128, false, false).unwrap();
2223 let wide_128 = gen_sum_code_v0(&path, 128, true, false).unwrap();
2224 assert_ne!(narrow_128.iscc, wide_128.iscc);
2225
2226 assert_eq!(narrow_128.datahash, wide_128.datahash);
2228 assert_eq!(narrow_128.filesize, wide_128.filesize);
2229
2230 std::fs::remove_file(&path).ok();
2231 }
2232
2233 #[test]
2234 fn test_gen_sum_code_v0_bits_64() {
2235 let data = b"Testing 64-bit gen_sum_code_v0.";
2236 let path = write_temp_file("sum_bits64", data);
2237
2238 let sum_result = gen_sum_code_v0(&path, 64, false, false).unwrap();
2239
2240 let data_result = gen_data_code_v0(data, 64).unwrap();
2241 let instance_result = gen_instance_code_v0(data, 64).unwrap();
2242 let iscc_result =
2243 gen_iscc_code_v0(&[&data_result.iscc, &instance_result.iscc], false).unwrap();
2244
2245 assert_eq!(sum_result.iscc, iscc_result.iscc);
2246
2247 std::fs::remove_file(&path).ok();
2248 }
2249
2250 #[test]
2251 fn test_gen_sum_code_v0_bits_128() {
2252 let data = b"Testing 128-bit gen_sum_code_v0.";
2253 let path = write_temp_file("sum_bits128", data);
2254
2255 let sum_result = gen_sum_code_v0(&path, 128, false, false).unwrap();
2256
2257 let data_result = gen_data_code_v0(data, 128).unwrap();
2258 let instance_result = gen_instance_code_v0(data, 128).unwrap();
2259 let iscc_result =
2260 gen_iscc_code_v0(&[&data_result.iscc, &instance_result.iscc], false).unwrap();
2261
2262 assert_eq!(sum_result.iscc, iscc_result.iscc);
2263 assert_eq!(sum_result.datahash, instance_result.datahash);
2264 assert_eq!(sum_result.filesize, data.len() as u64);
2265
2266 std::fs::remove_file(&path).ok();
2267 }
2268
2269 #[test]
2270 fn test_gen_sum_code_v0_large_data() {
2271 let data: Vec<u8> = (0..50_000).map(|i| (i % 256) as u8).collect();
2273 let path = write_temp_file("sum_large", &data);
2274
2275 let sum_result = gen_sum_code_v0(&path, 64, false, false).unwrap();
2276
2277 let data_result = gen_data_code_v0(&data, 64).unwrap();
2278 let instance_result = gen_instance_code_v0(&data, 64).unwrap();
2279 let iscc_result =
2280 gen_iscc_code_v0(&[&data_result.iscc, &instance_result.iscc], false).unwrap();
2281
2282 assert_eq!(sum_result.iscc, iscc_result.iscc);
2283 assert_eq!(sum_result.datahash, instance_result.datahash);
2284 assert_eq!(sum_result.filesize, data.len() as u64);
2285
2286 std::fs::remove_file(&path).ok();
2287 }
2288
2289 #[test]
2290 fn test_gen_sum_code_v0_units_enabled() {
2291 let data = b"Hello, ISCC World! This is a test of gen_sum_code_v0 units.";
2292 let path = write_temp_file("sum_units_on", data);
2293
2294 let sum_result = gen_sum_code_v0(&path, 64, false, true).unwrap();
2295
2296 let units = sum_result.units.as_ref().expect("units should be Some");
2298 assert_eq!(
2299 units.len(),
2300 2,
2301 "units should contain [Data-Code, Instance-Code]"
2302 );
2303
2304 let (maintype, ..) = iscc_decode(&units[0]).unwrap();
2306 assert_eq!(
2307 maintype, 3,
2308 "first unit should be a Data-Code (MainType::Data = 3)"
2309 );
2310
2311 let (maintype, ..) = iscc_decode(&units[1]).unwrap();
2313 assert_eq!(
2314 maintype, 4,
2315 "second unit should be an Instance-Code (MainType::Instance = 4)"
2316 );
2317
2318 let data_result = gen_data_code_v0(data, 64).unwrap();
2320 let instance_result = gen_instance_code_v0(data, 64).unwrap();
2321 assert_eq!(units[0], data_result.iscc);
2322 assert_eq!(units[1], instance_result.iscc);
2323
2324 let iscc_result =
2326 gen_iscc_code_v0(&[&data_result.iscc, &instance_result.iscc], false).unwrap();
2327 assert_eq!(sum_result.iscc, iscc_result.iscc);
2328
2329 std::fs::remove_file(&path).ok();
2330 }
2331
2332 #[test]
2333 fn test_gen_sum_code_v0_units_disabled() {
2334 let data = b"Hello, ISCC World! This is a test of gen_sum_code_v0 no units.";
2335 let path = write_temp_file("sum_units_off", data);
2336
2337 let sum_result = gen_sum_code_v0(&path, 64, false, false).unwrap();
2338
2339 assert_eq!(
2340 sum_result.units, None,
2341 "units should be None when add_units is false"
2342 );
2343
2344 let data_result = gen_data_code_v0(data, 64).unwrap();
2346 let instance_result = gen_instance_code_v0(data, 64).unwrap();
2347 let iscc_result =
2348 gen_iscc_code_v0(&[&data_result.iscc, &instance_result.iscc], false).unwrap();
2349 assert_eq!(sum_result.iscc, iscc_result.iscc);
2350
2351 std::fs::remove_file(&path).ok();
2352 }
2353}