#![allow(dead_code)]
use crate::encoding::LinearPattern;
use crate::error::Error;
use crate::options::Options;
pub(crate) const POSICODE_LA0: i16 = -1;
pub(crate) const POSICODE_LA1: i16 = -2;
pub(crate) const POSICODE_LA2: i16 = -3;
pub(crate) const POSICODE_SF0: i16 = -4;
pub(crate) const POSICODE_SF1: i16 = -5;
pub(crate) const POSICODE_SF2: i16 = -6;
pub(crate) const POSICODE_FN1: i16 = -7;
pub(crate) const POSICODE_FN2: i16 = -8;
pub(crate) const POSICODE_FN3: i16 = -9;
pub(crate) const POSICODE_FN4: i16 = -10;
pub(crate) const LIMITED_NA: i16 = -98;
#[rustfmt::skip]
pub(crate) const POSICODE_CHARMAPSNORMAL: [[i16; 3]; 46] = [
[b'0' as i16, b'^' as i16, b'\'' as i16],
[b'1' as i16, b';' as i16, 27],
[b'2' as i16, b'<' as i16, 28],
[b'3' as i16, b'=' as i16, 29],
[b'4' as i16, b'>' as i16, 30],
[b'5' as i16, b'?' as i16, 31],
[b'6' as i16, b'@' as i16, b'!' as i16],
[b'7' as i16, b'[' as i16, b'"' as i16],
[b'8' as i16, 92, b'#' as i16], [b'9' as i16, b']' as i16, b'&' as i16],
[b'A' as i16, b'a' as i16, 1],
[b'B' as i16, b'b' as i16, 2],
[b'C' as i16, b'c' as i16, 3],
[b'D' as i16, b'd' as i16, 4],
[b'E' as i16, b'e' as i16, 5],
[b'F' as i16, b'f' as i16, 6],
[b'G' as i16, b'g' as i16, 7],
[b'H' as i16, b'h' as i16, 8],
[b'I' as i16, b'i' as i16, 9],
[b'J' as i16, b'j' as i16, 10],
[b'K' as i16, b'k' as i16, 11],
[b'L' as i16, b'l' as i16, 12],
[b'M' as i16, b'm' as i16, 13],
[b'N' as i16, b'n' as i16, 14],
[b'O' as i16, b'o' as i16, 15],
[b'P' as i16, b'p' as i16, 16],
[b'Q' as i16, b'q' as i16, 17],
[b'R' as i16, b'r' as i16, 18],
[b'S' as i16, b's' as i16, 19],
[b'T' as i16, b't' as i16, 20],
[b'U' as i16, b'u' as i16, 21],
[b'V' as i16, b'v' as i16, 22],
[b'W' as i16, b'w' as i16, 23],
[b'X' as i16, b'x' as i16, 24],
[b'Y' as i16, b'y' as i16, 25],
[b'Z' as i16, b'z' as i16, 26],
[b'-' as i16, b'_' as i16, 40],
[b'.' as i16, b'`' as i16, 41],
[b' ' as i16, 127, 0],
[b'$' as i16, b'{' as i16, b'*' as i16],
[b'/' as i16, b'|' as i16, b',' as i16],
[b'+' as i16, b'}' as i16, b':' as i16],
[b'%' as i16, b'~' as i16, POSICODE_FN1],
[POSICODE_LA1, POSICODE_LA0, POSICODE_FN2],
[POSICODE_SF1, POSICODE_SF0, POSICODE_FN3],
[POSICODE_SF2, POSICODE_SF2, POSICODE_FN4],
];
#[rustfmt::skip]
pub(crate) const POSICODE_CHARMAPSLIMITED: [[i16; 3]; 38] = [
[b'0' as i16, LIMITED_NA, LIMITED_NA],
[b'1' as i16, LIMITED_NA, LIMITED_NA],
[b'2' as i16, LIMITED_NA, LIMITED_NA],
[b'3' as i16, LIMITED_NA, LIMITED_NA],
[b'4' as i16, LIMITED_NA, LIMITED_NA],
[b'5' as i16, LIMITED_NA, LIMITED_NA],
[b'6' as i16, LIMITED_NA, LIMITED_NA],
[b'7' as i16, LIMITED_NA, LIMITED_NA],
[b'8' as i16, LIMITED_NA, LIMITED_NA],
[b'9' as i16, LIMITED_NA, LIMITED_NA],
[b'A' as i16, LIMITED_NA, LIMITED_NA],
[b'B' as i16, LIMITED_NA, LIMITED_NA],
[b'C' as i16, LIMITED_NA, LIMITED_NA],
[b'D' as i16, LIMITED_NA, LIMITED_NA],
[b'E' as i16, LIMITED_NA, LIMITED_NA],
[b'F' as i16, LIMITED_NA, LIMITED_NA],
[b'G' as i16, LIMITED_NA, LIMITED_NA],
[b'H' as i16, LIMITED_NA, LIMITED_NA],
[b'I' as i16, LIMITED_NA, LIMITED_NA],
[b'J' as i16, LIMITED_NA, LIMITED_NA],
[b'K' as i16, LIMITED_NA, LIMITED_NA],
[b'L' as i16, LIMITED_NA, LIMITED_NA],
[b'M' as i16, LIMITED_NA, LIMITED_NA],
[b'N' as i16, LIMITED_NA, LIMITED_NA],
[b'O' as i16, LIMITED_NA, LIMITED_NA],
[b'P' as i16, LIMITED_NA, LIMITED_NA],
[b'Q' as i16, LIMITED_NA, LIMITED_NA],
[b'R' as i16, LIMITED_NA, LIMITED_NA],
[b'S' as i16, LIMITED_NA, LIMITED_NA],
[b'T' as i16, LIMITED_NA, LIMITED_NA],
[b'U' as i16, LIMITED_NA, LIMITED_NA],
[b'V' as i16, LIMITED_NA, LIMITED_NA],
[b'W' as i16, LIMITED_NA, LIMITED_NA],
[b'X' as i16, LIMITED_NA, LIMITED_NA],
[b'Y' as i16, LIMITED_NA, LIMITED_NA],
[b'Z' as i16, LIMITED_NA, LIMITED_NA],
[b'-' as i16, LIMITED_NA, LIMITED_NA],
[b'.' as i16, LIMITED_NA, LIMITED_NA],
];
pub(crate) const POSICODE_C2W: [[u32; 8]; 5] = [
[495, 330, 210, 126, 70, 35, 15, 5],
[165, 120, 84, 56, 35, 20, 10, 4],
[45, 36, 28, 21, 15, 10, 6, 3],
[9, 8, 7, 6, 5, 4, 3, 2],
[1, 1, 1, 1, 1, 1, 1, 1],
];
pub(crate) const POSICODE_ENCS_A: [&str; 48] = [
"141112",
"131212",
"121312",
"111412",
"131113",
"121213",
"111313",
"121114",
"111214",
"111115",
"181111",
"171211",
"161311",
"151411",
"141511",
"131611",
"121711",
"111811",
"171112",
"161212",
"151312",
"141412",
"131512",
"121612",
"111712",
"161113",
"151213",
"141313",
"131413",
"121513",
"111613",
"151114",
"141214",
"131314",
"121414",
"111514",
"141115",
"131215",
"121315",
"111415",
"131116",
"121216",
"111316",
"121117",
"111217",
"111118",
"1<111112",
"111111111;1",
];
pub(crate) const POSICODE_ENCS_B: [&str; 48] = [
"151213",
"141313",
"131413",
"121513",
"141214",
"131314",
"121414",
"131215",
"121315",
"121216",
"191212",
"181312",
"171412",
"161512",
"151612",
"141712",
"131812",
"121912",
"181213",
"171313",
"161413",
"151513",
"141613",
"131713",
"121813",
"171214",
"161314",
"151414",
"141514",
"131614",
"121714",
"161215",
"151315",
"141415",
"131515",
"121615",
"151216",
"141316",
"131416",
"121516",
"141217",
"131317",
"121417",
"131218",
"121318",
"121219",
"1<121312",
"121212121<1",
];
pub(crate) const POSICODE_ENCS_LIMITEDA: [&str; 40] = [
"111411", "111312", "111213", "111114", "121311", "121212", "121113", "141111", "131211",
"131112", "171111", "161211", "151311", "141411", "131511", "121611", "111711", "161112",
"151212", "141312", "131412", "121512", "111612", "151113", "141213", "131313", "121413",
"111513", "141114", "131214", "121314", "111414", "131115", "121215", "111315", "121116",
"111216", "111117", "151111", "1",
];
pub(crate) const POSICODE_ENCS_LIMITEDB: [&str; 40] = [
"121512", "121413", "121314", "121215", "131412", "131313", "131214", "151212", "141312",
"141213", "181212", "171312", "161412", "151512", "141612", "131712", "121812", "171213",
"161313", "151413", "141513", "131613", "121713", "161214", "151314", "141414", "131514",
"121614", "151215", "141315", "131415", "121515", "141216", "131316", "121416", "131217",
"121317", "121218", "141212", "1",
];
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PosicodeVersion {
#[default]
A,
B,
LimitedA,
LimitedB,
}
impl PosicodeVersion {
pub(crate) fn encs(self) -> &'static [&'static str] {
match self {
Self::A => &POSICODE_ENCS_A,
Self::B => &POSICODE_ENCS_B,
Self::LimitedA => &POSICODE_ENCS_LIMITEDA,
Self::LimitedB => &POSICODE_ENCS_LIMITEDB,
}
}
pub(crate) fn charmap(self) -> &'static [[i16; 3]] {
match self {
Self::A | Self::B => &POSICODE_CHARMAPSNORMAL,
Self::LimitedA | Self::LimitedB => &POSICODE_CHARMAPSLIMITED,
}
}
}
impl std::str::FromStr for PosicodeVersion {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"a" => Ok(Self::A),
"b" => Ok(Self::B),
"limiteda" => Ok(Self::LimitedA),
"limitedb" => Ok(Self::LimitedB),
_ => Err(()),
}
}
}
fn lookup_limited(b: u8) -> Option<u8> {
POSICODE_CHARMAPSLIMITED
.iter()
.position(|row| row[0] == i16::from(b))
.map(|i| i as u8)
}
fn compute_v(cws: &[u8]) -> u32 {
let mut v: u32 = 0;
for &cw_in in cws {
let mut cw: u32 = u32::from(cw_in);
for _ in 0..6 {
if ((cw ^ v) & 1) != 0 {
v ^= 7682;
}
v >>= 1;
cw >>= 1;
}
}
v
}
fn decompose_check_digits(v: u32) -> [u8; 6] {
let mut d: [u8; 6] = [2; 6];
let mut r: usize = 0;
let mut c: usize = 0;
let mut w: u32 = 0;
let mut sum: u32 = 0;
for _ in 0..10_000 {
if sum == v {
break;
}
if r >= POSICODE_C2W.len() || c >= POSICODE_C2W[0].len() {
break;
}
let t = sum + POSICODE_C2W[r][c];
if t == v {
w += 1;
d[r] = (w + 2) as u8;
sum = t;
} else if t > v {
d[r] = (w + 2) as u8;
r += 1;
w = 0;
} else {
c += 1;
w += 1;
sum = t;
}
}
let head_sum: i32 =
(d[0] as i32) + (d[1] as i32) + (d[2] as i32) + (d[3] as i32) + (d[4] as i32);
let tail = 20 - head_sum;
d[5] = tail.max(0) as u8;
d
}
fn build_cbs(d: [u8; 6]) -> [u8; 12] {
let mut cbs: [u8; 12] = [1; 12];
for (i, &di) in d.iter().enumerate() {
let pos = (5 - i) * 2 + 1;
cbs[pos] = di.saturating_sub(1);
}
cbs
}
fn pattern_to_widths(pat: &str) -> Vec<u8> {
pat.bytes().map(|b| b.saturating_sub(48)).collect()
}
fn finalize_sbs(cws: &[u8], version: PosicodeVersion) -> Vec<u8> {
let mut v = compute_v(cws);
let is_limited = matches!(
version,
PosicodeVersion::LimitedA | PosicodeVersion::LimitedB
);
if is_limited {
v &= 1023;
if v > 824 && v < 853 {
v += 292;
}
} else {
v = (v & 1023) + 45;
}
let mut d = decompose_check_digits(v);
if matches!(version, PosicodeVersion::B | PosicodeVersion::LimitedB) {
for di in &mut d {
*di = di.saturating_add(1);
}
}
let cbs = build_cbs(d);
let encs = version.encs();
let start_pat = pattern_to_widths(encs[encs.len() - 2]);
let stop_pat = pattern_to_widths(encs[encs.len() - 1]);
let mut bars: Vec<u8> =
Vec::with_capacity(start_pat.len() + cws.len() * 6 + 12 + stop_pat.len());
bars.extend_from_slice(&start_pat);
for &cw in cws {
let pat = pattern_to_widths(encs[cw as usize]);
debug_assert_eq!(pat.len(), 6, "cw {cw} pattern not 6 modules wide");
bars.extend_from_slice(&pat);
}
bars.extend_from_slice(&cbs);
bars.extend_from_slice(&stop_pat);
bars
}
fn encode_limited(data: &str, version: PosicodeVersion) -> Result<LinearPattern, Error> {
debug_assert!(
matches!(
version,
PosicodeVersion::LimitedA | PosicodeVersion::LimitedB
),
"encode_limited only handles LimitedA / LimitedB",
);
let bytes = data.as_bytes();
if bytes.is_empty() {
return Err(Error::InvalidData(
"posicode: empty input is not encodable".into(),
));
}
if bytes.len() > 500 {
return Err(Error::InvalidData(format!(
"posicode: payload of {} bytes exceeds BWIPP's 500-byte limit",
bytes.len()
)));
}
let mut cws: Vec<u8> = Vec::with_capacity(bytes.len());
for (i, &b) in bytes.iter().enumerate() {
match lookup_limited(b) {
Some(cw) => cws.push(cw),
None => {
return Err(Error::InvalidData(format!(
"posicode limited: byte 0x{b:02x} at position {i} is not in the \
limited alphabet (0-9, A-Z, '-', '.')"
)));
}
}
}
Ok(LinearPattern {
bars: finalize_sbs(&cws, version),
text: None,
})
}
fn normal_sets() -> &'static [std::collections::HashMap<i16, u8>; 3] {
use std::collections::HashMap;
use std::sync::OnceLock;
static SETS: OnceLock<[HashMap<i16, u8>; 3]> = OnceLock::new();
SETS.get_or_init(|| {
let mut sets: [HashMap<i16, u8>; 3] = [HashMap::new(), HashMap::new(), HashMap::new()];
for (row_idx, row) in POSICODE_CHARMAPSNORMAL.iter().enumerate() {
for (set_idx, &val) in row.iter().enumerate() {
sets[set_idx].insert(val, row_idx as u8);
}
}
sets
})
}
fn insert_fn4_markers(msg: &[i16]) -> Vec<i16> {
let msglen = msg.len();
let mut num_sa: Vec<usize> = vec![0; msglen + 1];
let mut num_ea: Vec<usize> = vec![0; msglen + 1];
for i in (0..msglen).rev() {
let c = msg[i];
if c >= 0 {
if c >= 128 {
num_ea[i] = num_ea[i + 1] + 1;
} else {
num_sa[i] = num_sa[i + 1] + 1;
}
}
}
let mut out: Vec<i16> = Vec::with_capacity(msglen * 2);
let mut ea = false;
for (i, &c) in msg.iter().enumerate() {
if c >= 0 && ea == (c < 128) {
let run = if ea { num_sa[i] } else { num_ea[i] };
let threshold = if run + i == msglen { 3 } else { 5 };
if run < threshold {
out.push(POSICODE_FN4);
} else {
ea = !ea;
out.push(POSICODE_FN4);
out.push(POSICODE_FN4);
}
}
if c >= 0 {
out.push(c & 127);
} else {
out.push(c);
}
}
out
}
fn select_codewords_normal(msg: &[i16]) -> Vec<u8> {
let sets = normal_sets();
let mut cws: Vec<u8> = Vec::with_capacity(msg.len() * 2);
let mut i: usize = 0;
let mut cset: usize = 0;
while i < msg.len() {
let char1: i16 = msg[i];
let char2: i16 = if i + 1 < msg.len() { msg[i + 1] } else { -99 };
if let Some(&cw) = sets[cset].get(&char1) {
cws.push(cw);
i += 1;
continue;
}
if sets[2].contains_key(&char1) {
cws.push(sets[cset][&POSICODE_SF2]);
cws.push(sets[2][&char1]);
i += 1;
continue;
}
let other = 1 - cset; let char2_in_cset = sets[cset].contains_key(&char2);
if !char2_in_cset {
let latch_sentinel = if cset == 0 {
POSICODE_LA1
} else {
POSICODE_LA0
};
cws.push(sets[cset][&latch_sentinel]);
cset = other;
continue;
} else {
let shift_sentinel = if cset == 0 {
POSICODE_SF1
} else {
POSICODE_SF0
};
cws.push(sets[cset][&shift_sentinel]);
cws.push(sets[other][&char1]);
i += 1;
continue;
}
}
cws
}
fn encode_normal(data: &str, version: PosicodeVersion) -> Result<LinearPattern, Error> {
debug_assert!(
matches!(version, PosicodeVersion::A | PosicodeVersion::B),
"encode_normal only handles A / B",
);
let bytes = data.as_bytes();
if bytes.is_empty() {
return Err(Error::InvalidData(
"posicode: empty input is not encodable".into(),
));
}
if bytes.len() > 500 {
return Err(Error::InvalidData(format!(
"posicode: payload of {} bytes exceeds BWIPP's 500-byte limit",
bytes.len()
)));
}
let initial_msg: Vec<i16> = bytes.iter().map(|&b| i16::from(b)).collect();
let processed_msg = insert_fn4_markers(&initial_msg);
let sets = normal_sets();
for (i, &c) in processed_msg.iter().enumerate() {
let in_any =
sets[0].contains_key(&c) || sets[1].contains_key(&c) || sets[2].contains_key(&c);
if !in_any {
return Err(Error::InvalidData(format!(
"posicode: byte 0x{:02x} at processed position {i} is not encodable \
in any POSICODE set",
c as u8
)));
}
}
let cws = select_codewords_normal(&processed_msg);
Ok(LinearPattern {
bars: finalize_sbs(&cws, version),
text: None,
})
}
pub fn encode(data: &str, opts: &Options) -> Result<LinearPattern, Error> {
let version_str = opts.get("version").unwrap_or("a");
let version = match version_str.parse::<PosicodeVersion>() {
Ok(v) => v,
Err(()) => {
return Err(Error::InvalidOption(format!(
"posicode: version `{version_str}` is not one of \
`a` / `b` / `limiteda` / `limitedb`"
)));
}
};
match version {
PosicodeVersion::LimitedA | PosicodeVersion::LimitedB => encode_limited(data, version),
PosicodeVersion::A | PosicodeVersion::B => encode_normal(data, version),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normal_charmap_shape_matches_bwipp() {
assert_eq!(POSICODE_CHARMAPSNORMAL.len(), 46);
assert_eq!(
POSICODE_CHARMAPSNORMAL[0],
[b'0' as i16, b'^' as i16, b'\'' as i16]
);
assert_eq!(POSICODE_CHARMAPSNORMAL[35], [b'Z' as i16, b'z' as i16, 26]);
assert_eq!(POSICODE_CHARMAPSNORMAL[38], [b' ' as i16, 127, 0]);
assert_eq!(
POSICODE_CHARMAPSNORMAL[42],
[b'%' as i16, b'~' as i16, POSICODE_FN1]
);
assert_eq!(
POSICODE_CHARMAPSNORMAL[45],
[POSICODE_SF2, POSICODE_SF2, POSICODE_FN4]
);
}
#[test]
fn limited_charmap_shape_matches_bwipp() {
assert_eq!(POSICODE_CHARMAPSLIMITED.len(), 38);
for (i, row) in POSICODE_CHARMAPSLIMITED.iter().enumerate() {
assert_eq!(row[1], LIMITED_NA, "row {i} set-1 should be LIMITED_NA");
assert_eq!(row[2], LIMITED_NA, "row {i} set-2 should be LIMITED_NA");
}
for (d, row) in POSICODE_CHARMAPSLIMITED.iter().take(10).enumerate() {
assert_eq!(row[0], (b'0' + d as u8) as i16);
}
for (a, row) in POSICODE_CHARMAPSLIMITED
.iter()
.skip(10)
.take(26)
.enumerate()
{
assert_eq!(row[0], (b'A' + a as u8) as i16);
}
assert_eq!(POSICODE_CHARMAPSLIMITED[36][0], b'-' as i16);
assert_eq!(POSICODE_CHARMAPSLIMITED[37][0], b'.' as i16);
}
#[test]
fn encs_table_lengths_match_bwipp() {
assert_eq!(POSICODE_ENCS_A.len(), 48);
assert_eq!(POSICODE_ENCS_B.len(), 48);
assert_eq!(POSICODE_ENCS_LIMITEDA.len(), 40);
assert_eq!(POSICODE_ENCS_LIMITEDB.len(), 40);
}
#[test]
fn codeword_patterns_are_6_modules() {
for (i, &p) in POSICODE_ENCS_A[..46].iter().enumerate() {
assert_eq!(p.len(), 6, "ENCS_A[{i}] should be 6 modules");
}
for (i, &p) in POSICODE_ENCS_B[..46].iter().enumerate() {
assert_eq!(p.len(), 6, "ENCS_B[{i}] should be 6 modules");
}
for (i, &p) in POSICODE_ENCS_LIMITEDA[..38].iter().enumerate() {
assert_eq!(p.len(), 6, "ENCS_LIMITEDA[{i}] should be 6 modules");
}
for (i, &p) in POSICODE_ENCS_LIMITEDB[..38].iter().enumerate() {
assert_eq!(p.len(), 6, "ENCS_LIMITEDB[{i}] should be 6 modules");
}
}
#[test]
fn start_stop_patterns_match_bwipp() {
assert_eq!(POSICODE_ENCS_A[46], "1<111112");
assert_eq!(POSICODE_ENCS_A[47], "111111111;1");
assert_eq!(POSICODE_ENCS_B[46], "1<121312");
assert_eq!(POSICODE_ENCS_B[47], "121212121<1");
assert_eq!(POSICODE_ENCS_LIMITEDA[38], "151111");
assert_eq!(POSICODE_ENCS_LIMITEDA[39], "1");
assert_eq!(POSICODE_ENCS_LIMITEDB[38], "141212");
assert_eq!(POSICODE_ENCS_LIMITEDB[39], "1");
}
#[test]
fn weight_table_matches_bwipp() {
assert_eq!(POSICODE_C2W.len(), 5);
for row in &POSICODE_C2W {
assert_eq!(row.len(), 8);
}
assert_eq!(POSICODE_C2W[0][0], 495);
assert_eq!(POSICODE_C2W[0][7], 5);
assert_eq!(POSICODE_C2W[4], [1, 1, 1, 1, 1, 1, 1, 1]);
}
#[test]
fn version_from_str_round_trips() {
use std::str::FromStr;
assert_eq!(PosicodeVersion::from_str("a"), Ok(PosicodeVersion::A));
assert_eq!(PosicodeVersion::from_str("b"), Ok(PosicodeVersion::B));
assert_eq!(
PosicodeVersion::from_str("limiteda"),
Ok(PosicodeVersion::LimitedA)
);
assert_eq!(
PosicodeVersion::from_str("limitedb"),
Ok(PosicodeVersion::LimitedB)
);
assert_eq!(
PosicodeVersion::from_str("A"),
Err(()),
"uppercase `A` must reject — BWIPP version IDs are case-sensitive"
);
assert_eq!(
PosicodeVersion::from_str("c"),
Err(()),
"unknown letter `c` must reject — only a/b/limiteda/limitedb are valid"
);
assert_eq!(
PosicodeVersion::from_str(""),
Err(()),
"empty string must reject — no default version"
);
}
#[test]
fn version_table_accessors() {
assert_eq!(PosicodeVersion::A.encs().len(), 48);
assert_eq!(PosicodeVersion::B.encs().len(), 48);
assert_eq!(PosicodeVersion::LimitedA.encs().len(), 40);
assert_eq!(PosicodeVersion::LimitedB.encs().len(), 40);
assert_eq!(PosicodeVersion::A.charmap().len(), 46);
assert_eq!(PosicodeVersion::LimitedA.charmap().len(), 38);
}
#[test]
fn encode_rejects_unknown_version() {
let mut opts = Options::default();
opts.extras.push(("version".into(), "c".into()));
let err = encode("HELLO", &opts).unwrap_err();
match err {
Error::InvalidOption(msg) => {
assert!(
msg.contains("posicode:"),
"missing posicode prefix: {msg:?}"
);
assert!(
msg.contains("version `c`"),
"missing version-value Debug echo `version \\`c\\``: {msg:?}"
);
assert!(
msg.contains("is not one of"),
"missing `is not one of` predicate: {msg:?}"
);
assert!(
msg.contains("`a`")
&& msg.contains("`b`")
&& msg.contains("`limiteda`")
&& msg.contains("`limitedb`"),
"missing one of the valid-version identifiers in enumeration: {msg:?}"
);
}
other => panic!("expected InvalidOption(version), got {other:?}"),
}
}
#[test]
fn encode_default_routes_to_version_a() {
let p = encode("HELLO", &Options::default()).expect(
"encode(\"HELLO\", default) (POSICODE default-version dispatch; no `version` extra → version-a, not Unimplemented) must succeed",
);
assert!(
p.bars.len() > 30,
"expected nontrivial sbs, got {:?}",
p.bars
);
}
fn limiteda_opts() -> Options {
let mut opts = Options::default();
opts.extras.push(("version".into(), "limiteda".into()));
opts
}
#[test]
fn lookup_limited_matches_charmap() {
assert_eq!(lookup_limited(b'0'), Some(0));
assert_eq!(lookup_limited(b'9'), Some(9));
assert_eq!(lookup_limited(b'A'), Some(10));
assert_eq!(lookup_limited(b'Z'), Some(35));
assert_eq!(lookup_limited(b'-'), Some(36));
assert_eq!(lookup_limited(b'.'), Some(37));
assert_eq!(lookup_limited(b'a'), None);
assert_eq!(lookup_limited(b' '), None);
assert_eq!(lookup_limited(0), None);
}
#[test]
fn build_cbs_reverse_interleave_with_sub_one() {
let cbs = build_cbs([3, 5, 7, 4, 6, 2]);
assert_eq!(cbs, [1, 1, 1, 5, 1, 3, 1, 6, 1, 4, 1, 2]);
let cbs0 = build_cbs([3, 5, 7, 4, 6, 0]);
assert_eq!(cbs0[1], 0, "d[5]=0 → saturating_sub gives 0");
assert_eq!(&cbs0[2..], &cbs[2..]);
}
#[test]
fn pattern_to_widths_subtracts_ascii_zero() {
assert_eq!(pattern_to_widths("0"), vec![0]);
assert_eq!(pattern_to_widths("9"), vec![9]);
assert_eq!(pattern_to_widths(":"), vec![10], "':' (58) - 48 = 10");
assert_eq!(pattern_to_widths(";"), vec![11]);
assert_eq!(pattern_to_widths("<"), vec![12]);
assert_eq!(pattern_to_widths(""), Vec::<u8>::new());
assert_eq!(pattern_to_widths("123"), vec![1, 2, 3]);
assert_eq!(pattern_to_widths("151111"), vec![1, 5, 1, 1, 1, 1]);
assert_eq!(pattern_to_widths("/"), vec![0], "'/' (47) saturates to 0");
assert_eq!(pattern_to_widths("\0"), vec![0]);
}
#[test]
fn compute_v_matches_bwip_js_oracle() {
assert_eq!(compute_v(&[0]), 0);
let raw_v_1 = compute_v(&[1]);
assert_eq!(raw_v_1 & 1023, 553);
let cws_digits: Vec<u8> = (0..10).collect();
let mut v = compute_v(&cws_digits) & 1023;
if v > 824 && v < 853 {
v += 292;
}
assert_eq!(v, 296);
}
#[test]
fn decompose_check_digits_matches_bwip_js_oracle() {
assert_eq!(decompose_check_digits(0), [2, 2, 2, 2, 2, 10]);
assert_eq!(decompose_check_digits(553), [3, 2, 3, 6, 2, 4]);
assert_eq!(decompose_check_digits(272), [2, 3, 6, 4, 2, 3]);
assert_eq!(decompose_check_digits(889), [4, 2, 5, 2, 2, 5]);
assert_eq!(decompose_check_digits(296), [2, 4, 2, 3, 6, 3]);
}
#[test]
fn build_cbs_matches_bwip_js_oracle() {
assert_eq!(
build_cbs([2, 2, 2, 2, 2, 10]),
[1, 9, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
);
assert_eq!(
build_cbs([3, 2, 3, 6, 2, 4]),
[1, 3, 1, 1, 1, 5, 1, 2, 1, 1, 1, 2]
);
}
#[test]
fn encode_limiteda_digit_zero_matches_bwip_js_sbs() {
let p = encode("0", &limiteda_opts()).expect(
"encode(\"0\", limiteda) (POSICODE limiteda single-digit '0' → cw 0 → pattern \"111411\") must succeed",
);
let want: Vec<u8> = vec![
1, 5, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 9, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ];
assert_eq!(
p.bars, want,
"limiteda '0' sbs must match bwip-js byte-for-byte"
);
}
#[test]
fn encode_limiteda_digit_one_matches_bwip_js_sbs() {
let p = encode("1", &limiteda_opts()).expect(
"encode(\"1\", limiteda) (POSICODE limiteda single-digit '1' → cw 1 → pattern \"111312\") must succeed",
);
let want: Vec<u8> = vec![
1, 5, 1, 1, 1, 1, 1, 1, 1, 3, 1, 2, 1, 3, 1, 1, 1, 5, 1, 2, 1, 1, 1, 2, 1, ];
assert_eq!(p.bars, want);
}
#[test]
fn encode_limiteda_uppercase_a_matches_bwip_js_sbs() {
let p = encode("A", &limiteda_opts()).expect(
"encode(\"A\", limiteda) (POSICODE limiteda uppercase 'A' → cw 10 → pattern \"171111\") must succeed",
);
let want: Vec<u8> = vec![
1, 5, 1, 1, 1, 1, 1, 7, 1, 1, 1, 1, 1, 2, 1, 1, 1, 3, 1, 5, 1, 2, 1, 1, 1, ];
assert_eq!(p.bars, want);
}
#[test]
fn encode_limiteda_uppercase_z_matches_bwip_js_sbs() {
let p = encode("Z", &limiteda_opts()).expect(
"encode(\"Z\", limiteda) (POSICODE limiteda uppercase 'Z' → cw 35 charmap-boundary → pattern \"121116\") must succeed",
);
let want: Vec<u8> = vec![
1, 5, 1, 1, 1, 1, 1, 2, 1, 1, 1, 6, 1, 4, 1, 1, 1, 1, 1, 4, 1, 1, 1, 3, 1, ];
assert_eq!(p.bars, want);
}
#[test]
fn encode_limiteda_digit_run_matches_bwip_js_sbs() {
let p = encode("0123456789", &limiteda_opts()).expect(
"encode(\"0123456789\", limiteda) (POSICODE limiteda 10-digit run exercising cw 0..9 + v=296 check decomposition; 79-module SBS oracle) must succeed",
);
let want: Vec<u8> = vec![
1, 5, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 3, 1, 2, 1, 1, 1, 2, 1, 3, 1, 1, 1, 1, 1, 4, 1, 2, 1, 3, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 3, 1, 4, 1, 1, 1, 1, 1, 3, 1, 2, 1, 1, 1, 3, 1, 1, 1, 2, 1, 2, 1, 5, 1, 2, 1, 1, 1, 3, 1, 1, 1, ];
assert_eq!(p.bars, want);
}
#[test]
fn encode_limiteda_rejects_empty() {
let err = encode("", &limiteda_opts()).unwrap_err();
match err {
Error::InvalidData(msg) => {
assert!(
msg.contains("posicode:"),
"missing posicode prefix: {msg:?}"
);
assert!(
msg.contains("empty input is not encodable"),
"missing full predicate `empty input is not encodable`: {msg:?}"
);
}
other => panic!("expected InvalidData(empty), got {other:?}"),
}
}
#[test]
fn encode_limiteda_rejects_lowercase() {
let err = encode("hello", &limiteda_opts()).unwrap_err();
match err {
Error::InvalidData(msg) => {
assert!(
msg.contains("posicode limited:"),
"missing posicode limited prefix: {msg:?}"
);
assert!(
msg.contains("byte 0x68"),
"missing `byte 0x68` hex echo for 'h': {msg:?}"
);
assert!(
msg.contains("at position 0"),
"missing `at position 0` position-echo: {msg:?}"
);
assert!(
msg.contains("0-9, A-Z, '-', '.'"),
"missing valid-alphabet enumeration: {msg:?}"
);
}
other => panic!("expected InvalidData(limited alphabet), got {other:?}"),
}
}
#[test]
fn encode_limiteda_rejects_space() {
let err = encode("AB C", &limiteda_opts()).unwrap_err();
match err {
Error::InvalidData(msg) => {
assert!(
msg.contains("posicode limited:"),
"missing posicode limited prefix: {msg:?}"
);
assert!(
msg.contains("byte 0x20"),
"missing `byte 0x20` hex echo for space: {msg:?}"
);
assert!(
msg.contains("at position 2"),
"missing `at position 2` position-echo: {msg:?}"
);
assert!(
msg.contains("0-9, A-Z, '-', '.'"),
"missing valid-alphabet enumeration: {msg:?}"
);
}
other => panic!("expected InvalidData with position 2, got {other:?}"),
}
}
#[test]
fn encode_limiteda_rejects_overlong() {
let payload: String = "A".repeat(501);
let err = encode(&payload, &limiteda_opts()).unwrap_err();
match err {
Error::InvalidData(msg) => {
assert!(
msg.contains("posicode:"),
"missing posicode prefix: {msg:?}"
);
assert!(
msg.contains("exceeds BWIPP's 500-byte limit"),
"missing full predicate `exceeds BWIPP's 500-byte limit`: {msg:?}"
);
assert!(
msg.contains("payload of 501 bytes"),
"missing `payload of 501 bytes` value-echo: {msg:?}"
);
}
other => panic!("expected InvalidData(overlong), got {other:?}"),
}
}
#[test]
fn payload_length_cap_is_strictly_five_hundred() {
let exactly_500_limited: String = "A".repeat(500);
encode(&exactly_500_limited, &limiteda_opts())
.expect("500-byte limiteda payload should encode (boundary not yet hit)");
let exactly_500_normal: String = "A".repeat(500);
encode(&exactly_500_normal, &Options::default())
.expect("500-byte normal posicode payload should encode (boundary not yet hit)");
let length_501: String = "A".repeat(501);
match encode(&length_501, &limiteda_opts()) {
Err(Error::InvalidData(msg)) => {
assert!(
msg.contains("posicode:"),
"limited path: missing posicode prefix: {msg:?}"
);
assert!(
msg.contains("exceeds BWIPP's 500-byte limit"),
"limited path: missing full predicate: {msg:?}"
);
assert!(
msg.contains("payload of 501 bytes"),
"limited path: missing payload-bytes echo: {msg:?}"
);
}
other => panic!("limited path: expected InvalidData(501 overflow), got {other:?}"),
}
match encode(&length_501, &Options::default()) {
Err(Error::InvalidData(msg)) => {
assert!(
msg.contains("posicode:"),
"normal path: missing posicode prefix: {msg:?}"
);
assert!(
msg.contains("exceeds BWIPP's 500-byte limit"),
"normal path: missing full predicate: {msg:?}"
);
assert!(
msg.contains("payload of 501 bytes"),
"normal path: missing payload-bytes echo: {msg:?}"
);
}
other => panic!("normal path: expected InvalidData(501 overflow), got {other:?}"),
}
}
fn limitedb_opts() -> Options {
let mut opts = Options::default();
opts.extras.push(("version".into(), "limitedb".into()));
opts
}
#[test]
fn encode_limitedb_digit_zero_matches_bwip_js_sbs() {
let p = encode("0", &limitedb_opts()).expect(
"encode(\"0\", limitedb) (POSICODE limitedb single-digit '0' → cw 0 → pattern \"121512\"; +1-module-wider than limiteda) must succeed",
);
let want: Vec<u8> = vec![
1, 4, 1, 2, 1, 2, 1, 2, 1, 5, 1, 2, 1, 10, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, ];
assert_eq!(p.bars, want);
}
#[test]
fn encode_limitedb_digit_one_matches_bwip_js_sbs() {
let p = encode("1", &limitedb_opts()).expect(
"encode(\"1\", limitedb) (POSICODE limitedb single-digit '1' → cw 1 → pattern \"121413\") must succeed",
);
let want: Vec<u8> = vec![
1, 4, 1, 2, 1, 2, 1, 2, 1, 4, 1, 3, 1, 4, 1, 2, 1, 6, 1, 3, 1, 2, 1, 3, 1, ];
assert_eq!(p.bars, want);
}
#[test]
fn encode_limitedb_uppercase_a_matches_bwip_js_sbs() {
let p = encode("A", &limitedb_opts()).expect(
"encode(\"A\", limitedb) (POSICODE limitedb uppercase 'A' → cw 10 → pattern \"181212\") must succeed",
);
let want: Vec<u8> = vec![
1, 4, 1, 2, 1, 2, 1, 8, 1, 2, 1, 2, 1, 3, 1, 2, 1, 4, 1, 6, 1, 3, 1, 2, 1, ];
assert_eq!(p.bars, want);
}
#[test]
fn encode_limitedb_uppercase_z_matches_bwip_js_sbs() {
let p = encode("Z", &limitedb_opts()).expect(
"encode(\"Z\", limitedb) (POSICODE limitedb uppercase 'Z' → cw 35 charmap-boundary → pattern \"131217\") must succeed",
);
let want: Vec<u8> = vec![
1, 4, 1, 2, 1, 2, 1, 3, 1, 2, 1, 7, 1, 5, 1, 2, 1, 2, 1, 5, 1, 2, 1, 4, 1, ];
assert_eq!(p.bars, want);
}
#[test]
fn encode_limitedb_digit_run_matches_bwip_js_sbs() {
let p = encode("0123456789", &limitedb_opts()).expect(
"encode(\"0123456789\", limitedb) (POSICODE limitedb 10-digit run exercising limitedb cw 0..9; 79-module SBS oracle) must succeed",
);
let want: Vec<u8> = vec![
1, 4, 1, 2, 1, 2, 1, 2, 1, 5, 1, 2, 1, 2, 1, 4, 1, 3, 1, 2, 1, 3, 1, 4, 1, 2, 1, 2, 1, 5, 1, 3, 1, 4, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 2, 1, 4, 1, 5, 1, 2, 1, 2, 1, 4, 1, 3, 1, 2, 1, 4, 1, 2, 1, 3, 1, 3, 1, 6, 1, 3, 1, 2, 1, 4, 1, 2, 1, ];
assert_eq!(p.bars, want);
}
#[test]
fn encode_limitedb_rejects_invalid_inputs() {
match encode("", &limitedb_opts()).unwrap_err() {
Error::InvalidData(msg) => {
assert!(
msg.contains("posicode:"),
"empty arm: missing `posicode:` prefix: {msg}"
);
assert!(
msg.contains("empty input is not encodable"),
"empty arm: missing `empty input is not encodable` predicate: {msg}"
);
assert!(
!msg.contains("limited:"),
"empty arm: empty path uses bare `posicode:`, not `posicode limited:`: {msg}"
);
assert!(
!msg.contains("500-byte"),
"empty arm: wrong arm — 500-byte cap leaked: {msg}"
);
}
other => panic!("empty input should reject as InvalidData, got {other:?}"),
}
match encode("hello", &limitedb_opts()).unwrap_err() {
Error::InvalidData(msg) => {
assert!(
msg.contains("posicode limited:"),
"non-alphabet arm: missing `posicode limited:` prefix: {msg}"
);
assert!(
msg.contains("byte 0x68"),
"non-alphabet arm: missing `byte 0x68` hex-echo of 'h': {msg}"
);
assert!(
msg.contains("position 0"),
"non-alphabet arm: missing `position 0` index echo: {msg}"
);
assert!(
msg.contains("limited alphabet (0-9, A-Z, '-', '.')"),
"non-alphabet arm: missing full alphabet spec: {msg}"
);
}
other => panic!("\"hello\" should reject as InvalidData, got {other:?}"),
}
let payload: String = "A".repeat(501);
match encode(&payload, &limitedb_opts()).unwrap_err() {
Error::InvalidData(msg) => {
assert!(
msg.contains("posicode:"),
"overflow arm: missing `posicode:` prefix: {msg}"
);
assert!(
msg.contains("payload of 501 bytes"),
"overflow arm: missing `payload of 501 bytes` value echo: {msg}"
);
assert!(
msg.contains("500-byte limit"),
"overflow arm: missing `500-byte limit` predicate: {msg}"
);
assert!(
!msg.contains("limited alphabet"),
"overflow arm: wrong arm — limited-alphabet diagnostic leaked: {msg}"
);
}
other => panic!("501-byte payload should reject as InvalidData, got {other:?}"),
}
}
#[test]
fn limitedb_d_is_limiteda_d_plus_one() {
for (v, _label) in &[
(0u32, "0"),
(553, "1"),
(272, "A"),
(889, "Z"),
(296, "0..9"),
] {
let mut da = decompose_check_digits(*v);
for di in &mut da {
*di = di.saturating_add(1);
}
let want: [u8; 6] = match v {
0 => [3, 3, 3, 3, 3, 11],
553 => [4, 3, 4, 7, 3, 5],
272 => [3, 4, 7, 5, 3, 4],
889 => [5, 3, 6, 3, 3, 6],
296 => [3, 5, 3, 4, 7, 4],
_ => unreachable!(),
};
assert_eq!(da, want, "limitedb d for v={v} mismatch");
}
}
fn version_a_opts() -> Options {
let mut opts = Options::default();
opts.extras.push(("version".into(), "a".into()));
opts
}
fn version_b_opts() -> Options {
let mut opts = Options::default();
opts.extras.push(("version".into(), "b".into()));
opts
}
#[test]
fn normal_sets_lookup_matches_charmap() {
let sets = normal_sets();
assert_eq!(sets[0][&i16::from(b'0')], 0);
assert_eq!(sets[0][&i16::from(b'9')], 9);
assert_eq!(sets[0][&i16::from(b'A')], 10);
assert_eq!(sets[0][&i16::from(b'Z')], 35);
assert_eq!(sets[0][&i16::from(b'-')], 36);
assert_eq!(sets[0][&i16::from(b'.')], 37);
assert_eq!(sets[0][&i16::from(b' ')], 38);
assert_eq!(sets[0][&POSICODE_LA1], 43);
assert_eq!(sets[0][&POSICODE_SF1], 44);
assert_eq!(sets[0][&POSICODE_SF2], 45);
assert_eq!(sets[1][&i16::from(b'a')], 10);
assert_eq!(sets[1][&i16::from(b'z')], 35);
assert_eq!(sets[1][&POSICODE_LA0], 43);
assert_eq!(sets[1][&POSICODE_SF0], 44);
assert_eq!(sets[1][&POSICODE_SF2], 45);
assert_eq!(sets[2][&1], 10);
assert_eq!(sets[2][&26], 35);
assert_eq!(sets[2][&POSICODE_FN1], 42);
assert_eq!(sets[2][&POSICODE_FN4], 45);
}
#[test]
fn select_codewords_normal_set0_only() {
let msg: Vec<i16> = "HELLO".bytes().map(i16::from).collect();
let cws = select_codewords_normal(&msg);
assert_eq!(cws, vec![17, 14, 21, 21, 24]);
}
#[test]
fn select_codewords_normal_set1_latch() {
let msg: Vec<i16> = "abc".bytes().map(i16::from).collect();
let cws = select_codewords_normal(&msg);
assert_eq!(cws, vec![43, 10, 11, 12]);
}
#[test]
fn select_codewords_normal_set0_then_sf1() {
let msg: Vec<i16> = "Ab".bytes().map(i16::from).collect();
let cws = select_codewords_normal(&msg);
assert_eq!(cws, vec![10, 43, 11]);
}
#[test]
fn select_codewords_normal_set0_sf1_for_single_char() {
let msg: Vec<i16> = "AbC".bytes().map(i16::from).collect();
let cws = select_codewords_normal(&msg);
assert_eq!(cws, vec![10, 44, 11, 12]);
}
#[test]
fn select_codewords_normal_char2_sentinel_is_negative() {
let msg: Vec<i16> = "aaB".bytes().map(i16::from).collect();
let cws = select_codewords_normal(&msg);
assert_eq!(
cws,
vec![43, 10, 10, 43, 11],
"select_codewords_normal: at end-of-message in cset=1, \
a Path-C char must trigger a latch (LA0=43) not a shift \
(SF0=44). The mutant that removes the negation on the \
-99 sentinel makes char2=99 ('c'), which IS in set 1, \
flipping the latch/shift decision."
);
}
#[test]
fn decompose_check_digits_bounds_check_breaks_on_either_axis() {
assert_eq!(
decompose_check_digits(u32::MAX),
[2, 2, 2, 2, 2, 10],
"decompose_check_digits(u32::MAX) should saturate the C2W \
row 0 walk and return the all-min d-array. The 556 \
`||→&&` mutant would skip the bound break and panic on \
POSICODE_C2W[0][8] before the function returns."
);
}
#[test]
fn finalize_sbs_limited_range_adjustment() {
let cases: &[(&[u8], &str)] = &[
(&[11u8], "v=825 inside-range, +=292 applies"),
(&[8, 34], "v=852 inside-range top-boundary"),
(&[11, 26], "v=853 just-outside top-boundary"),
(&[1, 0, 33], "v=824 just-outside bottom-boundary"),
];
let bars: Vec<Vec<u8>> = cases
.iter()
.map(|(cws, _)| finalize_sbs(cws, PosicodeVersion::LimitedA))
.collect();
assert_eq!(
bars,
vec![
vec![1, 5, 1, 1, 1, 1, 1, 6, 1, 2, 1, 1, 1, 2, 1, 2, 1, 1, 1, 3, 1, 2, 1, 4, 1,],
vec![
1, 5, 1, 1, 1, 1, 1, 3, 1, 2, 1, 1, 1, 1, 1, 3, 1, 5, 1, 1, 1, 1, 1, 2, 1, 3,
1, 3, 1, 4, 1,
],
vec![
1, 5, 1, 1, 1, 1, 1, 6, 1, 2, 1, 1, 1, 2, 1, 4, 1, 3, 1, 6, 1, 1, 1, 1, 1, 2,
1, 1, 1, 3, 1,
],
vec![
1, 5, 1, 1, 1, 1, 1, 1, 1, 3, 1, 2, 1, 1, 1, 4, 1, 1, 1, 2, 1, 2, 1, 5, 1, 1,
1, 1, 1, 1, 1, 1, 1, 8, 1, 2, 1,
],
],
"finalize_sbs bars shifted — one of the 635-636 mutants on \
the limited-mode v range adjustment activated; the cws \
inputs hit v ∈ {{824, 825, 852, 853}} after the v&=1023 mask, \
so any of the boundary or arithmetic mutants diverges the \
d-decomposition and the cbs bar pattern"
);
}
#[test]
fn select_codewords_normal_sf2_for_control_byte() {
let msg: Vec<i16> = vec![i16::from(b'A'), 1];
let cws = select_codewords_normal(&msg);
assert_eq!(cws, vec![10, 45, 10]);
}
#[test]
fn select_codewords_normal_set0_sf1_at_start() {
let msg: Vec<i16> = "aB".bytes().map(i16::from).collect();
let cws = select_codewords_normal(&msg);
assert_eq!(cws, vec![44, 10, 11]);
}
#[test]
fn encode_a_digit_zero_matches_bwip_js_sbs() {
let p = encode("0", &version_a_opts()).expect(
"encode(\"0\", version_a) (POSICODE version-a single-digit '0' → cw 0 → pattern \"141112\"; 37-module SBS) must succeed",
);
let want: Vec<u8> = vec![
1, 12, 1, 1, 1, 1, 1, 2, 1, 4, 1, 1, 1, 2, 1, 8, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 11, 1, ];
assert_eq!(
p.bars, want,
"version a '0' sbs must match bwip-js byte-for-byte"
);
}
#[test]
fn encode_a_digit_one_matches_bwip_js_sbs() {
let p = encode("1", &version_a_opts()).expect(
"encode(\"1\", version_a) (POSICODE version-a single-digit '1' → cw 1 → pattern \"131212\") must succeed",
);
let want: Vec<u8> = vec![
1, 12, 1, 1, 1, 1, 1, 2, 1, 3, 1, 2, 1, 2, 1, 1, 1, 4, 1, 1, 1, 5, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 11, 1, ];
assert_eq!(p.bars, want);
}
#[test]
fn encode_a_uppercase_a_matches_bwip_js_sbs() {
let p = encode("A", &version_a_opts()).expect(
"encode(\"A\", version_a) (POSICODE version-a set-0 uppercase 'A' → cw 10 → pattern \"181111\") must succeed",
);
let want: Vec<u8> = vec![
1, 12, 1, 1, 1, 1, 1, 2, 1, 8, 1, 1, 1, 1, 1, 2, 1, 5, 1, 1, 1, 2, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 11, 1, ];
assert_eq!(p.bars, want);
}
#[test]
fn encode_a_uppercase_z_matches_bwip_js_sbs() {
let p = encode("Z", &version_a_opts()).expect(
"encode(\"Z\", version_a) (POSICODE version-a set-0 uppercase 'Z' last-set-0 → cw 35 → pattern \"111514\") must succeed",
);
let want: Vec<u8> = vec![
1, 12, 1, 1, 1, 1, 1, 2, 1, 1, 1, 5, 1, 4, 1, 1, 1, 5, 1, 1, 1, 2, 1, 2, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 11, 1, ];
assert_eq!(p.bars, want);
}
#[test]
fn encode_a_hello_matches_bwip_js_sbs() {
let p = encode("HELLO", &version_a_opts()).expect(
"encode(\"HELLO\", version_a) (POSICODE version-a 5-byte set-0 stream → cws [17,14,21,21,24] without latch/shift/FN4) must succeed",
);
let want: Vec<u8> = vec![
1, 12, 1, 1, 1, 1, 1, 2, 1, 1, 1, 8, 1, 1, 1, 4, 1, 5, 1, 1, 1, 4, 1, 4, 1, 2, 1, 4, 1, 4, 1, 2, 1, 1, 1, 7, 1, 2, 1, 3, 1, 2, 1, 2, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 11, 1, ];
assert_eq!(p.bars, want);
}
#[test]
fn encode_a_digit_run_matches_bwip_js_sbs() {
let p = encode("12345", &version_a_opts()).expect(
"encode(\"12345\", version_a) (POSICODE version-a 5-digit set-0 run → cws [1,2,3,4,5] without latch/shift) must succeed",
);
let want: Vec<u8> = vec![
1, 12, 1, 1, 1, 1, 1, 2, 1, 3, 1, 2, 1, 2, 1, 2, 1, 3, 1, 2, 1, 1, 1, 4, 1, 2, 1, 3, 1, 1, 1, 3, 1, 2, 1, 2, 1, 3, 1, 6, 1, 1, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 11, 1, ];
assert_eq!(p.bars, want);
}
#[test]
fn encode_a_lowercase_run_matches_bwip_js_sbs() {
let p = encode("abc", &version_a_opts()).expect(
"encode(\"abc\", version_a) (POSICODE version-a LA1 latch at pos 0 → set-1 emission for 'abc' (cws 10,11,12)) must succeed",
);
let want: Vec<u8> = vec![
1, 12, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 7, 1, 8, 1, 1, 1, 1, 1, 7, 1, 2, 1, 1, 1, 6, 1, 3, 1, 1, 1, 1, 1, 3, 1, 5, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 11, 1, ];
assert_eq!(p.bars, want);
}
#[test]
fn encode_a_la1_mid_message_matches_bwip_js_sbs() {
let p = encode("Ab", &version_a_opts()).expect(
"encode(\"Ab\", version_a) (POSICODE version-a LA1 mid-message latch: set0 → set1 between 'A' and 'b' (cws 10,43,11)) must succeed",
);
let want: Vec<u8> = vec![
1, 12, 1, 1, 1, 1, 1, 2, 1, 8, 1, 1, 1, 1, 1, 2, 1, 1, 1, 7, 1, 7, 1, 2, 1, 1, 1, 4, 1, 1, 1, 2, 1, 3, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 11, 1, ];
assert_eq!(p.bars, want);
}
#[test]
fn encode_a_sf1_single_shift_matches_bwip_js_sbs() {
let p = encode("AbC", &version_a_opts()).expect(
"encode(\"AbC\", version_a) (POSICODE version-a SF1 single-shift; set0 → set1 for 'b' only, return to set0 for 'C' (cws 10,44,11,12); guards SF1 vs LA1 confusion) must succeed",
);
let want: Vec<u8> = vec![
1, 12, 1, 1, 1, 1, 1, 2, 1, 8, 1, 1, 1, 1, 1, 1, 1, 2, 1, 7, 1, 7, 1, 2, 1, 1, 1, 6, 1, 3, 1, 1, 1, 2, 1, 4, 1, 4, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 11, 1, ];
assert_eq!(p.bars, want);
}
#[test]
fn encode_a_sf2_control_byte_matches_bwip_js_sbs() {
let p = encode("A\x01", &version_a_opts()).expect(
"encode(\"A\\x01\", version_a) (POSICODE version-a SF2 single-shift-to-set-2 for control byte; cws 10,45,10 distinguishing SF2 vs SF1) must succeed",
);
let want: Vec<u8> = vec![
1, 12, 1, 1, 1, 1, 1, 2, 1, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 8, 1, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 4, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 11, 1, ];
assert_eq!(p.bars, want);
}
#[test]
fn encode_b_digit_zero_matches_bwip_js_sbs() {
let p = encode("0", &version_b_opts()).expect(
"encode(\"0\", version_b) (POSICODE version-b single-digit '0' → cw 0 → pattern \"151213\"; exercises wider bar table + d[i]+=1) must succeed",
);
let want: Vec<u8> = vec![
1, 12, 1, 2, 1, 3, 1, 2, 1, 5, 1, 2, 1, 3, 1, 9, 1, 2, 1, 2, 1, 3, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 12, 1, ];
assert_eq!(
p.bars, want,
"version b '0' sbs must match bwip-js byte-for-byte"
);
}
#[test]
fn encode_b_uppercase_a_matches_bwip_js_sbs() {
let p = encode("A", &version_b_opts()).expect(
"encode(\"A\", version_b) (POSICODE version-b set-0 uppercase 'A' → cw 10 → pattern \"191212\") must succeed",
);
let want: Vec<u8> = vec![
1, 12, 1, 2, 1, 3, 1, 2, 1, 9, 1, 2, 1, 2, 1, 3, 1, 6, 1, 2, 1, 3, 1, 4, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 12, 1, ];
assert_eq!(p.bars, want);
}
#[test]
fn encode_b_hello_matches_bwip_js_sbs() {
let p = encode("HELLO", &version_b_opts()).expect(
"encode(\"HELLO\", version_b) (POSICODE version-b 5-byte set-0 stream → cws [17,14,21,21,24] under wider bar table) must succeed",
);
let want: Vec<u8> = vec![
1, 12, 1, 2, 1, 3, 1, 2, 1, 2, 1, 9, 1, 2, 1, 5, 1, 6, 1, 2, 1, 5, 1, 5, 1, 3, 1, 5, 1, 5, 1, 3, 1, 2, 1, 8, 1, 3, 1, 4, 1, 3, 1, 3, 1, 2, 1, 4, 1, 4, 1, 2, 1, 2, 1, 2, 1, 2, 1, 12, 1, ];
assert_eq!(p.bars, want);
}
#[test]
fn encode_b_lowercase_run_matches_bwip_js_sbs() {
let p = encode("abc", &version_b_opts()).expect(
"encode(\"abc\", version_b) (POSICODE version-b LA1 latch at pos 0 → set-1 emission for 'abc' under version-b bar table) must succeed",
);
let want: Vec<u8> = vec![
1, 12, 1, 2, 1, 3, 1, 2, 1, 3, 1, 2, 1, 8, 1, 9, 1, 2, 1, 2, 1, 8, 1, 3, 1, 2, 1, 7, 1, 4, 1, 2, 1, 2, 1, 4, 1, 6, 1, 4, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 12, 1, ];
assert_eq!(p.bars, want);
}
#[test]
fn encode_a_rejects_empty() {
let err = encode("", &version_a_opts()).unwrap_err();
match err {
Error::InvalidData(msg) => {
assert!(
msg.contains("posicode:"),
"missing posicode prefix: {msg:?}"
);
assert!(
msg.contains("empty input is not encodable"),
"missing full predicate `empty input is not encodable`: {msg:?}"
);
}
other => panic!("expected InvalidData(empty), got {other:?}"),
}
}
#[test]
fn encode_a_rejects_overlong() {
let payload: String = "A".repeat(501);
let err = encode(&payload, &version_a_opts()).unwrap_err();
match err {
Error::InvalidData(msg) => {
assert!(
msg.contains("posicode:"),
"missing posicode prefix: {msg:?}"
);
assert!(
msg.contains("exceeds BWIPP's 500-byte limit"),
"missing full predicate `exceeds BWIPP's 500-byte limit`: {msg:?}"
);
assert!(
msg.contains("payload of 501 bytes"),
"missing `payload of 501 bytes` value-echo: {msg:?}"
);
}
other => panic!("expected InvalidData(overlong), got {other:?}"),
}
}
#[test]
fn fn4_insertion_is_identity_for_ascii_only() {
let msg: Vec<i16> = "HELLO".bytes().map(i16::from).collect();
let out = insert_fn4_markers(&msg);
assert_eq!(out, msg);
}
#[test]
fn fn4_insertion_single_shift_for_trailing_extended() {
let msg: Vec<i16> = "A\u{0080}".bytes().map(i16::from).collect();
let out = insert_fn4_markers(&msg);
assert_eq!(
out,
vec![0x41, POSICODE_FN4, 0xc2 & 0x7f, POSICODE_FN4, 0x80 & 0x7f]
);
}
#[test]
fn fn4_insertion_flips_mode_on_long_extended_run() {
let msg: Vec<i16> = "A\u{0080}\u{0081}\u{0082}".bytes().map(i16::from).collect();
let out = insert_fn4_markers(&msg);
assert_eq!(
out,
vec![
0x41, POSICODE_FN4, POSICODE_FN4, 0xC2 & 0x7f, 0x80 & 0x7f, 0xC2 & 0x7f, 0x81 & 0x7f, 0xC2 & 0x7f, 0x82 & 0x7f, ],
"insert_fn4_markers: a run of 6 extended bytes after a single \
standard byte must flip the ea state, emit 2×FN4, and pass \
the subsequent extended bytes through with their high bit \
stripped (no further FN4 markers)"
);
}
#[test]
fn fn4_insertion_num_sa_pre_pass_accumulates() {
let msg: Vec<i16> = "\u{0080}\u{0081}\u{0082}AAAAAA"
.bytes()
.map(i16::from)
.collect();
let out = insert_fn4_markers(&msg);
assert_eq!(
out,
vec![
POSICODE_FN4, POSICODE_FN4, 0xC2 & 0x7f, 0x80 & 0x7f, 0xC2 & 0x7f, 0x81 & 0x7f, 0xC2 & 0x7f, 0x82 & 0x7f, POSICODE_FN4, POSICODE_FN4, 0x41, 0x41,
0x41,
0x41,
0x41,
0x41,
],
"insert_fn4_markers: after the extended-byte run flips ea=true, \
the 6-byte standard run at i=6..11 must trigger a SINGLE \
mode-flip-back (ea=false + 2×FN4) at i=6, then the remaining \
5 standard bytes pass through without FN4. The mutant on \
line 805 (num_sa[i+1]+1 → *1) zeros the run counter, so the \
threshold check evaluates to 0<5=true and each of the 6 \
standard bytes gets its own FN4."
);
}
#[test]
fn fn4_insertion_threshold_boundary_at_three_run_length() {
let msg: Vec<i16> = vec![0x41, 0x41, 0xC2, 0x80, 0xC2];
let out = insert_fn4_markers(&msg);
assert_eq!(
out,
vec![
0x41, 0x41, POSICODE_FN4, POSICODE_FN4, 0xC2 & 0x7f, 0x80 & 0x7f, 0xC2 & 0x7f, ],
"insert_fn4_markers: msg=[0x41,0x41,0xC2,0x80,0xC2] places \
run+i (3+2=5) exactly at msglen=5, picking the smaller \
threshold=3. run==threshold, so run<threshold is false → \
enter mode-flip branch. The 818-819 mutants all flip the \
condition's outcome, emitting a single FN4 instead and \
leaving ea=false for the subsequent extended bytes."
);
}
#[test]
fn encode_a_with_fn4_extended_byte_matches_bwip_js_sbs() {
let p = encode("A\u{0080}", &version_a_opts()).expect(
"encode(\"A\\u{0080}\", version_a) (POSICODE version-a FN4 extended-byte path; 3-byte UTF-8 → 8 cws with 5 SF2 shifts incl. 2 FN4 sentinels) must succeed",
);
let want: Vec<u8> = vec![
1, 12, 1, 1, 1, 1, 1, 2, 1, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 8, 1, 1, 1, 1, 1, 8, 1, 7, 1, 2, 1, 1, 1, 1, 1, 1, 1, 8, 1, 1, 1, 1, 1, 8, 1, 1, 1, 1, 1, 8, 1, 2, 1, 3, 1, 5, 1, 2, 1, 1, 1, 1, 1, 1, 1, 7, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 11, 1, ];
assert_eq!(p.bars, want);
}
#[test]
fn encode_a_with_fn4_leading_extended_matches_bwip_js_sbs() {
let p = encode("\u{00c1}A", &version_a_opts()).expect(
"encode(\"\\u{00c1}A\", version_a) (POSICODE version-a FN4 leading-extended path; FN4 sentinel at pos 0 before any set-0 cw) must succeed",
);
let want: Vec<u8> = vec![
1, 12, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 8, 1, 1, 1, 1, 1, 8, 1, 6, 1, 3, 1, 1, 1, 1, 1, 1, 1, 8, 1, 1, 1, 1, 1, 8, 1, 1, 1, 1, 1, 8, 1, 8, 1, 1, 1, 1, 1, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 3, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 11, 1, ];
assert_eq!(p.bars, want);
}
#[test]
fn lookup_limited_table_anchors_plus_boundary_rejections() {
for d in 0..=9u8 {
assert_eq!(
lookup_limited(b'0' + d),
Some(d),
"digit '{}' → {d}",
(b'0' + d) as char
);
}
for (i, c) in (b'A'..=b'Z').enumerate() {
assert_eq!(
lookup_limited(c),
Some(10 + i as u8),
"letter '{}' → {}",
c as char,
10 + i as u8
);
}
assert_eq!(lookup_limited(b'-'), Some(36), "'-' → 36 (penultimate)");
assert_eq!(lookup_limited(b'.'), Some(37), "'.' → 37 (last)");
assert_eq!(lookup_limited(b'a'), None, "lowercase 'a' not in limited");
assert_eq!(lookup_limited(b'z'), None, "lowercase 'z' not in limited");
assert_eq!(lookup_limited(b' '), None, "space not in limited");
assert_eq!(lookup_limited(b'/'), None, "'/' just before '0'");
assert_eq!(lookup_limited(b':'), None, "':' just after '9'");
assert_eq!(lookup_limited(b'@'), None, "'@' just before 'A'");
assert_eq!(lookup_limited(b'['), None, "'[' just after 'Z'");
assert_eq!(lookup_limited(0), None, "NUL not in limited");
assert_eq!(lookup_limited(255), None, "0xFF not in limited");
}
}