use cmx::{
error::Error,
profile::RawProfile,
tag::tagdata::{
curve::CurveType, lut8::Lut8Type, CurveData, Lut8Data, MultiLocalizedUnicodeData,
ParametricCurveData,
},
};
fn minimal_header(profile_size: u32, month: u16, day: u16, hour: u16) -> [u8; 128] {
let mut h = [0u8; 128];
h[0..4].copy_from_slice(&profile_size.to_be_bytes());
h[8] = 0x04;
h[9] = 0x30;
h[12..16].copy_from_slice(b"mntr");
h[16..20].copy_from_slice(b"RGB ");
h[20..24].copy_from_slice(b"XYZ ");
h[24..26].copy_from_slice(&2024u16.to_be_bytes()); h[26..28].copy_from_slice(&month.to_be_bytes());
h[28..30].copy_from_slice(&day.to_be_bytes());
h[30..32].copy_from_slice(&hour.to_be_bytes());
h[36..40].copy_from_slice(b"acsp");
h
}
#[test]
fn parametric_curve_invalid_count_returns_error() {
let mut para = ParametricCurveData::default();
assert!(matches!(
para.set_parameters_slice(&[1.0, 2.0]),
Err(Error::UnsupportedParameterCount(2))
));
assert!(matches!(
para.set_parameters_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]),
Err(Error::UnsupportedParameterCount(6))
));
}
#[test]
fn parametric_curve_valid_counts_succeed() {
let mut para = ParametricCurveData::default();
assert!(para.set_parameters_slice(&[2.2]).is_ok()); assert!(para.set_parameters_slice(&[2.2, 0.9, 0.1]).is_ok()); assert!(para.set_parameters_slice(&[2.2, 0.9, 0.1, 0.03]).is_ok()); assert!(para
.set_parameters_slice(&[2.4, 0.948, 0.052, 0.077, 0.040])
.is_ok()); assert!(para
.set_parameters_slice(&[2.4, 0.948, 0.052, 0.077, 0.040, 0.0, 0.0])
.is_ok()); }
#[test]
fn creation_date_invalid_month_returns_error() {
let total = 128 + 4; let mut bytes = vec![0u8; total];
let header = minimal_header(total as u32, 13, 1, 0); bytes[..128].copy_from_slice(&header);
bytes[128..132].copy_from_slice(&0u32.to_be_bytes());
let profile = RawProfile::from_bytes(&bytes).expect("profile should parse");
assert!(
matches!(profile.creation_date(), Err(Error::InvalidDate(_))),
"month=13 should yield InvalidDate"
);
}
#[test]
fn creation_date_invalid_day_returns_error() {
let total = 128 + 4;
let mut bytes = vec![0u8; total];
let header = minimal_header(total as u32, 1, 32, 0); bytes[..128].copy_from_slice(&header);
bytes[128..132].copy_from_slice(&0u32.to_be_bytes());
let profile = RawProfile::from_bytes(&bytes).expect("profile should parse");
assert!(
matches!(profile.creation_date(), Err(Error::InvalidDate(_))),
"day=32 should yield InvalidDate"
);
}
#[test]
fn creation_date_invalid_hour_returns_error() {
let total = 128 + 4;
let mut bytes = vec![0u8; total];
let header = minimal_header(total as u32, 1, 1, 25); bytes[..128].copy_from_slice(&header);
bytes[128..132].copy_from_slice(&0u32.to_be_bytes());
let profile = RawProfile::from_bytes(&bytes).expect("profile should parse");
assert!(
matches!(profile.creation_date(), Err(Error::InvalidDate(_))),
"hour=25 should yield InvalidDate"
);
}
#[test]
fn creation_date_valid_date_succeeds() {
let total = 128 + 4;
let mut bytes = vec![0u8; total];
let header = minimal_header(total as u32, 6, 15, 12);
bytes[..128].copy_from_slice(&header);
bytes[128..132].copy_from_slice(&0u32.to_be_bytes());
let profile = RawProfile::from_bytes(&bytes).expect("profile should parse");
let date = profile
.creation_date()
.expect("valid date should not error");
use chrono::Datelike;
assert_eq!(date.year(), 2024);
assert_eq!(date.month(), 6);
assert_eq!(date.day(), 15);
}
fn build_profile_with_tags(tags: &[([u8; 4], u32, u32)]) -> Vec<u8> {
let table_end = 132 + tags.len() * 12;
let data_end = tags
.iter()
.map(|(_, off, sz)| *off as usize + *sz as usize)
.max()
.unwrap_or(table_end)
.max(table_end);
let total = data_end;
let mut buf = vec![0u8; total];
let header = minimal_header(total as u32, 1, 1, 0);
buf[..128].copy_from_slice(&header);
buf[128..132].copy_from_slice(&(tags.len() as u32).to_be_bytes());
for (i, (sig, offset, size)) in tags.iter().enumerate() {
let base = 132 + i * 12;
buf[base..base + 4].copy_from_slice(sig);
buf[base + 4..base + 8].copy_from_slice(&offset.to_be_bytes());
buf[base + 8..base + 12].copy_from_slice(&size.to_be_bytes());
}
buf
}
#[test]
fn share_tags_same_offset_different_size_is_rejected() {
let offset = 156u32;
let buf = build_profile_with_tags(&[
(*b"rXYZ", offset, 8),
(*b"gXYZ", offset, 16), ]);
let result = RawProfile::from_bytes(&buf);
assert!(
result.is_err(),
"mismatched sizes at same offset should be rejected"
);
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("corrupt") || msg.contains("offset") || msg.contains("size"),
"error message should mention the corruption: {msg}"
);
}
#[test]
fn share_tags_same_offset_same_size_is_accepted() {
let offset = 156u32;
let buf = build_profile_with_tags(&[
(*b"rXYZ", offset, 8),
(*b"gXYZ", offset, 8), ]);
assert!(
RawProfile::from_bytes(&buf).is_ok(),
"identical offset+size should be accepted as shared tag data"
);
}
fn build_mluc(records: &[([u8; 2], [u8; 2], u32, u32)], string_bytes: &[u8]) -> Vec<u8> {
let n = records.len() as u32;
let mut buf = Vec::new();
buf.extend_from_slice(b"mluc");
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&n.to_be_bytes());
buf.extend_from_slice(&12u32.to_be_bytes()); for (lang, ctry, length, offset) in records {
buf.extend_from_slice(lang);
buf.extend_from_slice(ctry);
buf.extend_from_slice(&length.to_be_bytes());
buf.extend_from_slice(&offset.to_be_bytes());
}
buf.extend_from_slice(string_bytes);
buf
}
#[test]
fn mluc_oob_offset_skips_record_gracefully() {
let data = build_mluc(
&[(*b"en", [0, 0], 4, 10_000)], &[],
);
let mluc = MultiLocalizedUnicodeData(data);
let mluc_type =
cmx::tag::tagdata::multi_localized_unicode::MultiLocalizedUnicodeType::from(&mluc);
assert!(
mluc_type.is_empty(),
"bad record should be skipped, leaving an empty MLUC"
);
}
#[test]
fn mluc_invalid_utf16_uses_lossy_fallback() {
let string_bytes: &[u8] = &[0xD8, 0x00, 0x00, 0x0A];
let data = build_mluc(&[(*b"en", [0, 0], 4, 28)], string_bytes);
let mluc = MultiLocalizedUnicodeData(data);
let mluc_type =
cmx::tag::tagdata::multi_localized_unicode::MultiLocalizedUnicodeType::from(&mluc);
assert!(
!mluc_type.is_empty(),
"invalid UTF-16 should produce a lossy entry, not be dropped"
);
}
fn lut8_header_bytes(n: u8, m: u8, g: u8) -> Vec<u8> {
let mut h = vec![0u8; 48];
h[0..4].copy_from_slice(b"mft1"); h[8] = n;
h[9] = m;
h[10] = g;
h
}
#[test]
#[should_panic(expected = "CLUT size overflow")]
fn lut8_clut_overflow_panics_with_message() {
let data = Lut8Data(lut8_header_bytes(9, 3, 255));
let _ = Lut8Type::from(&data);
}
#[test]
#[should_panic(expected = "truncated data")]
fn lut8_truncated_data_panics_with_message() {
let data = Lut8Data(lut8_header_bytes(2, 3, 4));
let _ = Lut8Type::from(&data);
}
fn curve_data_bytes(points: u32, extra_byte: bool) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(b"curv"); buf.extend_from_slice(&0u32.to_be_bytes()); buf.extend_from_slice(&points.to_be_bytes()); for i in 0..points {
buf.extend_from_slice(&(i as u16).to_be_bytes());
}
if extra_byte {
buf.push(0xFF); }
buf
}
#[test]
fn curve_even_length_payload_succeeds() {
let data = CurveData(curve_data_bytes(4, false));
let ct = CurveType::from(&data);
let serialised = toml::to_string(&ct).expect("serialisation should succeed");
assert!(serialised.contains("points"));
}
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "not a multiple of 2")]
fn curve_odd_length_payload_fires_debug_assert() {
let data = CurveData(curve_data_bytes(4, true)); let _ = CurveType::from(&data);
}