Skip to main content

iscc_lib/
codec.rs

1//! ISCC codec: type enums, header encoding/decoding, base32, and component encoding.
2//!
3//! Provides the foundational encoding primitives that all `gen_*_v0` functions
4//! depend on to produce ISCC-encoded output strings. This is a Tier 2 module —
5//! available to Rust consumers but not exposed through FFI bindings.
6
7use crate::{IsccError, IsccResult};
8use std::borrow::Cow;
9
10// ---- Type Enums ----
11
12/// ISCC MainType identifier.
13///
14/// Integer values match the `iscc-core` Python reference (MT enum).
15#[repr(u8)]
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
17pub enum MainType {
18    Meta = 0,
19    Semantic = 1,
20    Content = 2,
21    Data = 3,
22    Instance = 4,
23    Iscc = 5,
24    Id = 6,
25    Flake = 7,
26}
27
28impl TryFrom<u8> for MainType {
29    type Error = IsccError;
30
31    fn try_from(value: u8) -> Result<Self, Self::Error> {
32        match value {
33            0 => Ok(Self::Meta),
34            1 => Ok(Self::Semantic),
35            2 => Ok(Self::Content),
36            3 => Ok(Self::Data),
37            4 => Ok(Self::Instance),
38            5 => Ok(Self::Iscc),
39            6 => Ok(Self::Id),
40            7 => Ok(Self::Flake),
41            _ => Err(IsccError::InvalidInput(format!(
42                "invalid MainType: {value}"
43            ))),
44        }
45    }
46}
47
48/// ISCC SubType identifier.
49///
50/// A unified enum covering all subtype contexts (ST, ST_CC, ST_ISCC).
51/// The interpretation depends on the MainType context. Integer values
52/// match the `iscc-core` Python reference.
53#[repr(u8)]
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum SubType {
56    /// No specific subtype (general) / Text content (ST_CC context).
57    None = 0,
58    /// Image content.
59    Image = 1,
60    /// Audio content.
61    Audio = 2,
62    /// Video content.
63    Video = 3,
64    /// Mixed content.
65    Mixed = 4,
66    /// ISCC composite summary (only 2 mandatory units, no optional).
67    Sum = 5,
68    /// ISCC no specific content type (3+ units, mixed subtypes).
69    IsccNone = 6,
70    /// ISCC wide mode (256-bit Data+Instance composite).
71    Wide = 7,
72}
73
74impl SubType {
75    /// Alias for `None` (value 0) in Content-Code / Semantic-Code context.
76    pub const TEXT: Self = Self::None;
77}
78
79impl TryFrom<u8> for SubType {
80    type Error = IsccError;
81
82    fn try_from(value: u8) -> Result<Self, Self::Error> {
83        match value {
84            0 => Ok(Self::None),
85            1 => Ok(Self::Image),
86            2 => Ok(Self::Audio),
87            3 => Ok(Self::Video),
88            4 => Ok(Self::Mixed),
89            5 => Ok(Self::Sum),
90            6 => Ok(Self::IsccNone),
91            7 => Ok(Self::Wide),
92            _ => Err(IsccError::InvalidInput(format!("invalid SubType: {value}"))),
93        }
94    }
95}
96
97/// ISCC version identifier.
98///
99/// `V1` exists only for the experimental ISCC-IDv1 (MainType `Id`); every other
100/// MainType permits only `V0`. This enum is `#[non_exhaustive]` so future
101/// versions can be added without a further SemVer-major break.
102#[repr(u8)]
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104#[non_exhaustive]
105pub enum Version {
106    V0 = 0,
107    V1 = 1,
108}
109
110impl TryFrom<u8> for Version {
111    type Error = IsccError;
112
113    fn try_from(value: u8) -> Result<Self, Self::Error> {
114        match value {
115            0 => Ok(Self::V0),
116            1 => Ok(Self::V1),
117            _ => Err(IsccError::InvalidInput(format!("invalid Version: {value}"))),
118        }
119    }
120}
121
122/// Validate a MainType/Version combination.
123///
124/// Version 1 is accepted only for MainType `Id` (the experimental ISCC-IDv1);
125/// every other MainType permits only Version 0. This is the MainType-aware gate
126/// the context-free `Version::try_from` cannot express: `try_from` maps the raw
127/// nibble, while this function rejects a Version-1 header on any non-`Id` type.
128fn validate_version(mtype: MainType, version: Version) -> IsccResult<()> {
129    match (mtype, version) {
130        (_, Version::V0) | (MainType::Id, Version::V1) => Ok(()),
131        (_, other) => Err(IsccError::InvalidInput(format!(
132            "invalid Version: {} for MainType {mtype:?}",
133            other as u8
134        ))),
135    }
136}
137
138// ---- Bit Manipulation Helpers ----
139
140/// Read bit at position `bit_pos` from byte slice (MSB-first ordering).
141fn get_bit(data: &[u8], bit_pos: usize) -> bool {
142    let byte_idx = bit_pos / 8;
143    let bit_idx = 7 - (bit_pos % 8);
144    (data[byte_idx] >> bit_idx) & 1 == 1
145}
146
147/// Extract `count` bits starting at `bit_pos` as a u32 (MSB-first).
148fn extract_bits(data: &[u8], bit_pos: usize, count: usize) -> u32 {
149    let mut value = 0u32;
150    for i in 0..count {
151        value = (value << 1) | u32::from(get_bit(data, bit_pos + i));
152    }
153    value
154}
155
156/// Convert a bit slice (big-endian, MSB first) to a u32.
157#[cfg(test)]
158fn bits_to_u32(bits: &[bool]) -> u32 {
159    bits.iter().fold(0u32, |acc, &b| (acc << 1) | u32::from(b))
160}
161
162/// Convert bytes to a bit vector (big-endian, MSB first).
163#[cfg(test)]
164fn bytes_to_bits(bytes: &[u8]) -> Vec<bool> {
165    bytes
166        .iter()
167        .flat_map(|&byte| (0..8).rev().map(move |i| (byte >> i) & 1 == 1))
168        .collect()
169}
170
171/// Convert a bit vector to bytes, padding with zero bits on the right.
172fn bits_to_bytes(bits: &[bool]) -> Vec<u8> {
173    bits.chunks(8)
174        .map(|chunk| {
175            chunk.iter().enumerate().fold(
176                0u8,
177                |byte, (i, &bit)| if bit { byte | (1 << (7 - i)) } else { byte },
178            )
179        })
180        .collect()
181}
182
183// ---- Varnibble Encoding ----
184
185/// Encode an integer as a variable-length nibble (varnibble) bit sequence.
186///
187/// Encoding scheme:
188/// - `0xxx` (4 bits, 1 nibble): values 0–7
189/// - `10xxxxxx` (8 bits, 2 nibbles): values 8–71
190/// - `110xxxxxxxxx` (12 bits, 3 nibbles): values 72–583
191/// - `1110xxxxxxxxxxxx` (16 bits, 4 nibbles): values 584–4679
192fn encode_varnibble(value: u32) -> IsccResult<Vec<bool>> {
193    match value {
194        0..=7 => {
195            // 4 bits: value fits directly (leading 0 implicit in 4-bit encoding)
196            Ok((0..4).rev().map(|i| (value >> i) & 1 == 1).collect())
197        }
198        8..=71 => {
199            // 8 bits: prefix 10 + 6 data bits for (value - 8)
200            let v = value - 8;
201            let mut bits = vec![true, false];
202            bits.extend((0..6).rev().map(|i| (v >> i) & 1 == 1));
203            Ok(bits)
204        }
205        72..=583 => {
206            // 12 bits: prefix 110 + 9 data bits for (value - 72)
207            let v = value - 72;
208            let mut bits = vec![true, true, false];
209            bits.extend((0..9).rev().map(|i| (v >> i) & 1 == 1));
210            Ok(bits)
211        }
212        584..=4679 => {
213            // 16 bits: prefix 1110 + 12 data bits for (value - 584)
214            let v = value - 584;
215            let mut bits = vec![true, true, true, false];
216            bits.extend((0..12).rev().map(|i| (v >> i) & 1 == 1));
217            Ok(bits)
218        }
219        _ => Err(IsccError::InvalidInput(format!(
220            "varnibble value out of range (0-4679): {value}"
221        ))),
222    }
223}
224
225/// Decode the first varnibble from a byte slice at the given bit position.
226///
227/// Operates directly on `&[u8]` with bitwise extraction, avoiding any
228/// intermediate `Vec<bool>` allocation. Returns the decoded integer and
229/// the number of bits consumed.
230fn decode_varnibble_from_bytes(data: &[u8], bit_pos: usize) -> IsccResult<(u32, usize)> {
231    let available = data.len() * 8 - bit_pos;
232    if available < 4 {
233        return Err(IsccError::InvalidInput(
234            "insufficient bits for varnibble".into(),
235        ));
236    }
237
238    if !get_bit(data, bit_pos) {
239        // 0xxx — 4 bits, values 0–7
240        Ok((extract_bits(data, bit_pos, 4), 4))
241    } else if available >= 8 && !get_bit(data, bit_pos + 1) {
242        // 10xxxxxx — 8 bits, values 8–71
243        Ok((extract_bits(data, bit_pos + 2, 6) + 8, 8))
244    } else if available >= 12 && !get_bit(data, bit_pos + 2) {
245        // 110xxxxxxxxx — 12 bits, values 72–583
246        Ok((extract_bits(data, bit_pos + 3, 9) + 72, 12))
247    } else if available >= 16 && !get_bit(data, bit_pos + 3) {
248        // 1110xxxxxxxxxxxx — 16 bits, values 584–4679
249        Ok((extract_bits(data, bit_pos + 4, 12) + 584, 16))
250    } else {
251        Err(IsccError::InvalidInput(
252            "invalid varnibble prefix or insufficient bits".into(),
253        ))
254    }
255}
256
257// ---- Header Encoding ----
258
259/// Encode ISCC header fields into bytes.
260///
261/// Concatenates varnibble-encoded MainType, SubType, Version, and length,
262/// then pads to byte boundary with zero bits on the right.
263/// Result is 2 bytes minimum (typical case), up to 8 bytes maximum.
264pub fn encode_header(
265    mtype: MainType,
266    stype: SubType,
267    version: Version,
268    length: u32,
269) -> IsccResult<Vec<u8>> {
270    validate_version(mtype, version)?;
271
272    let mut bits = Vec::new();
273    bits.extend(encode_varnibble(mtype as u32)?);
274    bits.extend(encode_varnibble(stype as u32)?);
275    bits.extend(encode_varnibble(version as u32)?);
276    bits.extend(encode_varnibble(length)?);
277
278    // Pad to byte boundary with zero bits (equivalent to bitarray.fill())
279    let remainder = bits.len() % 8;
280    if remainder != 0 {
281        bits.resize(bits.len() + (8 - remainder), false);
282    }
283
284    Ok(bits_to_bytes(&bits))
285}
286
287/// Decode ISCC header from bytes.
288///
289/// Operates directly on `&[u8]` with bitwise extraction, avoiding any
290/// intermediate `Vec<bool>` allocation. Returns `(MainType, SubType,
291/// Version, length, tail_bytes)` where `tail_bytes` contains any
292/// remaining data after the header.
293pub fn decode_header(data: &[u8]) -> IsccResult<(MainType, SubType, Version, u32, Vec<u8>)> {
294    let mut bit_pos = 0;
295
296    let (mtype_val, consumed) = decode_varnibble_from_bytes(data, bit_pos)?;
297    bit_pos += consumed;
298
299    let (stype_val, consumed) = decode_varnibble_from_bytes(data, bit_pos)?;
300    bit_pos += consumed;
301
302    let (version_val, consumed) = decode_varnibble_from_bytes(data, bit_pos)?;
303    bit_pos += consumed;
304
305    let (length, consumed) = decode_varnibble_from_bytes(data, bit_pos)?;
306    bit_pos += consumed;
307
308    // Strip 4-bit zero padding if header bits are not byte-aligned.
309    // Since each varnibble is a multiple of 4 bits, misalignment is always 4 bits.
310    if bit_pos % 8 != 0 && bit_pos + 4 <= data.len() * 8 && extract_bits(data, bit_pos, 4) == 0 {
311        bit_pos += 4;
312    }
313
314    // Advance to next byte boundary for tail extraction
315    let tail_byte_start = bit_pos.div_ceil(8);
316    let tail = if tail_byte_start < data.len() {
317        data[tail_byte_start..].to_vec()
318    } else {
319        vec![]
320    };
321
322    // Range-check each varnibble before narrowing so a multi-nibble value that
323    // does not fit in a u8 is rejected instead of wrapping (e.g. 262 -> 6 = Id).
324    let mtype_u8 = u8::try_from(mtype_val)
325        .map_err(|_| IsccError::InvalidInput(format!("invalid MainType: {mtype_val}")))?;
326    let stype_u8 = u8::try_from(stype_val)
327        .map_err(|_| IsccError::InvalidInput(format!("invalid SubType: {stype_val}")))?;
328    let version_u8 = u8::try_from(version_val)
329        .map_err(|_| IsccError::InvalidInput(format!("invalid Version: {version_val}")))?;
330
331    let mtype = MainType::try_from(mtype_u8)?;
332    let stype = SubType::try_from(stype_u8)?;
333    let version = Version::try_from(version_u8)?;
334    validate_version(mtype, version)?;
335
336    Ok((mtype, stype, version, length, tail))
337}
338
339// ---- Length Encoding ----
340
341/// Encode bit length to header length field value.
342///
343/// Semantics depend on MainType:
344/// - META/SEMANTIC/CONTENT/DATA/INSTANCE/FLAKE: `(bit_length / 32) - 1`
345/// - ISCC: pass-through (0–7, unit composition flags)
346/// - ID: `(bit_length - 64) / 8`
347pub fn encode_length(mtype: MainType, length: u32) -> IsccResult<u32> {
348    match mtype {
349        MainType::Meta
350        | MainType::Semantic
351        | MainType::Content
352        | MainType::Data
353        | MainType::Instance
354        | MainType::Flake => {
355            if length >= 32 && length % 32 == 0 {
356                Ok(length / 32 - 1)
357            } else {
358                Err(IsccError::InvalidInput(format!(
359                    "invalid length {length} for {mtype:?} (must be multiple of 32, >= 32)"
360                )))
361            }
362        }
363        MainType::Iscc => {
364            if length <= 7 {
365                Ok(length)
366            } else {
367                Err(IsccError::InvalidInput(format!(
368                    "invalid length {length} for ISCC (must be 0-7)"
369                )))
370            }
371        }
372        MainType::Id => {
373            if (64..=96).contains(&length) && (length - 64) % 8 == 0 {
374                Ok((length - 64) / 8)
375            } else {
376                Err(IsccError::InvalidInput(format!(
377                    "invalid length {length} for ID (must be 64-96, step 8)"
378                )))
379            }
380        }
381    }
382}
383
384/// Decode header length field to actual bit length.
385///
386/// Inverse of `encode_length`. Returns the number of bits in the digest.
387/// - META/SEMANTIC/CONTENT/DATA/INSTANCE/FLAKE: `(length + 1) * 32`
388/// - ISCC + Wide: 256
389/// - ISCC + other: `popcount(length) * 64 + 128`
390/// - ID: `length * 8 + 64`
391pub fn decode_length(mtype: MainType, length: u32, stype: SubType) -> u32 {
392    match mtype {
393        MainType::Meta
394        | MainType::Semantic
395        | MainType::Content
396        | MainType::Data
397        | MainType::Instance
398        | MainType::Flake => (length + 1) * 32,
399        MainType::Iscc => {
400            if stype == SubType::Wide {
401                256
402            } else {
403                length.count_ones() * 64 + 128
404            }
405        }
406        MainType::Id => length * 8 + 64,
407    }
408}
409
410// ---- Unit Encoding ----
411
412/// Encode optional ISCC-UNIT MainTypes as a unit combination index (0–7).
413///
414/// Maps the optional units (Meta, Semantic, Content) present in a composite
415/// ISCC-CODE to a bitfield index. Data and Instance are mandatory and must
416/// not be included. The bitfield pattern is:
417/// bit 0 = Content, bit 1 = Semantic, bit 2 = Meta.
418pub fn encode_units(main_types: &[MainType]) -> IsccResult<u32> {
419    let mut result = 0u32;
420    for &mt in main_types {
421        match mt {
422            MainType::Content => result |= 1,
423            MainType::Semantic => result |= 2,
424            MainType::Meta => result |= 4,
425            _ => {
426                return Err(IsccError::InvalidInput(format!(
427                    "{mt:?} is not a valid optional unit type"
428                )));
429            }
430        }
431    }
432    Ok(result)
433}
434
435/// Decode a unit combination index (0–7) to a sorted list of optional MainTypes.
436///
437/// Inverse of `encode_units`. Decodes the 3-bit bitfield:
438/// bit 0 = Content, bit 1 = Semantic, bit 2 = Meta. Results are returned
439/// in MainType discriminant order (Meta, Semantic, Content) so they are
440/// automatically sorted.
441pub fn decode_units(unit_id: u32) -> IsccResult<Vec<MainType>> {
442    if unit_id > 7 {
443        return Err(IsccError::InvalidInput(format!(
444            "invalid unit_id: {unit_id} (must be 0-7)"
445        )));
446    }
447    let mut result = Vec::new();
448    if unit_id & 4 != 0 {
449        result.push(MainType::Meta);
450    }
451    if unit_id & 2 != 0 {
452        result.push(MainType::Semantic);
453    }
454    if unit_id & 1 != 0 {
455        result.push(MainType::Content);
456    }
457    Ok(result)
458}
459
460// ---- Prefix Validation ----
461
462/// Valid two-character ISCC prefixes, mirroring `iscc_core.constants.PREFIXES`.
463/// Note: `MA` and `ME` are ambiguous between ID-V0 and ID-V1.
464pub(crate) const PREFIXES: [&str; 26] = [
465    "AA", // META-NONE
466    "CA", // SEMANTIC-TEXT
467    "CE", // SEMANTIC-IMAGE
468    "CI", // SEMANTIC-AUDIO
469    "CM", // SEMANTIC-VIDEO
470    "CQ", // SEMANTIC-MIXED
471    "EA", // CONTENT-TEXT
472    "EE", // CONTENT-IMAGE
473    "EI", // CONTENT-AUDIO
474    "EM", // CONTENT-VIDEO
475    "EQ", // CONTENT-MIXED
476    "GA", // DATA-NONE
477    "IA", // INSTANCE-NONE
478    "KA", // ISCC-TEXT
479    "KE", // ISCC-IMAGE
480    "KI", // ISCC-AUDIO
481    "KM", // ISCC-VIDEO
482    "KQ", // ISCC-MIXED
483    "KU", // ISCC-SUM
484    "KY", // ISCC-NONE
485    "K4", // ISCC-WIDE
486    "MA", // ID-PRIVATE-V0 / ID-REALM_0-V1 (ambiguous)
487    "ME", // ID-BITCOIN-V0 / ID-REALM_1-V1 (ambiguous)
488    "MI", // ID-ETHEREUM-V0
489    "MM", // ID-POLYGON-V0
490    "OA", // FLAKE-NONE
491];
492
493// ---- Base32 Encoding ----
494
495/// Encode bytes as base32 (RFC 4648, uppercase, no padding).
496pub fn encode_base32(data: &[u8]) -> String {
497    data_encoding::BASE32_NOPAD.encode(data)
498}
499
500/// Decode base32 string to bytes (case-insensitive, no padding expected).
501pub fn decode_base32(code: &str) -> IsccResult<Vec<u8>> {
502    let upper = code.to_uppercase();
503    data_encoding::BASE32_NOPAD
504        .decode(upper.as_bytes())
505        .map_err(|e| IsccError::InvalidInput(format!("base32 decode error: {e}")))
506}
507
508// ---- Base64 Encoding ----
509
510/// Encode bytes as base64url (RFC 4648 §5, no padding).
511pub fn encode_base64(data: &[u8]) -> String {
512    data_encoding::BASE64URL_NOPAD.encode(data)
513}
514
515// ---- Component Encoding ----
516
517/// Encode an ISCC-UNIT with header and body as a base32 string.
518///
519/// Produces the base32-encoded string (without "ISCC:" prefix). Callers
520/// add the prefix when constructing the final ISCC string.
521///
522/// Note: ISCC-CODEs (MainType::Iscc) are not encoded via this function —
523/// `gen_iscc_code_v0` constructs the composite header directly.
524pub fn encode_component(
525    mtype: MainType,
526    stype: SubType,
527    version: Version,
528    bit_length: u32,
529    digest: &[u8],
530) -> IsccResult<String> {
531    if mtype == MainType::Iscc {
532        return Err(IsccError::InvalidInput(
533            "ISCC MainType is not a unit; use gen_iscc_code_v0 instead".into(),
534        ));
535    }
536
537    let encoded_length = encode_length(mtype, bit_length)?;
538    let nbytes = (bit_length / 8) as usize;
539    let header = encode_header(mtype, stype, version, encoded_length)?;
540    let body = &digest[..nbytes.min(digest.len())];
541
542    let mut component = header;
543    component.extend_from_slice(body);
544
545    Ok(encode_base32(&component))
546}
547
548/// Clean up an ISCC string to its bare base32 form.
549///
550/// Mirrors `iscc_core.codec.iscc_clean`: trims surrounding whitespace, removes an
551/// optional scheme prefix (matched case-insensitively against `iscc`), and strips
552/// the hyphen group separators of the canonical display form (e.g.
553/// `ISCC:KACY-PXW4-…`). A single-part input whose first character is a multibase
554/// prefix (`f`, `b`, `v`, `z`, `u`) keeps its dashes intact, since `-` may be
555/// significant in multibase-encoded data.
556///
557/// Returns the cleaned code with no scheme prefix and no dashes.
558///
559/// # Errors
560///
561/// Returns `IsccError::InvalidInput` when a two-part input uses a scheme other
562/// than `iscc` (case-insensitive), when the input contains more than one colon,
563/// or when the cleaned result is empty (mirrors the reference erroring on empty
564/// input; `decode_base32("")` returns `Ok(empty)`, so an empty code would
565/// otherwise be silently accepted as a zero-unit ISCC).
566pub(crate) fn iscc_clean(iscc: &str) -> IsccResult<Cow<'_, str>> {
567    let trimmed = iscc.trim();
568    let cleaned: Cow<'_, str> = match trimmed.split_once(':') {
569        None => {
570            // Single part, no scheme prefix. Preserve dashes for multibase-encoded
571            // inputs; strip them otherwise. Borrow when nothing needs removing.
572            let is_multibase = matches!(
573                trimmed.as_bytes().first(),
574                Some(b'f' | b'b' | b'v' | b'z' | b'u')
575            );
576            if is_multibase || !trimmed.contains('-') {
577                Cow::Borrowed(trimmed)
578            } else {
579                Cow::Owned(trimmed.replace('-', ""))
580            }
581        }
582        Some((scheme, rest)) => {
583            let scheme = scheme.trim();
584            let code = rest.trim();
585            // A second colon means the string is malformed (more than one part).
586            if code.contains(':') {
587                return Err(IsccError::InvalidInput(format!(
588                    "Malformed ISCC string: {iscc}"
589                )));
590            }
591            if !scheme.eq_ignore_ascii_case("iscc") {
592                return Err(IsccError::InvalidInput(format!("Invalid scheme: {scheme}")));
593            }
594            if code.contains('-') {
595                Cow::Owned(code.replace('-', ""))
596            } else {
597                Cow::Borrowed(code)
598            }
599        }
600    };
601
602    if cleaned.is_empty() {
603        return Err(IsccError::InvalidInput("Empty ISCC string".to_string()));
604    }
605
606    Ok(cleaned)
607}
608
609/// Decompose a composite ISCC-CODE or ISCC sequence into individual ISCC-UNITs.
610///
611/// Accepts a normalized ISCC-CODE or a concatenated sequence of ISCC-UNITs.
612/// The input is cleaned via [`iscc_clean`] (scheme prefix, dashes, whitespace)
613/// before decoding. Returns a list of base32-encoded ISCC-UNIT strings (without
614/// "ISCC:" prefix).
615pub fn iscc_decompose(iscc_code: &str) -> IsccResult<Vec<String>> {
616    let clean = iscc_clean(iscc_code)?;
617    let mut raw_code = decode_base32(&clean)?;
618    let mut components = Vec::new();
619
620    while !raw_code.is_empty() {
621        let (mt, st, vs, ln, body) = decode_header(&raw_code)?;
622
623        // Standard ISCC-UNIT with tail continuation
624        if mt != MainType::Iscc {
625            let ln_bits = decode_length(mt, ln, st);
626            let nbytes = (ln_bits / 8) as usize;
627            if body.len() < nbytes {
628                return Err(IsccError::InvalidInput(format!(
629                    "truncated ISCC body: expected {nbytes} bytes, got {}",
630                    body.len()
631                )));
632            }
633            let code = encode_component(mt, st, vs, ln_bits, &body[..nbytes])?;
634            components.push(code);
635            raw_code = body[nbytes..].to_vec();
636            continue;
637        }
638
639        // ISCC-CODE: decode into constituent units
640        let main_types = decode_units(ln)?;
641
642        // Wide mode: 128-bit Data-Code + 128-bit Instance-Code
643        if st == SubType::Wide {
644            if body.len() < 32 {
645                return Err(IsccError::InvalidInput(format!(
646                    "truncated ISCC body: expected 32 bytes, got {}",
647                    body.len()
648                )));
649            }
650            let data_code = encode_component(MainType::Data, SubType::None, vs, 128, &body[..16])?;
651            let instance_code =
652                encode_component(MainType::Instance, SubType::None, vs, 128, &body[16..32])?;
653            components.push(data_code);
654            components.push(instance_code);
655            break;
656        }
657
658        // Non-wide ISCC-CODE: total body = dynamic units × 8 + Data 8 + Instance 8
659        let expected_body = main_types.len() * 8 + 16;
660        if body.len() < expected_body {
661            return Err(IsccError::InvalidInput(format!(
662                "truncated ISCC body: expected {expected_body} bytes, got {}",
663                body.len()
664            )));
665        }
666
667        // Rebuild dynamic units (Meta, Semantic, Content)
668        for (idx, &mtype) in main_types.iter().enumerate() {
669            let stype = if mtype == MainType::Meta {
670                SubType::None
671            } else {
672                st
673            };
674            let code = encode_component(mtype, stype, vs, 64, &body[idx * 8..])?;
675            components.push(code);
676        }
677
678        // Rebuild static units (Data-Code, Instance-Code)
679        let data_code = encode_component(
680            MainType::Data,
681            SubType::None,
682            vs,
683            64,
684            &body[body.len() - 16..body.len() - 8],
685        )?;
686        let instance_code = encode_component(
687            MainType::Instance,
688            SubType::None,
689            vs,
690            64,
691            &body[body.len() - 8..],
692        )?;
693        components.push(data_code);
694        components.push(instance_code);
695        break;
696    }
697
698    Ok(components)
699}
700
701#[cfg(test)]
702mod tests {
703    use super::*;
704
705    // ---- iscc_clean tests ----
706
707    #[test]
708    fn test_iscc_clean_strips_scheme_and_dashes() {
709        // Canonical display form: scheme prefix + hyphen groups.
710        assert_eq!(
711            iscc_clean("ISCC:KACY-PXW4-45FT-YNJ3").unwrap(),
712            "KACYPXW445FTYNJ3"
713        );
714    }
715
716    #[test]
717    fn test_iscc_clean_case_insensitive_scheme() {
718        assert_eq!(
719            iscc_clean("iscc:KACYPXW445FTYNJ3").unwrap(),
720            "KACYPXW445FTYNJ3"
721        );
722        assert_eq!(
723            iscc_clean("Iscc:KACYPXW445FTYNJ3").unwrap(),
724            "KACYPXW445FTYNJ3"
725        );
726    }
727
728    #[test]
729    fn test_iscc_clean_trims_whitespace() {
730        assert_eq!(iscc_clean("  ISCC: KACY-PXW4  ").unwrap(), "KACYPXW4");
731    }
732
733    #[test]
734    fn test_iscc_clean_no_prefix() {
735        assert_eq!(
736            iscc_clean("KACY-PXW4-45FT-YNJ3").unwrap(),
737            "KACYPXW445FTYNJ3"
738        );
739    }
740
741    #[test]
742    fn test_iscc_clean_preserves_multibase_dashes() {
743        // A multibase-prefixed input (starts with 'u') must keep its dashes intact.
744        assert_eq!(iscc_clean("uABC-DEF").unwrap(), "uABC-DEF");
745        // Every multibase prefix is preserved verbatim.
746        for prefix in ['f', 'b', 'v', 'z', 'u'] {
747            let input = format!("{prefix}AA-BB");
748            assert_eq!(iscc_clean(&input).unwrap(), input);
749        }
750    }
751
752    #[test]
753    fn test_iscc_clean_rejects_bad_scheme() {
754        assert!(matches!(
755            iscc_clean("http:KACYPXW445FTYNJ3"),
756            Err(IsccError::InvalidInput(_))
757        ));
758    }
759
760    #[test]
761    fn test_iscc_clean_rejects_extra_colon() {
762        assert!(matches!(
763            iscc_clean("ISCC:KACY:PXW4"),
764            Err(IsccError::InvalidInput(_))
765        ));
766    }
767
768    // ---- Varnibble roundtrip tests ----
769
770    #[test]
771    fn test_varnibble_roundtrip() {
772        let test_values = [0, 1, 7, 8, 71, 72, 583, 584, 4679];
773        for &value in &test_values {
774            let bits = encode_varnibble(value).unwrap();
775            let bytes = bits_to_bytes(&bits);
776            let (decoded, consumed) = decode_varnibble_from_bytes(&bytes, 0).unwrap();
777            assert_eq!(decoded, value, "roundtrip failed for value {value}");
778            assert_eq!(consumed, bits.len(), "consumed mismatch for value {value}");
779        }
780    }
781
782    #[test]
783    fn test_varnibble_bit_lengths() {
784        // 0-7: 4 bits (1 nibble)
785        assert_eq!(encode_varnibble(0).unwrap().len(), 4);
786        assert_eq!(encode_varnibble(7).unwrap().len(), 4);
787        // 8-71: 8 bits (2 nibbles)
788        assert_eq!(encode_varnibble(8).unwrap().len(), 8);
789        assert_eq!(encode_varnibble(71).unwrap().len(), 8);
790        // 72-583: 12 bits (3 nibbles)
791        assert_eq!(encode_varnibble(72).unwrap().len(), 12);
792        assert_eq!(encode_varnibble(583).unwrap().len(), 12);
793        // 584-4679: 16 bits (4 nibbles)
794        assert_eq!(encode_varnibble(584).unwrap().len(), 16);
795        assert_eq!(encode_varnibble(4679).unwrap().len(), 16);
796    }
797
798    #[test]
799    fn test_varnibble_out_of_range() {
800        assert!(encode_varnibble(4680).is_err());
801    }
802
803    #[test]
804    fn test_varnibble_boundary_values() {
805        // Verify exact bit patterns at boundaries
806        let bits_0 = encode_varnibble(0).unwrap();
807        assert_eq!(bits_0, vec![false, false, false, false]); // 0000
808
809        let bits_7 = encode_varnibble(7).unwrap();
810        assert_eq!(bits_7, vec![false, true, true, true]); // 0111
811
812        let bits_8 = encode_varnibble(8).unwrap();
813        assert_eq!(
814            bits_8,
815            vec![true, false, false, false, false, false, false, false]
816        ); // 10 000000
817    }
818
819    // ---- Bitwise extraction tests ----
820
821    #[test]
822    fn test_extract_bits_basic() {
823        // 0xA5 = 1010_0101 in binary
824        let data = [0xA5u8];
825        assert_eq!(extract_bits(&data, 0, 4), 0b1010); // first nibble
826        assert_eq!(extract_bits(&data, 4, 4), 0b0101); // second nibble
827        assert_eq!(extract_bits(&data, 0, 8), 0xA5); // full byte
828        assert_eq!(extract_bits(&data, 1, 3), 0b010); // bits 1-3
829        assert_eq!(extract_bits(&data, 0, 1), 1); // MSB
830        assert_eq!(extract_bits(&data, 7, 1), 1); // LSB
831
832        // Multi-byte: 0xFF 0x00 = 1111_1111 0000_0000
833        let data2 = [0xFF, 0x00];
834        assert_eq!(extract_bits(&data2, 0, 8), 0xFF);
835        assert_eq!(extract_bits(&data2, 8, 8), 0x00);
836        assert_eq!(extract_bits(&data2, 4, 8), 0xF0); // crossing byte boundary
837        assert_eq!(extract_bits(&data2, 6, 4), 0b1100); // crossing byte boundary
838    }
839
840    #[test]
841    fn test_decode_varnibble_from_bytes_boundary_values() {
842        // Test decoding at non-zero bit offsets within a byte slice.
843        // Encode two varnibbles into a single byte sequence and decode both.
844
845        // varnibble(3) = 0011 (4 bits) + varnibble(8) = 10_000000 (8 bits) = 12 bits
846        let bits_3 = encode_varnibble(3).unwrap();
847        let bits_8 = encode_varnibble(8).unwrap();
848        let mut combined_bits = bits_3.clone();
849        combined_bits.extend(&bits_8);
850        let bytes = bits_to_bytes(&combined_bits);
851
852        // Decode first varnibble at bit 0
853        let (val1, consumed1) = decode_varnibble_from_bytes(&bytes, 0).unwrap();
854        assert_eq!(val1, 3);
855        assert_eq!(consumed1, 4);
856
857        // Decode second varnibble at bit 4 (non-zero offset)
858        let (val2, consumed2) = decode_varnibble_from_bytes(&bytes, 4).unwrap();
859        assert_eq!(val2, 8);
860        assert_eq!(consumed2, 8);
861
862        // Test with a 3-nibble value at offset
863        // varnibble(0) = 0000 (4 bits) + varnibble(72) = 110_000000000 (12 bits)
864        let bits_0 = encode_varnibble(0).unwrap();
865        let bits_72 = encode_varnibble(72).unwrap();
866        let mut combined2 = bits_0;
867        combined2.extend(&bits_72);
868        let bytes2 = bits_to_bytes(&combined2);
869
870        let (val3, consumed3) = decode_varnibble_from_bytes(&bytes2, 4).unwrap();
871        assert_eq!(val3, 72);
872        assert_eq!(consumed3, 12);
873
874        // Test insufficient bits at offset
875        let single_byte = [0x00u8];
876        let result = decode_varnibble_from_bytes(&single_byte, 6);
877        assert!(result.is_err(), "should fail with only 2 bits available");
878    }
879
880    // ---- Header encoding tests ----
881
882    #[test]
883    fn test_encode_header_meta_v0() {
884        // encode_header(META=0, NONE=0, V0=0, length=1) → 2 bytes
885        let header = encode_header(MainType::Meta, SubType::None, Version::V0, 1).unwrap();
886        assert_eq!(header, vec![0x00, 0x01]);
887    }
888
889    #[test]
890    fn test_encode_header_with_padding() {
891        // encode_header(META=0, NONE=0, V0=0, length=8)
892        // varnibble(0)=4b + varnibble(0)=4b + varnibble(0)=4b + varnibble(8)=8b = 20 bits
893        // Padded to 24 bits = 3 bytes
894        let header = encode_header(MainType::Meta, SubType::None, Version::V0, 8).unwrap();
895        assert_eq!(header.len(), 3);
896        // bits: 0000 0000 0000 10|000000 0000
897        //       ^^^^ ^^^^ ^^^^ ^^^^^^^^ ^^^^(pad)
898        assert_eq!(header, vec![0x00, 0x08, 0x00]);
899    }
900
901    #[test]
902    fn test_encode_header_data_type() {
903        // DATA=3, NONE=0, V0=0, length=1
904        let header = encode_header(MainType::Data, SubType::None, Version::V0, 1).unwrap();
905        // varnibble(3)=0011, varnibble(0)=0000, varnibble(0)=0000, varnibble(1)=0001
906        // bits: 0011 0000 0000 0001
907        assert_eq!(header, vec![0x30, 0x01]);
908    }
909
910    #[test]
911    fn test_encode_header_instance_type() {
912        // INSTANCE=4, NONE=0, V0=0, length=1
913        let header = encode_header(MainType::Instance, SubType::None, Version::V0, 1).unwrap();
914        // varnibble(4)=0100, varnibble(0)=0000, varnibble(0)=0000, varnibble(1)=0001
915        // bits: 0100 0000 0000 0001
916        assert_eq!(header, vec![0x40, 0x01]);
917    }
918
919    #[test]
920    fn test_decode_header_roundtrip_all_main_types() {
921        let main_types = [
922            MainType::Meta,
923            MainType::Semantic,
924            MainType::Content,
925            MainType::Data,
926            MainType::Instance,
927            MainType::Iscc,
928            MainType::Id,
929            MainType::Flake,
930        ];
931
932        for &mtype in &main_types {
933            let header = encode_header(mtype, SubType::None, Version::V0, 1).unwrap();
934            let (dec_mtype, dec_stype, dec_version, dec_length, tail) =
935                decode_header(&header).unwrap();
936            assert_eq!(dec_mtype, mtype, "MainType mismatch for {mtype:?}");
937            assert_eq!(dec_stype, SubType::None);
938            assert_eq!(dec_version, Version::V0);
939            assert_eq!(dec_length, 1);
940            assert!(tail.is_empty(), "unexpected tail for {mtype:?}");
941        }
942    }
943
944    #[test]
945    fn test_decode_header_with_tail() {
946        // Simulate header + 8 bytes body
947        let header = encode_header(MainType::Meta, SubType::None, Version::V0, 1).unwrap();
948        let body = vec![0xAA, 0xBB, 0xCC, 0xDD, 0x11, 0x22, 0x33, 0x44];
949        let mut data = header;
950        data.extend_from_slice(&body);
951
952        let (mtype, stype, version, length, tail) = decode_header(&data).unwrap();
953        assert_eq!(mtype, MainType::Meta);
954        assert_eq!(stype, SubType::None);
955        assert_eq!(version, Version::V0);
956        assert_eq!(length, 1);
957        assert_eq!(tail, body);
958    }
959
960    #[test]
961    fn test_decode_header_with_padding_and_tail() {
962        // Header with padding (3 bytes) + body
963        let header = encode_header(MainType::Meta, SubType::None, Version::V0, 8).unwrap();
964        assert_eq!(header.len(), 3); // 20 bits padded to 24
965
966        let body = vec![0xFF, 0xEE];
967        let mut data = header;
968        data.extend_from_slice(&body);
969
970        let (mtype, _stype, _version, length, tail) = decode_header(&data).unwrap();
971        assert_eq!(mtype, MainType::Meta);
972        assert_eq!(length, 8);
973        assert_eq!(tail, body);
974    }
975
976    #[test]
977    fn test_decode_header_subtypes() {
978        // Test with non-zero subtype
979        let header = encode_header(MainType::Content, SubType::Image, Version::V0, 1).unwrap();
980        let (mtype, stype, version, length, _tail) = decode_header(&header).unwrap();
981        assert_eq!(mtype, MainType::Content);
982        assert_eq!(stype, SubType::Image);
983        assert_eq!(version, Version::V0);
984        assert_eq!(length, 1);
985    }
986
987    // ---- Length encoding tests ----
988
989    #[test]
990    fn test_encode_length_standard_types() {
991        // (bit_length / 32) - 1
992        assert_eq!(encode_length(MainType::Meta, 32).unwrap(), 0);
993        assert_eq!(encode_length(MainType::Meta, 64).unwrap(), 1);
994        assert_eq!(encode_length(MainType::Meta, 96).unwrap(), 2);
995        assert_eq!(encode_length(MainType::Meta, 128).unwrap(), 3);
996        assert_eq!(encode_length(MainType::Meta, 256).unwrap(), 7);
997        assert_eq!(encode_length(MainType::Data, 64).unwrap(), 1);
998        assert_eq!(encode_length(MainType::Instance, 64).unwrap(), 1);
999    }
1000
1001    #[test]
1002    fn test_encode_length_iscc() {
1003        // Pass-through for ISCC (0-7)
1004        for i in 0..=7 {
1005            assert_eq!(encode_length(MainType::Iscc, i).unwrap(), i);
1006        }
1007        assert!(encode_length(MainType::Iscc, 8).is_err());
1008    }
1009
1010    #[test]
1011    fn test_encode_length_id() {
1012        // (bit_length - 64) / 8
1013        assert_eq!(encode_length(MainType::Id, 64).unwrap(), 0);
1014        assert_eq!(encode_length(MainType::Id, 72).unwrap(), 1);
1015        assert_eq!(encode_length(MainType::Id, 80).unwrap(), 2);
1016        assert_eq!(encode_length(MainType::Id, 96).unwrap(), 4);
1017    }
1018
1019    #[test]
1020    fn test_encode_length_invalid() {
1021        // Not a multiple of 32
1022        assert!(encode_length(MainType::Meta, 48).is_err());
1023        // Too small
1024        assert!(encode_length(MainType::Meta, 0).is_err());
1025        // ID out of range
1026        assert!(encode_length(MainType::Id, 63).is_err());
1027        assert!(encode_length(MainType::Id, 97).is_err());
1028    }
1029
1030    #[test]
1031    fn test_decode_length_standard_types() {
1032        // (length + 1) * 32
1033        assert_eq!(decode_length(MainType::Meta, 0, SubType::None), 32);
1034        assert_eq!(decode_length(MainType::Meta, 1, SubType::None), 64);
1035        assert_eq!(decode_length(MainType::Meta, 7, SubType::None), 256);
1036        assert_eq!(decode_length(MainType::Data, 1, SubType::None), 64);
1037    }
1038
1039    #[test]
1040    fn test_decode_length_iscc() {
1041        // Wide → 256
1042        assert_eq!(decode_length(MainType::Iscc, 0, SubType::Wide), 256);
1043        // Non-wide → popcount(length) * 64 + 128
1044        assert_eq!(decode_length(MainType::Iscc, 0, SubType::Sum), 128); // 0 optional units
1045        assert_eq!(decode_length(MainType::Iscc, 1, SubType::None), 192); // 1 optional unit
1046        assert_eq!(decode_length(MainType::Iscc, 3, SubType::None), 256); // 2 optional units
1047        assert_eq!(decode_length(MainType::Iscc, 7, SubType::None), 320); // 3 optional units
1048    }
1049
1050    #[test]
1051    fn test_decode_length_id() {
1052        // length * 8 + 64
1053        assert_eq!(decode_length(MainType::Id, 0, SubType::None), 64);
1054        assert_eq!(decode_length(MainType::Id, 1, SubType::None), 72);
1055        assert_eq!(decode_length(MainType::Id, 4, SubType::None), 96);
1056    }
1057
1058    #[test]
1059    fn test_encode_decode_length_roundtrip() {
1060        for &mtype in &[
1061            MainType::Meta,
1062            MainType::Data,
1063            MainType::Instance,
1064            MainType::Content,
1065        ] {
1066            for bit_length in (32..=256).step_by(32) {
1067                let encoded = encode_length(mtype, bit_length).unwrap();
1068                let decoded = decode_length(mtype, encoded, SubType::None);
1069                assert_eq!(
1070                    decoded, bit_length,
1071                    "roundtrip failed for {mtype:?} bit_length={bit_length}"
1072                );
1073            }
1074        }
1075    }
1076
1077    // ---- Base32 tests ----
1078
1079    #[test]
1080    fn test_base32_roundtrip() {
1081        let test_data: &[&[u8]] = &[
1082            &[0x00],
1083            &[0xFF],
1084            &[0x00, 0x01, 0x02, 0x03],
1085            &[0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE],
1086            &[0; 10],
1087            &[0xFF; 10],
1088        ];
1089
1090        for data in test_data {
1091            let encoded = encode_base32(data);
1092            let decoded = decode_base32(&encoded).unwrap();
1093            assert_eq!(&decoded, data, "base32 roundtrip failed for {data:?}");
1094        }
1095    }
1096
1097    #[test]
1098    fn test_base32_no_padding() {
1099        let encoded = encode_base32(&[0x00, 0x01]);
1100        assert!(!encoded.contains('='), "base32 should not contain padding");
1101    }
1102
1103    #[test]
1104    fn test_base32_case_insensitive_decode() {
1105        let data = vec![0xDE, 0xAD, 0xBE, 0xEF];
1106        let encoded = encode_base32(&data);
1107        let lower = encoded.to_lowercase();
1108        let decoded = decode_base32(&lower).unwrap();
1109        assert_eq!(decoded, data);
1110    }
1111
1112    // ---- Base64 encoding tests ----
1113
1114    #[test]
1115    fn test_encode_base64_empty() {
1116        assert_eq!(encode_base64(&[]), "");
1117    }
1118
1119    #[test]
1120    fn test_encode_base64_known_value() {
1121        // Python: base64.urlsafe_b64encode(bytes([0,1,2,3])).decode().rstrip("=") == "AAECAw"
1122        assert_eq!(encode_base64(&[0, 1, 2, 3]), "AAECAw");
1123    }
1124
1125    #[test]
1126    fn test_encode_base64_roundtrip() {
1127        let data: &[&[u8]] = &[
1128            &[0xFF],
1129            &[0xDE, 0xAD, 0xBE, 0xEF],
1130            &[0; 10],
1131            &[0xFF; 10],
1132            b"Hello World",
1133        ];
1134        for input in data {
1135            let encoded = encode_base64(input);
1136            let decoded = data_encoding::BASE64URL_NOPAD
1137                .decode(encoded.as_bytes())
1138                .unwrap();
1139            assert_eq!(&decoded, input, "base64 roundtrip failed for {input:?}");
1140        }
1141    }
1142
1143    #[test]
1144    fn test_encode_base64_no_padding() {
1145        // Various lengths that would normally produce padding
1146        for len in 1..=10 {
1147            let data = vec![0xABu8; len];
1148            let encoded = encode_base64(&data);
1149            assert!(
1150                !encoded.contains('='),
1151                "base64 output must not contain padding for len={len}"
1152            );
1153        }
1154    }
1155
1156    // ---- encode_component tests ----
1157
1158    #[test]
1159    fn test_encode_component_meta_known_vector() {
1160        // gen_meta_code_v0("Hello World") → "ISCC:AAAWKLHFPV6OPKDG"
1161        // Decode the known output to extract the digest, then re-encode
1162        let known_code = "AAAWKLHFPV6OPKDG";
1163        let raw = decode_base32(known_code).unwrap();
1164        assert_eq!(raw.len(), 10); // 2 header bytes + 8 digest bytes
1165
1166        // Verify header decodes correctly
1167        let (mtype, stype, version, length, tail) = decode_header(&raw).unwrap();
1168        assert_eq!(mtype, MainType::Meta);
1169        assert_eq!(stype, SubType::None);
1170        assert_eq!(version, Version::V0);
1171        assert_eq!(length, 1); // encode_length(META, 64) = 1
1172        assert_eq!(tail.len(), 8); // 64-bit digest
1173
1174        // Re-encode from extracted digest
1175        let result =
1176            encode_component(MainType::Meta, SubType::None, Version::V0, 64, &tail).unwrap();
1177        assert_eq!(result, known_code);
1178    }
1179
1180    #[test]
1181    fn test_encode_component_rejects_iscc_maintype() {
1182        assert!(
1183            encode_component(MainType::Iscc, SubType::Sum, Version::V0, 128, &[0; 16],).is_err()
1184        );
1185    }
1186
1187    #[test]
1188    fn test_encode_component_data_type() {
1189        // Encode a Data-Code component and verify roundtrip
1190        let digest = [0xAA; 32];
1191        let code =
1192            encode_component(MainType::Data, SubType::None, Version::V0, 64, &digest).unwrap();
1193
1194        // Decode and verify
1195        let raw = decode_base32(&code).unwrap();
1196        let (mtype, stype, version, length, tail) = decode_header(&raw).unwrap();
1197        assert_eq!(mtype, MainType::Data);
1198        assert_eq!(stype, SubType::None);
1199        assert_eq!(version, Version::V0);
1200        assert_eq!(length, 1); // encode_length(DATA, 64) = 1
1201        assert_eq!(tail, &digest[..8]); // 64 bits = 8 bytes
1202    }
1203
1204    #[test]
1205    fn test_encode_component_content_image() {
1206        let digest = [0x55; 16];
1207        let code =
1208            encode_component(MainType::Content, SubType::Image, Version::V0, 128, &digest).unwrap();
1209
1210        let raw = decode_base32(&code).unwrap();
1211        let (mtype, stype, _version, length, tail) = decode_header(&raw).unwrap();
1212        assert_eq!(mtype, MainType::Content);
1213        assert_eq!(stype, SubType::Image);
1214        assert_eq!(length, 3); // encode_length(CONTENT, 128) = 3
1215        assert_eq!(tail, &digest[..]); // 128 bits = 16 bytes
1216    }
1217
1218    // ---- TryFrom tests ----
1219
1220    #[test]
1221    fn test_maintype_try_from() {
1222        for v in 0..=7u8 {
1223            assert!(MainType::try_from(v).is_ok());
1224        }
1225        assert!(MainType::try_from(8).is_err());
1226    }
1227
1228    #[test]
1229    fn test_subtype_try_from() {
1230        for v in 0..=7u8 {
1231            assert!(SubType::try_from(v).is_ok());
1232        }
1233        assert!(SubType::try_from(8).is_err());
1234    }
1235
1236    #[test]
1237    fn test_version_try_from() {
1238        assert_eq!(Version::try_from(0).unwrap(), Version::V0);
1239        assert_eq!(Version::try_from(1).unwrap(), Version::V1);
1240        assert!(Version::try_from(2).is_err());
1241    }
1242
1243    // ---- ISCC-IDv1 Version 1 acceptance tests ----
1244
1245    #[test]
1246    fn test_iscc_decode_idv1_realm0() {
1247        // Reference: iscc_core.iscc_decode("ISCC:MAIGHFECJMOPMIAB")
1248        //   -> (6, 0, 1, 0, b'c\x94\x82K\x1c\xf6 \x01')
1249        let expected_body = vec![0x63, 0x94, 0x82, 0x4b, 0x1c, 0xf6, 0x20, 0x01];
1250        let with_prefix = crate::iscc_decode("ISCC:MAIGHFECJMOPMIAB").unwrap();
1251        assert_eq!(with_prefix, (6, 0, 1, 0, expected_body.clone()));
1252        // Same result via the bare (no-prefix) form.
1253        let no_prefix = crate::iscc_decode("MAIGHFECJMOPMIAB").unwrap();
1254        assert_eq!(no_prefix, (6, 0, 1, 0, expected_body));
1255    }
1256
1257    #[test]
1258    fn test_decompose_idv1_accepts_version1() {
1259        // A single ISCC-IDv1 unit must decompose without "invalid Version: 1".
1260        let result = iscc_decompose("ISCC:MAIGHFECJMOPMIAB").unwrap();
1261        assert_eq!(result, vec!["MAIGHFECJMOPMIAB"]);
1262    }
1263
1264    #[test]
1265    fn test_decode_header_idv1_version1() {
1266        // Header 0x6010 = MainType Id (6), realm 0, Version 1, length 0.
1267        let raw = decode_base32("MAIGHFECJMOPMIAB").unwrap();
1268        let (mtype, stype, version, length, tail) = decode_header(&raw).unwrap();
1269        assert_eq!(mtype, MainType::Id);
1270        assert_eq!(stype, SubType::None); // realm 0 travels as the nibble 0
1271        assert_eq!(version, Version::V1);
1272        assert_eq!(length, 0);
1273        assert_eq!(tail.len(), 8);
1274    }
1275
1276    #[test]
1277    fn test_encode_decode_header_idv1_roundtrip() {
1278        // Realm 1 (operational) travels as the SubType nibble 1 (cosmetically Image).
1279        let header = encode_header(MainType::Id, SubType::Image, Version::V1, 0).unwrap();
1280        let (mtype, stype, version, length, _tail) = decode_header(&header).unwrap();
1281        assert_eq!(mtype, MainType::Id);
1282        assert_eq!(stype, SubType::Image);
1283        assert_eq!(version, Version::V1);
1284        assert_eq!(length, 0);
1285    }
1286
1287    #[test]
1288    fn test_encode_header_rejects_version1_for_non_id() {
1289        // Version 1 is only valid for MainType Id; a Meta header must reject it.
1290        let result = encode_header(MainType::Meta, SubType::None, Version::V1, 1);
1291        assert!(result.is_err());
1292        assert!(result.unwrap_err().to_string().contains("invalid Version"));
1293    }
1294
1295    #[test]
1296    fn test_decode_header_rejects_version1_for_non_id() {
1297        // Craft a Meta header (MainType 0) with version nibble 1 and length 1.
1298        // varnibble(0)=0000 mtype, varnibble(0)=0000 stype, varnibble(1)=0001 version,
1299        // varnibble(1)=0001 length -> bits 0000 0000 0001 0001 = 0x00 0x11
1300        let raw = [0x00u8, 0x11u8];
1301        let result = decode_header(&raw);
1302        assert!(result.is_err());
1303        assert!(result.unwrap_err().to_string().contains("invalid Version"));
1304    }
1305
1306    #[test]
1307    fn test_decode_header_rejects_truncated_varnibble_fields() {
1308        // "MDFZAAAAAAAAAAAAAA" decodes to a header whose multi-nibble fields
1309        // overflow a u8 (version 257, MainType 262). Before the range check these
1310        // wrapped (257 -> 1, 262 -> 6 = Id) and canonicalized to a valid ISCC.
1311        assert!(crate::iscc_decode("MDFZAAAAAAAAAAAAAA").is_err());
1312        assert!(iscc_decompose("MDFZAAAAAAAAAAAAAA").is_err());
1313    }
1314
1315    #[test]
1316    fn test_iscc_decode_rejects_version1_for_non_id() {
1317        // The Tier 1 iscc_decode must also reject a non-Id Version-1 header.
1318        let iscc = encode_base32(&[0x00u8, 0x11u8]);
1319        let result = crate::iscc_decode(&iscc);
1320        assert!(result.is_err());
1321        assert!(result.unwrap_err().to_string().contains("invalid Version"));
1322    }
1323
1324    #[test]
1325    fn test_subtype_text_alias() {
1326        assert_eq!(SubType::TEXT, SubType::None);
1327        assert_eq!(SubType::TEXT as u8, 0);
1328    }
1329
1330    // ---- Bit helper tests ----
1331
1332    #[test]
1333    fn test_bits_to_u32() {
1334        assert_eq!(bits_to_u32(&[false, false, false, false]), 0);
1335        assert_eq!(bits_to_u32(&[false, true, true, true]), 7);
1336        assert_eq!(bits_to_u32(&[true, false, false, false]), 8);
1337        assert_eq!(bits_to_u32(&[true, true, true, true]), 15);
1338    }
1339
1340    #[test]
1341    fn test_bytes_bits_roundtrip() {
1342        let data = vec![0x00, 0x01, 0xFF, 0xAB];
1343        let bits = bytes_to_bits(&data);
1344        assert_eq!(bits.len(), 32);
1345        let bytes = bits_to_bytes(&bits);
1346        assert_eq!(bytes, data);
1347    }
1348
1349    // ---- encode_units tests ----
1350
1351    #[test]
1352    fn test_encode_units_empty() {
1353        assert_eq!(encode_units(&[]).unwrap(), 0);
1354    }
1355
1356    #[test]
1357    fn test_encode_units_content_only() {
1358        assert_eq!(encode_units(&[MainType::Content]).unwrap(), 1);
1359    }
1360
1361    #[test]
1362    fn test_encode_units_semantic_only() {
1363        assert_eq!(encode_units(&[MainType::Semantic]).unwrap(), 2);
1364    }
1365
1366    #[test]
1367    fn test_encode_units_semantic_content() {
1368        assert_eq!(
1369            encode_units(&[MainType::Semantic, MainType::Content]).unwrap(),
1370            3
1371        );
1372    }
1373
1374    #[test]
1375    fn test_encode_units_meta_only() {
1376        assert_eq!(encode_units(&[MainType::Meta]).unwrap(), 4);
1377    }
1378
1379    #[test]
1380    fn test_encode_units_meta_content() {
1381        assert_eq!(
1382            encode_units(&[MainType::Meta, MainType::Content]).unwrap(),
1383            5
1384        );
1385    }
1386
1387    #[test]
1388    fn test_encode_units_meta_semantic() {
1389        assert_eq!(
1390            encode_units(&[MainType::Meta, MainType::Semantic]).unwrap(),
1391            6
1392        );
1393    }
1394
1395    #[test]
1396    fn test_encode_units_all_optional() {
1397        assert_eq!(
1398            encode_units(&[MainType::Meta, MainType::Semantic, MainType::Content]).unwrap(),
1399            7
1400        );
1401    }
1402
1403    #[test]
1404    fn test_encode_units_rejects_data() {
1405        assert!(encode_units(&[MainType::Data]).is_err());
1406    }
1407
1408    #[test]
1409    fn test_encode_units_rejects_instance() {
1410        assert!(encode_units(&[MainType::Instance]).is_err());
1411    }
1412
1413    #[test]
1414    fn test_encode_units_rejects_iscc() {
1415        assert!(encode_units(&[MainType::Iscc]).is_err());
1416    }
1417
1418    // ---- decode_units tests ----
1419
1420    #[test]
1421    fn test_decode_units_empty() {
1422        assert_eq!(decode_units(0).unwrap(), vec![]);
1423    }
1424
1425    #[test]
1426    fn test_decode_units_content() {
1427        assert_eq!(decode_units(1).unwrap(), vec![MainType::Content]);
1428    }
1429
1430    #[test]
1431    fn test_decode_units_semantic() {
1432        assert_eq!(decode_units(2).unwrap(), vec![MainType::Semantic]);
1433    }
1434
1435    #[test]
1436    fn test_decode_units_semantic_content() {
1437        assert_eq!(
1438            decode_units(3).unwrap(),
1439            vec![MainType::Semantic, MainType::Content]
1440        );
1441    }
1442
1443    #[test]
1444    fn test_decode_units_meta() {
1445        assert_eq!(decode_units(4).unwrap(), vec![MainType::Meta]);
1446    }
1447
1448    #[test]
1449    fn test_decode_units_meta_content() {
1450        assert_eq!(
1451            decode_units(5).unwrap(),
1452            vec![MainType::Meta, MainType::Content]
1453        );
1454    }
1455
1456    #[test]
1457    fn test_decode_units_meta_semantic() {
1458        assert_eq!(
1459            decode_units(6).unwrap(),
1460            vec![MainType::Meta, MainType::Semantic]
1461        );
1462    }
1463
1464    #[test]
1465    fn test_decode_units_all() {
1466        assert_eq!(
1467            decode_units(7).unwrap(),
1468            vec![MainType::Meta, MainType::Semantic, MainType::Content]
1469        );
1470    }
1471
1472    #[test]
1473    fn test_decode_units_invalid() {
1474        assert!(decode_units(8).is_err());
1475        assert!(decode_units(255).is_err());
1476    }
1477
1478    #[test]
1479    fn test_decode_units_roundtrip_with_encode_units() {
1480        for unit_id in 0..=7u32 {
1481            let types = decode_units(unit_id).unwrap();
1482            let encoded = encode_units(&types).unwrap();
1483            assert_eq!(encoded, unit_id, "roundtrip failed for unit_id={unit_id}");
1484        }
1485    }
1486
1487    // ---- iscc_decompose tests ----
1488
1489    #[test]
1490    fn test_decompose_single_meta_unit() {
1491        // A single Meta-Code unit passes through unchanged
1492        let result = iscc_decompose("AAAYPXW445FTYNJ3").unwrap();
1493        assert_eq!(result, vec!["AAAYPXW445FTYNJ3"]);
1494    }
1495
1496    #[test]
1497    fn test_decompose_single_unit_with_prefix() {
1498        // Accepts "ISCC:" prefix and returns without prefix
1499        let result = iscc_decompose("ISCC:AAAYPXW445FTYNJ3").unwrap();
1500        assert_eq!(result, vec!["AAAYPXW445FTYNJ3"]);
1501    }
1502
1503    #[test]
1504    fn test_decompose_single_unit_maintype() {
1505        // Verify the decomposed unit decodes to the expected MainType
1506        let result = iscc_decompose("AAAYPXW445FTYNJ3").unwrap();
1507        assert_eq!(result.len(), 1);
1508        let raw = decode_base32(&result[0]).unwrap();
1509        let (mt, _, _, _, _) = decode_header(&raw).unwrap();
1510        assert_eq!(mt, MainType::Meta);
1511    }
1512
1513    #[test]
1514    fn test_decompose_standard_iscc_code() {
1515        // test_0000_standard: Meta + Content(Text) + Data + Instance → composite
1516        let codes = [
1517            "AAAYPXW445FTYNJ3",
1518            "EAARMJLTQCUWAND2",
1519            "GABVVC5DMJJGYKZ4ZBYVNYABFFYXG",
1520            "IADWIK7A7JTUAQ2D6QARX7OBEIK3OOUAM42LOBLCZ4ZOGDLRHMDL6TQ",
1521        ];
1522        let composite = crate::gen_iscc_code_v0(
1523            &codes.iter().map(|s| *s as &str).collect::<Vec<&str>>(),
1524            false,
1525        )
1526        .unwrap();
1527
1528        let decomposed = iscc_decompose(&composite.iscc).unwrap();
1529
1530        // Should produce 4 units: Meta, Content, Data, Instance
1531        assert_eq!(decomposed.len(), 4);
1532
1533        // Verify MainTypes in order
1534        let main_types: Vec<MainType> = decomposed
1535            .iter()
1536            .map(|code| {
1537                let raw = decode_base32(code).unwrap();
1538                let (mt, _, _, _, _) = decode_header(&raw).unwrap();
1539                mt
1540            })
1541            .collect();
1542        assert_eq!(
1543            main_types,
1544            vec![
1545                MainType::Meta,
1546                MainType::Content,
1547                MainType::Data,
1548                MainType::Instance
1549            ]
1550        );
1551
1552        // Data and Instance are always the last two
1553        let raw_data = decode_base32(&decomposed[2]).unwrap();
1554        let (mt_d, _, _, _, _) = decode_header(&raw_data).unwrap();
1555        assert_eq!(mt_d, MainType::Data);
1556
1557        let raw_inst = decode_base32(&decomposed[3]).unwrap();
1558        let (mt_i, _, _, _, _) = decode_header(&raw_inst).unwrap();
1559        assert_eq!(mt_i, MainType::Instance);
1560    }
1561
1562    #[test]
1563    fn test_decompose_no_meta() {
1564        // test_0001_no_meta: Content(Text) + Data + Instance → composite (no Meta)
1565        let codes = [
1566            "EAARMJLTQCUWAND2",
1567            "GABVVC5DMJJGYKZ4ZBYVNYABFFYXG",
1568            "IADWIK7A7JTUAQ2D6QARX7OBEIK3OOUAM42LOBLCZ4ZOGDLRHMDL6TQ",
1569        ];
1570        let composite = crate::gen_iscc_code_v0(
1571            &codes.iter().map(|s| *s as &str).collect::<Vec<&str>>(),
1572            false,
1573        )
1574        .unwrap();
1575
1576        let decomposed = iscc_decompose(&composite.iscc).unwrap();
1577
1578        // Should produce 3 units: Content, Data, Instance (no Meta)
1579        assert_eq!(decomposed.len(), 3);
1580
1581        let main_types: Vec<MainType> = decomposed
1582            .iter()
1583            .map(|code| {
1584                let raw = decode_base32(code).unwrap();
1585                let (mt, _, _, _, _) = decode_header(&raw).unwrap();
1586                mt
1587            })
1588            .collect();
1589        assert_eq!(
1590            main_types,
1591            vec![MainType::Content, MainType::Data, MainType::Instance]
1592        );
1593    }
1594
1595    #[test]
1596    fn test_decompose_sum_only() {
1597        // test_0002: Data + Instance only (Sum SubType)
1598        let codes = [
1599            "GABVVC5DMJJGYKZ4ZBYVNYABFFYXG",
1600            "IADWIK7A7JTUAQ2D6QARX7OBEIK3OOUAM42LOBLCZ4ZOGDLRHMDL6TQ",
1601        ];
1602        let composite = crate::gen_iscc_code_v0(
1603            &codes.iter().map(|s| *s as &str).collect::<Vec<&str>>(),
1604            false,
1605        )
1606        .unwrap();
1607
1608        let decomposed = iscc_decompose(&composite.iscc).unwrap();
1609
1610        // Should produce 2 units: Data, Instance
1611        assert_eq!(decomposed.len(), 2);
1612
1613        let main_types: Vec<MainType> = decomposed
1614            .iter()
1615            .map(|code| {
1616                let raw = decode_base32(code).unwrap();
1617                let (mt, _, _, _, _) = decode_header(&raw).unwrap();
1618                mt
1619            })
1620            .collect();
1621        assert_eq!(main_types, vec![MainType::Data, MainType::Instance]);
1622    }
1623
1624    #[test]
1625    fn test_decompose_conformance_roundtrip() {
1626        // Use gen_iscc_code_v0 conformance vectors to verify decompose
1627        let json_str = include_str!("../tests/data.json");
1628        let data: serde_json::Value = serde_json::from_str(json_str).unwrap();
1629        let section = &data["gen_iscc_code_v0"];
1630        let cases = section.as_object().unwrap();
1631
1632        for (tc_name, tc) in cases {
1633            let expected_iscc = tc["outputs"]["iscc"].as_str().unwrap();
1634            let inputs = tc["inputs"].as_array().unwrap();
1635            let codes_json = inputs[0].as_array().unwrap();
1636            let input_codes: Vec<&str> = codes_json.iter().map(|v| v.as_str().unwrap()).collect();
1637
1638            let decomposed = iscc_decompose(expected_iscc).unwrap();
1639
1640            // Each decomposed code decodes to a valid MainType
1641            for code in &decomposed {
1642                let raw = decode_base32(code).unwrap();
1643                let (mt, _, _, _, _) = decode_header(&raw).unwrap();
1644                assert_ne!(
1645                    mt,
1646                    MainType::Iscc,
1647                    "decomposed unit should not be ISCC in {tc_name}"
1648                );
1649            }
1650
1651            // Data and Instance are always the last two units
1652            let last_two: Vec<MainType> = decomposed[decomposed.len() - 2..]
1653                .iter()
1654                .map(|code| {
1655                    let raw = decode_base32(code).unwrap();
1656                    let (mt, _, _, _, _) = decode_header(&raw).unwrap();
1657                    mt
1658                })
1659                .collect();
1660            assert_eq!(
1661                last_two,
1662                vec![MainType::Data, MainType::Instance],
1663                "last two units must be Data+Instance in {tc_name}"
1664            );
1665
1666            // Number of decomposed units matches number of input codes
1667            assert_eq!(
1668                decomposed.len(),
1669                input_codes.len(),
1670                "decomposed unit count mismatch in {tc_name}"
1671            );
1672        }
1673    }
1674
1675    // ---- iscc_decompose truncation tests ----
1676
1677    /// Build a truncated ISCC string: valid header for given params, but fewer body bytes than needed.
1678    ///
1679    /// For ISCC MainType, `length_field` is the raw unit_id (0-7).
1680    /// For other MainTypes, `length_field` is the raw header length field value.
1681    fn make_truncated_iscc(
1682        mtype: MainType,
1683        stype: SubType,
1684        length_field: u32,
1685        body_len: usize,
1686    ) -> String {
1687        let header = encode_header(mtype, stype, Version::V0, length_field).unwrap();
1688        let mut raw = header;
1689        raw.extend(vec![0xABu8; body_len]);
1690        encode_base32(&raw)
1691    }
1692
1693    #[test]
1694    fn test_decompose_truncated_standard_unit() {
1695        // Meta-Code header for 64 bits (8 bytes expected), but only 4 body bytes provided.
1696        // encode_length(Meta, 64) = 64/32 - 1 = 1
1697        let length_field = encode_length(MainType::Meta, 64).unwrap();
1698        let iscc = make_truncated_iscc(MainType::Meta, SubType::None, length_field, 4);
1699        let result = iscc_decompose(&iscc);
1700        assert!(
1701            result.is_err(),
1702            "expected error for truncated standard unit"
1703        );
1704        let err = result.unwrap_err().to_string();
1705        assert!(
1706            err.contains("truncated ISCC body"),
1707            "error should mention truncation: {err}"
1708        );
1709    }
1710
1711    #[test]
1712    fn test_decompose_truncated_wide_mode() {
1713        // ISCC-CODE Wide header expects 32 body bytes, provide only 16.
1714        // For Wide ISCC-CODE, length field is unit_id (0 = no optional units)
1715        let iscc = make_truncated_iscc(MainType::Iscc, SubType::Wide, 0, 16);
1716        let result = iscc_decompose(&iscc);
1717        assert!(result.is_err(), "expected error for truncated wide mode");
1718        let err = result.unwrap_err().to_string();
1719        assert!(
1720            err.contains("truncated ISCC body"),
1721            "error should mention truncation: {err}"
1722        );
1723    }
1724
1725    #[test]
1726    fn test_decompose_truncated_dynamic_units() {
1727        // ISCC-CODE with Meta+Content (unit_id=5, bit0=Content+bit2=Meta)
1728        // Dynamic units: 2 × 8 = 16 bytes, static: 16 bytes, total: 32 bytes needed
1729        // Provide only 8 body bytes (enough for 1 dynamic unit, not all)
1730        let unit_id = 5; // Meta + Content
1731        let iscc = make_truncated_iscc(MainType::Iscc, SubType::None, unit_id, 8);
1732        let result = iscc_decompose(&iscc);
1733        assert!(
1734            result.is_err(),
1735            "expected error for truncated dynamic units"
1736        );
1737        let err = result.unwrap_err().to_string();
1738        assert!(
1739            err.contains("truncated ISCC body"),
1740            "error should mention truncation: {err}"
1741        );
1742    }
1743
1744    #[test]
1745    fn test_decompose_truncated_static_units() {
1746        // ISCC-CODE with Content only (unit_id=1)
1747        // Dynamic: 1 × 8 = 8, static: 16, total: 24 bytes needed
1748        // Provide only 16 body bytes (dynamic ok, but static Data+Instance missing)
1749        let unit_id = 1; // Content only
1750        let iscc = make_truncated_iscc(MainType::Iscc, SubType::None, unit_id, 16);
1751        let result = iscc_decompose(&iscc);
1752        assert!(result.is_err(), "expected error for truncated static units");
1753        let err = result.unwrap_err().to_string();
1754        assert!(
1755            err.contains("truncated ISCC body"),
1756            "error should mention truncation: {err}"
1757        );
1758    }
1759
1760    #[test]
1761    fn test_decompose_empty_body() {
1762        // Meta-Code header for 64 bits but zero body bytes
1763        let length_field = encode_length(MainType::Meta, 64).unwrap();
1764        let iscc = make_truncated_iscc(MainType::Meta, SubType::None, length_field, 0);
1765        let result = iscc_decompose(&iscc);
1766        assert!(result.is_err(), "expected error for empty body");
1767        let err = result.unwrap_err().to_string();
1768        assert!(
1769            err.contains("truncated ISCC body"),
1770            "error should mention truncation: {err}"
1771        );
1772    }
1773
1774    #[test]
1775    fn test_decompose_valid_still_works() {
1776        // A valid ISCC-CODE should still decompose correctly (regression guard)
1777        // Build: Meta(64) + Content-Text(64) + Data(64) + Instance(64)
1778        let meta_body = [0x11u8; 8];
1779        let content_body = [0x22u8; 8];
1780        let data_body = [0x33u8; 8];
1781        let instance_body = [0x44u8; 8];
1782
1783        let meta_code =
1784            encode_component(MainType::Meta, SubType::None, Version::V0, 64, &meta_body).unwrap();
1785        let content_code = encode_component(
1786            MainType::Content,
1787            SubType::None,
1788            Version::V0,
1789            64,
1790            &content_body,
1791        )
1792        .unwrap();
1793        let data_code =
1794            encode_component(MainType::Data, SubType::None, Version::V0, 64, &data_body).unwrap();
1795        let instance_code = encode_component(
1796            MainType::Instance,
1797            SubType::None,
1798            Version::V0,
1799            64,
1800            &instance_body,
1801        )
1802        .unwrap();
1803
1804        // Concatenate as a sequence of ISCC-UNITs (not a single ISCC-CODE)
1805        let sequence = format!("{meta_code}{content_code}{data_code}{instance_code}");
1806        let raw = decode_base32(&sequence).unwrap();
1807        let full_iscc = encode_base32(&raw);
1808
1809        let result = iscc_decompose(&full_iscc);
1810        assert!(
1811            result.is_ok(),
1812            "valid ISCC sequence should decompose: {result:?}"
1813        );
1814        let units = result.unwrap();
1815        assert_eq!(units.len(), 4, "should decompose into 4 units");
1816    }
1817}