#![cfg(feature = "alloc")]
use alloc::vec::Vec;
use crate::composition::CompositionFailure;
use crate::label::KappaLabel;
extern crate alloc;
pub fn decode_operand<const N: usize>(
operand: &KappaLabel<N>,
) -> Result<(&str, Vec<u8>), CompositionFailure> {
let axis = operand
.sigma_axis()
.ok_or(CompositionFailure::MalformedOperand)?;
let hex_digest = operand
.sigma_axis_digest_hex()
.ok_or(CompositionFailure::MalformedOperand)?;
if hex_digest.len() % 2 != 0 {
return Err(CompositionFailure::MalformedOperand);
}
let mut raw = Vec::with_capacity(hex_digest.len() / 2);
let bytes = hex_digest.as_bytes();
for pair in bytes.chunks_exact(2) {
let hi = hex_nibble(pair[0]).ok_or(CompositionFailure::MalformedOperand)?;
let lo = hex_nibble(pair[1]).ok_or(CompositionFailure::MalformedOperand)?;
raw.push((hi << 4) | lo);
}
Ok((axis, raw))
}
fn hex_nibble(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(10 + b - b'a'),
_ => None,
}
}
pub fn check_axis(
operand_axis: &str,
expected_axis: &'static str,
) -> Result<(), CompositionFailure> {
if operand_axis == expected_axis {
Ok(())
} else {
let static_axis = match operand_axis {
"sha256" => "sha256",
"blake3" => "blake3",
"sha3-256" => "sha3-256",
"keccak256" => "keccak256",
"sha512" => "sha512",
_ => return Err(CompositionFailure::MalformedOperand),
};
Err(CompositionFailure::OperandSigmaAxisMismatch {
expected_axis,
operand_axis: static_axis,
})
}
}
pub fn canonicalize_g2<const N: usize>(left: &KappaLabel<N>, right: &KappaLabel<N>) -> Vec<u8> {
let l = left.as_bytes();
let r = right.as_bytes();
let mut out = Vec::with_capacity(N + N);
if l <= r {
out.extend_from_slice(l);
out.extend_from_slice(r);
} else {
out.extend_from_slice(r);
out.extend_from_slice(l);
}
out
}
pub fn canonicalize_f4<const N: usize>(
operand: &KappaLabel<N>,
) -> Result<Vec<u8>, CompositionFailure> {
let (axis, raw) = decode_operand(operand)?;
let complement: Vec<u8> = raw.iter().map(|b| !b).collect();
let canon_raw: &[u8] = if raw[..] <= complement[..] {
&raw[..]
} else {
&complement[..]
};
Ok(emit_canonical(axis, canon_raw))
}
const DEGREE_5_TAG: u8 = 0x05;
const DEGREE_6_TAG: u8 = 0x06;
pub fn canonicalize_e6<const N: usize>(
operand: &KappaLabel<N>,
) -> Result<Vec<u8>, CompositionFailure> {
let (_axis, raw) = decode_operand(operand)?;
if raw.is_empty() {
return Err(CompositionFailure::MalformedOperand);
}
let tag = match raw[0] % 9 {
0..=7 => DEGREE_5_TAG,
8 => DEGREE_6_TAG,
_ => unreachable!("u8 % 9 is in 0..=8"),
};
let mut out = Vec::with_capacity(1 + N);
out.push(tag);
out.extend_from_slice(operand.as_bytes());
Ok(out)
}
const S4_PERMUTATIONS: [[usize; 4]; 24] = [
[0, 1, 2, 3],
[0, 1, 3, 2],
[0, 2, 1, 3],
[0, 2, 3, 1],
[0, 3, 1, 2],
[0, 3, 2, 1],
[1, 0, 2, 3],
[1, 0, 3, 2],
[1, 2, 0, 3],
[1, 2, 3, 0],
[1, 3, 0, 2],
[1, 3, 2, 0],
[2, 0, 1, 3],
[2, 0, 3, 1],
[2, 1, 0, 3],
[2, 1, 3, 0],
[2, 3, 0, 1],
[2, 3, 1, 0],
[3, 0, 1, 2],
[3, 0, 2, 1],
[3, 1, 0, 2],
[3, 1, 2, 0],
[3, 2, 0, 1],
[3, 2, 1, 0],
];
pub fn canonicalize_e7<const N: usize>(
operand: &KappaLabel<N>,
) -> Result<Vec<u8>, CompositionFailure> {
let (axis, raw) = decode_operand(operand)?;
if raw.len() % 4 != 0 || raw.is_empty() {
return Err(CompositionFailure::MalformedOperand);
}
let q = raw.len() / 4;
let quarters: [&[u8]; 4] = [
&raw[0..q],
&raw[q..2 * q],
&raw[2 * q..3 * q],
&raw[3 * q..],
];
let mut canon: Option<Vec<u8>> = None;
for perm in S4_PERMUTATIONS.iter() {
let mut candidate = Vec::with_capacity(raw.len());
for &idx in perm.iter() {
candidate.extend_from_slice(quarters[idx]);
}
match &canon {
None => canon = Some(candidate),
Some(current) if candidate < *current => canon = Some(candidate),
_ => {}
}
}
let canon_raw = canon.expect("S4_PERMUTATIONS is non-empty");
Ok(emit_canonical(axis, &canon_raw))
}
pub fn canonicalize_e8<const N: usize>(operand: &KappaLabel<N>) -> Vec<u8> {
operand.as_bytes().to_vec()
}
fn emit_canonical(axis: &str, raw: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(axis.len() + 1 + 2 * raw.len());
out.extend_from_slice(axis.as_bytes());
out.push(b':');
for &byte in raw {
out.push(hex_lo(byte >> 4));
out.push(hex_lo(byte & 0x0F));
}
out
}
fn hex_lo(nibble: u8) -> u8 {
match nibble {
0..=9 => b'0' + nibble,
10..=15 => b'a' + (nibble - 10),
_ => unreachable!("nibble is `& 0x0F`"),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn label<const N: usize>(s: &str) -> KappaLabel<N> {
KappaLabel::from_bytes(s.as_bytes()).expect("test label admits")
}
#[test]
fn g2_is_commutative() {
let a =
label::<71>("sha256:0000000000000000000000000000000000000000000000000000000000000000");
let b =
label::<71>("sha256:1111111111111111111111111111111111111111111111111111111111111111");
let ab = canonicalize_g2(&a, &b);
let ba = canonicalize_g2(&b, &a);
assert_eq!(ab, ba, "CS-G2 commutativity is structural");
assert_eq!(ab.len(), 142, "G2 canonical form is 2N bytes");
}
#[test]
fn f4_mirror_collapses() {
let a =
label::<71>("sha256:0000000000000000000000000000000000000000000000000000000000000000");
let m =
label::<71>("sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
let ca = canonicalize_f4(&a).expect("a canonicalizes");
let cm = canonicalize_f4(&m).expect("m canonicalizes");
assert_eq!(ca, cm, "CS-F4 mirror collapses to lex-min representative");
}
#[test]
fn e6_prepends_degree_tag() {
let a =
label::<71>("sha256:0000000000000000000000000000000000000000000000000000000000000000");
let canon = canonicalize_e6(&a).expect("canonicalizes");
assert_eq!(canon.len(), 72, "CS-E6 canonical form is N + 1 bytes");
assert!(canon[0] == DEGREE_5_TAG || canon[0] == DEGREE_6_TAG);
assert_eq!(&canon[1..], a.as_bytes());
}
#[test]
fn e6_partition_distinguishes_classes() {
let a =
label::<71>("sha256:0000000000000000000000000000000000000000000000000000000000000000");
let ca = canonicalize_e6(&a).expect("canonicalizes");
assert_eq!(ca[0], DEGREE_5_TAG, "first byte 0x00 → degree-5");
let b =
label::<71>("sha256:0800000000000000000000000000000000000000000000000000000000000000");
let cb = canonicalize_e6(&b).expect("canonicalizes");
assert_eq!(cb[0], DEGREE_6_TAG, "first byte 0x08 → degree-6");
}
#[test]
fn e7_preserves_width_and_collapses_orbit() {
let a =
label::<71>("sha256:0102030405060708090a0b0c0d0e0f1011121314151617181920212223242526");
let ca = canonicalize_e7(&a).expect("canonicalizes");
assert_eq!(ca.len(), a.as_bytes().len(), "CS-E7 preserves width");
let ca_label = KappaLabel::<71>::from_bytes(&ca).expect("canonical is a label");
let ca2 = canonicalize_e7(&ca_label).expect("canonicalizes");
assert_eq!(ca, ca2, "CS-E7 lex-min is an orbit fixed point");
}
#[test]
fn e8_is_identity() {
let a =
label::<71>("sha256:0000000000000000000000000000000000000000000000000000000000000000");
let canon = canonicalize_e8(&a);
assert_eq!(
canon,
a.as_bytes(),
"CS-E8 is identity on canonical-form bytes"
);
}
#[test]
fn axis_check_admits_match() {
assert!(check_axis("sha256", "sha256").is_ok());
}
#[test]
fn axis_check_rejects_mismatch() {
match check_axis("blake3", "sha256") {
Err(CompositionFailure::OperandSigmaAxisMismatch {
expected_axis,
operand_axis,
}) => {
assert_eq!(expected_axis, "sha256");
assert_eq!(operand_axis, "blake3");
}
_ => panic!("σ-axis mismatch must be reported"),
}
}
}