#![allow(dead_code)]
use crate::encoding::BitMatrix;
use crate::error::Error;
use super::code49_patterns::{PATTERNS_0, PATTERNS_1};
pub(crate) const S1: i16 = -1;
pub(crate) const S2: i16 = -2;
pub(crate) const FN1: i16 = -3;
pub(crate) const FN2: i16 = -4;
pub(crate) const FN3: i16 = -5;
pub(crate) const NS: i16 = -6;
#[rustfmt::skip]
pub(crate) const CHARMAP: [i16; 49] = [
48, 49, 50, 51, 52, 53, 54, 55, 56, 57,
65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80,
81, 82, 83, 84, 85, 86, 87, 88, 89, 90,
b'-' as i16, b'.' as i16, b' ' as i16, b'$' as i16,
b'/' as i16, b'+' as i16, b'%' as i16,
S1, S2, FN1, FN2, FN3, NS,
];
pub(crate) const METRICS: [[u16; 2]; 7] =
[[2, 9], [3, 16], [4, 23], [5, 30], [6, 37], [7, 42], [8, 49]];
#[rustfmt::skip]
pub(crate) const SAMVAL: [u16; 44] = [
12, 22,
13, 23, 33,
14, 24, 34, 44,
15, 25, 35, 45, 55,
16, 26, 36, 46, 56, 66,
17, 27, 37, 47, 57, 67, 77,
18, 28, 38, 48, 58, 68, 78, 88,
19, 29, 39, 49, 59, 69, 79, 89, 99,
];
pub(crate) const PARITY: [&str; 8] = [
"1001", "0101", "1100", "0011", "1010", "0110", "1111", "0000",
];
#[rustfmt::skip]
pub(crate) const WEIGHTX: [u16; 33] = [
20,
1, 9, 31, 26, 2, 12, 17, 23, 37, 18, 22, 6, 27, 44, 15, 43,
39, 11, 13, 5, 41, 33, 36, 8, 4, 32, 3, 19, 40, 25, 29, 10,
];
#[rustfmt::skip]
pub(crate) const WEIGHTY: [u16; 33] = [
16,
9, 31, 26, 2, 12, 17, 23, 37, 18, 22, 6, 27, 44, 15, 43, 39,
11, 13, 5, 41, 33, 36, 8, 4, 32, 3, 19, 40, 25, 29, 10, 24,
];
#[rustfmt::skip]
pub(crate) const WEIGHTZ: [u16; 33] = [
38,
31, 26, 2, 12, 17, 23, 37, 18, 22, 6, 27, 44, 15, 43, 39, 11,
13, 5, 41, 33, 36, 8, 4, 32, 3, 19, 40, 25, 29, 10, 24, 30,
];
#[inline]
pub(crate) fn lookup_direct(b: u8) -> Option<u16> {
CHARMAP
.iter()
.position(|&v| v == i16::from(b))
.map(|i| i as u16)
}
pub(crate) const PAD_CW: u16 = 48;
pub(crate) fn pick_symbol_size(data_count: usize) -> Option<(u16, u16)> {
METRICS
.iter()
.find(|row| usize::from(row[1]) >= data_count)
.map(|row| (row[0], row[1]))
}
pub(crate) fn encode_cws_direct(input: &[u8]) -> Result<Vec<u16>, Error> {
if input.is_empty() {
return Err(Error::InvalidData("code49: empty input".to_string()));
}
let mut codewords: Vec<u16> = Vec::with_capacity(input.len());
for (idx, &b) in input.iter().enumerate() {
let cw = lookup_direct(b).ok_or_else(|| {
Error::InvalidData(format!(
"code49 direct-lookup path: byte 0x{b:02x} at position {idx} \
isn't a direct CHARMAP member — lowercase, control bytes, and \
high bytes need S1/S2/NS shift handling (Stage 3+)"
))
})?;
if cw > 42 {
return Err(Error::InvalidData(format!(
"code49: byte 0x{b:02x} mapped to marker codeword {cw} \
(not a direct alphabet member)"
)));
}
codewords.push(cw);
}
let (_rows, dcws) = pick_symbol_size(codewords.len()).ok_or_else(|| {
Error::InvalidData(format!(
"code49: payload of {} bytes exceeds the r=8 ceiling (49 codewords)",
codewords.len()
))
})?;
let dcws = usize::from(dcws);
while codewords.len() < dcws {
codewords.push(PAD_CW);
}
Ok(codewords)
}
fn base48(count: usize, digits: &[u8]) -> Vec<u16> {
let mut value: u64 = 0;
for &b in digits {
debug_assert!(b.is_ascii_digit());
value = value * 10 + u64::from(b - b'0');
}
let mut out = vec![0u16; count];
for i in (0..count).rev() {
out[i] = (value % 48) as u16;
value /= 48;
}
out
}
pub(crate) fn encode_cws_ns_digits(digits: &[u8]) -> Result<Vec<u16>, Error> {
if digits.is_empty() {
return Err(Error::InvalidData("code49: empty input".to_string()));
}
if digits.len() < 5 {
return Err(Error::InvalidData(format!(
"code49 NS-shift path requires ≥5 digits (got {}); use \
encode_cws_direct for shorter pure-digit runs",
digits.len()
)));
}
for (idx, &b) in digits.iter().enumerate() {
if !b.is_ascii_digit() {
return Err(Error::InvalidData(format!(
"code49 NS-shift path: non-digit byte 0x{b:02x} at position {idx}"
)));
}
}
let n = digits.len();
let r = n % 5;
let pre = if r == 2 { n - 7 } else { n - r };
let mut cws: Vec<u16> = Vec::with_capacity(n * 2 / 3 + 3);
let mut idx = 0;
while idx < pre {
cws.extend(base48(3, &digits[idx..idx + 5]));
idx += 5;
}
let remainder = &digits[pre..];
match remainder.len() {
0 => {}
1 => {
cws.push(u16::from(remainder[0] - b'0'));
}
3 => {
cws.extend(base48(2, remainder));
}
4 => {
let mut padded = Vec::with_capacity(6);
padded.push(b'1');
padded.push(b'0');
padded.extend_from_slice(remainder);
cws.extend(base48(3, &padded));
}
7 => {
let mut padded = Vec::with_capacity(6);
padded.push(b'1');
padded.push(b'0');
padded.extend_from_slice(&remainder[..4]);
cws.extend(base48(3, &padded));
cws.extend(base48(2, &remainder[4..]));
}
_ => {
return Err(Error::InvalidData(format!(
"code49 NS-shift internal: unexpected remainder length {} for n={n}",
remainder.len()
)));
}
}
let (_rows, dcws) = pick_symbol_size(cws.len()).ok_or_else(|| {
Error::InvalidData(format!(
"code49: payload of {} digits produces {} codewords, exceeds r=8 ceiling",
n,
cws.len()
))
})?;
let dcws = usize::from(dcws);
while cws.len() < dcws {
cws.push(PAD_CW);
}
Ok(cws)
}
fn charvals(b: u8) -> Option<(Option<u16>, u16)> {
match b {
0 => Some((Some(43), 38)),
1..=26 => Some((Some(43), 10 + u16::from(b - 1))),
27..=31 => Some((Some(43), u16::from(b - 26))),
b' ' => Some((None, 38)),
b'!' => Some((Some(43), 6)),
b'"' => Some((Some(43), 7)),
b'#' => Some((Some(43), 8)),
b'$' => Some((None, 39)),
b'%' => Some((None, 42)),
b'&' => Some((Some(43), 9)),
b'\'' => Some((Some(43), 0)),
b'(' => Some((Some(43), 36)),
b')' => Some((Some(43), 37)),
b'*' => Some((Some(43), 39)),
b'+' => Some((None, 41)),
b',' => Some((Some(43), 40)),
b'-' => Some((None, 36)),
b'.' => Some((None, 37)),
b'/' => Some((None, 40)),
b'0'..=b'9' => Some((None, u16::from(b - b'0'))),
b':' => Some((Some(43), 41)),
b';' => Some((Some(44), 1)),
b'<' => Some((Some(44), 2)),
b'=' => Some((Some(44), 3)),
b'>' => Some((Some(44), 4)),
b'?' => Some((Some(44), 5)),
b'@' => Some((Some(44), 6)),
b'A'..=b'Z' => Some((None, u16::from(b - b'A' + 10))),
b'[' => Some((Some(44), 7)),
b'\\' => Some((Some(44), 8)),
b']' => Some((Some(44), 9)),
b'^' => Some((Some(44), 0)),
b'_' => Some((Some(44), 36)),
b'`' => Some((Some(44), 37)),
b'a'..=b'z' => Some((Some(44), u16::from(b - b'a' + 10))),
b'{' => Some((Some(44), 39)),
b'|' => Some((Some(44), 40)),
b'}' => Some((Some(44), 41)),
b'~' => Some((Some(44), 42)),
127 => Some((Some(44), 38)),
_ => None,
}
}
pub(crate) const MODE_ALPHA: u16 = 0;
pub(crate) const MODE_NS_DIGITS: u16 = 2;
pub(crate) const MODE_FIRST_S1: u16 = 4;
pub(crate) const MODE_FIRST_S2: u16 = 5;
pub(crate) fn encode_cws_alpha(input: &[u8]) -> Result<(Vec<u16>, u16), Error> {
if input.is_empty() {
return Err(Error::InvalidData("code49: empty input".to_string()));
}
let mut cws: Vec<u16> = Vec::with_capacity(input.len() * 2);
let (first_shift, first_target) = charvals(input[0]).ok_or_else(|| {
Error::InvalidData(format!(
"code49 alpha path: byte 0x{:02x} at position 0 is non-ASCII",
input[0]
))
})?;
let mode = match first_shift {
Some(43) => MODE_FIRST_S1,
Some(44) => MODE_FIRST_S2,
Some(_) => unreachable!("only S1/S2 shifts in charvals"),
None => MODE_ALPHA,
};
cws.push(first_target);
for (idx, &b) in input.iter().enumerate().skip(1) {
let (shift, target) = charvals(b).ok_or_else(|| {
Error::InvalidData(format!(
"code49 alpha path: byte 0x{b:02x} at position {idx} is non-ASCII"
))
})?;
if let Some(s) = shift {
cws.push(s);
}
cws.push(target);
}
let (_rows, dcws) = pick_symbol_size(cws.len()).ok_or_else(|| {
Error::InvalidData(format!(
"code49 alpha path: payload of {} bytes produces {} cws, exceeds r=8 ceiling",
input.len(),
cws.len()
))
})?;
let dcws = usize::from(dcws);
while cws.len() < dcws {
cws.push(PAD_CW);
}
Ok((cws, mode))
}
pub(crate) fn encode_cws(input: &[u8]) -> Result<(Vec<u16>, u16), Error> {
if input.is_empty() {
return Err(Error::InvalidData("code49: empty input".to_string()));
}
let leading_digits = input.iter().take_while(|b| b.is_ascii_digit()).count();
if leading_digits >= 5 {
let cws = encode_cws_ns_digits(&input[..leading_digits])?;
if leading_digits != input.len() {
return Err(Error::InvalidData(
"code49: payload has a leading NS-digit run followed by non-digit content; this Rust port encodes either all-digit-NS or all-alpha but not mixed runs (use BWIPP for mixed)".to_string(),
));
}
Ok((cws, MODE_NS_DIGITS))
} else {
encode_cws_alpha(input)
}
}
fn calccheck(ccs: &[u16], rows: u16, weights: &[u16]) -> u32 {
let pair_count = (usize::from(rows) - 1) * 4;
let mut score: u32 = 0;
for i in 0..pair_count {
let pair = u32::from(ccs[i * 2]) * 49 + u32::from(ccs[i * 2 + 1]);
score += pair * u32::from(weights[i + 1]);
}
score
}
pub(crate) fn build_ccs(cws: &[u16], rows: u16, dcws: u16, mode: u16) -> Result<Vec<u16>, Error> {
let r = usize::from(rows);
let dcws_usz = usize::from(dcws);
if cws.len() != dcws_usz {
return Err(Error::InvalidData(format!(
"code49 internal: cws.len()={} but dcws={} (r={r})",
cws.len(),
dcws_usz
)));
}
if !(2..=8).contains(&r) {
return Err(Error::InvalidData(format!(
"code49 internal: r={r} not in 2..=8"
)));
}
let mut ccs = vec![0u16; r * 8];
let mut j = 0usize;
for i in 0..r - 1 {
let row = &cws[j..j + 7];
let row_sum: u32 = row.iter().map(|&c| u32::from(c)).sum();
for (k, &c) in row.iter().enumerate() {
ccs[i * 8 + k] = c;
}
ccs[i * 8 + 7] = (row_sum % 49) as u16;
j += 7;
}
if j < dcws_usz {
let remaining = dcws_usz - j;
let lastrow_start = (r - 1) * 8;
ccs[lastrow_start..lastrow_start + remaining].copy_from_slice(&cws[j..]);
}
let cr7 = (r as u16 - 2) * 7 + mode;
let last_idx = r * 8;
ccs[last_idx - 2] = cr7;
if r >= 7 {
let score_z = calccheck(&ccs, rows, &WEIGHTZ);
let cr7_z = u32::from(cr7) * u32::from(WEIGHTZ[0]);
let check_z = (cr7_z + score_z) % 2401;
let lastrow_start = (r - 1) * 8;
ccs[lastrow_start] = (check_z / 49) as u16;
ccs[lastrow_start + 1] = (check_z % 49) as u16;
}
let lastrow_start = (r - 1) * 8;
let wr1 = u32::from(ccs[lastrow_start]) * 49 + u32::from(ccs[lastrow_start + 1]);
let score_y = calccheck(&ccs, rows, &WEIGHTY);
let cr7_y = u32::from(cr7) * u32::from(WEIGHTY[0]);
let wr1_idx = r * 4 - 3;
let check_y = (cr7_y + score_y + wr1 * u32::from(WEIGHTY[wr1_idx])) % 2401;
ccs[lastrow_start + 2] = (check_y / 49) as u16;
ccs[lastrow_start + 3] = (check_y % 49) as u16;
let wr2 = check_y;
let score_x = calccheck(&ccs, rows, &WEIGHTX);
let cr7_x = u32::from(cr7) * u32::from(WEIGHTX[0]);
let wr2_idx = r * 4 - 2;
let check_x =
(cr7_x + score_x + wr1 * u32::from(WEIGHTX[wr1_idx]) + wr2 * u32::from(WEIGHTX[wr2_idx]))
% 2401;
ccs[lastrow_start + 4] = (check_x / 49) as u16;
ccs[lastrow_start + 5] = (check_x % 49) as u16;
let lastrow_sum: u32 = ccs[last_idx - 8..last_idx - 1]
.iter()
.map(|&c| u32::from(c))
.sum();
ccs[last_idx - 1] = (lastrow_sum % 49) as u16;
Ok(ccs)
}
fn build_seprow() -> [u8; 81] {
let mut row = [1u8; 81];
for cell in row.iter_mut().take(10) {
*cell = 0;
}
row[80] = 0;
row
}
fn build_row_bits(row_idx: usize, rows: usize, ccrow: &[u16]) -> [u8; 81] {
debug_assert_eq!(ccrow.len(), 8);
let p: &str = if row_idx + 1 == rows {
"0000"
} else {
PARITY[row_idx]
};
let scrow = [
u32::from(ccrow[0]) * 49 + u32::from(ccrow[1]),
u32::from(ccrow[2]) * 49 + u32::from(ccrow[3]),
u32::from(ccrow[4]) * 49 + u32::from(ccrow[5]),
u32::from(ccrow[6]) * 49 + u32::from(ccrow[7]),
];
let p_bytes = p.as_bytes();
let mut sbs: Vec<u8> = Vec::with_capacity(41);
sbs.push(10);
sbs.push(1);
sbs.push(1);
for k in 0..4 {
let table: &[&str; 2401] = if p_bytes[k] == b'1' {
&PATTERNS_1
} else {
&PATTERNS_0
};
let pattern = table[scrow[k] as usize];
for c in pattern.bytes() {
sbs.push(c - b'0');
}
}
sbs.push(4);
sbs.push(1);
let mut row = [0u8; 81];
let mut current: u8 = 1;
let mut idx = 0;
for &w in &sbs {
current = 1 - current;
for _ in 0..w {
row[idx] = current;
idx += 1;
}
}
debug_assert_eq!(idx, 81, "sbs widths must sum to 81");
row
}
pub fn encode(input: &[u8]) -> Result<BitMatrix, Error> {
let (cws, mode) = encode_cws(input)?;
let (rows, dcws) = pick_symbol_size(cws.len()).ok_or_else(|| {
Error::InvalidData(format!(
"code49: payload yields {} cws, exceeds r=8 ceiling",
cws.len()
))
})?;
let ccs = build_ccs(&cws, rows, dcws, mode)?;
let r = usize::from(rows);
let rowheight: usize = 8;
let sepheight: usize = 1;
let pixx: usize = 81;
let seprow = build_seprow();
let allone = [1u8; 81];
let numcomprows = 2 * r + 1;
let mut compressed: Vec<[u8; 81]> = Vec::with_capacity(numcomprows);
let mut mults: Vec<usize> = Vec::with_capacity(numcomprows);
compressed.push(allone);
mults.push(sepheight);
for i in 0..r {
let ccrow = &ccs[i * 8..i * 8 + 8];
compressed.push(build_row_bits(i, r, ccrow));
mults.push(rowheight);
if i + 1 < r {
compressed.push(seprow);
mults.push(sepheight);
}
}
compressed.push(allone);
mults.push(sepheight);
debug_assert_eq!(compressed.len(), numcomprows);
let symhgt: usize = mults.iter().sum();
let mut bm = BitMatrix::new(pixx, symhgt);
let mut y = 0;
for (row, &mult) in compressed.iter().zip(mults.iter()) {
for _ in 0..mult {
for (x, &bit) in row.iter().enumerate() {
if bit != 0 {
bm.set(x, y, true);
}
}
y += 1;
}
}
Ok(bm)
}
pub(crate) fn encode_pixs(input: &[u8]) -> Result<Vec<u8>, Error> {
let (cws, mode) = encode_cws(input)?;
let (rows, dcws) = pick_symbol_size(cws.len()).ok_or_else(|| {
Error::InvalidData(format!(
"code49: payload yields {} cws, exceeds r=8 ceiling",
cws.len()
))
})?;
let ccs = build_ccs(&cws, rows, dcws, mode)?;
let r = usize::from(rows);
let seprow = build_seprow();
let allone = [1u8; 81];
let numcomprows = 2 * r + 1;
let mut pixs: Vec<u8> = Vec::with_capacity(numcomprows * 81);
pixs.extend_from_slice(&allone);
for i in 0..r {
let ccrow = &ccs[i * 8..i * 8 + 8];
pixs.extend_from_slice(&build_row_bits(i, r, ccrow));
if i + 1 < r {
pixs.extend_from_slice(&seprow);
}
}
pixs.extend_from_slice(&allone);
debug_assert_eq!(pixs.len(), numcomprows * 81);
Ok(pixs)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn charmap_shape() {
assert_eq!(CHARMAP.len(), 49);
}
#[test]
fn charmap_anchors() {
assert_eq!(CHARMAP[0], i16::from(b'0'));
assert_eq!(CHARMAP[9], i16::from(b'9'));
assert_eq!(CHARMAP[10], i16::from(b'A'));
assert_eq!(CHARMAP[35], i16::from(b'Z'));
assert_eq!(CHARMAP[36], i16::from(b'-'));
assert_eq!(CHARMAP[37], i16::from(b'.'));
assert_eq!(CHARMAP[38], i16::from(b' '));
assert_eq!(CHARMAP[39], i16::from(b'$'));
assert_eq!(CHARMAP[40], i16::from(b'/'));
assert_eq!(CHARMAP[41], i16::from(b'+'));
assert_eq!(CHARMAP[42], i16::from(b'%'));
assert_eq!(CHARMAP[43], S1);
assert_eq!(CHARMAP[44], S2);
assert_eq!(CHARMAP[45], FN1);
assert_eq!(CHARMAP[46], FN2);
assert_eq!(CHARMAP[47], FN3);
assert_eq!(CHARMAP[48], NS);
}
#[test]
fn metrics_shape_and_anchors() {
assert_eq!(METRICS.len(), 7);
assert_eq!(METRICS[0], [2, 9]);
assert_eq!(METRICS[6], [8, 49]);
let expected_dcws = [9, 16, 23, 30, 37, 42, 49];
for (i, &dcws) in expected_dcws.iter().enumerate() {
assert_eq!(
METRICS[i][0],
(i + 2) as u16,
"METRICS[{i}] rows should be {}",
i + 2
);
assert_eq!(METRICS[i][1], dcws, "METRICS[{i}] dcws should be {dcws}");
}
}
#[test]
fn samval_shape_and_anchors() {
assert_eq!(SAMVAL.len(), 44);
assert_eq!(SAMVAL[0], 12);
assert_eq!(SAMVAL[1], 22);
assert_eq!(SAMVAL[43], 99);
}
#[test]
fn parity_shape() {
assert_eq!(PARITY.len(), 8);
for (i, &entry) in PARITY.iter().enumerate() {
assert_eq!(entry.len(), 4, "PARITY[{i}] should be 4 chars");
assert!(
entry.chars().all(|c| c == '0' || c == '1'),
"PARITY[{i}] = {entry:?} should be binary",
);
}
}
#[test]
fn patterns_shape_and_anchors() {
assert_eq!(PATTERNS_0.len(), 2401);
assert_eq!(PATTERNS_1.len(), 2401);
for (i, &p) in PATTERNS_0.iter().enumerate() {
assert_eq!(p.len(), 8, "PATTERNS_0[{i}] should be 8 chars");
for c in p.chars() {
let d = c.to_digit(10).unwrap_or(99);
assert!(
(1..=6).contains(&d),
"PATTERNS_0[{i}] = {p:?} has invalid digit {c:?}"
);
}
}
for (i, &p) in PATTERNS_1.iter().enumerate() {
assert_eq!(p.len(), 8, "PATTERNS_1[{i}] should be 8 chars");
}
assert_eq!(PATTERNS_0[0], "11521132");
assert_eq!(PATTERNS_0[1], "25112131");
assert_eq!(PATTERNS_0[2400], "22421131");
assert_eq!(PATTERNS_1[0], "22121116");
assert_eq!(PATTERNS_1[2400], "11113162");
}
#[test]
fn weight_tables_shape_and_anchors() {
for (name, table) in [
("WEIGHTX", &WEIGHTX[..]),
("WEIGHTY", &WEIGHTY[..]),
("WEIGHTZ", &WEIGHTZ[..]),
] {
assert_eq!(table.len(), 33, "{name} should have 33 entries");
}
assert_eq!(WEIGHTX[0], 20);
assert_eq!(WEIGHTY[0], 16);
assert_eq!(WEIGHTZ[0], 38);
assert_eq!(WEIGHTY[1], WEIGHTX[2]); assert_eq!(WEIGHTY[2], WEIGHTX[3]); assert_eq!(WEIGHTZ[1], WEIGHTX[3]); assert_eq!(WEIGHTZ[2], WEIGHTX[4]); assert_eq!(WEIGHTX[1], 1);
assert_eq!(WEIGHTX[32], 10);
assert_eq!(WEIGHTZ[32], 30);
}
#[test]
fn charvals_pins_lookup_categories_and_boundaries() {
assert_eq!(charvals(b'A'), Some((None, 10)));
assert_eq!(charvals(b'M'), Some((None, 22)));
assert_eq!(charvals(b'Z'), Some((None, 35)));
assert_eq!(charvals(b'0'), Some((None, 0)));
assert_eq!(charvals(b'5'), Some((None, 5)));
assert_eq!(charvals(b'9'), Some((None, 9)));
assert_eq!(charvals(b' '), Some((None, 38)));
assert_eq!(charvals(b'$'), Some((None, 39)));
assert_eq!(charvals(b'%'), Some((None, 42)));
assert_eq!(charvals(b'+'), Some((None, 41)));
assert_eq!(charvals(b'-'), Some((None, 36)));
assert_eq!(charvals(b'.'), Some((None, 37)));
assert_eq!(charvals(b'/'), Some((None, 40)));
assert_eq!(charvals(0), Some((Some(43), 38)), "NUL → S1+space");
assert_eq!(charvals(1), Some((Some(43), 10)), "1 → S1+'A'");
assert_eq!(charvals(26), Some((Some(43), 35)), "26 → S1+'Z'");
assert_eq!(charvals(27), Some((Some(43), 1)), "27 → S1+'1'");
assert_eq!(charvals(31), Some((Some(43), 5)), "31 → S1+'5'");
assert_eq!(charvals(b'!'), Some((Some(43), 6)));
assert_eq!(charvals(b'#'), Some((Some(43), 8)));
assert_eq!(charvals(b'&'), Some((Some(43), 9)));
assert_eq!(charvals(b'\''), Some((Some(43), 0)));
assert_eq!(charvals(b'*'), Some((Some(43), 39)));
assert_eq!(charvals(b':'), Some((Some(43), 41)));
assert_eq!(charvals(b';'), Some((Some(44), 1)));
assert_eq!(charvals(b'@'), Some((Some(44), 6)));
assert_eq!(charvals(b'a'), Some((Some(44), 10)));
assert_eq!(charvals(b'm'), Some((Some(44), 22)));
assert_eq!(charvals(b'z'), Some((Some(44), 35)));
assert_eq!(charvals(b'['), Some((Some(44), 7)));
assert_eq!(charvals(b'~'), Some((Some(44), 42)));
assert_eq!(charvals(127), Some((Some(44), 38)), "DEL → S2+space");
assert_eq!(charvals(128), None);
assert_eq!(charvals(200), None);
assert_eq!(charvals(255), None);
}
#[test]
fn calccheck_weighted_base49_pair_sum() {
let ccs: [u16; 8] = [1, 2, 3, 4, 5, 6, 7, 8];
let weights: [u16; 5] = [99, 10, 20, 30, 40];
let score = calccheck(&ccs, 2, &weights);
assert_eq!(score, 25100, "weighted sum of 4 base-49 pairs");
}
#[test]
fn base48_decimal_to_base48_with_high_to_low_output() {
assert_eq!(base48(3, b"12345"), vec![5u16, 17, 9]);
assert_eq!(base48(2, b"99"), vec![2u16, 3]);
assert_eq!(base48(1, b"7"), vec![7u16]);
assert_eq!(base48(2, b"00"), vec![0u16, 0]);
assert_eq!(base48(2, b"47"), vec![0u16, 47]);
assert_eq!(base48(2, b"48"), vec![1u16, 0]);
assert_eq!(base48(2, b"96"), vec![2u16, 0]);
}
#[test]
fn build_seprow_layout_is_10_zeros_then_ones_then_zero() {
let row = build_seprow();
assert_eq!(row.len(), 81);
for i in 0..10 {
assert_eq!(row[i], 0, "leading pos {i} must be 0");
}
for i in 10..80 {
assert_eq!(row[i], 1, "middle pos {i} must be 1");
}
assert_eq!(row[80], 0, "trailing pos 80 must be 0");
assert_eq!(row.iter().filter(|&&v| v == 0).count(), 11);
assert_eq!(row.iter().filter(|&&v| v == 1).count(), 70);
}
#[test]
fn build_row_bits_invariant_layout_and_parity_branch() {
let ccrow = [0u16; 8];
let last = build_row_bits(0, 1, &ccrow);
assert_eq!(last.len(), 81);
for i in 0..10 {
assert_eq!(last[i], 0, "quiet zone pos {i} must be 0");
}
assert_eq!(last[10], 1, "start bar at pos 10");
assert_eq!(last[11], 0, "separator after start bar");
for i in 76..80 {
assert_eq!(last[i], 1, "stop bar pos {i} must be 1");
}
assert_eq!(last[80], 0, "trailing separator at pos 80");
let first_of_two = build_row_bits(0, 2, &ccrow);
assert_ne!(
last, first_of_two,
"row_idx+1==rows branch must pick literal \"0000\" parity, \
distinct from PARITY[0]=\"1001\""
);
assert_eq!(first_of_two.len(), 81);
assert_eq!(first_of_two[0], 0);
assert_eq!(first_of_two[10], 1);
assert_eq!(first_of_two[76], 1);
assert_eq!(first_of_two[80], 0);
}
#[test]
fn build_row_bits_pins_scrow_arithmetic_with_non_zero_ccrow() {
assert_eq!(PATTERNS_0[0], "11521132", "PATTERNS_0[0] table check");
assert_eq!(PATTERNS_0[49], "11122225", "PATTERNS_0[49] table check");
let ccrow_a = [1u16, 0, 0, 0, 0, 0, 0, 0];
let row_a = build_row_bits(0, 1, &ccrow_a);
let p49_bits: [u8; 16] = [1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0];
for (off, &want) in p49_bits.iter().enumerate() {
let i = 12 + off;
assert_eq!(
row_a[i], want,
"case 1 (scrow[0]=49=ccrow[0]*49+ccrow[1]): pos {i} should be {want} \
per PATTERNS_0[49]=\"11122225\""
);
}
let p0_bits_2nd: [u8; 16] = [1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0];
for (off, &want) in p0_bits_2nd.iter().enumerate() {
let i = 28 + off;
assert_eq!(
row_a[i], want,
"case 1 second-pattern region (scrow[1]=0): pos {i} should be {want} \
per PATTERNS_0[0]=\"11521132\""
);
}
let ccrow_b = [0u16, 0, 1, 0, 0, 0, 0, 0];
let row_b = build_row_bits(0, 1, &ccrow_b);
let p0_bits_1st: [u8; 16] = [1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0];
for (off, &want) in p0_bits_1st.iter().enumerate() {
let i = 12 + off;
assert_eq!(
row_b[i], want,
"case 2 first-pattern region (scrow[0]=0): pos {i} should be {want} \
per PATTERNS_0[0]=\"11521132\""
);
}
let p49_bits_2nd: [u8; 16] = [1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0];
for (off, &want) in p49_bits_2nd.iter().enumerate() {
let i = 28 + off;
assert_eq!(
row_b[i], want,
"case 2 second-pattern region (scrow[1]=49=ccrow[2]*49+ccrow[3]): \
pos {i} should be {want} per PATTERNS_0[49]=\"11122225\""
);
}
assert_ne!(
&row_a[12..28],
&row_b[12..28],
"non-zero ccrow[0] vs non-zero ccrow[2] must yield different bits at pos 12-27"
);
}
#[test]
fn lookup_direct_spot_checks() {
assert_eq!(lookup_direct(b'0'), Some(0));
assert_eq!(lookup_direct(b'9'), Some(9));
assert_eq!(lookup_direct(b'A'), Some(10));
assert_eq!(lookup_direct(b'Z'), Some(35));
assert_eq!(lookup_direct(b'-'), Some(36));
assert_eq!(lookup_direct(b' '), Some(38));
assert_eq!(lookup_direct(b'%'), Some(42));
assert_eq!(lookup_direct(b'a'), None);
assert_eq!(lookup_direct(b'!'), None);
assert_eq!(lookup_direct(0), None);
}
#[test]
fn encode_produces_valid_bitmatrix_for_supported_inputs() {
for input in [
b"12345".as_ref(),
b"A".as_ref(),
b"ABC".as_ref(),
b"ABCDEFGHI".as_ref(),
b"Hi".as_ref(),
b"ABCDEFGHIJKLMNOP".as_ref(),
] {
let bm = encode(input).unwrap_or_else(|e| {
panic!(
"encode({:?}) failed: {e:?}",
std::str::from_utf8(input).unwrap_or("<non-utf8>"),
)
});
assert_eq!(bm.width(), 81, "code49 width must be 81 modules");
assert!(bm.height() >= 19, "code49 height must be ≥ 19");
}
let err = encode(b"").unwrap_err();
let Error::InvalidData(msg) = err else {
panic!("encode(b\"\") must yield InvalidData; got {err:?}");
};
assert!(
msg.contains("code49:"),
"empty-input diagnostic must carry the symbology tag; got {msg:?}"
);
assert!(
msg.contains("empty input"),
"empty-input diagnostic must call out 'empty input'; got {msg:?}"
);
assert!(
!msg.contains("mode") && !msg.contains("shift") && !msg.contains("alpha"),
"empty-input diagnostic must not leak the downstream arms; got {msg:?}"
);
}
#[test]
fn encode_pixs_matches_bwip_js_golden_for_12345() {
let pixs = encode_pixs(b"12345").expect(
"encode_pixs(b\"12345\") (Code 49 NS-digits path, r=2 → 5 compressed rows × 81 cells = 405-cell golden) must succeed",
);
assert_eq!(pixs.len(), 5 * 81);
let expected: [u8; 405] = [
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1,
1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1,
1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1,
0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1,
0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
];
assert_eq!(pixs.as_slice(), expected.as_slice());
}
#[test]
fn build_ccs_matches_bwip_js_goldens() {
let cases: &[(&[u8], u16, &[u16])] = &[
(
b"12345",
2,
&[5, 17, 9, 48, 48, 48, 48, 27, 48, 48, 13, 23, 0, 13, 2, 0],
),
(
b"A",
0,
&[10, 48, 48, 48, 48, 48, 48, 4, 48, 48, 46, 28, 6, 5, 0, 34],
),
(
b"ABC",
0,
&[10, 11, 12, 48, 48, 48, 48, 29, 48, 48, 2, 39, 2, 15, 0, 7],
),
(
b"ABCDEFGHI",
0,
&[10, 11, 12, 13, 14, 15, 16, 42, 17, 18, 8, 16, 10, 2, 0, 22],
),
(
b"Hi",
0,
&[
17, 44, 18, 48, 48, 48, 48, 26, 48, 48, 12, 36, 34, 32, 0, 14,
],
),
(
b"ABCDEFGHIJKLMNOP",
0,
&[
10, 11, 12, 13, 14, 15, 16, 42, 17, 18, 19, 20, 21, 22, 23, 42, 24, 25, 1, 38,
38, 3, 7, 38,
],
),
];
for &(input, want_mode, want_ccs) in cases {
let (cws, mode) = encode_cws(input).unwrap_or_else(|e| {
panic!(
"encode_cws({:?}) (Code 49 cws-level corpus item) must succeed; got Err: {e:?}",
std::str::from_utf8(input).unwrap_or("<non-utf8>")
)
});
assert_eq!(
mode,
want_mode,
"encode_cws({:?}) mode",
std::str::from_utf8(input).unwrap_or("<non-utf8>"),
);
let (rows, dcws) = pick_symbol_size(cws.len()).expect("payload fits");
let ccs = build_ccs(&cws, rows, dcws, mode).unwrap_or_else(|e| {
panic!(
"build_ccs({:?}, rows={rows}, dcws={dcws}, mode={mode}) (Code 49 ccs corpus item) must succeed; got Err: {e:?}",
std::str::from_utf8(input).unwrap_or("<non-utf8>")
)
});
assert_eq!(
ccs,
want_ccs,
"build_ccs({:?})",
std::str::from_utf8(input).unwrap_or("<non-utf8>"),
);
}
}
#[test]
fn pick_symbol_size_picks_smallest_metrics_row() {
for n in 0..=9 {
assert_eq!(pick_symbol_size(n), Some((2, 9)));
}
for n in 10..=16 {
assert_eq!(pick_symbol_size(n), Some((3, 16)));
}
for n in 17..=23 {
assert_eq!(pick_symbol_size(n), Some((4, 23)));
}
assert_eq!(pick_symbol_size(49), Some((8, 49)));
assert_eq!(pick_symbol_size(50), None);
}
#[test]
fn encode_cws_direct_matches_bwip_js_goldens() {
let cases: &[(&[u8], &[u16])] = &[
(b"1", &[1, 48, 48, 48, 48, 48, 48, 48, 48]),
(b"12", &[1, 2, 48, 48, 48, 48, 48, 48, 48]),
(b"A", &[10, 48, 48, 48, 48, 48, 48, 48, 48]),
(b"ABCDE", &[10, 11, 12, 13, 14, 48, 48, 48, 48]),
(b"ABCDEFGHI", &[10, 11, 12, 13, 14, 15, 16, 17, 18]),
(
b"ABCDEFGHIJ",
&[
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 48, 48, 48, 48, 48, 48,
],
),
(
b"ABCDEFGHIJKLMNOP",
&[
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
],
),
];
for &(input, expected) in cases {
let cws = encode_cws_direct(input).unwrap_or_else(|e| {
panic!(
"encode_cws_direct({:?}) failed: {e:?}",
std::str::from_utf8(input).unwrap_or("<non-utf8>"),
)
});
assert_eq!(
cws,
expected,
"encode_cws_direct({:?})",
std::str::from_utf8(input).unwrap_or("<non-utf8>"),
);
}
}
#[test]
fn encode_cws_direct_rejects_inputs_needing_shifts() {
match encode_cws_direct(b"abc") {
Err(Error::InvalidData(msg)) => {
assert!(
msg.contains("code49 direct-lookup path:"),
"missing direct-lookup arm prefix: {msg}"
);
assert!(
msg.contains("byte 0x61"),
"missing byte 0x61 hex echo for 'a': {msg}"
);
assert!(
msg.contains("at position 0"),
"missing `at position 0` position-echo: {msg}"
);
}
other => panic!("'abc' should reject as InvalidData, got {other:?}"),
}
match encode_cws_direct(b"Aa") {
Err(Error::InvalidData(msg)) => {
assert!(
msg.contains("code49 direct-lookup path:"),
"missing direct-lookup arm prefix: {msg}"
);
assert!(
msg.contains("byte 0x61"),
"missing byte 0x61 hex echo for 'a': {msg}"
);
assert!(
msg.contains("at position 1"),
"missing `at position 1` position echo: {msg}"
);
}
other => panic!("'Aa' should reject as InvalidData, got {other:?}"),
}
match encode_cws_direct(b"\tA") {
Err(Error::InvalidData(msg)) => {
assert!(
msg.contains("code49 direct-lookup path:"),
"missing direct-lookup arm prefix: {msg}"
);
assert!(
msg.contains("byte 0x09"),
"missing byte 0x09 hex echo for TAB: {msg}"
);
assert!(
msg.contains("at position 0"),
"missing `at position 0` position echo: {msg}"
);
}
other => panic!("'\\tA' should reject as InvalidData, got {other:?}"),
}
match encode_cws_direct(b"") {
Err(Error::InvalidData(msg)) => {
assert!(msg.contains("code49:"), "missing code49 prefix: {msg}");
assert!(
msg.contains("empty input"),
"missing `empty input` predicate: {msg}"
);
}
other => panic!("empty input should reject as InvalidData, got {other:?}"),
}
let huge: Vec<u8> = (0..50).map(|_| b'A').collect();
match encode_cws_direct(&huge) {
Err(Error::InvalidData(msg)) => {
assert!(msg.contains("code49:"), "missing code49 prefix: {msg}");
assert!(
msg.contains("payload of 50 bytes"),
"missing payload-of-50-bytes value-echo: {msg}"
);
assert!(
msg.contains("exceeds the r=8 ceiling"),
"missing `exceeds the r=8 ceiling` predicate: {msg}"
);
}
other => panic!("50-A overflow should reject as InvalidData, got {other:?}"),
}
}
#[test]
fn base48_matches_bwip_js_polynomial() {
assert_eq!(base48(3, b"12345"), vec![5, 17, 9]);
assert_eq!(base48(3, b"67890"), vec![29, 22, 18]);
assert_eq!(base48(2, b"678"), vec![14, 6]);
assert_eq!(base48(3, b"101234"), vec![43, 45, 2]);
assert_eq!(base48(2, b"567"), vec![11, 39]);
}
#[test]
fn encode_cws_ns_digits_matches_bwip_js_goldens() {
let cases: &[(&[u8], &[u16])] = &[
(b"12345", &[5, 17, 9]),
(b"123456", &[5, 17, 9, 6]),
(b"1234567", &[43, 45, 2, 11, 39]),
(b"12345678", &[5, 17, 9, 14, 6]),
(b"123456789", &[5, 17, 9, 46, 16, 37]),
(b"1234567890", &[5, 17, 9, 29, 22, 18]),
(b"12345678901", &[5, 17, 9, 29, 22, 18, 1]),
(b"1234567890123", &[5, 17, 9, 29, 22, 18, 2, 27]),
];
for &(input, expected_core) in cases {
let cws = encode_cws_ns_digits(input).unwrap_or_else(|e| {
panic!(
"encode_cws_ns_digits({:?}) failed: {e:?}",
std::str::from_utf8(input).unwrap_or("<non-utf8>"),
)
});
let core = &cws[..expected_core.len()];
assert_eq!(
core,
expected_core,
"encode_cws_ns_digits({:?}) core",
std::str::from_utf8(input).unwrap_or("<non-utf8>"),
);
for (i, &cw) in cws[expected_core.len()..].iter().enumerate() {
assert_eq!(
cw,
PAD_CW,
"encode_cws_ns_digits({:?}) pad[{}] should be {PAD_CW}",
std::str::from_utf8(input).unwrap_or("<non-utf8>"),
i
);
}
}
}
#[test]
fn encode_cws_ns_digits_rejects_invalid_inputs() {
match encode_cws_ns_digits(b"").unwrap_err() {
Error::InvalidData(msg) => {
assert!(
msg.contains("code49:"),
"empty arm missing `code49:` prefix: {msg}"
);
assert!(
msg.contains("empty input"),
"empty arm missing `empty input` predicate: {msg}"
);
assert!(
!msg.contains("NS-shift"),
"empty arm leaked NS-shift diagnostic: {msg}"
);
}
other => panic!("empty NS-shift input should reject as InvalidData, got {other:?}"),
}
match encode_cws_ns_digits(b"1234").unwrap_err() {
Error::InvalidData(msg) => {
assert!(
msg.contains("code49 NS-shift"),
"short arm missing `code49 NS-shift` prefix: {msg}"
);
assert!(
msg.contains("requires ≥5 digits"),
"short arm missing `requires ≥5 digits` predicate: {msg}"
);
assert!(
msg.contains("got 4"),
"short arm missing `got 4` length echo: {msg}"
);
assert!(
!msg.contains("non-digit"),
"short arm leaked non-digit diagnostic: {msg}"
);
}
other => panic!("4-digit NS-shift input should reject as InvalidData, got {other:?}"),
}
match encode_cws_ns_digits(b"123A5").unwrap_err() {
Error::InvalidData(msg) => {
assert!(
msg.contains("code49 NS-shift path:"),
"non-digit arm missing prefix: {msg}"
);
assert!(
msg.contains("non-digit byte"),
"non-digit arm missing `non-digit byte` predicate: {msg}"
);
assert!(
msg.contains("0x41"),
"non-digit arm missing hex echo `0x41` for 'A': {msg}"
);
assert!(
msg.contains("at position 3"),
"non-digit arm missing `at position 3`: {msg}"
);
}
other => panic!("`123A5` should reject as non-digit InvalidData, got {other:?}"),
}
}
#[test]
fn charvals_spot_checks() {
assert_eq!(charvals(b'0'), Some((None, 0)));
assert_eq!(charvals(b'9'), Some((None, 9)));
assert_eq!(charvals(b'A'), Some((None, 10)));
assert_eq!(charvals(b'Z'), Some((None, 35)));
assert_eq!(charvals(b'-'), Some((None, 36)));
assert_eq!(charvals(b' '), Some((None, 38)));
assert_eq!(charvals(b'%'), Some((None, 42)));
assert_eq!(charvals(b'a'), Some((Some(44), 10)));
assert_eq!(charvals(b'z'), Some((Some(44), 35)));
assert_eq!(charvals(0), Some((Some(43), 38)));
assert_eq!(charvals(1), Some((Some(43), 10)));
assert_eq!(charvals(26), Some((Some(43), 35)));
assert_eq!(charvals(31), Some((Some(43), 5)));
assert_eq!(charvals(b'['), Some((Some(44), 7)));
assert_eq!(charvals(b'`'), Some((Some(44), 37)));
assert_eq!(charvals(b'{'), Some((Some(44), 39)));
assert_eq!(charvals(127), Some((Some(44), 38)));
assert_eq!(charvals(128), None);
assert_eq!(charvals(255), None);
}
#[test]
fn encode_cws_alpha_matches_bwip_js_goldens() {
let cases: &[(&[u8], u16, &[u16])] = &[
(b"a", MODE_FIRST_S2, &[10, 48, 48, 48, 48, 48, 48, 48, 48]),
(b"abc", MODE_FIRST_S2, &[10, 44, 11, 44, 12, 48, 48, 48, 48]),
(
b"abcd",
MODE_FIRST_S2,
&[10, 44, 11, 44, 12, 44, 13, 48, 48],
),
(b"Hello", MODE_ALPHA, &[17, 44, 14, 44, 21, 44, 21, 44, 24]),
(b"Hi", MODE_ALPHA, &[17, 44, 18, 48, 48, 48, 48, 48, 48]),
(b"Aa", MODE_ALPHA, &[10, 44, 10, 48, 48, 48, 48, 48, 48]),
(b"aA", MODE_FIRST_S2, &[10, 10, 48, 48, 48, 48, 48, 48, 48]),
(b"ABCabc", MODE_ALPHA, &[10, 11, 12, 44, 10, 44, 11, 44, 12]),
];
for &(input, want_mode, want_cws) in cases {
let (cws, mode) = encode_cws_alpha(input).unwrap_or_else(|e| {
panic!(
"encode_cws_alpha({:?}) failed: {e:?}",
std::str::from_utf8(input).unwrap_or("<non-utf8>"),
)
});
assert_eq!(
mode,
want_mode,
"encode_cws_alpha({:?}) mode",
std::str::from_utf8(input).unwrap_or("<non-utf8>"),
);
assert_eq!(
cws,
want_cws,
"encode_cws_alpha({:?}) cws",
std::str::from_utf8(input).unwrap_or("<non-utf8>"),
);
}
}
#[test]
fn encode_cws_alpha_s1_first_byte_and_error_paths() {
let (cws_bang_a, mode_bang_a) =
encode_cws_alpha(b"!A").expect("encode_cws_alpha(\"!A\") must succeed");
assert_eq!(
mode_bang_a, MODE_FIRST_S1,
"leading S1-shifted byte (!) must select MODE_FIRST_S1"
);
assert_eq!(
&cws_bang_a[..2],
&[6u16, 10],
"leading S1 byte suppresses shift cw; cws[0]=target('!')=6, cws[1]=target('A')=10"
);
let (cws_nul_a, mode_nul_a) =
encode_cws_alpha(&[0u8, b'A']).expect("encode_cws_alpha([NUL, A]) must succeed");
assert_eq!(
mode_nul_a, MODE_FIRST_S1,
"leading NUL (S1-shifted control byte) must select MODE_FIRST_S1"
);
assert_eq!(
&cws_nul_a[..2],
&[38u16, 10],
"leading NUL: cws[0]=target(NUL)=38, cws[1]=target('A')=10"
);
let err_empty = encode_cws_alpha(&[]).expect_err("empty input must error");
let msg_empty = format!("{err_empty:?}");
assert!(
msg_empty.contains("empty"),
"empty-input error must mention 'empty', got: {msg_empty}"
);
let err_high0 = encode_cws_alpha(&[0xFF, b'A']).expect_err("high byte at pos 0 must error");
let msg_high0 = format!("{err_high0:?}");
assert!(
msg_high0.contains("position 0"),
"non-ASCII at pos 0 error must mention 'position 0', got: {msg_high0}"
);
assert!(
msg_high0.contains("0xff"),
"non-ASCII at pos 0 error must mention byte value 0xff, got: {msg_high0}"
);
let err_high2 =
encode_cws_alpha(&[b'A', b'B', 0xFE]).expect_err("high byte at pos 2 must error");
let msg_high2 = format!("{err_high2:?}");
assert!(
msg_high2.contains("position 2"),
"non-ASCII at pos 2 error must mention 'position 2', got: {msg_high2}"
);
assert!(
msg_high2.contains("0xfe"),
"non-ASCII at pos 2 error must mention byte value 0xfe, got: {msg_high2}"
);
}
#[test]
fn encode_cws_dispatches_correctly() {
let (cws, mode) =
encode_cws(b"12345").expect("encode_cws(b\"12345\") must dispatch to MODE_NS_DIGITS");
assert_eq!(mode, MODE_NS_DIGITS);
assert_eq!(&cws[..3], &[5, 17, 9]);
let (cws, mode) =
encode_cws(b"ABC").expect("encode_cws(b\"ABC\") must dispatch to MODE_ALPHA");
assert_eq!(mode, MODE_ALPHA);
assert_eq!(&cws[..3], &[10, 11, 12]);
let (cws, mode) =
encode_cws(b"abc").expect("encode_cws(b\"abc\") must dispatch to MODE_FIRST_S2");
assert_eq!(mode, MODE_FIRST_S2);
assert_eq!(&cws[..5], &[10, 44, 11, 44, 12]);
let (cws, mode) = encode_cws(b"12abc").expect(
"encode_cws(b\"12abc\") (2-digit prefix < 5 → falls through to MODE_ALPHA) must succeed",
);
assert_eq!(mode, MODE_ALPHA);
assert_eq!(cws, vec![1, 2, 44, 10, 44, 11, 44, 12, 48]);
let (cws, mode) = encode_cws(b"1234abc").expect(
"encode_cws(b\"1234abc\") (4-digit prefix < 5 → falls through to MODE_ALPHA) must succeed",
);
assert_eq!(mode, MODE_ALPHA);
assert_eq!(
cws,
vec![1, 2, 3, 4, 44, 10, 44, 11, 44, 12, 48, 48, 48, 48, 48, 48]
);
match encode_cws(b"").unwrap_err() {
Error::InvalidData(msg) => {
assert!(
msg.contains("code49:"),
"empty arm missing `code49:` prefix: {msg}"
);
assert!(
msg.contains("empty input"),
"empty arm missing `empty input` predicate: {msg}"
);
}
other => panic!("empty encode_cws input should reject as InvalidData, got {other:?}"),
}
}
#[test]
fn pick_symbol_size_boundaries() {
assert_eq!(pick_symbol_size(1), Some((2, 9)));
assert_eq!(pick_symbol_size(9), Some((2, 9)));
assert_eq!(pick_symbol_size(10), Some((3, 16)));
assert_eq!(pick_symbol_size(16), Some((3, 16)));
assert_eq!(pick_symbol_size(17), Some((4, 23)));
assert_eq!(pick_symbol_size(23), Some((4, 23)));
assert_eq!(pick_symbol_size(43), Some((8, 49)));
assert_eq!(pick_symbol_size(49), Some((8, 49)));
assert_eq!(pick_symbol_size(50), None);
assert_eq!(pick_symbol_size(1000), None);
}
#[test]
fn lookup_direct_known_bytes() {
assert!(lookup_direct(b'0').is_some());
assert!(lookup_direct(b'9').is_some());
assert!(lookup_direct(b'A').is_some());
assert!(lookup_direct(b'Z').is_some());
assert!(lookup_direct(b' ').is_some());
assert_eq!(lookup_direct(b'a'), None);
assert_eq!(lookup_direct(0x01), None);
assert_eq!(lookup_direct(0xFF), None);
}
#[test]
fn charvals_classifies_every_branch() {
assert_eq!(charvals(0), Some((Some(43), 38)));
assert_eq!(charvals(26), Some((Some(43), 35)));
assert_eq!(charvals(27), Some((Some(43), 1)));
assert_eq!(charvals(31), Some((Some(43), 5)));
assert_eq!(charvals(b' '), Some((None, 38)));
assert_eq!(charvals(b'!'), Some((Some(43), 6)));
assert_eq!(charvals(b'$'), Some((None, 39)));
assert_eq!(charvals(b'%'), Some((None, 42)));
assert_eq!(charvals(b'+'), Some((None, 41)));
assert_eq!(charvals(b'-'), Some((None, 36)));
assert_eq!(charvals(b'.'), Some((None, 37)));
assert_eq!(charvals(b'/'), Some((None, 40)));
}
#[test]
fn charvals_exhaustive_fingerprint_pinned() {
fn pack(r: Option<(Option<u16>, u16)>) -> u64 {
match r {
None => 0,
Some((None, v)) => 1u64 + ((v as u64) << 8),
Some((Some(s), v)) => {
(1u64 << 32) | (s as u64) | ((v as u64) << 16)
}
}
}
let mut acc: u64 = 0;
for b in 0u8..=255 {
acc = acc.wrapping_add(
pack(charvals(b))
.wrapping_mul((b as u64).wrapping_add(1).wrapping_mul(2_654_435_761)),
);
}
assert_eq!(acc, CHARVALS_FP, "charvals exhaustive fingerprint changed");
}
const CHARVALS_FP: u64 = 9322844644053756155;
#[test]
fn base48_high_to_low_with_count_truncation() {
use super::base48;
assert_eq!(base48(1, b"0"), vec![0], "'0' → [0]");
assert_eq!(base48(1, b"47"), vec![47], "47 → [47]");
assert_eq!(
base48(1, b"48"),
vec![0],
"48 in 1 slot truncates → [0] (high digit lost)"
);
assert_eq!(
base48(2, b"48"),
vec![1, 0],
"48 in 2 slots → [1, 0] (HIGH at index 0)"
);
assert_eq!(
base48(3, b"12345"),
vec![5, 17, 9],
"12345 → [5, 17, 9] (MSB-first; LSB-first mutant → [9, 17, 5])"
);
assert_eq!(base48(0, b"123"), Vec::<u16>::new(), "count=0 → empty");
assert_eq!(base48(3, b"00000"), vec![0, 0, 0], "00000 → [0, 0, 0]");
assert_eq!(
base48(2, b"47"),
vec![0, 47],
"47 in 2 slots → [0, 47] (leading zero, low value 47)"
);
assert_eq!(base48(3, b"99999"), vec![43, 19, 15]);
assert_eq!(base48(3, b"10000"), vec![4, 16, 16]);
assert_eq!(base48(4, b"5"), vec![0, 0, 0, 5], "5 in 4 slots: [0,0,0,5]");
for v in 0u8..48 {
let s = v.to_string();
assert_eq!(
base48(1, s.as_bytes()),
vec![v as u16],
"v={v} in 1 slot must be [v]"
);
}
}
#[test]
fn code49_sentinel_consts_pinned() {
assert_eq!(S1, -1, "S1 sentinel (Shift-1) must remain -1");
assert_eq!(S2, -2, "S2 sentinel (Shift-2) must remain -2");
assert_eq!(FN1, -3, "FN1 sentinel must remain -3");
assert_eq!(FN2, -4, "FN2 sentinel must remain -4");
assert_eq!(FN3, -5, "FN3 sentinel must remain -5");
assert_eq!(
NS, -6,
"NS sentinel (numeric-shift codeword) must remain -6"
);
}
#[test]
fn build_ccs_state_machine_fingerprint_pinned() {
fn fp(out: &[u16]) -> (usize, u64) {
let mut s: u64 = 0;
for (i, &v) in out.iter().enumerate() {
s = s.wrapping_add(
(v as u64).wrapping_mul((i as u64).wrapping_add(1).wrapping_mul(2_654_435_761)),
);
}
(out.len(), s)
}
const FP_CCS_R2_DIGITS: (usize, u64) = (16, 7647429427441); const FP_CCS_R2_ALPHA: (usize, u64) = (16, 10044384919624); const FP_CCS_R3_ALPHA: (usize, u64) = (24, 21981382536841); const FP_CCS_R4_ALPHA: (usize, u64) = (32, 45207695445591); const FP_CCS_R5_ALPHA: (usize, u64) = (40, 67082900551992); const FP_CCS_R6_ALPHA: (usize, u64) = (48, 85064048397006); const FP_CCS_R7_ALPHA: (usize, u64) = (56, 105158127107776); const FP_CCS_R8_ALPHA: (usize, u64) = (64, 145396718808775);
let cases: &[(&str, &[u8], (usize, u64))] = &[
("r2_digits", b"12345", FP_CCS_R2_DIGITS),
("r2_alpha", b"A", FP_CCS_R2_ALPHA),
("r3_alpha", b"ABCDEFGHIJ", FP_CCS_R3_ALPHA),
("r4_alpha", b"ABCDEFGHIJKLMNOPQ", FP_CCS_R4_ALPHA),
("r5_alpha", b"ABCDEFGHIJKLMNOPQRSTUVWXY", FP_CCS_R5_ALPHA),
(
"r6_alpha",
b"ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFG",
FP_CCS_R6_ALPHA,
),
(
"r7_alpha",
b"ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMN",
FP_CCS_R7_ALPHA,
),
(
"r8_alpha",
b"ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTU",
FP_CCS_R8_ALPHA,
),
];
for (idx, (tag, input, want)) in cases.iter().enumerate() {
let (cws, mode) = encode_cws(input).unwrap_or_else(|e| {
panic!("encode_cws({tag}) idx {idx} must succeed; got Err: {e:?}")
});
let (rows, dcws) = pick_symbol_size(cws.len())
.unwrap_or_else(|| panic!("pick_symbol_size({tag}) idx {idx} must fit"));
let ccs = build_ccs(&cws, rows, dcws, mode).unwrap_or_else(|e| {
panic!(
"build_ccs({tag}) idx {idx} rows={rows} dcws={dcws} mode={mode} \
must succeed; got Err: {e:?}"
)
});
let got = fp(&ccs);
assert_eq!(
got, *want,
"build_ccs case {tag} (idx {idx}): got {got:?}, want {want:?}"
);
}
}
#[test]
fn encode_cws_direct_accepts_percent_at_codeword_42() {
assert_eq!(lookup_direct(b'%'), Some(42));
let cws =
encode_cws_direct(b"A%Z").expect("'%' (cw 42) is a direct member and must be accepted");
assert_eq!(&cws[..3], &[10u16, 42u16, 35u16]);
}
#[test]
fn encode_pixel_predicate_distinguishes_set_and_clear() {
let bm = encode(b"CODE49").expect("encode must succeed");
assert!(bm.get(0, 0), "top bearer pixel (0,0) must be set");
assert!(!bm.get(0, 1), "quiet-zone pixel (0,1) must be clear");
}
#[test]
fn encode_row_advance_fills_every_row() {
let bm = encode(b"CODE49").expect("encode must succeed");
let h = bm.height();
assert!(h > 1, "symbol must have multiple rows");
assert!(
bm.get(0, h - 1),
"bottom bearer pixel (0, {}) must be set",
h - 1
);
}
#[test]
fn encode_pixs_numcomprows_uses_two_r_plus_one() {
let input = b"ABCDEFGHIJKL"; let (cws, _mode) = encode_cws(input).expect("alpha encode");
let (rows, _dcws) = pick_symbol_size(cws.len()).expect("fits");
assert_eq!(rows, 3, "12-byte payload must select r=3");
let pixs = encode_pixs(input).expect("encode_pixs must succeed");
assert_eq!(pixs.len(), 7 * 81);
}
#[test]
fn code49_equivalence_notes() {
for &(rows, dcws) in &[(7u16, 42u16), (8u16, 49u16)] {
let r = usize::from(rows);
let j = (r - 1) * 7;
assert_eq!(
j,
usize::from(dcws),
"r={r}: first-rows loop must terminate exactly at j==dcws"
);
}
let (cws, mode) = encode_cws(b"ABCDEFGHIJKL").expect("encode");
let (rows, dcws) = pick_symbol_size(cws.len()).expect("fits");
let ccs = build_ccs(&cws, rows, dcws, mode).expect("build_ccs");
let last_idx = ccs.len();
let sum_excl: u32 = ccs[last_idx - 8..last_idx - 1]
.iter()
.map(|&c| u32::from(c))
.sum();
assert_eq!(
ccs[last_idx - 1],
(sum_excl % 49) as u16,
"final check must equal the self-excluding row sum"
);
}
}