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
211fn iscc_normalize(iscc: &str) -> IsccResult<String> {
221 let clean = codec::iscc_clean(iscc)?;
223
224 let prefix: String = clean.to_uppercase().chars().take(2).collect();
229 if !codec::PREFIXES.contains(&prefix.as_str()) {
230 return Err(IsccError::InvalidInput(format!(
231 "ISCC starts with invalid prefix {prefix}"
232 )));
233 }
234
235 let raw = codec::decode_base32(&clean)?;
236 let (mt, st, _, _, _) = codec::decode_header(&raw)?;
237 let is_wide = mt == codec::MainType::Iscc && st == codec::SubType::Wide;
238
239 let decomposed = codec::iscc_decompose(&clean)?;
241 if decomposed.len() >= 2 {
242 let units: Vec<&str> = decomposed.iter().map(String::as_str).collect();
243 let composed = gen_iscc_code_v0(&units, is_wide)?.iscc;
244 Ok(composed
245 .strip_prefix("ISCC:")
246 .unwrap_or(&composed)
247 .to_string())
248 } else {
249 decomposed
250 .into_iter()
251 .next()
252 .ok_or_else(|| IsccError::InvalidInput("decomposed to zero ISCC-UNITs".to_string()))
253 }
254}
255
256pub fn iscc_decode(iscc: &str) -> IsccResult<(u8, u8, u8, u8, Vec<u8>)> {
277 let normalized = iscc_normalize(iscc)?;
278 let raw = codec::decode_base32(&normalized)?;
279 let (mt, st, vs, length_index, tail) = codec::decode_header(&raw)?;
280 let bit_length = codec::decode_length(mt, length_index, st);
281 let nbytes = (bit_length / 8) as usize;
282 Ok((
286 mt as u8,
287 st as u8,
288 vs as u8,
289 length_index as u8,
290 tail[..nbytes].to_vec(),
291 ))
292}
293
294#[cfg(feature = "meta-code")]
321pub fn json_to_data_url(json: &str) -> IsccResult<String> {
322 let parsed: serde_json::Value = serde_json::from_str(json)
323 .map_err(|e| IsccError::InvalidInput(format!("invalid JSON: {e}")))?;
324 let mut canonical_bytes = Vec::new();
325 serde_json_canonicalizer::to_writer(&parsed, &mut canonical_bytes)
326 .map_err(|e| IsccError::InvalidInput(format!("JSON canonicalization failed: {e}")))?;
327 Ok(build_meta_data_url(&canonical_bytes, &parsed))
328}
329
330#[cfg(feature = "meta-code")]
338pub fn gen_meta_code_v0(
339 name: &str,
340 description: Option<&str>,
341 meta: Option<&str>,
342 bits: u32,
343) -> IsccResult<MetaCodeResult> {
344 let name = utils::text_clean(name);
346 let name = utils::text_remove_newlines(&name);
347 let name = utils::text_trim(&name, META_TRIM_NAME);
348
349 if name.is_empty() {
350 return Err(IsccError::InvalidInput(
351 "name is empty after normalization".into(),
352 ));
353 }
354
355 let desc_str = description.unwrap_or("");
357 let desc_clean = utils::text_clean(desc_str);
358 let desc_clean = utils::text_trim(&desc_clean, META_TRIM_DESCRIPTION);
359
360 if let Some(meta_str) = meta {
362 const PRE_DECODE_LIMIT: usize = META_TRIM_META * 4 / 3 + 256;
363 if meta_str.len() > PRE_DECODE_LIMIT {
364 return Err(IsccError::InvalidInput(format!(
365 "meta string exceeds size limit ({} > {PRE_DECODE_LIMIT} bytes)",
366 meta_str.len()
367 )));
368 }
369 }
370
371 let meta_payload: Option<Vec<u8>> = match meta {
373 Some(meta_str) if meta_str.starts_with("data:") => Some(decode_data_url(meta_str)?),
374 Some(meta_str) => Some(parse_meta_json(meta_str)?),
375 None => None,
376 };
377
378 if let Some(ref payload) = meta_payload {
380 if payload.len() > META_TRIM_META {
381 return Err(IsccError::InvalidInput(format!(
382 "decoded meta payload exceeds size limit ({} > {META_TRIM_META} bytes)",
383 payload.len()
384 )));
385 }
386 }
387
388 if let Some(ref payload) = meta_payload {
390 let meta_code_digest = soft_hash_meta_v0_with_bytes(&name, payload);
391 let metahash = utils::multi_hash_blake3(payload);
392
393 let meta_code = codec::encode_component(
394 codec::MainType::Meta,
395 codec::SubType::None,
396 codec::Version::V0,
397 bits,
398 &meta_code_digest,
399 )?;
400
401 let meta_value = match meta {
403 Some(meta_str) if meta_str.starts_with("data:") => meta_str.to_string(),
404 Some(meta_str) => {
405 let parsed: serde_json::Value = serde_json::from_str(meta_str)
406 .map_err(|e| IsccError::InvalidInput(format!("invalid JSON: {e}")))?;
407 build_meta_data_url(payload, &parsed)
408 }
409 None => unreachable!(),
410 };
411
412 Ok(MetaCodeResult {
413 iscc: format!("ISCC:{meta_code}"),
414 name: name.clone(),
415 description: if desc_clean.is_empty() {
416 None
417 } else {
418 Some(desc_clean)
419 },
420 meta: Some(meta_value),
421 metahash,
422 })
423 } else {
424 let payload = if desc_clean.is_empty() {
426 name.clone()
427 } else {
428 format!("{name} {desc_clean}")
429 };
430 let payload = payload.trim().to_string();
431 let metahash = utils::multi_hash_blake3(payload.as_bytes());
432
433 let extra = if desc_clean.is_empty() {
435 None
436 } else {
437 Some(desc_clean.as_str())
438 };
439 let meta_code_digest = soft_hash_meta_v0(&name, extra);
440
441 let meta_code = codec::encode_component(
442 codec::MainType::Meta,
443 codec::SubType::None,
444 codec::Version::V0,
445 bits,
446 &meta_code_digest,
447 )?;
448
449 Ok(MetaCodeResult {
450 iscc: format!("ISCC:{meta_code}"),
451 name: name.clone(),
452 description: if desc_clean.is_empty() {
453 None
454 } else {
455 Some(desc_clean)
456 },
457 meta: None,
458 metahash,
459 })
460 }
461}
462
463#[cfg(feature = "text-processing")]
468fn soft_hash_text_v0(text: &str) -> Vec<u8> {
469 let ngrams = simhash::sliding_window_strs(text, TEXT_NGRAM_SIZE);
470 let features: Vec<u32> = ngrams
471 .iter()
472 .map(|ng| xxhash_rust::xxh32::xxh32(ng.as_bytes(), 0))
473 .collect();
474 minhash::alg_minhash_256(&features)
475}
476
477#[cfg(feature = "text-processing")]
483pub fn gen_text_code_v0(text: &str, bits: u32) -> IsccResult<TextCodeResult> {
484 let collapsed = utils::text_collapse(text);
485 let characters = collapsed.chars().count();
486 let hash_digest = soft_hash_text_v0(&collapsed);
487 let component = codec::encode_component(
488 codec::MainType::Content,
489 codec::SubType::TEXT,
490 codec::Version::V0,
491 bits,
492 &hash_digest,
493 )?;
494 Ok(TextCodeResult {
495 iscc: format!("ISCC:{component}"),
496 characters,
497 })
498}
499
500fn transpose_matrix(matrix: &[Vec<f64>]) -> Vec<Vec<f64>> {
502 let rows = matrix.len();
503 if rows == 0 {
504 return vec![];
505 }
506 let cols = matrix[0].len();
507 let mut result = vec![vec![0.0f64; rows]; cols];
508 for (r, row) in matrix.iter().enumerate() {
509 for (c, &val) in row.iter().enumerate() {
510 result[c][r] = val;
511 }
512 }
513 result
514}
515
516fn flatten_8x8(matrix: &[Vec<f64>], col: usize, row: usize) -> Vec<f64> {
521 let mut flat = Vec::with_capacity(64);
522 for matrix_row in matrix.iter().skip(row).take(8) {
523 for &val in matrix_row.iter().skip(col).take(8) {
524 flat.push(val);
525 }
526 }
527 flat
528}
529
530fn compute_median(values: &[f64]) -> f64 {
535 let mut sorted: Vec<f64> = values.to_vec();
536 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
537 let n = sorted.len();
538 if n % 2 == 1 {
539 sorted[n / 2]
540 } else {
541 (sorted[n / 2 - 1] + sorted[n / 2]) / 2.0
542 }
543}
544
545fn bits_to_bytes(bits: &[bool]) -> Vec<u8> {
547 bits.chunks(8)
548 .map(|chunk| {
549 let mut byte = 0u8;
550 for (i, &bit) in chunk.iter().enumerate() {
551 if bit {
552 byte |= 1 << (7 - i);
553 }
554 }
555 byte
556 })
557 .collect()
558}
559
560fn soft_hash_image_v0(pixels: &[u8], bits: u32) -> IsccResult<Vec<u8>> {
566 if pixels.len() != 1024 {
567 return Err(IsccError::InvalidInput(format!(
568 "expected 1024 pixels, got {}",
569 pixels.len()
570 )));
571 }
572 if bits > 256 {
573 return Err(IsccError::InvalidInput(format!(
574 "bits must be <= 256, got {bits}"
575 )));
576 }
577
578 let rows: Vec<Vec<f64>> = pixels
580 .chunks(32)
581 .map(|row| {
582 let row_f64: Vec<f64> = row.iter().map(|&p| p as f64).collect();
583 dct::alg_dct(&row_f64)
584 })
585 .collect::<IsccResult<Vec<Vec<f64>>>>()?;
586
587 let transposed = transpose_matrix(&rows);
589
590 let dct_cols: Vec<Vec<f64>> = transposed
592 .iter()
593 .map(|col| dct::alg_dct(col))
594 .collect::<IsccResult<Vec<Vec<f64>>>>()?;
595
596 let dct_matrix = transpose_matrix(&dct_cols);
598
599 let positions = [(0, 0), (1, 0), (0, 1), (1, 1)];
601 let mut bitstring = Vec::<bool>::with_capacity(256);
602
603 for (col, row) in positions {
604 let flat = flatten_8x8(&dct_matrix, col, row);
605 let median = compute_median(&flat);
606 for val in &flat {
607 bitstring.push(*val > median);
608 }
609 if bitstring.len() >= bits as usize {
610 break;
611 }
612 }
613
614 Ok(bits_to_bytes(&bitstring[..bits as usize]))
616}
617
618pub fn gen_image_code_v0(pixels: &[u8], bits: u32) -> IsccResult<ImageCodeResult> {
624 let hash_digest = soft_hash_image_v0(pixels, bits)?;
625 let component = codec::encode_component(
626 codec::MainType::Content,
627 codec::SubType::Image,
628 codec::Version::V0,
629 bits,
630 &hash_digest,
631 )?;
632 Ok(ImageCodeResult {
633 iscc: format!("ISCC:{component}"),
634 })
635}
636
637fn array_split<T>(slice: &[T], n: usize) -> Vec<&[T]> {
643 if n == 0 {
644 return vec![];
645 }
646 let len = slice.len();
647 let base = len / n;
648 let remainder = len % n;
649 let mut parts = Vec::with_capacity(n);
650 let mut offset = 0;
651 for i in 0..n {
652 let size = base + if i < remainder { 1 } else { 0 };
653 parts.push(&slice[offset..offset + size]);
654 offset += size;
655 }
656 parts
657}
658
659fn soft_hash_audio_v0(cv: &[i32]) -> Vec<u8> {
666 let digests: Vec<[u8; 4]> = cv.iter().map(|&v| v.to_be_bytes()).collect();
668
669 if digests.is_empty() {
670 return vec![0u8; 32];
671 }
672
673 let mut parts: Vec<u8> = simhash::alg_simhash_inner(&digests);
675
676 let quarters = array_split(&digests, 4);
678 for quarter in &quarters {
679 if quarter.is_empty() {
680 parts.extend_from_slice(&[0u8; 4]);
681 } else {
682 parts.extend_from_slice(&simhash::alg_simhash_inner(quarter));
683 }
684 }
685
686 let mut sorted_values: Vec<i32> = cv.to_vec();
688 sorted_values.sort();
689 let sorted_digests: Vec<[u8; 4]> = sorted_values.iter().map(|&v| v.to_be_bytes()).collect();
690 let thirds = array_split(&sorted_digests, 3);
691 for third in &thirds {
692 if third.is_empty() {
693 parts.extend_from_slice(&[0u8; 4]);
694 } else {
695 parts.extend_from_slice(&simhash::alg_simhash_inner(third));
696 }
697 }
698
699 parts
700}
701
702pub fn gen_audio_code_v0(cv: &[i32], bits: u32) -> IsccResult<AudioCodeResult> {
707 let hash_digest = soft_hash_audio_v0(cv);
708 let component = codec::encode_component(
709 codec::MainType::Content,
710 codec::SubType::Audio,
711 codec::Version::V0,
712 bits,
713 &hash_digest,
714 )?;
715 Ok(AudioCodeResult {
716 iscc: format!("ISCC:{component}"),
717 })
718}
719
720pub fn soft_hash_video_v0<S: AsRef<[i32]> + Ord>(
725 frame_sigs: &[S],
726 bits: u32,
727) -> IsccResult<Vec<u8>> {
728 if frame_sigs.is_empty() {
729 return Err(IsccError::InvalidInput(
730 "frame_sigs must not be empty".into(),
731 ));
732 }
733
734 let unique: std::collections::BTreeSet<&S> = frame_sigs.iter().collect();
736
737 let cols = frame_sigs[0].as_ref().len();
739 let mut vecsum = vec![0i64; cols];
740 for sig in &unique {
741 for (c, &val) in sig.as_ref().iter().enumerate() {
742 vecsum[c] += val as i64;
743 }
744 }
745
746 wtahash::alg_wtahash(&vecsum, bits)
747}
748
749pub fn gen_video_code_v0<S: AsRef<[i32]> + Ord>(
754 frame_sigs: &[S],
755 bits: u32,
756) -> IsccResult<VideoCodeResult> {
757 let digest = soft_hash_video_v0(frame_sigs, bits)?;
758 let component = codec::encode_component(
759 codec::MainType::Content,
760 codec::SubType::Video,
761 codec::Version::V0,
762 bits,
763 &digest,
764 )?;
765 Ok(VideoCodeResult {
766 iscc: format!("ISCC:{component}"),
767 })
768}
769
770fn soft_hash_codes_v0(cc_digests: &[Vec<u8>], bits: u32) -> IsccResult<Vec<u8>> {
777 if cc_digests.len() < 2 {
778 return Err(IsccError::InvalidInput(
779 "at least 2 Content-Codes required for mixing".into(),
780 ));
781 }
782
783 let nbytes = (bits / 8) as usize;
784 let mut prepared: Vec<Vec<u8>> = Vec::with_capacity(cc_digests.len());
785
786 for raw in cc_digests {
787 let (mtype, stype, _ver, blen, body) = codec::decode_header(raw)?;
788 if mtype != codec::MainType::Content {
789 return Err(IsccError::InvalidInput(
790 "all codes must be Content-Codes".into(),
791 ));
792 }
793 let unit_bits = codec::decode_length(mtype, blen, stype);
794 if unit_bits < bits {
795 return Err(IsccError::InvalidInput(format!(
796 "Content-Code too short for {bits}-bit length (has {unit_bits} bits)"
797 )));
798 }
799 let mut entry = Vec::with_capacity(nbytes);
800 entry.push(raw[0]); let take = std::cmp::min(nbytes - 1, body.len());
802 entry.extend_from_slice(&body[..take]);
803 while entry.len() < nbytes {
805 entry.push(0);
806 }
807 prepared.push(entry);
808 }
809
810 Ok(simhash::alg_simhash_inner(&prepared))
811}
812
813pub fn gen_mixed_code_v0(codes: &[&str], bits: u32) -> IsccResult<MixedCodeResult> {
819 let decoded: Vec<Vec<u8>> = codes
820 .iter()
821 .map(|code| {
822 let clean = codec::iscc_clean(code)?;
823 codec::decode_base32(&clean)
824 })
825 .collect::<IsccResult<Vec<Vec<u8>>>>()?;
826
827 let digest = soft_hash_codes_v0(&decoded, bits)?;
828
829 let component = codec::encode_component(
830 codec::MainType::Content,
831 codec::SubType::Mixed,
832 codec::Version::V0,
833 bits,
834 &digest,
835 )?;
836
837 Ok(MixedCodeResult {
838 iscc: format!("ISCC:{component}"),
839 parts: codes.iter().map(|s| s.to_string()).collect(),
840 })
841}
842
843pub fn gen_data_code_v0(data: &[u8], bits: u32) -> IsccResult<DataCodeResult> {
849 let chunks = cdc::alg_cdc_chunks_unchecked(data, false, cdc::DATA_AVG_CHUNK_SIZE);
850 let mut features: Vec<u32> = chunks
851 .iter()
852 .map(|chunk| xxhash_rust::xxh32::xxh32(chunk, 0))
853 .collect();
854
855 if features.is_empty() {
857 features.push(xxhash_rust::xxh32::xxh32(b"", 0));
858 }
859
860 let digest = minhash::alg_minhash_256(&features);
861 let component = codec::encode_component(
862 codec::MainType::Data,
863 codec::SubType::None,
864 codec::Version::V0,
865 bits,
866 &digest,
867 )?;
868
869 Ok(DataCodeResult {
870 iscc: format!("ISCC:{component}"),
871 })
872}
873
874pub fn gen_instance_code_v0(data: &[u8], bits: u32) -> IsccResult<InstanceCodeResult> {
879 let digest = blake3::hash(data);
880 let datahash = utils::multi_hash_blake3(data);
881 let filesize = data.len() as u64;
882 let component = codec::encode_component(
883 codec::MainType::Instance,
884 codec::SubType::None,
885 codec::Version::V0,
886 bits,
887 digest.as_bytes(),
888 )?;
889 Ok(InstanceCodeResult {
890 iscc: format!("ISCC:{component}"),
891 datahash,
892 filesize,
893 })
894}
895
896pub fn gen_iscc_code_v0(codes: &[&str], wide: bool) -> IsccResult<IsccCodeResult> {
905 let cleaned: Vec<std::borrow::Cow<'_, str>> = codes
907 .iter()
908 .map(|c| codec::iscc_clean(c))
909 .collect::<IsccResult<Vec<_>>>()?;
910
911 if cleaned.len() < 2 {
913 return Err(IsccError::InvalidInput(
914 "at least 2 ISCC unit codes required".into(),
915 ));
916 }
917
918 for code in &cleaned {
920 if code.len() < 16 {
921 return Err(IsccError::InvalidInput(format!(
922 "ISCC unit code too short (min 16 chars): {code}"
923 )));
924 }
925 }
926
927 let mut decoded: Vec<(
929 codec::MainType,
930 codec::SubType,
931 codec::Version,
932 u32,
933 Vec<u8>,
934 )> = Vec::with_capacity(cleaned.len());
935 for code in &cleaned {
936 let raw = codec::decode_base32(code)?;
937 let header = codec::decode_header(&raw)?;
938 decoded.push(header);
939 }
940
941 decoded.sort_by_key(|&(mt, ..)| mt);
943
944 let main_types: Vec<codec::MainType> = decoded.iter().map(|&(mt, ..)| mt).collect();
946
947 let n = main_types.len();
949 if main_types[n - 2] != codec::MainType::Data || main_types[n - 1] != codec::MainType::Instance
950 {
951 return Err(IsccError::InvalidInput(
952 "Data-Code and Instance-Code are mandatory".into(),
953 ));
954 }
955
956 let is_wide = wide
958 && decoded.len() == 2
959 && main_types == [codec::MainType::Data, codec::MainType::Instance]
960 && decoded
961 .iter()
962 .all(|&(mt, st, _, len, _)| codec::decode_length(mt, len, st) >= 128);
963
964 let st = if is_wide {
966 codec::SubType::Wide
967 } else {
968 let sc_subtypes: Vec<codec::SubType> = decoded
970 .iter()
971 .filter(|&&(mt, ..)| mt == codec::MainType::Semantic || mt == codec::MainType::Content)
972 .map(|&(_, st, ..)| st)
973 .collect();
974
975 if !sc_subtypes.is_empty() {
976 let first = sc_subtypes[0];
978 if sc_subtypes.iter().all(|&s| s == first) {
979 first
980 } else {
981 return Err(IsccError::InvalidInput(
982 "mixed SubTypes among Content/Semantic units".into(),
983 ));
984 }
985 } else if decoded.len() == 2 {
986 codec::SubType::Sum
987 } else {
988 codec::SubType::IsccNone
989 }
990 };
991
992 let optional_types = &main_types[..n - 2];
994 let encoded_length = codec::encode_units(optional_types)?;
995
996 let bytes_per_unit = if is_wide { 16 } else { 8 };
998 let mut digest = Vec::with_capacity(decoded.len() * bytes_per_unit);
999 for (_, _, _, _, tail) in &decoded {
1000 let take = bytes_per_unit.min(tail.len());
1001 digest.extend_from_slice(&tail[..take]);
1002 }
1003
1004 let header = codec::encode_header(
1006 codec::MainType::Iscc,
1007 st,
1008 codec::Version::V0,
1009 encoded_length,
1010 )?;
1011 let mut code_bytes = header;
1012 code_bytes.extend_from_slice(&digest);
1013 let code = codec::encode_base32(&code_bytes);
1014
1015 Ok(IsccCodeResult {
1017 iscc: format!("ISCC:{code}"),
1018 })
1019}
1020
1021pub fn gen_sum_code_v0(
1032 path: &std::path::Path,
1033 bits: u32,
1034 wide: bool,
1035 add_units: bool,
1036) -> IsccResult<SumCodeResult> {
1037 use std::io::Read;
1038
1039 let mut file = std::fs::File::open(path)
1040 .map_err(|e| IsccError::InvalidInput(format!("Cannot open file: {e}")))?;
1041
1042 let mut hasher = streaming::SumHasher::new();
1043
1044 let mut buf = vec![0u8; IO_READ_SIZE];
1045 loop {
1046 let n = file
1047 .read(&mut buf)
1048 .map_err(|e| IsccError::InvalidInput(format!("Cannot read file: {e}")))?;
1049 if n == 0 {
1050 break;
1051 }
1052 hasher.update(&buf[..n]);
1053 }
1054
1055 hasher.finalize(bits, wide, add_units)
1056}
1057
1058pub fn gen_iscc_id_v1(timestamp: u64, hub_id: u16, realm: u8) -> IsccResult<IsccIdResult> {
1077 if timestamp >= (1u64 << 52) {
1078 return Err(IsccError::InvalidInput("Timestamp overflow".into()));
1079 }
1080 if hub_id >= (1u16 << 12) {
1081 return Err(IsccError::InvalidInput("HUB-ID overflow".into()));
1082 }
1083 if realm != 0 && realm != 1 {
1084 return Err(IsccError::InvalidInput(
1085 "Realm-ID must be 0 (test) or 1 (operational)".into(),
1086 ));
1087 }
1088
1089 let body = (timestamp << 12) | u64::from(hub_id);
1091 let digest = body.to_be_bytes();
1092
1093 let component = codec::encode_component(
1094 codec::MainType::Id,
1095 codec::SubType::try_from(realm)?,
1096 codec::Version::V1,
1097 64,
1098 &digest,
1099 )?;
1100 Ok(IsccIdResult {
1101 iscc: format!("ISCC:{component}"),
1102 })
1103}
1104
1105#[cfg(test)]
1106mod tests {
1107 use super::*;
1108
1109 #[test]
1110 fn test_gen_iscc_id_v1_golden() {
1111 let result = gen_iscc_id_v1(1751831876325218, 1, 0).unwrap();
1113 assert_eq!(result.iscc, "ISCC:MAIGHFECJMOPMIAB");
1114 }
1115
1116 #[test]
1117 fn test_gen_iscc_id_v1_round_trip() {
1118 let max_ts = (1u64 << 52) - 1;
1120 for realm in [0u8, 1u8] {
1121 for hub_id in [0u16, 4095u16] {
1122 for timestamp in [0u64, max_ts] {
1123 let result = gen_iscc_id_v1(timestamp, hub_id, realm).unwrap();
1124 let (mt, st, vs, li, digest) = iscc_decode(&result.iscc).unwrap();
1125 assert_eq!(mt, 6, "MainType Id");
1126 assert_eq!(st, realm, "SubType is realm");
1127 assert_eq!(vs, 1, "Version V1");
1128 assert_eq!(li, 0, "64-bit length index");
1129 assert_eq!(digest.len(), 8, "8-byte body");
1130 let body = u64::from_be_bytes(digest.try_into().unwrap());
1131 assert_eq!(body >> 12, timestamp, "timestamp in high 52 bits");
1132 assert_eq!(body & 0xFFF, u64::from(hub_id), "hub_id in low 12 bits");
1133 }
1134 }
1135 }
1136 }
1137
1138 #[test]
1139 fn test_gen_iscc_id_v1_validation_ordering() {
1140 let err = gen_iscc_id_v1(1u64 << 52, 4096, 2).unwrap_err();
1142 assert_eq!(err.to_string(), "invalid input: Timestamp overflow");
1143
1144 let err = gen_iscc_id_v1(0, 1u16 << 12, 2).unwrap_err();
1146 assert_eq!(err.to_string(), "invalid input: HUB-ID overflow");
1147
1148 let err = gen_iscc_id_v1(0, 0, 2).unwrap_err();
1150 assert_eq!(
1151 err.to_string(),
1152 "invalid input: Realm-ID must be 0 (test) or 1 (operational)"
1153 );
1154 }
1155
1156 #[cfg(feature = "meta-code")]
1157 #[test]
1158 fn test_gen_meta_code_v0_title_only() {
1159 let result = gen_meta_code_v0("Die Unendliche Geschichte", None, None, 64).unwrap();
1160 assert_eq!(result.iscc, "ISCC:AAAZXZ6OU74YAZIM");
1161 assert_eq!(result.name, "Die Unendliche Geschichte");
1162 assert_eq!(result.description, None);
1163 assert_eq!(result.meta, None);
1164 }
1165
1166 #[cfg(feature = "meta-code")]
1167 #[test]
1168 fn test_gen_meta_code_v0_title_description() {
1169 let result = gen_meta_code_v0(
1170 "Die Unendliche Geschichte",
1171 Some("Von Michael Ende"),
1172 None,
1173 64,
1174 )
1175 .unwrap();
1176 assert_eq!(result.iscc, "ISCC:AAAZXZ6OU4E45RB5");
1177 assert_eq!(result.name, "Die Unendliche Geschichte");
1178 assert_eq!(result.description, Some("Von Michael Ende".to_string()));
1179 assert_eq!(result.meta, None);
1180 }
1181
1182 #[cfg(feature = "meta-code")]
1183 #[test]
1184 fn test_gen_meta_code_v0_json_meta() {
1185 let result = gen_meta_code_v0("Hello", None, Some(r#"{"some":"object"}"#), 64).unwrap();
1186 assert_eq!(result.iscc, "ISCC:AAAWKLHFXN63LHL2");
1187 assert!(result.meta.is_some());
1188 assert!(
1189 result
1190 .meta
1191 .unwrap()
1192 .starts_with("data:application/json;base64,")
1193 );
1194 }
1195
1196 #[cfg(feature = "meta-code")]
1197 #[test]
1198 fn test_gen_meta_code_v0_data_url_meta() {
1199 let result = gen_meta_code_v0(
1200 "Hello",
1201 None,
1202 Some("data:application/json;charset=utf-8;base64,eyJzb21lIjogIm9iamVjdCJ9"),
1203 64,
1204 )
1205 .unwrap();
1206 assert_eq!(result.iscc, "ISCC:AAAWKLHFXN43ICP2");
1207 assert_eq!(
1209 result.meta,
1210 Some("data:application/json;charset=utf-8;base64,eyJzb21lIjogIm9iamVjdCJ9".to_string())
1211 );
1212 }
1213
1214 #[cfg(feature = "meta-code")]
1220 #[test]
1221 fn test_gen_meta_code_v0_jcs_float_canonicalization() {
1222 let result = gen_meta_code_v0("Test", None, Some(r#"{"value":1.0}"#), 64).unwrap();
1225
1226 assert_eq!(
1228 result.iscc, "ISCC:AAAX4GX3RZH2I6QZ",
1229 "ISCC mismatch: parse_meta_json must use RFC 8785 (JCS) canonicalization"
1230 );
1231 assert_eq!(
1232 result.meta,
1233 Some("data:application/json;base64,eyJ2YWx1ZSI6MX0=".to_string()),
1234 "meta Data-URL mismatch: JCS should serialize 1.0 as 1"
1235 );
1236 assert_eq!(
1237 result.metahash, "1e2010b291d392b6999ffe4aa4661fb343fc371fca3bfb5bb4e8d8226fdf85743232",
1238 "metahash mismatch: canonical bytes differ between JCS and serde_json"
1239 );
1240 }
1241
1242 #[cfg(feature = "meta-code")]
1247 #[test]
1248 fn test_gen_meta_code_v0_jcs_large_float_canonicalization() {
1249 let result = gen_meta_code_v0("Test", None, Some(r#"{"value":1e20}"#), 64).unwrap();
1250
1251 assert_eq!(
1252 result.iscc, "ISCC:AAAX4GX3R32YH5P7",
1253 "ISCC mismatch: JCS should expand 1e20 to 100000000000000000000"
1254 );
1255 assert_eq!(
1256 result.meta,
1257 Some(
1258 "data:application/json;base64,eyJ2YWx1ZSI6MTAwMDAwMDAwMDAwMDAwMDAwMDAwfQ=="
1259 .to_string()
1260 ),
1261 "meta Data-URL mismatch: JCS should expand large float to integer form"
1262 );
1263 assert_eq!(
1264 result.metahash, "1e201ff83c1822c348717658a0b4713739646da7c59832691b337a457416ddd1c73d",
1265 "metahash mismatch: canonical bytes differ for large float"
1266 );
1267 }
1268
1269 #[cfg(feature = "meta-code")]
1270 #[test]
1271 fn test_gen_meta_code_v0_invalid_json() {
1272 assert!(matches!(
1273 gen_meta_code_v0("test", None, Some("not json"), 64),
1274 Err(IsccError::InvalidInput(_))
1275 ));
1276 }
1277
1278 #[cfg(feature = "meta-code")]
1279 #[test]
1280 fn test_gen_meta_code_v0_invalid_data_url() {
1281 assert!(matches!(
1282 gen_meta_code_v0("test", None, Some("data:no-comma-here"), 64),
1283 Err(IsccError::InvalidInput(_))
1284 ));
1285 }
1286
1287 #[cfg(feature = "meta-code")]
1288 #[test]
1289 fn test_gen_meta_code_v0_conformance() {
1290 let json_str = include_str!("../tests/data.json");
1291 let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
1292 let section = &data["gen_meta_code_v0"];
1293 let cases = section.as_object().unwrap();
1294
1295 let mut tested = 0;
1296
1297 for (tc_name, tc) in cases {
1298 let inputs = tc["inputs"].as_array().unwrap();
1299 let input_name = inputs[0].as_str().unwrap();
1300 let input_desc = inputs[1].as_str().unwrap();
1301 let meta_val = &inputs[2];
1302 let bits = inputs[3].as_u64().unwrap() as u32;
1303
1304 let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();
1305 let expected_metahash = tc["outputs"]["metahash"].as_str().unwrap();
1306
1307 let meta_arg: Option<String> = match meta_val {
1309 serde_json::Value::Null => None,
1310 serde_json::Value::String(s) => Some(s.clone()),
1311 serde_json::Value::Object(_) => Some(serde_json::to_string(meta_val).unwrap()),
1312 other => panic!("unexpected meta type in {tc_name}: {other:?}"),
1313 };
1314
1315 let desc = if input_desc.is_empty() {
1316 None
1317 } else {
1318 Some(input_desc)
1319 };
1320
1321 let result = gen_meta_code_v0(input_name, desc, meta_arg.as_deref(), bits)
1323 .unwrap_or_else(|e| panic!("gen_meta_code_v0 failed for {tc_name}: {e}"));
1324 assert_eq!(
1325 result.iscc, expected_iscc,
1326 "ISCC mismatch in test case {tc_name}"
1327 );
1328
1329 assert_eq!(
1331 result.metahash, expected_metahash,
1332 "metahash mismatch in test case {tc_name}"
1333 );
1334
1335 if let Some(expected_name) = tc["outputs"].get("name") {
1337 let expected_name = expected_name.as_str().unwrap();
1338 assert_eq!(
1339 result.name, expected_name,
1340 "name mismatch in test case {tc_name}"
1341 );
1342 }
1343
1344 if let Some(expected_desc) = tc["outputs"].get("description") {
1346 let expected_desc = expected_desc.as_str().unwrap();
1347 assert_eq!(
1348 result.description.as_deref(),
1349 Some(expected_desc),
1350 "description mismatch in test case {tc_name}"
1351 );
1352 }
1353
1354 if meta_arg.is_some() {
1356 assert!(
1357 result.meta.is_some(),
1358 "meta should be present in test case {tc_name}"
1359 );
1360 } else {
1361 assert!(
1362 result.meta.is_none(),
1363 "meta should be absent in test case {tc_name}"
1364 );
1365 }
1366
1367 tested += 1;
1368 }
1369
1370 assert_eq!(tested, 20, "expected 20 conformance tests to run");
1371 }
1372
1373 #[cfg(feature = "text-processing")]
1374 #[test]
1375 fn test_gen_text_code_v0_empty() {
1376 let result = gen_text_code_v0("", 64).unwrap();
1377 assert_eq!(result.iscc, "ISCC:EAASL4F2WZY7KBXB");
1378 assert_eq!(result.characters, 0);
1379 }
1380
1381 #[cfg(feature = "text-processing")]
1382 #[test]
1383 fn test_gen_text_code_v0_hello_world() {
1384 let result = gen_text_code_v0("Hello World", 64).unwrap();
1385 assert_eq!(result.iscc, "ISCC:EAASKDNZNYGUUF5A");
1386 assert_eq!(result.characters, 10); }
1388
1389 #[cfg(feature = "text-processing")]
1390 #[test]
1391 fn test_gen_text_code_v0_conformance() {
1392 let json_str = include_str!("../tests/data.json");
1393 let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
1394 let section = &data["gen_text_code_v0"];
1395 let cases = section.as_object().unwrap();
1396
1397 let mut tested = 0;
1398
1399 for (tc_name, tc) in cases {
1400 let inputs = tc["inputs"].as_array().unwrap();
1401 let input_text = inputs[0].as_str().unwrap();
1402 let bits = inputs[1].as_u64().unwrap() as u32;
1403
1404 let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();
1405 let expected_chars = tc["outputs"]["characters"].as_u64().unwrap() as usize;
1406
1407 let result = gen_text_code_v0(input_text, bits)
1409 .unwrap_or_else(|e| panic!("gen_text_code_v0 failed for {tc_name}: {e}"));
1410 assert_eq!(
1411 result.iscc, expected_iscc,
1412 "ISCC mismatch in test case {tc_name}"
1413 );
1414
1415 assert_eq!(
1417 result.characters, expected_chars,
1418 "character count mismatch in test case {tc_name}"
1419 );
1420
1421 tested += 1;
1422 }
1423
1424 assert_eq!(tested, 5, "expected 5 conformance tests to run");
1425 }
1426
1427 #[test]
1428 fn test_gen_image_code_v0_all_black() {
1429 let pixels = vec![0u8; 1024];
1430 let result = gen_image_code_v0(&pixels, 64).unwrap();
1431 assert_eq!(result.iscc, "ISCC:EEAQAAAAAAAAAAAA");
1432 }
1433
1434 #[test]
1435 fn test_gen_image_code_v0_all_white() {
1436 let pixels = vec![255u8; 1024];
1437 let result = gen_image_code_v0(&pixels, 128).unwrap();
1438 assert_eq!(result.iscc, "ISCC:EEBYAAAAAAAAAAAAAAAAAAAAAAAAA");
1439 }
1440
1441 #[test]
1442 fn test_gen_image_code_v0_invalid_pixel_count() {
1443 assert!(gen_image_code_v0(&[0u8; 100], 64).is_err());
1444 }
1445
1446 #[test]
1447 fn test_gen_image_code_v0_conformance() {
1448 let json_str = include_str!("../tests/data.json");
1449 let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
1450 let section = &data["gen_image_code_v0"];
1451 let cases = section.as_object().unwrap();
1452
1453 let mut tested = 0;
1454
1455 for (tc_name, tc) in cases {
1456 let inputs = tc["inputs"].as_array().unwrap();
1457 let pixels_json = inputs[0].as_array().unwrap();
1458 let bits = inputs[1].as_u64().unwrap() as u32;
1459 let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();
1460
1461 let pixels: Vec<u8> = pixels_json
1462 .iter()
1463 .map(|v| v.as_u64().unwrap() as u8)
1464 .collect();
1465
1466 let result = gen_image_code_v0(&pixels, bits)
1467 .unwrap_or_else(|e| panic!("gen_image_code_v0 failed for {tc_name}: {e}"));
1468 assert_eq!(
1469 result.iscc, expected_iscc,
1470 "ISCC mismatch in test case {tc_name}"
1471 );
1472
1473 tested += 1;
1474 }
1475
1476 assert_eq!(tested, 3, "expected 3 conformance tests to run");
1477 }
1478
1479 #[test]
1480 fn test_gen_audio_code_v0_empty() {
1481 let result = gen_audio_code_v0(&[], 64).unwrap();
1482 assert_eq!(result.iscc, "ISCC:EIAQAAAAAAAAAAAA");
1483 }
1484
1485 #[test]
1486 fn test_gen_audio_code_v0_single() {
1487 let result = gen_audio_code_v0(&[1], 128).unwrap();
1488 assert_eq!(result.iscc, "ISCC:EIBQAAAAAEAAAAABAAAAAAAAAAAAA");
1489 }
1490
1491 #[test]
1492 fn test_gen_audio_code_v0_negative() {
1493 let result = gen_audio_code_v0(&[-1, 0, 1], 256).unwrap();
1494 assert_eq!(
1495 result.iscc,
1496 "ISCC:EIDQAAAAAH777777AAAAAAAAAAAACAAAAAAP777774AAAAAAAAAAAAI"
1497 );
1498 }
1499
1500 #[test]
1501 fn test_gen_audio_code_v0_conformance() {
1502 let json_str = include_str!("../tests/data.json");
1503 let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
1504 let section = &data["gen_audio_code_v0"];
1505 let cases = section.as_object().unwrap();
1506
1507 let mut tested = 0;
1508
1509 for (tc_name, tc) in cases {
1510 let inputs = tc["inputs"].as_array().unwrap();
1511 let cv_json = inputs[0].as_array().unwrap();
1512 let bits = inputs[1].as_u64().unwrap() as u32;
1513 let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();
1514
1515 let cv: Vec<i32> = cv_json.iter().map(|v| v.as_i64().unwrap() as i32).collect();
1516
1517 let result = gen_audio_code_v0(&cv, bits)
1518 .unwrap_or_else(|e| panic!("gen_audio_code_v0 failed for {tc_name}: {e}"));
1519 assert_eq!(
1520 result.iscc, expected_iscc,
1521 "ISCC mismatch in test case {tc_name}"
1522 );
1523
1524 tested += 1;
1525 }
1526
1527 assert_eq!(tested, 5, "expected 5 conformance tests to run");
1528 }
1529
1530 #[test]
1531 fn test_array_split_even() {
1532 let data = vec![1, 2, 3, 4];
1533 let parts = array_split(&data, 4);
1534 assert_eq!(parts, vec![&[1][..], &[2][..], &[3][..], &[4][..]]);
1535 }
1536
1537 #[test]
1538 fn test_array_split_remainder() {
1539 let data = vec![1, 2, 3, 4, 5];
1540 let parts = array_split(&data, 3);
1541 assert_eq!(parts, vec![&[1, 2][..], &[3, 4][..], &[5][..]]);
1542 }
1543
1544 #[test]
1545 fn test_array_split_more_parts_than_elements() {
1546 let data = vec![1, 2];
1547 let parts = array_split(&data, 4);
1548 assert_eq!(
1549 parts,
1550 vec![&[1][..], &[2][..], &[][..] as &[i32], &[][..] as &[i32]]
1551 );
1552 }
1553
1554 #[test]
1555 fn test_array_split_empty() {
1556 let data: Vec<i32> = vec![];
1557 let parts = array_split(&data, 3);
1558 assert_eq!(
1559 parts,
1560 vec![&[][..] as &[i32], &[][..] as &[i32], &[][..] as &[i32]]
1561 );
1562 }
1563
1564 #[test]
1565 fn test_gen_video_code_v0_empty_frames() {
1566 let frames: Vec<Vec<i32>> = vec![];
1567 assert!(matches!(
1568 gen_video_code_v0(&frames, 64),
1569 Err(IsccError::InvalidInput(_))
1570 ));
1571 }
1572
1573 #[test]
1574 fn test_gen_video_code_v0_conformance() {
1575 let json_str = include_str!("../tests/data.json");
1576 let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
1577 let section = &data["gen_video_code_v0"];
1578 let cases = section.as_object().unwrap();
1579
1580 let mut tested = 0;
1581
1582 for (tc_name, tc) in cases {
1583 let inputs = tc["inputs"].as_array().unwrap();
1584 let frames_json = inputs[0].as_array().unwrap();
1585 let bits = inputs[1].as_u64().unwrap() as u32;
1586 let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();
1587
1588 let frame_sigs: Vec<Vec<i32>> = frames_json
1589 .iter()
1590 .map(|frame| {
1591 frame
1592 .as_array()
1593 .unwrap()
1594 .iter()
1595 .map(|v| v.as_i64().unwrap() as i32)
1596 .collect()
1597 })
1598 .collect();
1599
1600 let result = gen_video_code_v0(&frame_sigs, bits)
1601 .unwrap_or_else(|e| panic!("gen_video_code_v0 failed for {tc_name}: {e}"));
1602 assert_eq!(
1603 result.iscc, expected_iscc,
1604 "ISCC mismatch in test case {tc_name}"
1605 );
1606
1607 tested += 1;
1608 }
1609
1610 assert_eq!(tested, 3, "expected 3 conformance tests to run");
1611 }
1612
1613 #[test]
1614 fn test_gen_mixed_code_v0_conformance() {
1615 let json_str = include_str!("../tests/data.json");
1616 let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
1617 let section = &data["gen_mixed_code_v0"];
1618 let cases = section.as_object().unwrap();
1619
1620 let mut tested = 0;
1621
1622 for (tc_name, tc) in cases {
1623 let inputs = tc["inputs"].as_array().unwrap();
1624 let codes_json = inputs[0].as_array().unwrap();
1625 let bits = inputs[1].as_u64().unwrap() as u32;
1626 let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();
1627 let expected_parts: Vec<&str> = tc["outputs"]["parts"]
1628 .as_array()
1629 .unwrap()
1630 .iter()
1631 .map(|v| v.as_str().unwrap())
1632 .collect();
1633
1634 let codes: Vec<&str> = codes_json.iter().map(|v| v.as_str().unwrap()).collect();
1635
1636 let result = gen_mixed_code_v0(&codes, bits)
1637 .unwrap_or_else(|e| panic!("gen_mixed_code_v0 failed for {tc_name}: {e}"));
1638 assert_eq!(
1639 result.iscc, expected_iscc,
1640 "ISCC mismatch in test case {tc_name}"
1641 );
1642
1643 let result_parts: Vec<&str> = result.parts.iter().map(|s| s.as_str()).collect();
1645 assert_eq!(
1646 result_parts, expected_parts,
1647 "parts mismatch in test case {tc_name}"
1648 );
1649
1650 tested += 1;
1651 }
1652
1653 assert_eq!(tested, 2, "expected 2 conformance tests to run");
1654 }
1655
1656 #[test]
1657 fn test_gen_mixed_code_v0_too_few_codes() {
1658 assert!(matches!(
1659 gen_mixed_code_v0(&["EUA6GIKXN42IQV3S"], 64),
1660 Err(IsccError::InvalidInput(_))
1661 ));
1662 }
1663
1664 fn make_content_code_raw(stype: codec::SubType, bit_length: u32) -> Vec<u8> {
1666 let nbytes = (bit_length / 8) as usize;
1667 let body: Vec<u8> = (0..nbytes).map(|i| (i & 0xFF) as u8).collect();
1668 let base32 = codec::encode_component(
1669 codec::MainType::Content,
1670 stype,
1671 codec::Version::V0,
1672 bit_length,
1673 &body,
1674 )
1675 .unwrap();
1676 codec::decode_base32(&base32).unwrap()
1677 }
1678
1679 #[test]
1680 fn test_soft_hash_codes_v0_rejects_short_code() {
1681 let code_64 = make_content_code_raw(codec::SubType::None, 64);
1683 let code_32 = make_content_code_raw(codec::SubType::Image, 32);
1684 let result = soft_hash_codes_v0(&[code_64, code_32], 64);
1685 assert!(
1686 matches!(&result, Err(IsccError::InvalidInput(msg)) if msg.contains("too short")),
1687 "expected InvalidInput with 'too short', got {result:?}"
1688 );
1689 }
1690
1691 #[test]
1692 fn test_soft_hash_codes_v0_accepts_exact_length() {
1693 let code_a = make_content_code_raw(codec::SubType::None, 64);
1695 let code_b = make_content_code_raw(codec::SubType::Image, 64);
1696 let result = soft_hash_codes_v0(&[code_a, code_b], 64);
1697 assert!(result.is_ok(), "expected Ok, got {result:?}");
1698 }
1699
1700 #[test]
1701 fn test_soft_hash_codes_v0_accepts_longer_codes() {
1702 let code_a = make_content_code_raw(codec::SubType::None, 128);
1704 let code_b = make_content_code_raw(codec::SubType::Audio, 128);
1705 let result = soft_hash_codes_v0(&[code_a, code_b], 64);
1706 assert!(result.is_ok(), "expected Ok, got {result:?}");
1707 }
1708
1709 #[test]
1710 fn test_gen_data_code_v0_conformance() {
1711 let json_str = include_str!("../tests/data.json");
1712 let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
1713 let section = &data["gen_data_code_v0"];
1714 let cases = section.as_object().unwrap();
1715
1716 let mut tested = 0;
1717
1718 for (tc_name, tc) in cases {
1719 let inputs = tc["inputs"].as_array().unwrap();
1720 let stream_str = inputs[0].as_str().unwrap();
1721 let bits = inputs[1].as_u64().unwrap() as u32;
1722 let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();
1723
1724 let hex_data = stream_str
1726 .strip_prefix("stream:")
1727 .unwrap_or_else(|| panic!("expected 'stream:' prefix in test case {tc_name}"));
1728 let input_bytes = hex::decode(hex_data)
1729 .unwrap_or_else(|e| panic!("invalid hex in test case {tc_name}: {e}"));
1730
1731 let result = gen_data_code_v0(&input_bytes, bits)
1732 .unwrap_or_else(|e| panic!("gen_data_code_v0 failed for {tc_name}: {e}"));
1733 assert_eq!(
1734 result.iscc, expected_iscc,
1735 "ISCC mismatch in test case {tc_name}"
1736 );
1737
1738 tested += 1;
1739 }
1740
1741 assert_eq!(tested, 4, "expected 4 conformance tests to run");
1742 }
1743
1744 #[test]
1745 fn test_gen_instance_code_v0_empty() {
1746 let result = gen_instance_code_v0(b"", 64).unwrap();
1747 assert_eq!(result.iscc, "ISCC:IAA26E2JXH27TING");
1748 assert_eq!(result.filesize, 0);
1749 assert_eq!(
1750 result.datahash,
1751 "1e20af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"
1752 );
1753 }
1754
1755 #[test]
1756 fn test_gen_instance_code_v0_conformance() {
1757 let json_str = include_str!("../tests/data.json");
1758 let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
1759 let section = &data["gen_instance_code_v0"];
1760 let cases = section.as_object().unwrap();
1761
1762 for (name, tc) in cases {
1763 let inputs = tc["inputs"].as_array().unwrap();
1764 let stream_str = inputs[0].as_str().unwrap();
1765 let bits = inputs[1].as_u64().unwrap() as u32;
1766 let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();
1767
1768 let hex_data = stream_str
1770 .strip_prefix("stream:")
1771 .unwrap_or_else(|| panic!("expected 'stream:' prefix in test case {name}"));
1772 let input_bytes = hex::decode(hex_data)
1773 .unwrap_or_else(|e| panic!("invalid hex in test case {name}: {e}"));
1774
1775 let result = gen_instance_code_v0(&input_bytes, bits)
1776 .unwrap_or_else(|e| panic!("gen_instance_code_v0 failed for {name}: {e}"));
1777 assert_eq!(
1778 result.iscc, expected_iscc,
1779 "ISCC mismatch in test case {name}"
1780 );
1781
1782 if let Some(expected_datahash) = tc["outputs"].get("datahash") {
1784 let expected_datahash = expected_datahash.as_str().unwrap();
1785 assert_eq!(
1786 result.datahash, expected_datahash,
1787 "datahash mismatch in test case {name}"
1788 );
1789 }
1790
1791 if let Some(expected_filesize) = tc["outputs"].get("filesize") {
1793 let expected_filesize = expected_filesize.as_u64().unwrap();
1794 assert_eq!(
1795 result.filesize, expected_filesize,
1796 "filesize mismatch in test case {name}"
1797 );
1798 }
1799
1800 assert_eq!(
1802 result.filesize,
1803 input_bytes.len() as u64,
1804 "filesize should match input length in test case {name}"
1805 );
1806 }
1807 }
1808
1809 #[test]
1810 fn test_gen_iscc_code_v0_conformance() {
1811 let json_str = include_str!("../tests/data.json");
1812 let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
1813 let section = &data["gen_iscc_code_v0"];
1814 let cases = section.as_object().unwrap();
1815
1816 let mut tested = 0;
1817
1818 for (tc_name, tc) in cases {
1819 let inputs = tc["inputs"].as_array().unwrap();
1820 let codes_json = inputs[0].as_array().unwrap();
1821 let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();
1822
1823 let codes: Vec<&str> = codes_json.iter().map(|v| v.as_str().unwrap()).collect();
1824
1825 let result = gen_iscc_code_v0(&codes, false)
1826 .unwrap_or_else(|e| panic!("gen_iscc_code_v0 failed for {tc_name}: {e}"));
1827 assert_eq!(
1828 result.iscc, expected_iscc,
1829 "ISCC mismatch in test case {tc_name}"
1830 );
1831
1832 tested += 1;
1833 }
1834
1835 assert_eq!(tested, 5, "expected 5 conformance tests to run");
1836 }
1837
1838 #[test]
1839 fn test_gen_iscc_code_v0_too_few_codes() {
1840 assert!(matches!(
1841 gen_iscc_code_v0(&["AAAWKLHFPV6OPKDG"], false),
1842 Err(IsccError::InvalidInput(_))
1843 ));
1844 }
1845
1846 #[test]
1847 fn test_gen_iscc_code_v0_missing_instance() {
1848 assert!(matches!(
1850 gen_iscc_code_v0(&["AAAWKLHFPV6OPKDG", "AAAWKLHFPV6OPKDG"], false),
1851 Err(IsccError::InvalidInput(_))
1852 ));
1853 }
1854
1855 #[test]
1856 fn test_gen_iscc_code_v0_short_code() {
1857 assert!(matches!(
1859 gen_iscc_code_v0(&["AAAWKLHFPV6", "AAAWKLHFPV6OPKDG"], false),
1860 Err(IsccError::InvalidInput(_))
1861 ));
1862 }
1863
1864 #[cfg(feature = "meta-code")]
1871 #[test]
1872 fn test_gen_meta_code_empty_data_url_enters_meta_branch() {
1873 let result =
1874 gen_meta_code_v0("Test", None, Some("data:application/json;base64,"), 64).unwrap();
1875
1876 assert_eq!(result.name, "Test");
1878
1879 assert_eq!(
1881 result.meta,
1882 Some("data:application/json;base64,".to_string()),
1883 "empty Data-URL payload should still enter meta branch"
1884 );
1885
1886 let expected_metahash = utils::multi_hash_blake3(&[]);
1888 assert_eq!(
1889 result.metahash, expected_metahash,
1890 "metahash should be BLAKE3 of empty bytes"
1891 );
1892 }
1893
1894 #[cfg(feature = "meta-code")]
1900 #[test]
1901 fn test_soft_hash_meta_v0_with_bytes_empty_equals_name_only() {
1902 let name_only = soft_hash_meta_v0("test", None);
1903 let empty_bytes = soft_hash_meta_v0_with_bytes("test", &[]);
1904 assert_eq!(
1905 name_only, empty_bytes,
1906 "empty bytes should produce same digest as name-only (no interleaving)"
1907 );
1908 }
1909
1910 #[cfg(feature = "meta-code")]
1913 #[test]
1914 fn test_meta_trim_name_value() {
1915 assert_eq!(META_TRIM_NAME, 128);
1916 }
1917
1918 #[cfg(feature = "meta-code")]
1919 #[test]
1920 fn test_meta_trim_description_value() {
1921 assert_eq!(META_TRIM_DESCRIPTION, 4096);
1922 }
1923
1924 #[test]
1925 fn test_io_read_size_value() {
1926 assert_eq!(IO_READ_SIZE, 4_194_304);
1927 }
1928
1929 #[test]
1930 fn test_text_ngram_size_value() {
1931 assert_eq!(TEXT_NGRAM_SIZE, 13);
1932 }
1933
1934 #[test]
1938 fn test_encode_component_matches_codec() {
1939 let digest = [0xABu8; 8];
1940 let tier1 = encode_component(3, 0, 0, 64, &digest).unwrap();
1941 let tier2 = codec::encode_component(
1942 codec::MainType::Data,
1943 codec::SubType::None,
1944 codec::Version::V0,
1945 64,
1946 &digest,
1947 )
1948 .unwrap();
1949 assert_eq!(tier1, tier2);
1950 }
1951
1952 #[test]
1954 fn test_encode_component_round_trip() {
1955 let digest = [0x42u8; 32];
1956 let result = encode_component(0, 0, 0, 64, &digest).unwrap();
1957 assert!(!result.is_empty());
1959 }
1960
1961 #[test]
1963 fn test_encode_component_rejects_iscc() {
1964 let result = encode_component(5, 0, 0, 64, &[0u8; 8]);
1965 assert!(result.is_err());
1966 }
1967
1968 #[test]
1970 fn test_encode_component_rejects_short_digest() {
1971 let result = encode_component(0, 0, 0, 64, &[0u8; 4]);
1972 assert!(result.is_err());
1973 let err = result.unwrap_err().to_string();
1974 assert!(
1975 err.contains("digest length 4 < bit_length/8 (8)"),
1976 "unexpected error: {err}"
1977 );
1978 }
1979
1980 #[test]
1982 fn test_encode_component_rejects_invalid_mtype() {
1983 let result = encode_component(99, 0, 0, 64, &[0u8; 8]);
1984 assert!(result.is_err());
1985 }
1986
1987 #[test]
1989 fn test_encode_component_rejects_invalid_stype() {
1990 let result = encode_component(0, 99, 0, 64, &[0u8; 8]);
1991 assert!(result.is_err());
1992 }
1993
1994 #[test]
1996 fn test_encode_component_rejects_invalid_version() {
1997 let result = encode_component(0, 0, 99, 64, &[0u8; 8]);
1998 assert!(result.is_err());
1999 }
2000
2001 #[test]
2005 fn test_iscc_decode_round_trip_meta() {
2006 let digest = [0xaa_u8; 8];
2007 let encoded = encode_component(0, 0, 0, 64, &digest).unwrap();
2008 let (mt, st, vs, li, decoded_digest) = iscc_decode(&encoded).unwrap();
2009 assert_eq!(mt, 0, "MainType::Meta");
2010 assert_eq!(st, 0, "SubType::None");
2011 assert_eq!(vs, 0, "Version::V0");
2012 assert_eq!(li, 1, "length_index");
2014 assert_eq!(decoded_digest, digest.to_vec());
2015 }
2016
2017 #[test]
2019 fn test_iscc_decode_round_trip_content() {
2020 let digest = [0xbb_u8; 8];
2021 let encoded = encode_component(2, 0, 0, 64, &digest).unwrap();
2022 let (mt, st, vs, _li, decoded_digest) = iscc_decode(&encoded).unwrap();
2023 assert_eq!(mt, 2, "MainType::Content");
2024 assert_eq!(st, 0, "SubType::TEXT");
2025 assert_eq!(vs, 0, "Version::V0");
2026 assert_eq!(decoded_digest, digest.to_vec());
2027 }
2028
2029 #[test]
2031 fn test_iscc_decode_round_trip_data() {
2032 let digest = [0xcc_u8; 8];
2033 let encoded = encode_component(3, 0, 0, 64, &digest).unwrap();
2034 let (mt, _st, _vs, _li, decoded_digest) = iscc_decode(&encoded).unwrap();
2035 assert_eq!(mt, 3, "MainType::Data");
2036 assert_eq!(decoded_digest, digest.to_vec());
2037 }
2038
2039 #[test]
2041 fn test_iscc_decode_round_trip_instance() {
2042 let digest = [0xdd_u8; 8];
2043 let encoded = encode_component(4, 0, 0, 64, &digest).unwrap();
2044 let (mt, _st, _vs, _li, decoded_digest) = iscc_decode(&encoded).unwrap();
2045 assert_eq!(mt, 4, "MainType::Instance");
2046 assert_eq!(decoded_digest, digest.to_vec());
2047 }
2048
2049 #[test]
2051 fn test_iscc_decode_with_prefix() {
2052 let digest = [0xaa_u8; 8];
2053 let encoded = encode_component(0, 0, 0, 64, &digest).unwrap();
2054 let with_prefix = format!("ISCC:{encoded}");
2055 let (mt, st, vs, li, decoded_digest) = iscc_decode(&with_prefix).unwrap();
2056 assert_eq!(mt, 0);
2057 assert_eq!(st, 0);
2058 assert_eq!(vs, 0);
2059 assert_eq!(li, 1);
2060 assert_eq!(decoded_digest, digest.to_vec());
2061 }
2062
2063 #[test]
2065 fn test_iscc_decode_with_dashes() {
2066 let digest = [0xaa_u8; 8];
2067 let encoded = encode_component(0, 0, 0, 64, &digest).unwrap();
2068 let with_dashes = format!("{}-{}-{}", &encoded[..4], &encoded[4..8], &encoded[8..]);
2070 let (mt, st, vs, li, decoded_digest) = iscc_decode(&with_dashes).unwrap();
2071 assert_eq!(mt, 0);
2072 assert_eq!(st, 0);
2073 assert_eq!(vs, 0);
2074 assert_eq!(li, 1);
2075 assert_eq!(decoded_digest, digest.to_vec());
2076 }
2077
2078 #[test]
2080 fn test_iscc_decode_invalid_base32() {
2081 let result = iscc_decode("MAAA!!!!");
2083 assert!(result.is_err());
2084 let err = result.unwrap_err().to_string();
2085 assert!(err.contains("base32"), "expected base32 error: {err}");
2086 }
2087
2088 #[test]
2092 fn test_iscc_decode_rejects_invalid_prefix() {
2093 let err = iscc_decode("ISCC:MQIAAAAAAAAAAAAA")
2096 .unwrap_err()
2097 .to_string();
2098 assert!(err.contains("invalid prefix MQ"), "got: {err}");
2099
2100 let err = iscc_decode("M").unwrap_err().to_string();
2102 assert!(err.contains("invalid prefix M"), "got: {err}");
2103
2104 let err = iscc_decode("!!!INVALID!!!").unwrap_err().to_string();
2106 assert!(err.contains("invalid prefix !!"), "got: {err}");
2107
2108 assert!(iscc_decompose("ISCC:MQIAAAAAAAAAAAAA").is_ok());
2110
2111 assert!(iscc_decode("iscc:maighfecjmopmiab").is_ok());
2113 }
2114
2115 #[test]
2118 fn test_iscc_decode_known_meta_code() {
2119 let (mt, st, vs, li, digest) = iscc_decode("ISCC:AAAZXZ6OU74YAZIM").unwrap();
2120 assert_eq!(mt, 0, "MainType::Meta");
2121 assert_eq!(st, 0, "SubType::None");
2122 assert_eq!(vs, 0, "Version::V0");
2123 assert_eq!(li, 1, "length_index for 64-bit");
2124 assert_eq!(digest.len(), 8, "64-bit = 8 bytes");
2125 }
2126
2127 #[test]
2130 fn test_iscc_decode_known_instance_code() {
2131 let (mt, st, vs, li, digest) = iscc_decode("ISCC:IAA26E2JXH27TING").unwrap();
2132 assert_eq!(mt, 4, "MainType::Instance");
2133 assert_eq!(st, 0, "SubType::None");
2134 assert_eq!(vs, 0, "Version::V0");
2135 assert_eq!(li, 1, "length_index for 64-bit");
2136 assert_eq!(digest.len(), 8, "64-bit = 8 bytes");
2137 }
2138
2139 #[test]
2142 fn test_iscc_decode_known_data_code() {
2143 let (mt, st, vs, _li, digest) = iscc_decode("ISCC:GAAXL2XYM5BQIAZ3").unwrap();
2144 assert_eq!(mt, 3, "MainType::Data");
2145 assert_eq!(st, 0, "SubType::None");
2146 assert_eq!(vs, 0, "Version::V0");
2147 assert_eq!(digest.len(), 8, "64-bit = 8 bytes");
2148 }
2149
2150 #[test]
2153 fn test_iscc_decode_verification_round_trip() {
2154 let digest = [0xaa_u8; 8];
2155 let encoded = encode_component(0, 0, 0, 64, &digest).unwrap();
2156 let result = iscc_decode(&encoded).unwrap();
2157 assert_eq!(result, (0, 0, 0, 1, vec![0xaa; 8]));
2158 }
2159
2160 #[test]
2163 fn test_iscc_decode_normalizes_unit_sequence() {
2164 let data = gen_data_code_v0(&[0x61; 2000], 64).unwrap().iscc;
2165 let instance = gen_instance_code_v0(&[0x61; 2000], 64).unwrap().iscc;
2166 let composite = gen_iscc_code_v0(&[&data, &instance], false).unwrap().iscc;
2167
2168 let sequence = format!(
2169 "{}{}",
2170 data.strip_prefix("ISCC:").unwrap(),
2171 instance.strip_prefix("ISCC:").unwrap()
2172 );
2173 assert_eq!(
2174 iscc_decode(&sequence).unwrap(),
2175 iscc_decode(&composite).unwrap(),
2176 "a unit sequence must decode identically to its composite"
2177 );
2178 assert_eq!(iscc_decode(&sequence).unwrap().0, 5);
2180 }
2181
2182 #[test]
2191 fn test_iscc_decode_accepts_trailing_after_composite() {
2192 let data = gen_data_code_v0(&[0x61; 2000], 64).unwrap().iscc;
2193 let instance = gen_instance_code_v0(&[0x61; 2000], 64).unwrap().iscc;
2194 let composite = gen_iscc_code_v0(&[&data, &instance], false).unwrap().iscc;
2195
2196 let (mt, _st, _vs, _li, digest) = iscc_decode(&format!("{composite}AA")).unwrap();
2197 assert_eq!(mt, 5, "still decodes as an ISCC-CODE");
2198 assert_eq!(digest.len(), 16);
2199 }
2200
2201 #[cfg(feature = "meta-code")]
2206 #[test]
2207 fn test_iscc_decode_rejects_uncomposable_sequence() {
2208 let meta = gen_meta_code_v0("Hello", None, None, 64).unwrap().iscc;
2211 let text = gen_text_code_v0("Hello World", 64).unwrap().iscc;
2212 let sequence = format!(
2213 "{}{}",
2214 meta.strip_prefix("ISCC:").unwrap(),
2215 text.strip_prefix("ISCC:").unwrap()
2216 );
2217 assert!(iscc_decode(&sequence).is_err());
2218 }
2219
2220 #[test]
2222 fn test_iscc_decode_preserves_wide_subtype() {
2223 let data = gen_data_code_v0(&[0x61; 2000], 128).unwrap().iscc;
2224 let instance = gen_instance_code_v0(&[0x61; 2000], 128).unwrap().iscc;
2225 let wide = gen_iscc_code_v0(&[&data, &instance], true).unwrap().iscc;
2226
2227 let (mt, st, _vs, _li, digest) = iscc_decode(&wide).unwrap();
2228 assert_eq!(mt, 5);
2229 assert_eq!(st, 7, "SubType must stay WIDE through normalization");
2230 assert_eq!(digest.len(), 32);
2231 }
2232
2233 #[test]
2234 fn test_iscc_decode_truncated_input() {
2235 let digest = [0xff_u8; 32];
2237 let encoded = encode_component(0, 0, 0, 256, &digest).unwrap();
2238 let truncated = &encoded[..6];
2240 let result = iscc_decode(truncated);
2241 assert!(result.is_err(), "should fail on truncated input");
2242 }
2243
2244 #[cfg(feature = "meta-code")]
2248 #[test]
2249 fn test_json_to_data_url_basic() {
2250 let url = json_to_data_url(r#"{"key": "value"}"#).unwrap();
2251 assert!(
2252 url.starts_with("data:application/json;base64,"),
2253 "expected application/json prefix, got: {url}"
2254 );
2255 }
2256
2257 #[cfg(feature = "meta-code")]
2259 #[test]
2260 fn test_json_to_data_url_ld_json() {
2261 let url = json_to_data_url(r#"{"@context": "https://schema.org"}"#).unwrap();
2262 assert!(
2263 url.starts_with("data:application/ld+json;base64,"),
2264 "expected application/ld+json prefix, got: {url}"
2265 );
2266 }
2267
2268 #[cfg(feature = "meta-code")]
2270 #[test]
2271 fn test_json_to_data_url_jcs_ordering() {
2272 let url = json_to_data_url(r#"{"b":1,"a":2}"#).unwrap();
2273 let b64 = url.split_once(',').unwrap().1;
2275 let decoded = data_encoding::BASE64.decode(b64.as_bytes()).unwrap();
2276 let canonical = std::str::from_utf8(&decoded).unwrap();
2277 assert_eq!(canonical, r#"{"a":2,"b":1}"#, "JCS should sort keys");
2278 }
2279
2280 #[cfg(feature = "meta-code")]
2283 #[test]
2284 fn test_json_to_data_url_round_trip() {
2285 let input = r#"{"hello": "world", "num": 42}"#;
2286 let url = json_to_data_url(input).unwrap();
2287 let decoded_bytes = decode_data_url(&url).unwrap();
2288 let canonical: serde_json::Value =
2290 serde_json::from_slice(&decoded_bytes).expect("decoded bytes should be valid JSON");
2291 let original: serde_json::Value = serde_json::from_str(input).unwrap();
2292 assert_eq!(canonical, original, "round-trip preserves JSON semantics");
2293 }
2294
2295 #[cfg(feature = "meta-code")]
2297 #[test]
2298 fn test_json_to_data_url_invalid_json() {
2299 let result = json_to_data_url("not json");
2300 assert!(result.is_err(), "should reject invalid JSON");
2301 let err = result.unwrap_err().to_string();
2302 assert!(
2303 err.contains("invalid JSON"),
2304 "expected 'invalid JSON' in error: {err}"
2305 );
2306 }
2307
2308 #[cfg(feature = "meta-code")]
2321 #[test]
2322 fn test_json_to_data_url_conformance_0016() {
2323 let url = json_to_data_url(r#"{"some": "object"}"#).unwrap();
2324 assert!(
2326 url.starts_with("data:application/json;base64,"),
2327 "expected application/json prefix"
2328 );
2329 let b64 = url.split_once(',').unwrap().1;
2331 let decoded = data_encoding::BASE64.decode(b64.as_bytes()).unwrap();
2332 let canonical = std::str::from_utf8(&decoded).unwrap();
2333 assert_eq!(
2334 canonical, r#"{"some":"object"}"#,
2335 "JCS removes whitespace from JSON"
2336 );
2337 }
2338
2339 #[cfg(feature = "meta-code")]
2340 #[test]
2341 fn test_meta_trim_meta_value() {
2342 assert_eq!(META_TRIM_META, 128_000);
2343 }
2344
2345 #[cfg(feature = "meta-code")]
2346 #[test]
2347 fn test_gen_meta_code_v0_meta_at_limit() {
2348 let padding = "a".repeat(128_000 - 8);
2352 let json_str = format!(r#"{{"x":"{padding}"}}"#);
2353 let result = gen_meta_code_v0("test", None, Some(&json_str), 64);
2354 assert!(
2355 result.is_ok(),
2356 "payload at exactly META_TRIM_META should succeed"
2357 );
2358 }
2359
2360 #[cfg(feature = "meta-code")]
2361 #[test]
2362 fn test_gen_meta_code_v0_meta_over_limit() {
2363 let padding = "a".repeat(128_000 - 8 + 1);
2365 let json_str = format!(r#"{{"x":"{padding}"}}"#);
2366 let result = gen_meta_code_v0("test", None, Some(&json_str), 64);
2367 assert!(
2368 matches!(result, Err(IsccError::InvalidInput(ref msg)) if msg.contains("size limit")),
2369 "payload exceeding META_TRIM_META should return InvalidInput"
2370 );
2371 }
2372
2373 #[cfg(feature = "meta-code")]
2374 #[test]
2375 fn test_gen_meta_code_v0_data_url_pre_decode_reject() {
2376 let pre_decode_limit = META_TRIM_META * 4 / 3 + 256;
2379 let padding = "A".repeat(pre_decode_limit + 1);
2380 let data_url = format!("data:application/octet-stream;base64,{padding}");
2381 let result = gen_meta_code_v0("test", None, Some(&data_url), 64);
2382 assert!(
2383 matches!(result, Err(IsccError::InvalidInput(ref msg)) if msg.contains("size limit")),
2384 "oversized Data-URL should be rejected before decoding"
2385 );
2386 }
2387
2388 fn write_temp_file(name: &str, data: &[u8]) -> std::path::PathBuf {
2392 let path = std::env::temp_dir().join(format!("iscc_test_{name}"));
2393 std::fs::write(&path, data).expect("failed to write temp file");
2394 path
2395 }
2396
2397 #[test]
2398 fn test_gen_sum_code_v0_equivalence() {
2399 let data = b"Hello, ISCC World! This is a test of gen_sum_code_v0.";
2400 let path = write_temp_file("sum_equiv", data);
2401
2402 let sum_result = gen_sum_code_v0(&path, 64, false, false).unwrap();
2403
2404 let data_result = gen_data_code_v0(data, 64).unwrap();
2406 let instance_result = gen_instance_code_v0(data, 64).unwrap();
2407 let iscc_result =
2408 gen_iscc_code_v0(&[&data_result.iscc, &instance_result.iscc], false).unwrap();
2409
2410 assert_eq!(sum_result.iscc, iscc_result.iscc);
2411 assert_eq!(sum_result.datahash, instance_result.datahash);
2412 assert_eq!(sum_result.filesize, instance_result.filesize);
2413 assert_eq!(sum_result.filesize, data.len() as u64);
2414 assert_eq!(sum_result.units, None);
2415
2416 std::fs::remove_file(&path).ok();
2417 }
2418
2419 #[test]
2420 fn test_gen_sum_code_v0_empty_file() {
2421 let path = write_temp_file("sum_empty", b"");
2422
2423 let sum_result = gen_sum_code_v0(&path, 64, false, false).unwrap();
2424
2425 let data_result = gen_data_code_v0(b"", 64).unwrap();
2426 let instance_result = gen_instance_code_v0(b"", 64).unwrap();
2427 let iscc_result =
2428 gen_iscc_code_v0(&[&data_result.iscc, &instance_result.iscc], false).unwrap();
2429
2430 assert_eq!(sum_result.iscc, iscc_result.iscc);
2431 assert_eq!(sum_result.datahash, instance_result.datahash);
2432 assert_eq!(sum_result.filesize, 0);
2433
2434 std::fs::remove_file(&path).ok();
2435 }
2436
2437 #[test]
2438 fn test_gen_sum_code_v0_file_not_found() {
2439 let path = std::env::temp_dir().join("iscc_test_nonexistent_file_xyz");
2440 let result = gen_sum_code_v0(&path, 64, false, false);
2441 assert!(result.is_err());
2442 let err_msg = result.unwrap_err().to_string();
2443 assert!(
2444 err_msg.contains("Cannot open file"),
2445 "error message should mention file open failure: {err_msg}"
2446 );
2447 }
2448
2449 #[test]
2450 fn test_gen_sum_code_v0_wide_mode() {
2451 let data = b"Testing wide mode for gen_sum_code_v0 function.";
2452 let path = write_temp_file("sum_wide", data);
2453
2454 let narrow = gen_sum_code_v0(&path, 64, false, false).unwrap();
2455 let wide = gen_sum_code_v0(&path, 64, true, false).unwrap();
2456
2457 assert_eq!(narrow.iscc, wide.iscc);
2459
2460 let narrow_128 = gen_sum_code_v0(&path, 128, false, false).unwrap();
2462 let wide_128 = gen_sum_code_v0(&path, 128, true, false).unwrap();
2463 assert_ne!(narrow_128.iscc, wide_128.iscc);
2464
2465 assert_eq!(narrow_128.datahash, wide_128.datahash);
2467 assert_eq!(narrow_128.filesize, wide_128.filesize);
2468
2469 std::fs::remove_file(&path).ok();
2470 }
2471
2472 #[test]
2473 fn test_gen_sum_code_v0_bits_64() {
2474 let data = b"Testing 64-bit gen_sum_code_v0.";
2475 let path = write_temp_file("sum_bits64", data);
2476
2477 let sum_result = gen_sum_code_v0(&path, 64, false, false).unwrap();
2478
2479 let data_result = gen_data_code_v0(data, 64).unwrap();
2480 let instance_result = gen_instance_code_v0(data, 64).unwrap();
2481 let iscc_result =
2482 gen_iscc_code_v0(&[&data_result.iscc, &instance_result.iscc], false).unwrap();
2483
2484 assert_eq!(sum_result.iscc, iscc_result.iscc);
2485
2486 std::fs::remove_file(&path).ok();
2487 }
2488
2489 #[test]
2490 fn test_gen_sum_code_v0_bits_128() {
2491 let data = b"Testing 128-bit gen_sum_code_v0.";
2492 let path = write_temp_file("sum_bits128", data);
2493
2494 let sum_result = gen_sum_code_v0(&path, 128, false, false).unwrap();
2495
2496 let data_result = gen_data_code_v0(data, 128).unwrap();
2497 let instance_result = gen_instance_code_v0(data, 128).unwrap();
2498 let iscc_result =
2499 gen_iscc_code_v0(&[&data_result.iscc, &instance_result.iscc], false).unwrap();
2500
2501 assert_eq!(sum_result.iscc, iscc_result.iscc);
2502 assert_eq!(sum_result.datahash, instance_result.datahash);
2503 assert_eq!(sum_result.filesize, data.len() as u64);
2504
2505 std::fs::remove_file(&path).ok();
2506 }
2507
2508 #[test]
2509 fn test_gen_sum_code_v0_large_data() {
2510 let data: Vec<u8> = (0..50_000).map(|i| (i % 256) as u8).collect();
2512 let path = write_temp_file("sum_large", &data);
2513
2514 let sum_result = gen_sum_code_v0(&path, 64, false, false).unwrap();
2515
2516 let data_result = gen_data_code_v0(&data, 64).unwrap();
2517 let instance_result = gen_instance_code_v0(&data, 64).unwrap();
2518 let iscc_result =
2519 gen_iscc_code_v0(&[&data_result.iscc, &instance_result.iscc], false).unwrap();
2520
2521 assert_eq!(sum_result.iscc, iscc_result.iscc);
2522 assert_eq!(sum_result.datahash, instance_result.datahash);
2523 assert_eq!(sum_result.filesize, data.len() as u64);
2524
2525 std::fs::remove_file(&path).ok();
2526 }
2527
2528 #[test]
2529 fn test_gen_sum_code_v0_units_enabled() {
2530 let data = b"Hello, ISCC World! This is a test of gen_sum_code_v0 units.";
2531 let path = write_temp_file("sum_units_on", data);
2532
2533 let sum_result = gen_sum_code_v0(&path, 64, false, true).unwrap();
2534
2535 let units = sum_result.units.as_ref().expect("units should be Some");
2537 assert_eq!(
2538 units.len(),
2539 2,
2540 "units should contain [Data-Code, Instance-Code]"
2541 );
2542
2543 let (maintype, ..) = iscc_decode(&units[0]).unwrap();
2545 assert_eq!(
2546 maintype, 3,
2547 "first unit should be a Data-Code (MainType::Data = 3)"
2548 );
2549
2550 let (maintype, ..) = iscc_decode(&units[1]).unwrap();
2552 assert_eq!(
2553 maintype, 4,
2554 "second unit should be an Instance-Code (MainType::Instance = 4)"
2555 );
2556
2557 let data_result = gen_data_code_v0(data, 64).unwrap();
2559 let instance_result = gen_instance_code_v0(data, 64).unwrap();
2560 assert_eq!(units[0], data_result.iscc);
2561 assert_eq!(units[1], instance_result.iscc);
2562
2563 let iscc_result =
2565 gen_iscc_code_v0(&[&data_result.iscc, &instance_result.iscc], false).unwrap();
2566 assert_eq!(sum_result.iscc, iscc_result.iscc);
2567
2568 std::fs::remove_file(&path).ok();
2569 }
2570
2571 #[test]
2572 fn test_gen_sum_code_v0_units_disabled() {
2573 let data = b"Hello, ISCC World! This is a test of gen_sum_code_v0 no units.";
2574 let path = write_temp_file("sum_units_off", data);
2575
2576 let sum_result = gen_sum_code_v0(&path, 64, false, false).unwrap();
2577
2578 assert_eq!(
2579 sum_result.units, None,
2580 "units should be None when add_units is false"
2581 );
2582
2583 let data_result = gen_data_code_v0(data, 64).unwrap();
2585 let instance_result = gen_instance_code_v0(data, 64).unwrap();
2586 let iscc_result =
2587 gen_iscc_code_v0(&[&data_result.iscc, &instance_result.iscc], false).unwrap();
2588 assert_eq!(sum_result.iscc, iscc_result.iscc);
2589
2590 std::fs::remove_file(&path).ok();
2591 }
2592}