#![allow(dead_code)]
#[rustfmt::skip]
pub(crate) const ENCS: [u16; 113] = [
341, 171, 173, 181, 213, 342, 346, 362,
426, 174, 182, 186, 214, 218, 234, 299,
301, 309, 331, 333, 339, 345, 357, 361,
405, 421, 425, 87, 91, 93, 107, 109,
117, 151, 155, 157, 167, 179, 185, 203,
205, 211, 217, 229, 233, 302, 310, 314,
334, 348, 358, 364, 370, 372, 406, 410,
422, 428, 434, 436, 458, 466, 468, 94,
110, 118, 122, 158, 188, 206, 220, 230,
236, 242, 244, 279, 283, 285, 295, 307,
313, 327, 355, 369, 395, 397, 403, 409,
419, 433, 453, 457, 465, 47, 55, 59,
61, 79, 103, 115, 121, 143, 199, 227,
241, 286, 316, 376, 398, 412, 440, 454,
460,
];
pub(crate) const MASK_SEEDS: [u32; 4] = [0, 3, 7, 17];
pub(crate) const MASK_BITS: [&str; 4] = ["00", "01", "10", "11"];
pub(crate) const LAA: i16 = -1; pub(crate) const LAB: i16 = -2; pub(crate) const LAC: i16 = -3; pub(crate) const BIN: i16 = -4; pub(crate) const SFA: i16 = -5; pub(crate) const SFB: i16 = -6; pub(crate) const SB2: i16 = -7; pub(crate) const SB3: i16 = -8;
pub(crate) const SB4: i16 = -9;
pub(crate) const SB5: i16 = -10;
pub(crate) const SB6: i16 = -11;
pub(crate) const SFC: i16 = -12; pub(crate) const SC2: i16 = -13;
pub(crate) const SC3: i16 = -14;
pub(crate) const SC4: i16 = -15;
pub(crate) const SC5: i16 = -16;
pub(crate) const SC6: i16 = -17;
pub(crate) const SC7: i16 = -18;
pub(crate) const BSA: i16 = -19; pub(crate) const BSB: i16 = -20;
pub(crate) const TMA: i16 = -21; pub(crate) const TMB: i16 = -22;
pub(crate) const TMC: i16 = -23;
pub(crate) const TMS: i16 = -24;
pub(crate) const FN1: i16 = -25; pub(crate) const FN2: i16 = -26;
pub(crate) const FN3: i16 = -27;
pub(crate) const CRL: i16 = -28; pub(crate) const AIM: i16 = -29; pub(crate) const M05: i16 = -30; pub(crate) const M06: i16 = -31; pub(crate) const M12: i16 = -32;
pub(crate) const MAC: i16 = -33;
pub(crate) const LATCH_C: u16 = 107;
pub(crate) const MODE_C_FN1_AT_SEGSTART: u16 = 107;
#[rustfmt::skip]
pub(crate) const CHARMAPS: [[i16; 3]; 113] = [
[ 32, 32, 0], [ 33, 33, 1], [ 34, 34, 2], [ 35, 35, 3],
[ 36, 36, 4], [ 37, 37, 5], [ 38, 38, 6], [ 39, 39, 7],
[ 40, 40, 8], [ 41, 41, 9], [ 42, 42, 10], [ 43, 43, 11],
[ 44, 44, 12], [ 45, 45, 13], [ 46, 46, 14], [ 47, 47, 15],
[ 48, 48, 16], [ 49, 49, 17], [ 50, 50, 18], [ 51, 51, 19],
[ 52, 52, 20], [ 53, 53, 21], [ 54, 54, 22], [ 55, 55, 23],
[ 56, 56, 24], [ 57, 57, 25], [ 58, 58, 26], [ 59, 59, 27],
[ 60, 60, 28], [ 61, 61, 29], [ 62, 62, 30], [ 63, 63, 31],
[ 64, 64, 32], [ 65, 65, 33], [ 66, 66, 34], [ 67, 67, 35],
[ 68, 68, 36], [ 69, 69, 37], [ 70, 70, 38], [ 71, 71, 39],
[ 72, 72, 40], [ 73, 73, 41], [ 74, 74, 42], [ 75, 75, 43],
[ 76, 76, 44], [ 77, 77, 45], [ 78, 78, 46], [ 79, 79, 47],
[ 80, 80, 48], [ 81, 81, 49], [ 82, 82, 50], [ 83, 83, 51],
[ 84, 84, 52], [ 85, 85, 53], [ 86, 86, 54], [ 87, 87, 55],
[ 88, 88, 56], [ 89, 89, 57], [ 90, 90, 58], [ 91, 91, 59],
[ 92, 92, 60], [ 93, 93, 61], [ 94, 94, 62], [ 95, 95, 63],
[ 0, 96, 64], [ 1, 97, 65], [ 2, 98, 66], [ 3, 99, 67],
[ 4, 100, 68], [ 5, 101, 69], [ 6, 102, 70], [ 7, 103, 71],
[ 8, 104, 72], [ 9, 105, 73], [ 10, 106, 74], [ 11, 107, 75],
[ 12, 108, 76], [ 13, 109, 77], [ 14, 110, 78], [ 15, 111, 79],
[ 16, 112, 80], [ 17, 113, 81], [ 18, 114, 82], [ 19, 115, 83],
[ 20, 116, 84], [ 21, 117, 85], [ 22, 118, 86], [ 23, 119, 87],
[ 24, 120, 88], [ 25, 121, 89], [ 26, 122, 90], [ 27, 123, 91],
[ 28, 124, 92], [ 29, 125, 93], [ 30, 126, 94], [ 31, 127, 95],
[SFB, CRL, 96], [SB2, 9, 97], [SB3, 28, 98], [SB4, 29, 99],
[SB5, 30, AIM], [SB6, SFA, LAA], [LAB, LAA, SFB], [SC2, SC2, SB2],
[SC3, SC3, SB3], [SC4, SC4, SB4], [LAC, LAC, LAB], [FN1, FN1, FN1],
[FN2, FN2, FN2], [FN3, FN3, FN3], [BSA, BSA, BSA], [BSB, BSB, BSB],
[BIN, BIN, BIN],
];
fn lookup_codeword_in_mode(b: u8, col: usize) -> Option<u16> {
lookup_codeword_in_mode_i16(i16::from(b), col)
}
fn lookup_codeword_in_mode_i16(b: i16, col: usize) -> Option<u16> {
CHARMAPS
.iter()
.position(|row| row[col] == b)
.map(|i| i as u16)
}
pub(crate) fn encode_a(bytes: &[u8]) -> Option<Vec<u16>> {
bytes
.iter()
.map(|&b| lookup_codeword_in_mode(b, 0))
.collect()
}
pub(crate) fn encode_b(bytes: &[u8]) -> Option<Vec<u16>> {
bytes
.iter()
.map(|&b| lookup_codeword_in_mode(b, 1))
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PositionTables {
pub n_digits: Vec<u32>,
pub datum_a: Vec<bool>,
pub datum_b: Vec<bool>,
pub datum_c: Vec<bool>,
pub seventeen_ten: Vec<bool>,
pub ahead_a: Vec<u32>,
pub ahead_b: Vec<u32>,
pub ahead_c: Vec<u32>,
pub try_c: Vec<u32>,
pub until_end_seg: Vec<u32>,
pub binary: Vec<bool>,
}
pub(crate) fn build_position_tables(msg: &[u8]) -> PositionTables {
let lifted: Vec<i16> = msg.iter().map(|&b| i16::from(b)).collect();
build_position_tables_i16(&lifted)
}
pub(crate) fn build_position_tables_i16(msg: &[i16]) -> PositionTables {
let n = msg.len();
let mut n_digits = vec![0u32; n + 1];
let mut datum_a = vec![false; n + 1];
let mut datum_b = vec![false; n + 1];
let mut datum_c = vec![false; n + 1];
let mut seventeen_ten = vec![false; n + 1];
let mut ahead_a = vec![0u32; n + 1];
let mut ahead_b = vec![0u32; n + 1];
let mut ahead_c = vec![0u32; n + 1];
let mut try_c = vec![0u32; n + 1];
let mut until_end_seg = vec![0u32; n + 1];
let mut binary = vec![false; n + 8];
for i in (0..n).rev() {
let b = msg[i];
let is_digit = (b'0' as i16..=b'9' as i16).contains(&b);
if is_digit {
n_digits[i] = n_digits[i + 1] + 1;
}
if lookup_codeword_in_mode_i16(b, 0).is_some() {
datum_a[i] = true;
}
if lookup_codeword_in_mode_i16(b, 1).is_some() {
datum_b[i] = true;
}
let crlf = b == i16::from(b'\r') && i + 1 < n && msg[i + 1] == i16::from(b'\n');
if crlf {
datum_b[i] = true;
}
if n_digits[i] >= 2 {
datum_c[i] = true;
}
if b < 0 {
datum_c[i] = true;
}
if b >= 128 {
binary[i] = true;
}
if n_digits[i] >= 10
&& msg[i] == i16::from(b'1')
&& msg[i + 1] == i16::from(b'7')
&& msg[i + 8] == i16::from(b'1')
&& msg[i + 9] == i16::from(b'0')
{
seventeen_ten[i] = true;
}
if b < 0 && b != FN3 {
ahead_c[i] = ahead_c[i + 1] + 1;
} else {
ahead_c[i] = if n_digits[i] <= 1 {
0
} else {
ahead_c[i + 2] + 1
};
}
if n_digits[i] > 0 && ahead_c[i] > ahead_c[i + 1] {
try_c[i] = ahead_c[i];
}
if datum_a[i] && try_c[i] < 2 && b != FN3 {
ahead_a[i] = ahead_a[i + 1] + 1;
}
if datum_b[i] && try_c[i] < 2 && b != FN3 {
let next = if crlf { i + 2 } else { i + 1 };
ahead_b[i] = ahead_b[next] + 1;
}
if b != FN3 {
until_end_seg[i] = until_end_seg[i + 1] + 1;
}
}
PositionTables {
n_digits,
datum_a,
datum_b,
datum_c,
seventeen_ten,
ahead_a,
ahead_b,
ahead_c,
try_c,
until_end_seg,
binary,
}
}
pub(crate) const MODE_C_SHIFT_TO_B: [u16; 4] = [102, 103, 104, 105];
pub(crate) const MODE_C_LATCH_TO_B: u16 = 106;
pub(crate) const MODE_C_LATCH_TO_A: u16 = 101;
pub(crate) const MODE_B_SHIFT_TO_A_ONE: u16 = 101;
pub(crate) const MODE_B_LATCH_TO_A: u16 = 102;
pub(crate) const MODE_B_SHIFT_TO_C: [u16; 3] = [103, 104, 105];
pub(crate) const MODE_B_LATCH_TO_C: u16 = 106;
pub(crate) const MODE_B_CRLF: u16 = 96;
pub(crate) const MODE_A_SHIFT_TO_B: [u16; 6] = [96, 97, 98, 99, 100, 101];
pub(crate) const MODE_A_LATCH_TO_B: u16 = 102;
pub(crate) const MODE_A_SHIFT_TO_C: [u16; 3] = [103, 104, 105];
pub(crate) const MODE_A_LATCH_TO_C: u16 = 106;
pub(crate) const BIN_ENTER: u16 = 112;
pub(crate) const BIN_SHIFT_TO_C: [u16; 6] = [103, 104, 105, 106, 107, 108];
pub(crate) const BIN_TERM_TO_A: u16 = 109;
pub(crate) const BIN_TERM_TO_B: u16 = 110;
pub(crate) const BIN_LATCH_TO_C: u16 = 111;
pub(crate) const BIN_SEG_RESET: u16 = 112;
pub(crate) const MODE_C_PAD_CW: u16 = 106;
pub(crate) const BIN_FIRST_PAD_CW: u16 = 109;
pub(crate) fn pad_to_nd(cws: &mut Vec<u16>, nd: usize, final_mode_is_bin: bool) {
if cws.len() >= nd {
return;
}
cws.push(if final_mode_is_bin {
BIN_FIRST_PAD_CW
} else {
MODE_C_PAD_CW
});
while cws.len() < nd {
cws.push(MODE_C_PAD_CW);
}
}
pub(crate) fn encode_mode_b_run_from_c(bytes: &[u8]) -> Option<Vec<u16>> {
let n = bytes.len();
let body = encode_b(bytes)?;
let mut out = Vec::with_capacity(body.len() + 1);
match n {
0 => {}
1..=4 => out.push(MODE_C_SHIFT_TO_B[n - 1]),
_ => out.push(MODE_C_LATCH_TO_B),
}
out.extend_from_slice(&body);
Some(out)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum InitialAction {
NoPrologue,
Fn1ThenStayInC,
LatchToA,
LatchToB,
ShiftToBFor(u32),
}
pub(crate) fn dispatch_initial(tables: &PositionTables, msg: &[u8]) -> InitialAction {
if msg.is_empty() {
return InitialAction::NoPrologue;
}
if tables.n_digits[0] >= 2 {
return InitialAction::Fn1ThenStayInC;
}
let m = tables.ahead_a[0];
let n = tables.ahead_b[0];
if m > n {
return InitialAction::LatchToA;
}
if matches!(msg[0], 9 | 28 | 29 | 30) {
return InitialAction::LatchToA;
}
if n > 4 {
return InitialAction::LatchToB;
}
InitialAction::ShiftToBFor(n)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Mode {
A,
B,
C,
Bin,
}
fn enc_c_step(
msg: &[u8],
tables: &PositionTables,
i: &mut usize,
mode: &mut Mode,
cws: &mut Vec<u16>,
segstart: usize,
) {
if *i == segstart && tables.n_digits[*i] >= 2 {
cws.push(MODE_C_FN1_AT_SEGSTART);
}
if tables.datum_c[*i] && msg[*i].is_ascii_digit() && msg[*i + 1].is_ascii_digit() {
let pair = (u16::from(msg[*i] - b'0')) * 10 + u16::from(msg[*i + 1] - b'0');
cws.push(pair);
*i += 2;
return;
}
let m = tables.ahead_a[*i];
let n = tables.ahead_b[*i];
if m > n {
cws.push(MODE_C_LATCH_TO_A);
*mode = Mode::A;
return;
}
if *i == segstart && matches!(msg[*i], 9 | 28 | 29 | 30) {
cws.push(MODE_C_LATCH_TO_A);
*mode = Mode::A;
return;
}
if n > 4 {
cws.push(MODE_C_LATCH_TO_B);
*mode = Mode::B;
return;
}
if n == 0 {
return;
}
cws.push(MODE_C_SHIFT_TO_B[(n - 1) as usize]);
for _ in 0..n {
if msg[*i] == b'\r' && *i + 1 < msg.len() && msg[*i + 1] == b'\n' {
cws.push(MODE_B_CRLF);
*i += 2;
continue;
}
let cw = lookup_codeword_in_mode(msg[*i], 1)
.expect("AheadB unit is a CRLF pair or a column-B-encodable byte");
cws.push(cw);
*i += 1;
}
}
fn enc_b_step(msg: &[u8], i: &mut usize, _mode: &mut Mode, cws: &mut Vec<u16>) {
let cw = lookup_codeword_in_mode(msg[*i], 1)
.expect("enc_b_step requires byte to be encodable in mode B");
cws.push(cw);
*i += 1;
}
fn enc_a_step(msg: &[u8], i: &mut usize, _mode: &mut Mode, cws: &mut Vec<u16>) {
let cw = lookup_codeword_in_mode(msg[*i], 0)
.expect("enc_a_step requires byte to be encodable in mode A");
cws.push(cw);
*i += 1;
}
fn enc_c_step_i16(
msg: &[i16],
tables: &PositionTables,
i: &mut usize,
mode: &mut Mode,
cws: &mut Vec<u16>,
segstart: usize,
) {
if *i == segstart {
if tables.n_digits[*i] >= 2 {
cws.push(MODE_C_FN1_AT_SEGSTART);
}
if msg[*i] == FN1 && tables.n_digits[*i + 1] >= 2 {
*i += 1;
return;
}
}
if tables.datum_c[*i] {
let cur = msg[*i];
if matches!(cur, FN1 | FN2 | FN3) {
let cw = match cur {
FN1 => 107,
FN2 => 108,
FN3 => 109,
_ => unreachable!(),
};
cws.push(cw);
*i += 1;
return;
}
if is_ascii_digit_i16(cur) && *i + 1 < msg.len() && is_ascii_digit_i16(msg[*i + 1]) {
let pair = (cur - i16::from(b'0')) * 10 + (msg[*i + 1] - i16::from(b'0'));
cws.push(pair as u16);
*i += 2;
return;
}
}
if tables.binary[*i] {
cws.push(BIN_ENTER);
*mode = Mode::Bin;
return;
}
let m = tables.ahead_a[*i];
let n = tables.ahead_b[*i];
if m > n {
cws.push(MODE_C_LATCH_TO_A);
*mode = Mode::A;
return;
}
if *i == segstart && matches!(msg[*i], 9 | 28 | 29 | 30) {
cws.push(MODE_C_LATCH_TO_A);
*mode = Mode::A;
return;
}
if n > 4 {
cws.push(MODE_C_LATCH_TO_B);
*mode = Mode::B;
return;
}
if n == 0 {
return;
}
cws.push(MODE_C_SHIFT_TO_B[(n - 1) as usize]);
for _ in 0..n {
let cur = msg[*i];
if cur == i16::from(b'\r') && *i + 1 < msg.len() && msg[*i + 1] == i16::from(b'\n') {
cws.push(MODE_B_CRLF);
*i += 2;
continue;
}
let cw = lookup_codeword_in_mode_i16(cur, 1)
.expect("AheadB unit is a CRLF pair or a column-B-encodable byte");
cws.push(cw);
*i += 1;
}
}
fn enc_b_step_i16(
msg: &[i16],
tables: &PositionTables,
i: &mut usize,
mode: &mut Mode,
cws: &mut Vec<u16>,
) {
let n_try_c = tables.try_c[*i];
if n_try_c >= 2 {
if n_try_c > 4 {
cws.push(MODE_B_LATCH_TO_C);
*mode = Mode::C;
return;
}
cws.push(MODE_B_SHIFT_TO_C[(n_try_c - 2) as usize]);
for _ in 0..n_try_c {
let cur = msg[*i];
if cur < 0 {
let cw =
lookup_codeword_in_mode_i16(cur, 2).expect("marker is encodable in column C");
cws.push(cw);
*i += 1;
} else {
let pair = (cur - i16::from(b'0')) * 10 + (msg[*i + 1] - i16::from(b'0'));
cws.push(pair as u16);
*i += 2;
}
}
return;
}
if tables.datum_b[*i] {
let cur = msg[*i];
if matches!(cur, FN1 | FN2 | FN3) {
let cw = match cur {
FN1 => 107,
FN2 => 108,
FN3 => 109,
_ => unreachable!(),
};
cws.push(cw);
*i += 1;
return;
}
if cur == i16::from(b'\r') && *i + 1 < msg.len() && msg[*i + 1] == i16::from(b'\n') {
cws.push(MODE_B_CRLF);
*i += 2;
return;
}
let cw = lookup_codeword_in_mode_i16(cur, 1)
.expect("DatumB[i] is true implies col-B lookup succeeds");
cws.push(cw);
*i += 1;
return;
}
let n_a = tables.ahead_a[*i];
if n_a == 1 {
cws.push(MODE_B_SHIFT_TO_A_ONE);
let cw = lookup_codeword_in_mode_i16(msg[*i], 0)
.expect("AheadA[i] >= 1 implies col-A lookup succeeds");
cws.push(cw);
*i += 1;
return;
}
cws.push(MODE_B_LATCH_TO_A);
*mode = Mode::A;
}
fn enc_a_step_i16(
msg: &[i16],
tables: &PositionTables,
i: &mut usize,
mode: &mut Mode,
cws: &mut Vec<u16>,
) {
let n_try_c = tables.try_c[*i];
if n_try_c >= 2 {
if n_try_c > 4 {
cws.push(MODE_A_LATCH_TO_C);
*mode = Mode::C;
return;
}
cws.push(MODE_A_SHIFT_TO_C[(n_try_c - 2) as usize]);
for _ in 0..n_try_c {
let cur = msg[*i];
if cur < 0 {
let cw =
lookup_codeword_in_mode_i16(cur, 2).expect("marker is encodable in column C");
cws.push(cw);
*i += 1;
} else {
let pair = (cur - i16::from(b'0')) * 10 + (msg[*i + 1] - i16::from(b'0'));
cws.push(pair as u16);
*i += 2;
}
}
return;
}
if tables.datum_a[*i] {
let cur = msg[*i];
if matches!(cur, FN1 | FN2 | FN3) {
let cw = match cur {
FN1 => 107,
FN2 => 108,
FN3 => 109,
_ => unreachable!(),
};
cws.push(cw);
*i += 1;
return;
}
let cw = lookup_codeword_in_mode_i16(cur, 0)
.expect("DatumA[i] is true implies col-A lookup succeeds");
cws.push(cw);
*i += 1;
return;
}
let n_b = tables.ahead_b[*i];
if n_b > 6 {
cws.push(MODE_A_LATCH_TO_B);
*mode = Mode::B;
return;
}
if n_b == 0 {
return;
}
cws.push(MODE_A_SHIFT_TO_B[(n_b - 1) as usize]);
for _ in 0..n_b {
let cur = msg[*i];
if cur == i16::from(b'\r') && *i + 1 < msg.len() && msg[*i + 1] == i16::from(b'\n') {
cws.push(MODE_B_CRLF);
*i += 2;
} else {
let cw = lookup_codeword_in_mode_i16(cur, 1)
.expect("AheadB > 0 implies DatumB on the shifted bytes");
cws.push(cw);
*i += 1;
}
}
}
#[inline]
fn is_ascii_digit_i16(b: i16) -> bool {
(b'0' as i16..=b'9' as i16).contains(&b)
}
#[derive(Debug, Default)]
struct BinState {
bvals: [u8; 5],
bpos: usize,
}
impl BinState {
fn add_byte(&mut self, b: u8, cws: &mut Vec<u16>) {
self.bvals[self.bpos] = b;
self.bpos += 1;
if self.bpos == 5 {
self.finalise(cws);
}
}
fn finalise(&mut self, cws: &mut Vec<u16>) {
if self.bpos == 0 {
return;
}
cws.extend(base259_to_103(&self.bvals[..self.bpos]));
self.bpos = 0;
}
}
fn enc_bin_step_i16(
msg: &[i16],
tables: &PositionTables,
i: &mut usize,
mode: &mut Mode,
cws: &mut Vec<u16>,
bin: &mut BinState,
) {
let n_try_c = tables.try_c[*i];
if n_try_c >= 2 {
bin.finalise(cws);
if n_try_c > 7 {
cws.push(BIN_LATCH_TO_C);
*mode = Mode::C;
return;
}
cws.push(BIN_SHIFT_TO_C[(n_try_c - 2) as usize]);
for _ in 0..n_try_c {
let cur = msg[*i];
if cur < 0 {
let cw =
lookup_codeword_in_mode_i16(cur, 2).expect("marker is encodable in column C");
cws.push(cw);
*i += 1;
} else {
let pair = (cur - i16::from(b'0')) * 10 + (msg[*i + 1] - i16::from(b'0'));
cws.push(pair as u16);
*i += 2;
}
}
return;
}
let cur = msg[*i];
if cur >= 0 {
let in_bin_window = tables.binary[*i]
|| (*i + 1 < tables.binary.len() && tables.binary[*i + 1])
|| (*i + 2 < tables.binary.len() && tables.binary[*i + 2])
|| (*i + 3 < tables.binary.len() && tables.binary[*i + 3]);
if in_bin_window {
let byte_u8 = cur as u8;
bin.add_byte(byte_u8, cws);
*i += 1;
if *i == msg.len() {
bin.finalise(cws);
}
return;
}
}
bin.finalise(cws);
let m = tables.ahead_a[*i];
let n = tables.ahead_b[*i];
if m > n {
cws.push(BIN_TERM_TO_A);
*mode = Mode::A;
} else {
cws.push(BIN_TERM_TO_B);
*mode = Mode::B;
}
}
pub(crate) fn base259_to_103(bytes: &[u8]) -> Vec<u16> {
assert!(
!bytes.is_empty() && bytes.len() <= 5,
"base259_to_103: input length must be 1..=5, got {}",
bytes.len()
);
let inlen = bytes.len();
let mut padded = [0u32; 5];
let offset = 5 - inlen;
for (i, &b) in bytes.iter().enumerate() {
padded[offset + i] = u32::from(b);
}
let value_msb = padded[0] * 259 + padded[1];
let mscs = [
value_msb % 103,
(value_msb / 103) % 103,
(value_msb / 103) / 103,
];
let value_lsb = padded[2] * 67081 + padded[3] * 259 + padded[4];
let lscs = [
value_lsb % 103,
(value_lsb / 103) % 103,
((value_lsb / 103) / 103) % 103,
((value_lsb / 103) / 103) / 103,
];
let mut out = [0u32; 6];
let s5 = lscs[0] + mscs[0] * 42;
out[5] = s5 % 103;
let s4 = (s5 / 103) + lscs[1] + mscs[0] * 68 + mscs[1] * 42;
out[4] = s4 % 103;
let s3 = (s4 / 103) + lscs[2] + mscs[0] * 92 + mscs[1] * 68 + mscs[2] * 42;
out[3] = s3 % 103;
let s2 = (s3 / 103) + lscs[3] + mscs[0] * 15 + mscs[1] * 92 + mscs[2] * 68;
out[2] = s2 % 103;
let s1 = (s2 / 103) + mscs[1] * 15 + mscs[2] * 92;
out[1] = s1 % 103;
let s0 = (s1 / 103) + mscs[2] * 15;
out[0] = s0 % 103;
let start = 5 - inlen;
out[start..6].iter().map(|&v| v as u16).collect()
}
pub(crate) fn apply_rs_ecc(data: &[u16]) -> Vec<u16> {
apply_rs_ecc_with_leading(0, data)
}
pub(crate) fn apply_rs_ecc_with_leading(leading: u16, data: &[u16]) -> Vec<u16> {
let nd_data = data.len();
let nc = nd_data / 2 + 3;
let nd_loop = nd_data + 1;
let full_gen = crate::util::rs_gf113::generator_poly(nc);
let p: u32 = 113;
let mut lfsr = vec![0u32; nc];
for i in 0..nd_loop {
let datum: u32 = if i == 0 {
u32::from(leading)
} else {
u32::from(data[i - 1])
};
let tmp = (datum + p - lfsr[0]) % p;
for j in 0..nc - 1 {
let coef = full_gen[j + 1];
lfsr[j] = (lfsr[j + 1] + coef * tmp) % p;
}
lfsr[nc - 1] = (full_gen[nc] * tmp) % p;
}
let mut out: Vec<u16> = data.to_vec();
out.extend(lfsr.iter().map(|&x| x as u16));
out
}
pub(crate) const MASK_VALS: [u32; 4] = [0, 3, 7, 17];
pub(crate) fn mask_transform_cws(mask: u8, cws: &[u16]) -> Vec<u16> {
let mv: u32 = MASK_VALS[mask as usize];
cws.iter()
.enumerate()
.map(|(p, &c)| ((u32::from(c) + (p as u32) * mv) % 113) as u16)
.collect()
}
pub(crate) fn encode_for_mask(mask: u8, padded_cws: &[u16]) -> Vec<u16> {
let transformed = mask_transform_cws(mask, padded_cws);
apply_rs_ecc_with_leading(u16::from(mask), &transformed)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i8)]
pub(crate) enum DotCell {
Inactive = -1,
Empty = 0,
Set = 1,
}
pub(crate) fn init_outline(rows: usize, columns: usize) -> Vec<DotCell> {
let mut grid = vec![DotCell::Inactive; rows * columns];
for y in 0..rows {
for x in 0..columns {
let cell = if (x + y) % 2 == 0 {
DotCell::Inactive
} else {
DotCell::Empty
};
grid[y * columns + x] = cell;
}
}
let six_edges: [(usize, usize); 6] = if rows % 2 == 0 {
[
(columns - 1, rows - 2),
(0, rows - 2),
(columns - 2, rows - 1),
(1, rows - 1),
(columns - 1, 0),
(0, 0),
]
} else {
[
(columns - 2, 0),
(columns - 2, rows - 1),
(columns - 1, 1),
(columns - 1, rows - 2),
(0, 0),
(0, rows - 1),
]
};
for (x, y) in six_edges {
grid[y * columns + x] = DotCell::Set;
}
grid
}
pub(crate) fn render_pixs(rows: usize, columns: usize, bits: &str) -> Vec<i8> {
let mut pixs: Vec<i8> = init_outline(rows, columns)
.into_iter()
.map(|c| c as i8)
.collect();
let bits = bits.as_bytes();
assert!(bits.len() >= 6, "bits must have at least 6 entries");
let main_len = bits.len() - 6;
let mut posx: usize = 0;
let mut posy: isize = if rows % 2 == 0 {
0
} else {
(rows - 1) as isize
};
for &b in &bits[..main_len] {
let bit_val: i8 = (b - b'0') as i8;
loop {
let idx = (posy as usize) * columns + posx;
if pixs[idx] == -1 {
break;
}
if rows % 2 == 0 {
posy += 1;
if posy == rows as isize {
posy = 0;
posx += 1;
}
} else {
posx += 1;
if posx == columns {
posx = 0;
posy -= 1;
}
}
}
let idx = (posy as usize) * columns + posx;
pixs[idx] = bit_val;
}
let six_edges: [(usize, usize); 6] = if rows % 2 == 0 {
[
(columns - 1, rows - 2),
(0, rows - 2),
(columns - 2, rows - 1),
(1, rows - 1),
(columns - 1, 0),
(0, 0),
]
} else {
[
(columns - 2, 0),
(columns - 2, rows - 1),
(columns - 1, 1),
(columns - 1, rows - 2),
(0, 0),
(0, rows - 1),
]
};
for (i, &(x, y)) in six_edges.iter().enumerate() {
let bit_val: i8 = (bits[main_len + i] - b'0') as i8;
pixs[y * columns + x] = bit_val;
}
pixs
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DotCodeSymbol {
pub rows: usize,
pub columns: usize,
pub mask: u8,
pub pixs: Vec<i8>,
}
impl DotCodeSymbol {
pub fn to_bitmatrix(&self) -> crate::encoding::BitMatrix {
let mut m = crate::encoding::BitMatrix::new(self.columns, self.rows);
for y in 0..self.rows {
for x in 0..self.columns {
if self.pixs[y * self.columns + x] == 1 {
m.set(x, y, true);
}
}
}
m
}
pub fn to_dotmatrix(&self) -> crate::encoding::DotMatrix {
let mut m = crate::encoding::DotMatrix::new(self.columns, self.rows);
for y in 0..self.rows {
for x in 0..self.columns {
if self.pixs[y * self.columns + x] == 1 {
m.set(x, y, true);
}
}
}
m
}
}
pub(crate) fn apply_lit_mask(rows: usize, columns: usize, pixs: &mut [i8]) {
let six_edges: [(usize, usize); 6] = if rows % 2 == 0 {
[
(columns - 1, rows - 2),
(0, rows - 2),
(columns - 2, rows - 1),
(1, rows - 1),
(columns - 1, 0),
(0, 0),
]
} else {
[
(columns - 2, 0),
(columns - 2, rows - 1),
(columns - 1, 1),
(columns - 1, rows - 2),
(0, 0),
(0, rows - 1),
]
};
for &(x, y) in &six_edges {
pixs[y * columns + x] = 1;
}
}
pub(crate) fn pick_best_mask(padded_cws: &[u16], sz: SymbolSize) -> (u8, Vec<i8>) {
let mut best_mask: u8 = 0;
let mut best_score: i32 = i32::MIN;
let mut pixs_per_mask: [Option<Vec<i8>>; 4] = [None, None, None, None];
for mask in 0..=3u8 {
let rscws = encode_for_mask(mask, padded_cws);
let bits = build_bits(&rscws, mask, sz.ndots);
let pixs = render_pixs(sz.rows, sz.columns, &bits);
let score = eval_symbol(&pixs, sz.rows, sz.columns);
if score > best_score {
best_score = score;
best_mask = mask;
}
pixs_per_mask[mask as usize] = Some(pixs);
}
let threshold = (sz.rows * sz.columns) as i32 / 2;
if best_score > threshold {
let pixs = pixs_per_mask[best_mask as usize].take().unwrap();
return (best_mask, pixs);
}
let mut best_lit_mask: u8 = 0;
let mut best_lit_score: i32 = i32::MIN;
let mut litmasks: [Option<Vec<i8>>; 4] = [None, None, None, None];
for mask in 0..=3u8 {
let mut lit = pixs_per_mask[mask as usize].as_ref().unwrap().clone();
apply_lit_mask(sz.rows, sz.columns, &mut lit);
let score = eval_symbol(&lit, sz.rows, sz.columns);
if score > best_lit_score {
best_lit_score = score;
best_lit_mask = mask;
}
litmasks[mask as usize] = Some(lit);
}
let pixs = litmasks[best_lit_mask as usize].take().unwrap();
(best_lit_mask, pixs)
}
pub fn encode(input: &[u8]) -> Result<DotCodeSymbol, crate::error::Error> {
let lifted = parse_dotcode_input(input, false)?;
encode_with_markers(&lifted)
}
pub fn encode_with_markers(input: &[i16]) -> Result<DotCodeSymbol, crate::error::Error> {
let mut data = encode_message_with_markers(input)?;
let sz = pick_symbol_size_default(data.len());
let nc = sz.nd / 2 + 3;
let nw = sz.nd + nc;
if nw > 112 {
return Err(crate::error::Error::InvalidData(format!(
"DotCode: payload requires nw={nw} codewords (nd={}, nc={nc}); BWIPP's interleaved RS path activates for nw > 112 and is outside this port's coverage — emit a smaller symbol or use BWIPP directly.",
sz.nd
)));
}
pad_to_nd(&mut data, sz.nd, false);
let (mask, pixs) = pick_best_mask(&data, sz);
Ok(DotCodeSymbol {
rows: sz.rows,
columns: sz.columns,
mask,
pixs,
})
}
pub(crate) fn build_bits(rscws: &[u16], mask: u8, ndots: usize) -> String {
let nw = rscws.len();
let rembits = ndots
.checked_sub(nw * 9 + 2)
.expect("ndots must accommodate the 2-bit mask + 9-bit codewords");
let mut bits = String::with_capacity(ndots);
bits.push_str(MASK_BITS[mask as usize]);
for &cw in rscws {
let pat = ENCS[cw as usize];
bits.push_str(&format!("{pat:09b}"));
}
for _ in 0..rembits {
bits.push('1');
}
debug_assert_eq!(bits.len(), ndots);
bits
}
pub(crate) fn eval_symbol(pixs: &[i8], rows: usize, columns: usize) -> i32 {
let mut worst: i32 = i32::MAX;
for &(dir_x, fl) in &[(true, 0usize), (true, 1), (false, 0), (false, 1)] {
let along = if dir_x { columns } else { rows };
let perp = if dir_x { rows } else { columns };
let mut sum: i32 = 0;
let mut first: i32 = -1;
let mut last: i32 = -1;
for i in 0..along {
let (x, y) = if dir_x {
(i, (perp - 1) * fl)
} else {
((perp - 1) * fl, i)
};
if pixs[y * columns + x] == 1 {
if first == -1 {
first = i as i32;
}
last = i as i32;
sum += 1;
}
}
let metric = (sum + last - first) * (perp as i32);
if metric < worst {
worst = metric;
}
}
if worst == 0 {
return -99999;
}
let clearcol = |x: usize| -> bool {
let mut y = x & 1;
while y < rows {
if pixs[y * columns + x] == 1 {
return false;
}
y += 2;
}
true
};
let clearrow = |y: usize| -> bool {
let mut x = y & 1;
while x < columns {
if pixs[y * columns + x] == 1 {
return false;
}
x += 2;
}
true
};
let mut pen: i32 = 0;
if rows % 2 == 1 || rows <= 12 {
let mut run = 0u32;
let mut p: u64 = 0;
for x in 1..=columns.saturating_sub(2) {
if clearcol(x) {
run += 1;
p = if run == 1 {
rows as u64
} else {
p.saturating_mul(rows as u64)
};
} else {
run = 0;
pen = pen.saturating_add(p.min(i32::MAX as u64) as i32);
p = 0;
}
}
pen = pen.saturating_add(p.min(i32::MAX as u64) as i32);
}
if rows % 2 == 0 || columns <= 12 {
let mut run = 0u32;
let mut p: u64 = 0;
for y in 1..=rows.saturating_sub(2) {
if clearrow(y) {
run += 1;
p = if run == 1 {
columns as u64
} else {
p.saturating_mul(columns as u64)
};
} else {
run = 0;
pen = pen.saturating_add(p.min(i32::MAX as u64) as i32);
p = 0;
}
}
pen = pen.saturating_add(p.min(i32::MAX as u64) as i32);
}
let pc = columns + 4;
let pr = rows + 4;
let mut padded = vec![0i8; pc * pr];
for y in 0..rows {
for x in 0..columns {
padded[(y + 2) * pc + (x + 2)] = pixs[y * columns + x];
}
}
let mut outlier: i32 = 0;
for y in 2..=pr - 3 {
let mut x = (y & 1) + 2;
while x <= pc - 3 {
if padded[(y - 1) * pc + (x - 1)] == 1
|| padded[(y - 1) * pc + (x + 1)] == 1
|| padded[(y + 1) * pc + (x - 1)] == 1
|| padded[(y + 1) * pc + (x + 1)] == 1
{
} else if padded[y * pc + x] == 0 {
outlier += 1;
} else if padded[y * pc + (x - 2)] == 1
|| padded[(y - 2) * pc + x] == 1
|| padded[y * pc + (x + 2)] == 1
|| padded[(y + 2) * pc + x] == 1
{
} else {
outlier += 1;
}
x += 2;
}
}
worst - outlier * outlier - pen
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct SymbolSize {
pub rows: usize,
pub columns: usize,
pub nd: usize,
pub ndots: usize,
}
pub(crate) fn pick_symbol_size_default(n_data: usize) -> SymbolSize {
let nd0 = n_data;
let minarea = (((nd0 + 3) + nd0 / 2) * 9 + 2) * 2;
let ratio = 1.5_f64;
let hgt = (minarea as f64 / ratio).sqrt();
let wid = (minarea as f64 * ratio).sqrt();
let mut h = hgt as usize;
let mut w = wid as usize;
if (h + w) % 2 == 1 {
if h * w < minarea {
h += 1;
w += 1;
}
} else if hgt * (w as f64) < wid * (h as f64) {
w += 1;
if h * w < minarea {
w -= 1;
h += 1;
if h * w < minarea {
w += 2;
}
}
} else {
h += 1;
if h * w < minarea {
h -= 1;
w += 1;
if h * w < minarea {
h += 2;
}
}
}
let rows = h;
let columns = w;
let ndots = (rows * columns) / 2;
let mut nd = nd0;
loop {
let next = nd + 1;
if (((next + 3) + next / 2) * 9 + 2) > ndots {
break;
}
nd = next;
}
SymbolSize {
rows,
columns,
nd,
ndots,
}
}
pub(crate) fn parse_dotcode_input(
input: &[u8],
parsefnc: bool,
) -> Result<Vec<i16>, crate::error::Error> {
let mut out: Vec<i16> = Vec::with_capacity(input.len());
let mut idx = 0;
while idx < input.len() {
let b = input[idx];
if b != b'^' || !parsefnc {
out.push(i16::from(b));
idx += 1;
continue;
}
let rest_len = input.len() - idx;
if rest_len < 2 {
return Err(crate::error::Error::InvalidData(
"DotCode parsefnc: caret character truncated".to_string(),
));
}
if input[idx + 1] == b'^' {
out.push(94);
idx += 2;
continue;
}
if rest_len < 5 {
return Err(crate::error::Error::InvalidData(
"DotCode parsefnc: function character truncated".to_string(),
));
}
let tag = &input[idx + 1..idx + 5];
match tag {
b"FNC1" => {
out.push(FN1);
idx += 5;
}
b"FNC3" => {
out.push(FN3);
idx += 5;
}
_ if &tag[..3] == b"ECI" => {
if rest_len < 10 {
return Err(crate::error::Error::InvalidData(
"DotCode parsefnc: ECI truncated".to_string(),
));
}
let digits = &input[idx + 4..idx + 10];
for &d in digits {
if !d.is_ascii_digit() {
return Err(crate::error::Error::InvalidData(
"DotCode parsefnc: ECI must be 000000 to 999999".to_string(),
));
}
}
out.push(FN2);
out.extend(digits.iter().map(|&d| i16::from(d)));
idx += 10;
}
_ => {
let name = std::str::from_utf8(tag).unwrap_or("<non-utf8>");
return Err(crate::error::Error::InvalidData(format!(
"DotCode parsefnc: unknown function character: {name}"
)));
}
}
}
Ok(out)
}
pub(crate) fn encode_message(msg: &[u8]) -> Result<Vec<u16>, crate::error::Error> {
let lifted: Vec<i16> = msg.iter().map(|&b| i16::from(b)).collect();
encode_message_with_markers(&lifted)
}
pub(crate) fn encode_message_with_markers(msg: &[i16]) -> Result<Vec<u16>, crate::error::Error> {
let tables = build_position_tables_i16(msg);
let mut cws = Vec::new();
let mut bin = BinState::default();
let mut i = 0;
let mut mode = Mode::C;
let segstart = 0;
while i < msg.len() {
let prev_i = i;
let prev_mode = mode;
match mode {
Mode::C => enc_c_step_i16(msg, &tables, &mut i, &mut mode, &mut cws, segstart),
Mode::A => enc_a_step_i16(msg, &tables, &mut i, &mut mode, &mut cws),
Mode::B => enc_b_step_i16(msg, &tables, &mut i, &mut mode, &mut cws),
Mode::Bin => enc_bin_step_i16(msg, &tables, &mut i, &mut mode, &mut cws, &mut bin),
}
if i == prev_i && mode == prev_mode {
let byte = msg[prev_i];
return Err(crate::error::Error::InvalidData(format!(
"DotCode: byte {byte} at position {prev_i} requires an extended-marker codeword (macro / ECI / SeventeenTen) outside this port's scope"
)));
}
}
bin.finalise(&mut cws);
Ok(cws)
}
pub(crate) fn encode_short_pure(msg: &[u8]) -> Option<Vec<u16>> {
let tables = build_position_tables(msg);
match dispatch_initial(&tables, msg) {
InitialAction::NoPrologue => Some(Vec::new()),
InitialAction::Fn1ThenStayInC => {
if msg.iter().all(|b| b.is_ascii_digit()) {
encode_numeric_run_from_c(msg)
} else {
None
}
}
InitialAction::LatchToA => encode_mode_a_run_from_c(msg),
InitialAction::LatchToB => encode_mode_b_run_from_c(msg),
InitialAction::ShiftToBFor(n) => {
if msg.len() == n as usize {
encode_mode_b_run_from_c(msg)
} else {
None
}
}
}
}
pub(crate) fn encode_mode_a_run_from_c(bytes: &[u8]) -> Option<Vec<u16>> {
if bytes.is_empty() {
return Some(Vec::new());
}
let body = encode_a(bytes)?;
let mut out = Vec::with_capacity(body.len() + 1);
out.push(MODE_C_LATCH_TO_A);
out.extend_from_slice(&body);
Some(out)
}
pub(crate) fn encode_numeric_run_from_c(digits: &[u8]) -> Option<Vec<u16>> {
if !digits.iter().all(|b| b.is_ascii_digit()) {
return None;
}
let n = digits.len();
if n == 0 {
return Some(Vec::new());
}
if n == 1 {
let b_cw = encode_b(digits)?;
let mut out = Vec::with_capacity(2);
out.push(MODE_C_SHIFT_TO_B[0]);
out.extend_from_slice(&b_cw);
return Some(out);
}
let pairs = n / 2;
let mut out = Vec::with_capacity(1 + pairs + 2);
out.push(MODE_C_FN1_AT_SEGSTART);
let pair_cws = encode_c(&digits[..pairs * 2])?;
out.extend_from_slice(&pair_cws);
if n % 2 == 1 {
let trail = encode_b(&digits[n - 1..])?;
out.push(MODE_C_SHIFT_TO_B[0]);
out.extend_from_slice(&trail);
}
Some(out)
}
pub(crate) fn encode_c(digits: &[u8]) -> Option<Vec<u16>> {
if digits.len() % 2 != 0 {
return None;
}
let mut out = Vec::with_capacity(digits.len() / 2);
for pair in digits.chunks(2) {
if !pair[0].is_ascii_digit() || !pair[1].is_ascii_digit() {
return None;
}
let v = (pair[0] - b'0') as u16 * 10 + (pair[1] - b'0') as u16;
out.push(v);
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encs_table_shape() {
assert_eq!(ENCS.len(), 113);
for (i, &v) in ENCS.iter().enumerate() {
assert!(v < 512, "ENCS[{i}] = {v} doesn't fit in 9 bits");
}
}
#[test]
fn encs_known_values() {
assert_eq!(ENCS[0], 341);
assert_eq!(ENCS[1], 171);
assert_eq!(ENCS[112], 460);
assert_eq!(ENCS[38], 185);
}
#[test]
fn encs_popcount_in_valid_range() {
for (i, &v) in ENCS.iter().enumerate() {
let ones = v.count_ones();
assert!(
(3..=6).contains(&ones),
"ENCS[{i}] = {v:09b} has {ones} dots, expected 3..=6"
);
}
}
#[test]
fn mask_constants_match_bwipp() {
assert_eq!(MASK_SEEDS, [0, 3, 7, 17]);
assert_eq!(MASK_BITS, ["00", "01", "10", "11"]);
}
#[test]
fn lookup_codeword_in_mode_per_column_and_marker_pins() {
assert_eq!(lookup_codeword_in_mode(b'A', 0), Some(33));
assert_eq!(
lookup_codeword_in_mode(b'a', 0),
None,
"lowercase has no col-A mapping"
);
assert_eq!(
lookup_codeword_in_mode(0, 0),
Some(64),
"NUL → col-A row 64 (asymmetric: NOT row 0 from col-2)"
);
assert_eq!(
lookup_codeword_in_mode(b'a', 1),
Some(65),
"'a' → col-B row 65"
);
assert_eq!(lookup_codeword_in_mode(b'A', 1), Some(33));
assert_eq!(lookup_codeword_in_mode(96, 1), Some(64));
assert_eq!(
lookup_codeword_in_mode(96, 0),
None,
"backtick has no col-A mapping"
);
assert_eq!(lookup_codeword_in_mode(0, 2), Some(0), "col-C: 0 → row 0");
assert_eq!(lookup_codeword_in_mode(50, 2), Some(50));
assert_eq!(lookup_codeword_in_mode(95, 2), Some(95));
assert_eq!(
lookup_codeword_in_mode_i16(FN1, 0),
Some(107),
"FN1 col-A → row 107"
);
assert_eq!(lookup_codeword_in_mode_i16(FN1, 1), Some(107));
assert_eq!(lookup_codeword_in_mode_i16(FN1, 2), Some(107));
assert_eq!(
lookup_codeword_in_mode_i16(LAA, 0),
None,
"LAA has no col-A row (asymmetric marker placement)"
);
assert_eq!(
lookup_codeword_in_mode_i16(LAA, 1),
Some(102),
"LAA col-B → row 102 (`[LAB, LAA, SFB]`)"
);
assert_eq!(
lookup_codeword_in_mode_i16(LAA, 2),
Some(101),
"LAA col-C → row 101 (`[SB6, SFA, LAA]`)"
);
assert_eq!(
lookup_codeword_in_mode(b'A', 0),
lookup_codeword_in_mode_i16(b'A' as i16, 0),
"u8 wrapper matches i16 path"
);
assert_eq!(
lookup_codeword_in_mode(b'a', 1),
lookup_codeword_in_mode_i16(b'a' as i16, 1),
);
assert_eq!(lookup_codeword_in_mode(255, 0), None);
assert_eq!(lookup_codeword_in_mode(255, 1), None);
assert_eq!(lookup_codeword_in_mode(255, 2), None);
assert_eq!(lookup_codeword_in_mode_i16(-99, 0), None);
assert_eq!(lookup_codeword_in_mode_i16(-99, 1), None);
assert_eq!(lookup_codeword_in_mode_i16(-99, 2), None);
}
#[test]
fn encode_c_matches_bwipp_for_even_numeric() {
let cases: &[(&str, &[u16])] = &[
("1234", &[12, 34]),
("123456", &[12, 34, 56]),
("1234567890", &[12, 34, 56, 78, 90]),
("00990099", &[0, 99, 0, 99]),
("0000", &[0, 0]),
("9999", &[99, 99]),
];
for &(input, want) in cases {
let got = encode_c(input.as_bytes()).unwrap_or_else(|| {
panic!("encode_c({input:?}) returned None — expected Some({want:?})");
});
assert_eq!(got, want, "encode_c({input:?}) mismatch");
}
}
#[test]
fn encode_b_matches_bwipp_oracle() {
let cases: &[(&str, &[u16])] = &[
("ABCDE", &[33, 34, 35, 36, 37]),
("abcde", &[65, 66, 67, 68, 69]),
("Hello", &[40, 69, 76, 76, 79]),
("HELLO", &[40, 37, 44, 44, 47]),
("test", &[84, 69, 83, 84]),
];
for &(input, want) in cases {
let got = encode_b(input.as_bytes()).unwrap_or_else(|| {
panic!("encode_b({input:?}) returned None — expected Some({want:?})");
});
assert_eq!(got, want, "encode_b({input:?}) mismatch");
}
}
#[test]
fn encode_b_handles_lowercase_and_rejects_nul() {
assert_eq!(encode_b(b"abc"), Some(vec![65, 66, 67]));
assert_eq!(
encode_b(b"A1a"),
Some(vec![33, 17, 65]),
"uppercase 'A'=33, digit '1'=17, lowercase 'a'=65"
);
assert_eq!(encode_b(b" "), Some(vec![0]));
assert_eq!(
encode_b(b"\x7F"),
Some(vec![95]),
"DEL (0x7F) is the last valid mode-B byte"
);
assert_eq!(encode_b(b"\x00"), None, "NUL (0x00) has no mode-B codeword");
assert_eq!(encode_b(b"\x01"), None);
assert_eq!(encode_b(b"\x1F"), None);
assert_eq!(
encode_b(b"a\x00b"),
None,
"any single invalid byte poisons the whole encode"
);
assert_eq!(encode_b(b""), Some(Vec::new()));
}
#[test]
fn encode_a_handles_uppercase_and_rejects_lowercase() {
assert_eq!(encode_a(b"ABCDE"), Some(vec![33, 34, 35, 36, 37]));
assert_eq!(encode_a(b"12345"), Some(vec![17, 18, 19, 20, 21]));
assert!(encode_a(b"abc").is_none());
}
#[test]
fn position_tables_match_bwipp_rules() {
let pt = build_position_tables(b"12345");
assert_eq!(pt.n_digits, vec![5, 4, 3, 2, 1, 0]);
assert_eq!(pt.datum_a, vec![true, true, true, true, true, false]);
assert_eq!(pt.datum_b, vec![true, true, true, true, true, false]);
assert_eq!(pt.datum_c, vec![true, true, true, true, false, false]);
let pt = build_position_tables(b"abc");
assert_eq!(pt.n_digits, vec![0, 0, 0, 0]);
assert_eq!(pt.datum_a, vec![false, false, false, false]);
assert_eq!(pt.datum_b, vec![true, true, true, false]);
assert_eq!(pt.datum_c, vec![false, false, false, false]);
let pt = build_position_tables(b"ABC123abc");
assert_eq!(pt.n_digits, vec![0, 0, 0, 3, 2, 1, 0, 0, 0, 0]);
assert_eq!(
pt.datum_a,
vec![true, true, true, true, true, true, false, false, false, false],
);
assert_eq!(
pt.datum_b,
vec![true, true, true, true, true, true, true, true, true, false],
);
assert_eq!(
pt.datum_c,
vec![false, false, false, true, true, false, false, false, false, false],
);
let pt = build_position_tables(b"a\r\nb");
assert!(pt.datum_b[1], "CRLF should set DatumB[\\r] = true");
assert!(!pt.datum_b[2], "but DatumB[\\n] stays false");
assert_eq!(pt.datum_b, vec![true, true, false, true, false]);
}
#[test]
fn position_tables_scoring_fields() {
let pt = build_position_tables(b"abc");
assert_eq!(pt.ahead_a, vec![0, 0, 0, 0]);
assert_eq!(pt.ahead_b, vec![3, 2, 1, 0]);
assert_eq!(pt.ahead_c, vec![0, 0, 0, 0]);
assert_eq!(pt.try_c, vec![0, 0, 0, 0]);
assert_eq!(pt.until_end_seg, vec![3, 2, 1, 0]);
assert!(pt.seventeen_ten.iter().all(|&b| !b));
let pt = build_position_tables(b"ABC");
assert_eq!(pt.ahead_a, vec![3, 2, 1, 0]);
assert_eq!(pt.ahead_b, vec![3, 2, 1, 0]);
let pt = build_position_tables(b"12345");
assert_eq!(pt.ahead_c, vec![2, 2, 1, 1, 0, 0]);
assert_eq!(pt.try_c, vec![0, 2, 0, 1, 0, 0]);
assert_eq!(pt.ahead_a, vec![1, 0, 3, 2, 1, 0]);
let pt = build_position_tables(b"1723456710");
assert!(
pt.seventeen_ten[0],
"SeventeenTen should fire on AI-17/AI-10 prefix"
);
for (i, &v) in pt.seventeen_ten.iter().enumerate().skip(1) {
assert!(!v, "SeventeenTen[{i}] should not fire");
}
}
#[test]
fn pad_to_nd_rules() {
let mut cws = vec![1, 2, 3, 4];
pad_to_nd(&mut cws, 3, false);
assert_eq!(cws, vec![1, 2, 3, 4]);
let mut cws = vec![102, 33];
pad_to_nd(&mut cws, 5, false);
assert_eq!(cws, vec![102, 33, 106, 106, 106]);
let mut cws = vec![112]; pad_to_nd(&mut cws, 4, true);
assert_eq!(cws, vec![112, 109, 106, 106]);
let mut cws = vec![];
pad_to_nd(&mut cws, 3, true);
assert_eq!(cws, vec![109, 106, 106]);
}
#[test]
fn high_level_encode_picks_bwipp_mask_for_a() {
let sym = encode(b"A").unwrap();
assert_eq!(sym.rows, 10);
assert_eq!(sym.columns, 13);
assert_eq!(sym.pixs.len(), 130);
assert!(sym.pixs.iter().all(|&v| v == 0 || v == 1));
let six_corners = [(12, 8), (0, 8), (11, 9), (1, 9), (12, 0), (0, 0)];
for (x, y) in six_corners {
assert_eq!(
sym.pixs[y * 13 + x],
1,
"corner ({x},{y}) should be Set in litmask output",
);
}
let bwipp_final: &[i8] = &[
1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1,
0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0,
0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0,
0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0,
1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0,
];
assert_eq!(sym.pixs, bwipp_final, "pixs mismatch vs BWIPP final output");
}
#[test]
fn encode_picks_bwipp_mask_for_longer_inputs() {
let cases: &[(&[u8], u8)] = &[
(b"AB", 2),
(b"ABC", 3),
(b"Hello", 1),
(b"1234", 3),
(b"12345", 2),
(b"ABCDE", 1),
(b"ABCabc", 1),
(b"ab1234", 2),
(b"abc12345", 2),
];
for &(input, want) in cases {
let sym = encode(input).unwrap();
assert_eq!(
sym.mask, want,
"encode({input:?}) should pick mask={want}, got {}",
sym.mask
);
}
}
#[test]
fn apply_rs_ecc_length_prefix_and_with_leading_delegation() {
let r = apply_rs_ecc(&[]);
assert_eq!(r.len(), 3, "empty input → nc=3 ECC bytes");
assert_eq!(r, vec![0u16; 3], "empty + leading=0 → all-zero ECC");
assert_eq!(r, apply_rs_ecc_with_leading(0, &[]));
let data = [42u16, 99];
let r = apply_rs_ecc(&data);
assert_eq!(r.len(), 6, "len=2 → nc=4");
assert_eq!(&r[..2], &data[..], "prefix is data unchanged");
assert_eq!(r, apply_rs_ecc_with_leading(0, &data));
let data4 = [1u16, 2, 3, 4];
let r4 = apply_rs_ecc(&data4);
assert_eq!(r4.len(), 9, "len=4 → nc=5");
assert_eq!(&r4[..4], &data4[..]);
let with_0 = apply_rs_ecc_with_leading(0, &data);
let with_1 = apply_rs_ecc_with_leading(1, &data);
let with_3 = apply_rs_ecc_with_leading(3, &data);
assert_ne!(with_0[2..], with_1[2..], "leading must participate in LFSR");
assert_ne!(with_1[2..], with_3[2..]);
assert_eq!(&with_0[..2], &data[..]);
assert_eq!(&with_1[..2], &data[..]);
assert_eq!(&with_3[..2], &data[..]);
}
#[test]
fn eval_symbol_matches_bwipp_scores() {
let cases: &[(&[u8], [i32; 4])] = &[
(b"A", [4, 62, 12, 51]),
(b"AB", [12, 78, 88, 70]),
(b"ABC", [156, 178, 140, 191]),
(b"Hello", [245, 251, 243, 248]),
(b"1234", [74, 48, 74, 126]),
(b"12345", [196, 215, 228, 216]),
(b"ABCDE", [236, 245, 234, 243]),
(b"ABCabc", [231, 299, 296, 296]),
(b"ab1234", [188, 200, 222, 152]),
(b"abc12345", [250, 291, 296, 296]),
];
let mut fails: Vec<String> = Vec::new();
for &(input, want_scores) in cases {
let mut data = encode_message(input).unwrap();
let sz = pick_symbol_size_default(data.len());
pad_to_nd(&mut data, sz.nd, false);
for mask in 0..=3u8 {
let rscws = encode_for_mask(mask, &data);
let bits = build_bits(&rscws, mask, sz.ndots);
let pixs = render_pixs(sz.rows, sz.columns, &bits);
let got = eval_symbol(&pixs, sz.rows, sz.columns);
if got != want_scores[mask as usize] {
fails.push(format!(
"{:?} mask={mask}: got {got}, want {}",
std::str::from_utf8(input).unwrap_or("<non-utf8>"),
want_scores[mask as usize]
));
}
}
}
assert!(
fails.is_empty(),
"eval_symbol mismatches:\n {}",
fails.join("\n ")
);
}
#[test]
fn render_pixs_corpus_matches_oracle() {
let corpus = include_str!("../../../tests/fixtures/dotcode_pixs.txt");
let mut tested = 0usize;
for line in corpus.lines() {
if line.is_empty() || line.starts_with('#') {
continue;
}
let mut parts = line.splitn(3, '\t');
let input = parts.next().expect("missing input");
let mask: u8 = parts
.next()
.expect("missing mask")
.parse()
.expect("bad mask");
let want: Vec<i8> = parts
.next()
.expect("missing pixs csv")
.split(',')
.map(|s| s.parse().expect("bad pixs cell"))
.collect();
let mut data = encode_message(input.as_bytes()).unwrap();
let sz = pick_symbol_size_default(data.len());
pad_to_nd(&mut data, sz.nd, false);
let rscws = encode_for_mask(mask, &data);
let bits = build_bits(&rscws, mask, sz.ndots);
let pixs = render_pixs(sz.rows, sz.columns, &bits);
assert_eq!(
pixs.len(),
sz.rows * sz.columns,
"wrong pixs length for {input:?} mask={mask}",
);
assert_eq!(pixs, want, "render_pixs mismatch for {input:?} mask={mask}",);
assert!(pixs.iter().all(|&v| v == 0 || v == 1));
tested += 1;
}
assert!(tested >= 10, "expected ≥10 corpus cases, ran {tested}");
}
#[test]
fn render_pixs_matches_oracle_full_pipeline() {
let mut data = encode_message(b"A").unwrap();
let sz = pick_symbol_size_default(data.len());
pad_to_nd(&mut data, sz.nd, false);
let rscws = encode_for_mask(0, &data);
let bits = build_bits(&rscws, 0, sz.ndots);
let pixs = render_pixs(sz.rows, sz.columns, &bits);
assert_eq!(pixs.len(), sz.rows * sz.columns);
let want: &[i8] = &[
1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1,
0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0,
0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1,
0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0,
0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0,
];
assert_eq!(pixs, want);
assert!(pixs.iter().all(|&v| v == 0 || v == 1));
}
#[test]
fn init_outline_parity_and_corners() {
let g = init_outline(10, 13);
assert_eq!(g.len(), 10 * 13);
let corners: [(usize, usize); 6] = [(12, 8), (0, 8), (11, 9), (1, 9), (12, 0), (0, 0)];
for y in 0..10 {
for x in 0..13 {
let c = g[y * 13 + x];
let is_corner = corners.contains(&(x, y));
if (x + y) % 2 == 0 {
if is_corner {
assert_eq!(c, DotCell::Set, "({x},{y}) corner should be Set");
} else {
assert_eq!(c, DotCell::Inactive, "({x},{y}) should be Inactive");
}
} else {
assert_eq!(c, DotCell::Empty, "({x},{y}) should be Empty");
}
}
}
for &(x, y) in &[(12, 8), (0, 8), (11, 9), (1, 9), (12, 0), (0, 0)] {
assert_eq!(
g[y * 13 + x],
DotCell::Set,
"corner ({x},{y}) should be Set"
);
}
let g = init_outline(11, 16);
assert_eq!(g.len(), 11 * 16);
for &(x, y) in &[(14, 0), (14, 10), (15, 1), (15, 9), (0, 0), (0, 10)] {
assert_eq!(
g[y * 16 + x],
DotCell::Set,
"corner ({x},{y}) should be Set"
);
}
let active = g
.iter()
.filter(|&&c| matches!(c, DotCell::Empty | DotCell::Set))
.count();
let expected_active = (11_usize * 16).div_ceil(2) + 6;
assert_eq!(active, expected_active);
}
#[test]
fn build_bits_matches_oracle() {
let cases: &[(&[u8], u8, usize, &str)] = &[
(
b"A",
0,
65,
"00011000111010010111100111100010111100101110010011010110010011101",
),
(
b"A",
1,
65,
"01011000111010100111111001100011001101101100101101011100010011011",
),
(
b"A",
2,
65,
"10011000111011001101101101010011001110101011100000111011010011101",
),
(
b"A",
3,
65,
"11011000111101100110001010111010001111100110011101100011101001110",
),
(
b"AB",
0,
65,
"00011100011010010111010011011000111011110110001010111100001111010",
),
(
b"ABCDE",
0,
117,
"001001111000100101110100110110100111010101001110101100111001011100101101010011011011110011001110001011100010111111111",
),
(
b"1234",
0,
65,
"00101111000011010110010011011110010011101111000011101001010010111",
),
];
for &(input, mask, ndots, want) in cases {
let mut data = encode_message(input).unwrap();
let sz = pick_symbol_size_default(data.len());
assert_eq!(sz.ndots, ndots, "ndots mismatch for {input:?}");
pad_to_nd(&mut data, sz.nd, false);
let rscws = encode_for_mask(mask, &data);
let bits = build_bits(&rscws, mask, sz.ndots);
assert_eq!(
bits,
want,
"build_bits mismatch for {:?} mask={mask}",
std::str::from_utf8(input).unwrap_or("<non-utf8>")
);
}
}
#[test]
fn apply_lit_mask_lights_six_edge_corners_per_parity() {
let mut pixs = vec![0i8; 4 * 5];
apply_lit_mask(4, 5, &mut pixs);
let lit_even: Vec<usize> = pixs
.iter()
.enumerate()
.filter(|(_, &v)| v == 1)
.map(|(i, _)| i)
.collect();
assert_eq!(lit_even, vec![0, 4, 10, 14, 16, 18], "even-row corners");
let mut pixs = vec![0i8; 5 * 5];
apply_lit_mask(5, 5, &mut pixs);
let lit_odd: Vec<usize> = pixs
.iter()
.enumerate()
.filter(|(_, &v)| v == 1)
.map(|(i, _)| i)
.collect();
assert_eq!(lit_odd, vec![0, 3, 9, 19, 20, 23], "odd-row corners");
assert_ne!(lit_even, lit_odd);
}
#[test]
fn mask_transform_cws_per_position_arithmetic() {
assert_eq!(mask_transform_cws(0, &[]), Vec::<u16>::new());
assert_eq!(mask_transform_cws(0, &[42]), vec![42]);
assert_eq!(
mask_transform_cws(0, &[10, 20, 30, 40, 50]),
vec![10, 20, 30, 40, 50],
"mask=0 is the identity (MASK_VALS[0] = 0)"
);
assert_eq!(
mask_transform_cws(1, &[10, 20, 30]),
vec![10, 23, 36],
"mask=1: +3 per position"
);
assert_eq!(
mask_transform_cws(2, &[50, 50, 50]),
vec![50, 57, 64],
"mask=2: +7 per position"
);
assert_eq!(
mask_transform_cws(3, &[0, 0, 0, 0]),
vec![0, 17, 34, 51],
"mask=3: +17 per position"
);
let mut input = vec![0u16; 11];
input[10] = 50;
let out = mask_transform_cws(3, &input);
assert_eq!(out[10], 107, "mask=3, p=10, c=50: wraps via % 113");
let input = vec![0u16; 21];
let out = mask_transform_cws(3, &input);
assert_eq!(out[20], 1, "mask=3, p=20, c=0: 340 % 113 = 1");
assert_eq!(out[0], 0, "p=0 has zero contribution from mask shift");
}
#[test]
fn encode_for_mask_4_candidates_input_a() {
let mut data = encode_message(b"A").unwrap();
let sz = pick_symbol_size_default(data.len());
pad_to_nd(&mut data, sz.nd, false);
let expected: &[(u8, &[u16])] = &[
(0, &[102, 33, 106, 68, 52, 12, 35]),
(1, &[102, 36, 112, 40, 22, 49, 34]),
(2, &[102, 40, 7, 69, 49, 95, 35]),
(3, &[102, 50, 27, 101, 79, 82, 48]),
];
for &(mask, want) in expected {
let got = encode_for_mask(mask, &data);
assert_eq!(got, want, "encode_for_mask(mask={mask}, A)");
}
}
#[test]
fn rs_ecc_matches_oracle() {
let cases: &[(&[u8], &[u16])] = &[
(b"A", &[102, 33, 106, 68, 52, 12, 35]),
(b"AB", &[103, 33, 34, 95, 89, 68, 66]),
(b"ABC", &[104, 33, 34, 35, 52, 10, 99, 21, 18]),
(b"ABCD", &[105, 33, 34, 35, 36, 52, 101, 92, 30, 75]),
(b"ABCDE", &[106, 33, 34, 35, 36, 37, 45, 3, 31, 112, 90, 84]),
(b"Hello", &[106, 40, 69, 76, 76, 79, 50, 93, 6, 78, 101, 91]),
(b"12", &[107, 12, 106, 34, 46, 64, 49]),
(b"1234", &[107, 12, 34, 86, 107, 44, 33]),
(b"12345", &[107, 12, 34, 102, 21, 37, 17, 50, 17, 14]),
];
for &(input, want) in cases {
let mut data = encode_message(input).unwrap();
let sz = pick_symbol_size_default(data.len());
pad_to_nd(&mut data, sz.nd, false);
let full = apply_rs_ecc(&data);
assert_eq!(
full,
want,
"RS ECC mismatch for {:?} (data={:?})",
std::str::from_utf8(input).unwrap_or("<non-utf8>"),
data,
);
}
}
#[test]
fn pick_symbol_size_default_matches_oracle() {
let cases: &[(usize, usize, usize, usize)] = &[
(2, 10, 13, 3), (3, 10, 13, 3), (4, 11, 16, 4), (5, 12, 17, 5), (6, 13, 18, 6), (7, 13, 20, 7), ];
for &(n_data, exp_rows, exp_cols, exp_nd) in cases {
let s = pick_symbol_size_default(n_data);
assert_eq!(
(s.rows, s.columns, s.nd),
(exp_rows, exp_cols, exp_nd),
"pick_symbol_size_default({n_data}) mismatch"
);
}
}
#[test]
fn full_pipeline_matches_oracle() {
let cases: &[(&[u8], &[u16])] = &[
(b"1", &[102, 17, 106]),
(b"12", &[107, 12, 106]),
(b"1234", &[107, 12, 34]),
(b"12345", &[107, 12, 34, 102, 21]),
(b"123456", &[107, 12, 34, 56]),
(b"00990099", &[107, 0, 99, 0, 99]),
(b"A", &[102, 33, 106]),
(b"AB", &[103, 33, 34]),
(b"ABC", &[104, 33, 34, 35]),
(b"ABCD", &[105, 33, 34, 35, 36]),
(b"ABCDE", &[106, 33, 34, 35, 36, 37]),
(b"Hello", &[106, 40, 69, 76, 76, 79]),
(b"abc12", &[106, 65, 66, 67, 17, 18]),
(b"1abc", &[105, 17, 65, 66, 67]),
(b"\tABC", &[101, 73, 33, 34, 35]),
(b"1234abc", &[107, 12, 34, 104, 65, 66, 67]),
(b"12abc", &[107, 12, 104, 65, 66, 67]),
(b"ab1234", &[103, 65, 66, 12, 34]),
(b"abc12345", &[105, 65, 66, 67, 17, 23, 45]),
(b"ABCabc", &[106, 33, 34, 35, 65, 66, 67]),
];
for &(input, want) in cases {
let mut cws = encode_message(input).unwrap();
let sz = pick_symbol_size_default(cws.len());
pad_to_nd(&mut cws, sz.nd, false);
assert_eq!(
cws,
want,
"full pipeline for {:?}",
std::str::from_utf8(input).unwrap_or("<non-utf8>"),
);
}
}
#[test]
fn encode_message_oracle_round_trip() {
let cases: &[(&[u8], usize, &[u16])] = &[
(b"12", 3, &[107, 12, 106]),
(b"1234", 3, &[107, 12, 34]),
(b"12345", 5, &[107, 12, 34, 102, 21]),
(b"123456", 4, &[107, 12, 34, 56]),
(b"00990099", 5, &[107, 0, 99, 0, 99]),
(b"1", 3, &[102, 17, 106]),
(b"A", 3, &[102, 33, 106]),
(b"AB", 3, &[103, 33, 34]),
(b"ABC", 4, &[104, 33, 34, 35]),
(b"ABCD", 5, &[105, 33, 34, 35, 36]),
(b"ABCDE", 6, &[106, 33, 34, 35, 36, 37]),
(b"Hello", 6, &[106, 40, 69, 76, 76, 79]),
(b"abc12", 6, &[106, 65, 66, 67, 17, 18]),
(b"1abc", 5, &[105, 17, 65, 66, 67]),
(b"\tABC", 5, &[101, 73, 33, 34, 35]),
(b"\t\tABC", 6, &[101, 73, 73, 33, 34, 35]),
(b"\tABCDE", 7, &[101, 73, 33, 34, 35, 36, 37]),
(b"\t\t\t\t", 5, &[101, 73, 73, 73, 73]),
(b"1234abc", 7, &[107, 12, 34, 104, 65, 66, 67]),
(b"12abc", 6, &[107, 12, 104, 65, 66, 67]),
(b"ab1234", 5, &[103, 65, 66, 12, 34]),
(b"abc12345", 7, &[105, 65, 66, 67, 17, 23, 45]),
(b"ABCabc", 7, &[106, 33, 34, 35, 65, 66, 67]),
];
for &(input, nd, want) in cases {
let mut cws = encode_message(input).unwrap();
pad_to_nd(&mut cws, nd, false);
assert_eq!(
cws,
want,
"encode_message pipeline for {:?} (nd={nd})",
std::str::from_utf8(input).unwrap_or("<non-utf8>"),
);
}
}
#[test]
fn encode_message_handles_non_ascii_inputs_via_bin_escape() {
for input in [
"héllo".as_bytes(),
"ünicode".as_bytes(),
"café".as_bytes(),
&[0xC3u8][..],
&[0xFFu8][..],
&[0x80u8][..],
] {
let cws = encode_message(input)
.unwrap_or_else(|e| panic!("Gap 6 should accept {input:?}: {e:?}"));
assert!(
cws.contains(&BIN_ENTER),
"BIN run for {input:?} must include the BIN_ENTER codeword 112, cws={cws:?}",
);
}
}
#[test]
fn high_level_encode_accepts_non_ascii_inputs_via_bin_escape() {
let sym = encode("héllo".as_bytes()).expect("Gap 6 should accept UTF-8 'héllo'");
assert!(
!sym.pixs.is_empty(),
"encode(\"héllo\" as UTF-8 bytes) (multi-byte non-ASCII via BIN escape, Gap 6 path) must produce non-empty pixs; got len={}",
sym.pixs.len()
);
assert!(
sym.mask <= 3,
"DotCode mask must be in 0..=3 (4 masks per spec); got mask={}",
sym.mask
);
}
#[test]
fn high_level_encode_rejects_long_payload_requiring_interleaved_rs() {
let long = "1".repeat(150);
let err = encode(long.as_bytes())
.expect_err("nw > 112 input should be rejected, not silently miscoded");
match err {
crate::error::Error::InvalidData(msg) => {
assert!(msg.contains("DotCode:"), "missing DotCode prefix: {msg:?}");
assert!(
msg.contains("payload requires nw="),
"missing `payload requires nw=` predicate: {msg:?}"
);
assert!(
msg.contains("interleaved RS path activates for nw > 112"),
"missing full rationale anchor: {msg:?}"
);
assert!(
msg.contains("nw=120") && msg.contains("nd=78") && msg.contains("nc=42"),
"missing nw=120/nd=78/nc=42 value-echo (150-digit input expectation): {msg:?}"
);
}
other => panic!("unexpected error variant: {other:?}"),
}
}
#[test]
fn high_level_encode_accepts_short_payload_under_threshold() {
let short = "1".repeat(60);
let _sym = encode(short.as_bytes())
.expect("short payload must still encode after the nw > 112 guard");
}
#[test]
fn encode_short_pure_oracle_round_trip() {
let cases: &[(&[u8], usize, &[u16])] = &[
(b"", 0, &[]),
(b"12", 3, &[107, 12, 106]),
(b"1234", 3, &[107, 12, 34]),
(b"12345", 5, &[107, 12, 34, 102, 21]),
(b"123456", 4, &[107, 12, 34, 56]),
(b"00990099", 5, &[107, 0, 99, 0, 99]),
(b"1", 3, &[102, 17, 106]),
(b"A", 3, &[102, 33, 106]),
(b"AB", 3, &[103, 33, 34]),
(b"ABC", 4, &[104, 33, 34, 35]),
(b"ABCD", 5, &[105, 33, 34, 35, 36]),
(b"ABCDE", 6, &[106, 33, 34, 35, 36, 37]),
(b"Hello", 6, &[106, 40, 69, 76, 76, 79]),
(b"abc12", 6, &[106, 65, 66, 67, 17, 18]),
(b"1abc", 5, &[105, 17, 65, 66, 67]),
(b"\tABC", 5, &[101, 73, 33, 34, 35]),
(b"\t\tABC", 6, &[101, 73, 73, 33, 34, 35]),
(b"\tABCDE", 7, &[101, 73, 33, 34, 35, 36, 37]),
(b"\t\t\t\t", 5, &[101, 73, 73, 73, 73]),
];
for &(input, nd, want) in cases {
let mut cws = encode_short_pure(input).unwrap_or_else(|| {
panic!("encode_short_pure({input:?}) returned None — expected Some(...)")
});
pad_to_nd(&mut cws, nd, false);
assert_eq!(
cws,
want,
"encode_short_pure pipeline for {:?} (nd={nd})",
std::str::from_utf8(input).unwrap_or("<non-utf8>"),
);
}
let rejects: &[&[u8]] = &[
b"1234abc", b"12abc", b"ab1234", b"abc12345", ];
for input in rejects {
assert!(
encode_short_pure(input).is_none(),
"encode_short_pure({input:?}) should reject mid-message-transition inputs"
);
}
}
#[test]
fn dispatch_initial_matches_bwipp_oracle() {
fn pick(msg: &[u8]) -> InitialAction {
let tables = build_position_tables(msg);
dispatch_initial(&tables, msg)
}
assert_eq!(pick(b""), InitialAction::NoPrologue);
assert_eq!(pick(b"12"), InitialAction::Fn1ThenStayInC);
assert_eq!(pick(b"1234"), InitialAction::Fn1ThenStayInC);
assert_eq!(pick(b"12345"), InitialAction::Fn1ThenStayInC);
assert_eq!(pick(b"1234abc"), InitialAction::Fn1ThenStayInC);
assert_eq!(pick(b"1"), InitialAction::ShiftToBFor(1));
assert_eq!(pick(b"A"), InitialAction::ShiftToBFor(1));
assert_eq!(pick(b"AB"), InitialAction::ShiftToBFor(2));
assert_eq!(pick(b"ABC"), InitialAction::ShiftToBFor(3));
assert_eq!(pick(b"ABCD"), InitialAction::ShiftToBFor(4));
assert_eq!(pick(b"ABCDE"), InitialAction::LatchToB);
assert_eq!(pick(b"Hello"), InitialAction::LatchToB);
assert_eq!(pick(b"abc12"), InitialAction::LatchToB);
assert_eq!(pick(b"\tABC"), InitialAction::LatchToA);
assert_eq!(pick(b"\t\tABC"), InitialAction::LatchToA);
assert_eq!(pick(b"\t\t\t\t"), InitialAction::LatchToA);
}
#[test]
fn encode_mode_a_run_from_c_matches_bwipp_oracle() {
assert_eq!(
encode_mode_a_run_from_c(b"\tABC"),
Some(vec![101, 73, 33, 34, 35]),
);
assert_eq!(
encode_mode_a_run_from_c(b"\t\tABC"),
Some(vec![101, 73, 73, 33, 34, 35]),
);
assert_eq!(
encode_mode_a_run_from_c(b"\tABCDE"),
Some(vec![101, 73, 33, 34, 35, 36, 37]),
);
assert_eq!(
encode_mode_a_run_from_c(b"\t\t\t\t"),
Some(vec![101, 73, 73, 73, 73]),
);
assert!(encode_mode_a_run_from_c(b"abc").is_none());
}
#[test]
fn mode_a_pipeline_matches_full_oracle_cws() {
let cases: &[(&[u8], usize, &[u16])] = &[
(b"\tABC", 5, &[101, 73, 33, 34, 35]),
(b"\t\tABC", 6, &[101, 73, 73, 33, 34, 35]),
(b"\tABCDE", 7, &[101, 73, 33, 34, 35, 36, 37]),
(b"\t\t\t\t", 5, &[101, 73, 73, 73, 73]),
];
for &(input, nd, want) in cases {
let mut cws = encode_mode_a_run_from_c(input).unwrap();
pad_to_nd(&mut cws, nd, false);
assert_eq!(cws, want, "mode-A pipeline for {input:?} (nd={nd})");
}
}
#[test]
fn mode_b_pipeline_matches_full_oracle_cws() {
let cases: &[(&[u8], usize, &[u16])] = &[
(b"A", 3, &[102, 33, 106]),
(b"AB", 3, &[103, 33, 34]),
(b"ABC", 4, &[104, 33, 34, 35]),
(b"ABCD", 5, &[105, 33, 34, 35, 36]),
(b"ABCDE", 6, &[106, 33, 34, 35, 36, 37]),
(b"Hello", 6, &[106, 40, 69, 76, 76, 79]),
];
for &(input, nd, want) in cases {
let mut cws = encode_mode_b_run_from_c(input).unwrap();
pad_to_nd(&mut cws, nd, false);
assert_eq!(
cws,
want,
"mode-B pipeline for {:?} (nd={nd})",
std::str::from_utf8(input).unwrap(),
);
}
}
#[test]
fn numeric_pipeline_matches_full_oracle_cws() {
let cases: &[(&[u8], usize, &[u16])] = &[
(b"12", 3, &[107, 12, 106]),
(b"1234", 3, &[107, 12, 34]),
(b"12345", 5, &[107, 12, 34, 102, 21]),
(b"123456", 4, &[107, 12, 34, 56]),
(b"00990099", 5, &[107, 0, 99, 0, 99]),
(b"1", 3, &[102, 17, 106]),
];
for &(input, nd, want) in cases {
let mut cws = encode_numeric_run_from_c(input).unwrap();
pad_to_nd(&mut cws, nd, false);
assert_eq!(
cws,
want,
"numeric pipeline for {:?} (nd={nd})",
std::str::from_utf8(input).unwrap()
);
}
}
#[test]
fn encode_numeric_run_from_c_matches_bwipp_oracle() {
assert_eq!(encode_numeric_run_from_c(b"1"), Some(vec![102, 17]));
assert_eq!(encode_numeric_run_from_c(b"12"), Some(vec![107, 12]));
assert_eq!(encode_numeric_run_from_c(b"1234"), Some(vec![107, 12, 34]));
assert_eq!(
encode_numeric_run_from_c(b"12345"),
Some(vec![107, 12, 34, 102, 21]),
);
assert_eq!(
encode_numeric_run_from_c(b"123456"),
Some(vec![107, 12, 34, 56]),
);
assert_eq!(
encode_numeric_run_from_c(b"00990099"),
Some(vec![107, 0, 99, 0, 99]),
);
assert!(encode_numeric_run_from_c(b"12a4").is_none());
assert_eq!(encode_numeric_run_from_c(b""), Some(vec![]));
}
#[test]
fn encode_mode_b_run_from_c_matches_bwipp_oracle() {
assert_eq!(encode_mode_b_run_from_c(b"A"), Some(vec![102, 33]));
assert_eq!(encode_mode_b_run_from_c(b"AB"), Some(vec![103, 33, 34]));
assert_eq!(
encode_mode_b_run_from_c(b"ABC"),
Some(vec![104, 33, 34, 35])
);
assert_eq!(
encode_mode_b_run_from_c(b"ABCD"),
Some(vec![105, 33, 34, 35, 36]),
);
assert_eq!(
encode_mode_b_run_from_c(b"ABCDE"),
Some(vec![106, 33, 34, 35, 36, 37]),
);
assert_eq!(
encode_mode_b_run_from_c(b"Hello"),
Some(vec![106, 40, 69, 76, 76, 79]),
);
}
#[test]
fn encode_c_rejects_odd_or_non_digit() {
assert!(encode_c(b"123").is_none());
assert!(encode_c(b"12A4").is_none());
assert!(encode_c(b"").is_some()); }
#[test]
fn charset_markers_are_distinct_and_negative() {
let all = [
LAA, LAB, LAC, BIN, SFA, SFB, SB2, SB3, SB4, SB5, SB6, SFC, SC2, SC3, SC4, SC5, SC6,
SC7, BSA, BSB, TMA, TMB, TMC, TMS, FN1, FN2, FN3, CRL, AIM, M05, M06, M12, MAC,
];
assert_eq!(all.len(), 33);
for (i, &a) in all.iter().enumerate() {
assert!(a < 0, "marker at index {i} is {a}, not negative");
for &b in &all[i + 1..] {
assert_ne!(a, b, "duplicate marker {a}");
}
}
}
#[test]
fn charmaps_table_shape() {
assert_eq!(CHARMAPS.len(), 113);
let markers = [
LAA, LAB, LAC, BIN, SFA, SFB, SB2, SB3, SB4, SB5, SB6, SFC, SC2, SC3, SC4, SC5, SC6,
SC7, BSA, BSB, TMA, TMB, TMC, TMS, FN1, FN2, FN3, CRL, AIM, M05, M06, M12, MAC,
];
for (i, row) in CHARMAPS.iter().enumerate() {
for (j, &v) in row.iter().enumerate() {
let ok = if v >= 0 {
(0..=127).contains(&v) || v as usize == i
} else {
markers.contains(&v)
};
assert!(
ok,
"CHARMAPS[{i}][{j}] = {v} is neither ASCII nor a known marker"
);
}
}
}
#[test]
fn charmaps_ascii_section_layout() {
for (i, row) in CHARMAPS.iter().enumerate().take(64) {
let want_byte = 32 + i as i16;
assert_eq!(row[0], want_byte, "row {i} col A");
assert_eq!(row[1], want_byte, "row {i} col B");
assert_eq!(row[2], i as i16, "row {i} col C");
}
for (i, row) in CHARMAPS.iter().enumerate().take(96).skip(64) {
assert_eq!(row[0], (i - 64) as i16, "row {i} col A (control byte)");
assert_eq!(row[1], (i + 32) as i16, "row {i} col B (lowercase/ctl)");
assert_eq!(row[2], i as i16, "row {i} col C");
}
}
#[test]
fn charmaps_marker_rows_match_bwipp() {
let cases: &[(usize, [i16; 3])] = &[
(96, [SFB, CRL, 96]),
(97, [SB2, 9, 97]),
(98, [SB3, 28, 98]),
(99, [SB4, 29, 99]),
(100, [SB5, 30, AIM]),
(101, [SB6, SFA, LAA]),
(102, [LAB, LAA, SFB]),
(103, [SC2, SC2, SB2]),
(104, [SC3, SC3, SB3]),
(105, [SC4, SC4, SB4]),
(106, [LAC, LAC, LAB]),
(107, [FN1, FN1, FN1]),
(108, [FN2, FN2, FN2]),
(109, [FN3, FN3, FN3]),
(110, [BSA, BSA, BSA]),
(111, [BSB, BSB, BSB]),
(112, [BIN, BIN, BIN]),
];
for &(i, want) in cases {
assert_eq!(CHARMAPS[i], want, "CHARMAPS[{i}]");
}
}
#[test]
fn charmaps_reverse_lookup_for_known_bytes() {
let cases: &[(u8, u16, u16)] = &[
(b' ', 0, 0), (b'A', 33, 33), (b'9', 25, 25), (b'_', 63, 63), ];
for &(byte, want_a, want_b) in cases {
let row_a = CHARMAPS
.iter()
.position(|r| r[0] == byte as i16)
.unwrap_or_else(|| panic!("byte 0x{byte:02x} not in column A"));
let row_b = CHARMAPS
.iter()
.position(|r| r[1] == byte as i16)
.unwrap_or_else(|| panic!("byte 0x{byte:02x} not in column B"));
assert_eq!(row_a as u16, want_a, "Avals[0x{byte:02x}]");
assert_eq!(row_b as u16, want_b, "Bvals[0x{byte:02x}]");
}
}
#[test]
fn latch_c_matches_fn1_row() {
let fn1_row = CHARMAPS
.iter()
.position(|r| r[2] == FN1)
.expect("FN1 missing from column C");
assert_eq!(fn1_row as u16, LATCH_C);
}
#[test]
fn parse_dotcode_input_plain_ascii_passthrough() {
let bytes = b"Hello123";
let expected: Vec<i16> = bytes.iter().map(|&b| i16::from(b)).collect();
assert_eq!(parse_dotcode_input(bytes, true).unwrap(), expected);
assert_eq!(parse_dotcode_input(bytes, false).unwrap(), expected);
}
#[test]
fn parse_dotcode_input_fnc1_emits_fn1_marker() {
let parsed = parse_dotcode_input(b"AB^FNC1CD", true).unwrap();
assert_eq!(
parsed,
vec![
i16::from(b'A'),
i16::from(b'B'),
FN1,
i16::from(b'C'),
i16::from(b'D'),
]
);
}
#[test]
fn parse_dotcode_input_fnc3_emits_fn3_marker() {
let parsed = parse_dotcode_input(b"^FNC3X", true).unwrap();
assert_eq!(parsed, vec![FN3, i16::from(b'X')]);
}
#[test]
fn parse_dotcode_input_eci_expands_to_fn2_plus_six_digits() {
let parsed = parse_dotcode_input(b"^ECI123456A", true).unwrap();
assert_eq!(
parsed,
vec![
FN2,
i16::from(b'1'),
i16::from(b'2'),
i16::from(b'3'),
i16::from(b'4'),
i16::from(b'5'),
i16::from(b'6'),
i16::from(b'A'),
]
);
}
#[test]
fn parse_dotcode_input_double_caret_emits_literal_caret() {
let parsed = parse_dotcode_input(b"A^^B", true).unwrap();
assert_eq!(parsed, vec![i16::from(b'A'), 94, i16::from(b'B')]);
}
#[test]
fn parse_dotcode_input_parsefnc_off_leaves_caret_literal() {
let raw = b"^FNC1abc";
let expected: Vec<i16> = raw.iter().map(|&b| i16::from(b)).collect();
assert_eq!(parse_dotcode_input(raw, false).unwrap(), expected);
}
#[test]
fn parse_dotcode_input_unknown_function_rejected() {
for name in [
&b"^FNC2"[..],
b"^M05A",
b"^MACR",
b"^XYZW",
b"^fnc1", ] {
let err = parse_dotcode_input(name, true).unwrap_err();
assert!(
matches!(err, crate::error::Error::InvalidData(_)),
"expected InvalidData rejecting {:?}, got {err:?}",
std::str::from_utf8(name).unwrap_or("<non-utf8>"),
);
}
}
#[test]
fn parse_dotcode_input_eci_validates_digits() {
for bad in [
&b"^ECI"[..], b"^ECI12", b"^ECI12345", b"^ECI12345A", b"^ECIabcdef", b"^ECI-12345", ] {
let err = parse_dotcode_input(bad, true).unwrap_err();
assert!(
matches!(err, crate::error::Error::InvalidData(_)),
"expected InvalidData rejecting {:?}, got {err:?}",
std::str::from_utf8(bad).unwrap_or("<non-utf8>"),
);
}
let parsed = parse_dotcode_input(b"^ECI000000", true).unwrap();
assert_eq!(
parsed,
vec![
FN2,
b'0'.into(),
b'0'.into(),
b'0'.into(),
b'0'.into(),
b'0'.into(),
b'0'.into()
]
);
let parsed = parse_dotcode_input(b"^ECI999999", true).unwrap();
assert_eq!(
parsed,
vec![
FN2,
b'9'.into(),
b'9'.into(),
b'9'.into(),
b'9'.into(),
b'9'.into(),
b'9'.into()
]
);
}
#[test]
fn parse_dotcode_input_truncated_caret_rejected() {
for trunc in [&b"abc^"[..], b"^", b"^F", b"^FN", b"^FNC"] {
let err = parse_dotcode_input(trunc, true).unwrap_err();
assert!(
matches!(err, crate::error::Error::InvalidData(_)),
"expected InvalidData rejecting truncated {:?}, got {err:?}",
std::str::from_utf8(trunc).unwrap_or("<non-utf8>"),
);
}
}
#[test]
fn parse_dotcode_input_high_bytes_passthrough() {
let bytes: &[u8] = b"caf\xC3\xA9";
let expected: Vec<i16> = vec![0x63, 0x61, 0x66, 0xC3, 0xA9].into_iter().collect();
assert_eq!(parse_dotcode_input(bytes, true).unwrap(), expected);
assert_eq!(parse_dotcode_input(bytes, false).unwrap(), expected);
}
#[test]
fn parse_dotcode_input_multiple_escapes_interleave() {
let parsed = parse_dotcode_input(b"^FNC1AB^^CD^FNC3^ECI000026end", true).unwrap();
let want: Vec<i16> = vec![
FN1,
i16::from(b'A'),
i16::from(b'B'),
94, i16::from(b'C'),
i16::from(b'D'),
FN3,
FN2,
i16::from(b'0'),
i16::from(b'0'),
i16::from(b'0'),
i16::from(b'0'),
i16::from(b'2'),
i16::from(b'6'),
i16::from(b'e'),
i16::from(b'n'),
i16::from(b'd'),
];
assert_eq!(parsed, want);
}
#[test]
fn encode_with_markers_fn1_at_segstart_then_mixed() {
let parsed = parse_dotcode_input(b"^FNC1A1234", true).unwrap();
let cws = encode_message_with_markers(&parsed).unwrap();
assert_eq!(cws, vec![107, 102, 33, 12, 34]);
}
#[test]
fn encode_with_markers_fn1_at_segstart_collapses_with_digit_prepend() {
let parsed = parse_dotcode_input(b"^FNC11234", true).unwrap();
let cws = encode_message_with_markers(&parsed).unwrap();
assert_eq!(cws, vec![12, 34]);
}
#[test]
fn encode_with_markers_fn1_then_mode_b_run() {
let parsed = parse_dotcode_input(b"^FNC1ABC", true).unwrap();
let cws = encode_message_with_markers(&parsed).unwrap();
assert_eq!(cws, vec![107, 104, 33, 34, 35]);
}
#[test]
fn encode_with_markers_inline_fn1_in_mode_b_run() {
let parsed = parse_dotcode_input(b"AB^FNC1CD", true).unwrap();
let cws = encode_message_with_markers(&parsed).unwrap();
assert_eq!(cws, vec![106, 33, 34, 107, 35, 36]);
}
#[test]
fn encode_with_markers_fn1_between_digit_pairs() {
let parsed = parse_dotcode_input(b"12^FNC134", true).unwrap();
let cws = encode_message_with_markers(&parsed).unwrap();
assert_eq!(cws, vec![107, 12, 107, 34]);
}
#[test]
fn encode_with_markers_trailing_fn1() {
let parsed = parse_dotcode_input(b"12^FNC1", true).unwrap();
let cws = encode_message_with_markers(&parsed).unwrap();
assert_eq!(cws, vec![107, 12, 107]);
}
#[test]
fn encode_with_markers_trailing_fn1_after_mode_b_run() {
let parsed = parse_dotcode_input(b"ABC^FNC1", true).unwrap();
let cws = encode_message_with_markers(&parsed).unwrap();
assert_eq!(cws, vec![105, 33, 34, 35, 107]);
}
#[test]
fn encode_with_markers_full_symbol_matches_bwip_js() {
let parsed = parse_dotcode_input(b"^FNC1A1234", true).unwrap();
let sym = encode_with_markers(&parsed).unwrap();
assert_eq!(sym.rows, 12);
assert_eq!(sym.columns, 17);
assert_eq!(sym.pixs.len(), sym.rows * sym.columns);
assert!(sym.mask <= 3);
}
#[test]
fn encode_routes_through_parser_with_parsefnc_off() {
for input in [&b"1234"[..], b"ABC", b"Hello", b"1A"] {
let sym = encode(input).unwrap();
assert!(
!sym.pixs.is_empty(),
"encode({input:?}) (parsefnc=off byte-to-i16 cast path, corpus item) must produce non-empty DotCode pixs; got len={}",
sym.pixs.len()
);
}
let _ = encode(b"ABC^FNC1XY");
}
#[test]
fn encode_message_with_markers_mode_b_then_back_to_c() {
let parsed = parse_dotcode_input(b"abcdef1234", false).unwrap();
let cws = encode_message_with_markers(&parsed).unwrap();
assert_eq!(cws, vec![106, 65, 66, 67, 68, 69, 70, 103, 12, 34]);
}
#[test]
fn encode_message_with_markers_uppercase_then_digits_latches_b_first() {
let parsed = parse_dotcode_input(b"ABCDE1234", false).unwrap();
let cws = encode_message_with_markers(&parsed).unwrap();
assert_eq!(cws, vec![106, 33, 34, 35, 36, 37, 103, 12, 34]);
}
#[test]
fn encode_message_with_markers_inline_fn1_in_mode_b_post_latch() {
let parsed = parse_dotcode_input(b"abc^FNC1xyz", true).unwrap();
let cws = encode_message_with_markers(&parsed).unwrap();
assert_eq!(cws, vec![106, 65, 66, 67, 107, 88, 89, 90]);
}
#[test]
fn encode_message_with_markers_short_mode_b_run_then_digits() {
let parsed = parse_dotcode_input(b"abcd1234", false).unwrap();
let cws = encode_message_with_markers(&parsed).unwrap();
assert_eq!(cws, vec![105, 65, 66, 67, 68, 12, 34]);
}
#[test]
fn encode_message_with_markers_tab_letters_digits_mode_a_to_c_shift() {
let parsed = parse_dotcode_input(b"\tABC1234", false).unwrap();
let cws = encode_message_with_markers(&parsed).unwrap();
assert_eq!(cws, vec![101, 73, 33, 34, 35, 103, 12, 34]);
}
#[test]
fn encode_message_with_markers_tab_uppercase_pure_mode_a() {
let parsed = parse_dotcode_input(b"\tABCDE", false).unwrap();
let cws = encode_message_with_markers(&parsed).unwrap();
assert_eq!(cws, vec![101, 73, 33, 34, 35, 36, 37]);
}
#[test]
fn encode_message_with_markers_tab_lowercase_mode_a_to_b_shift() {
let parsed = parse_dotcode_input(b"\tabcdef", false).unwrap();
let cws = encode_message_with_markers(&parsed).unwrap();
assert_eq!(cws, vec![101, 73, 101, 65, 66, 67, 68, 69, 70]);
}
#[test]
fn base259_to_103_matches_bwipp_polynomial() {
let cws = base259_to_103(&[195, 131, 194, 169]);
assert_eq!(cws, vec![30, 18, 53, 59, 61]);
let cws = base259_to_103(&[0]);
assert_eq!(cws.len(), 2);
let cws = base259_to_103(&[1, 2, 3, 4, 5]);
assert_eq!(cws.len(), 6);
}
#[test]
fn bin_state_add_byte_advance_and_auto_flush_boundary() {
let mut bin = BinState::default();
assert_eq!(bin.bpos, 0);
let mut cws: Vec<u16> = Vec::new();
bin.add_byte(b'A', &mut cws);
assert_eq!(bin.bpos, 1, "first add_byte advances bpos to 1");
assert_eq!(bin.bvals[0], b'A');
assert!(
cws.is_empty(),
"add_byte must NOT flush before bpos reaches 5"
);
bin.add_byte(b'B', &mut cws);
bin.add_byte(b'C', &mut cws);
bin.add_byte(b'D', &mut cws);
assert_eq!(bin.bpos, 4, "after 4 adds bpos == 4");
assert_eq!(bin.bvals[..4], [b'A', b'B', b'C', b'D']);
assert!(cws.is_empty(), "no flush before 5th byte");
bin.add_byte(b'E', &mut cws);
assert_eq!(
bin.bpos, 0,
"auto-flush at bpos==5 must reset to 0 (not 5 or 6)"
);
assert_eq!(
cws.len(),
6,
"5-byte flush yields 6 base-103 codewords (per base259_to_103 contract)"
);
let expected = base259_to_103(b"ABCDE");
assert_eq!(cws, expected);
let cw_count_before_finalise = cws.len();
bin.finalise(&mut cws);
assert_eq!(bin.bpos, 0, "finalise on empty stays at 0");
assert_eq!(
cws.len(),
cw_count_before_finalise,
"finalise on bpos==0 must not append codewords"
);
let mut bin = BinState::default();
let mut cws: Vec<u16> = Vec::new();
bin.add_byte(b'Z', &mut cws);
bin.finalise(&mut cws);
assert_eq!(bin.bpos, 0, "finalise resets bpos");
assert_eq!(
cws.len(),
2,
"1-byte finalise produces 2 base-103 codewords"
);
assert_eq!(cws, base259_to_103(b"Z"));
let mut bin = BinState::default();
let mut cws: Vec<u16> = Vec::new();
for &b in &[1u8, 2, 3, 4, 5] {
bin.add_byte(b, &mut cws);
}
assert_eq!(bin.bpos, 0, "after 5-byte flush, bpos must be 0");
assert_eq!(cws.len(), 6);
bin.add_byte(99, &mut cws);
assert_eq!(bin.bpos, 1, "6th byte after flush → bpos = 1");
assert_eq!(
bin.bvals[0], 99,
"post-flush byte stored at bvals[0] (kills wrong-index mutant)"
);
assert_eq!(cws.len(), 6, "1 byte after flush does not re-flush");
}
#[test]
fn encode_message_with_markers_pure_binary_run() {
let parsed = parse_dotcode_input(&[195u8, 131, 194, 169], false).unwrap();
let cws = encode_message_with_markers(&parsed).unwrap();
assert_eq!(cws, vec![112, 30, 18, 53, 59, 61]);
}
#[test]
fn encode_message_with_markers_text_then_binary_run() {
let parsed = parse_dotcode_input(&[99u8, 97, 102, 195, 131, 194, 169], false).unwrap();
let cws = encode_message_with_markers(&parsed).unwrap();
assert_eq!(cws, vec![104, 67, 65, 70, 112, 30, 18, 53, 59, 61]);
}
#[test]
fn encode_message_with_markers_binary_then_text_exits_to_b() {
let parsed = parse_dotcode_input(&[195u8, 131, 194, 169, 66], false).unwrap();
let cws = encode_message_with_markers(&parsed).unwrap();
assert_eq!(cws, vec![112, 30, 18, 53, 59, 61, 110, 34]);
}
#[test]
fn encode_message_with_markers_crlf_payload_picks_mode_a() {
let parsed = parse_dotcode_input(b"AB\r\nCD", false).unwrap();
let cws = encode_message_with_markers(&parsed).unwrap();
assert_eq!(cws, vec![101, 33, 34, 77, 74, 35, 36]);
}
#[test]
fn pad_to_nd_branches() {
let mut cws = vec![1, 2, 3];
pad_to_nd(&mut cws, 3, false);
assert_eq!(cws, vec![1, 2, 3]);
let mut cws = vec![1, 2, 3, 4];
pad_to_nd(&mut cws, 3, false);
assert_eq!(cws, vec![1, 2, 3, 4]);
let mut cws = vec![1, 2];
pad_to_nd(&mut cws, 5, false);
assert_eq!(cws.len(), 5);
assert_eq!(cws[0..2], [1, 2]);
assert_eq!(cws[2], MODE_C_PAD_CW);
for &c in &cws[2..] {
assert_eq!(c, MODE_C_PAD_CW);
}
let mut cws = vec![1, 2];
pad_to_nd(&mut cws, 5, true);
assert_eq!(cws.len(), 5);
assert_eq!(cws[0..2], [1, 2]);
assert_eq!(cws[2], BIN_FIRST_PAD_CW);
for &c in &cws[3..] {
assert_eq!(c, MODE_C_PAD_CW);
}
}
#[test]
fn is_ascii_digit_i16_boundary() {
for c in b'0'..=b'9' {
assert!(
is_ascii_digit_i16(c as i16),
"ASCII '{}' (i16 {}) should be classified as digit",
c as char,
c as i16
);
}
assert!(!is_ascii_digit_i16(b'/' as i16), "'/' just before '0'");
assert!(!is_ascii_digit_i16(0));
assert!(!is_ascii_digit_i16(b':' as i16), "':' just after '9'");
assert!(!is_ascii_digit_i16(b'A' as i16));
assert!(!is_ascii_digit_i16(b'a' as i16));
assert!(!is_ascii_digit_i16(-1), "negative marker rejected");
assert!(!is_ascii_digit_i16(-128), "deep negative rejected");
assert!(!is_ascii_digit_i16(255));
assert!(!is_ascii_digit_i16(i16::MAX));
}
#[test]
fn lookup_codeword_in_mode_per_column() {
assert!(lookup_codeword_in_mode(b'A', 0).is_some());
assert!(lookup_codeword_in_mode(b'A', 2).is_some());
assert!(lookup_codeword_in_mode(b'a', 1).is_some());
assert!(lookup_codeword_in_mode(b'a', 0).is_none());
let _ = lookup_codeword_in_mode(b'0', 2);
assert_eq!(
lookup_codeword_in_mode(b'A', 0),
lookup_codeword_in_mode(b'A', 0)
);
if let (Some(a), Some(c)) = (
lookup_codeword_in_mode(b'A', 0),
lookup_codeword_in_mode(b'A', 2),
) {
if a != c {
assert_ne!(a, c);
}
}
}
#[test]
fn lookup_codeword_in_mode_i16_sentinel_rows_and_delegation() {
for col in 0..=2usize {
assert_eq!(
lookup_codeword_in_mode_i16(FN1, col),
Some(107),
"FN1 at col {col} → 107"
);
}
for col in 0..=2usize {
assert_eq!(
lookup_codeword_in_mode_i16(FN2, col),
Some(108),
"FN2 at col {col} → 108"
);
}
for col in 0..=2usize {
assert_eq!(
lookup_codeword_in_mode_i16(FN3, col),
Some(109),
"FN3 at col {col} → 109"
);
}
assert_ne!(
lookup_codeword_in_mode_i16(FN1, 0),
lookup_codeword_in_mode_i16(FN2, 0),
);
assert_ne!(
lookup_codeword_in_mode_i16(FN2, 0),
lookup_codeword_in_mode_i16(FN3, 0),
);
assert_eq!(
lookup_codeword_in_mode_i16(-1, 0),
None,
"unknown sentinel -1 → None"
);
for (b, col) in [(b'A', 0), (b'A', 1), (b'A', 2), (b'a', 1), (b'0', 0)] {
let direct = lookup_codeword_in_mode(b, col);
let via_i16 = lookup_codeword_in_mode_i16(i16::from(b), col);
assert_eq!(
direct, via_i16,
"delegation: lookup({b:?}, {col}) must match i16 path"
);
}
}
#[test]
fn enc_a_step_and_enc_b_step_advance_and_push_correct_codeword() {
let mut cws: Vec<u16> = Vec::new();
let mut i: usize = 0;
let mut mode = Mode::C;
let msg = b"A";
enc_a_step(msg, &mut i, &mut mode, &mut cws);
let want_a = lookup_codeword_in_mode(b'A', 0).expect("'A' encodable in mode A");
assert_eq!(cws, vec![want_a], "enc_a_step must push lookup result");
assert_eq!(i, 1, "enc_a_step must advance i by exactly 1");
let mut cws: Vec<u16> = Vec::new();
let mut i: usize = 0;
let mut mode = Mode::C;
let msg = b"a";
enc_b_step(msg, &mut i, &mut mode, &mut cws);
let want_b = lookup_codeword_in_mode(b'a', 1).expect("'a' encodable in mode B");
assert_eq!(cws, vec![want_b], "enc_b_step must push lookup result");
assert_eq!(i, 1);
let mut cws: Vec<u16> = Vec::new();
let mut i: usize = 1;
let mut mode = Mode::C;
let msg = b"XA";
enc_a_step(msg, &mut i, &mut mode, &mut cws);
let want_a_at_1 = lookup_codeword_in_mode(b'A', 0).expect("'A' encodable in mode A");
assert_eq!(
cws,
vec![want_a_at_1],
"enc_a_step at i=1 must read msg[1]='A'"
);
assert_eq!(i, 2);
let lookup_a_in_mode_a = lookup_codeword_in_mode(b'A', 0);
let lookup_lower_in_mode_b = lookup_codeword_in_mode(b'a', 1);
assert_ne!(
lookup_a_in_mode_a, lookup_lower_in_mode_b,
"'A' in mode A and 'a' in mode B must produce different codewords"
);
let mut cws: Vec<u16> = Vec::new();
let mut i: usize = 0;
let mut mode = Mode::C;
let msg = b"ABC";
enc_a_step(msg, &mut i, &mut mode, &mut cws);
enc_a_step(msg, &mut i, &mut mode, &mut cws);
enc_a_step(msg, &mut i, &mut mode, &mut cws);
assert_eq!(cws.len(), 3, "3 enc_a_step calls must push 3 codewords");
assert_eq!(i, 3, "3 enc_a_step calls must advance i to 3");
assert_eq!(
cws,
vec![
lookup_codeword_in_mode(b'A', 0).unwrap(),
lookup_codeword_in_mode(b'B', 0).unwrap(),
lookup_codeword_in_mode(b'C', 0).unwrap(),
],
"enc_a_step pushes per-byte codewords in order"
);
}
#[test]
fn apply_rs_ecc_with_leading_structure_and_sensitivity() {
let empty_ecc = apply_rs_ecc(&[]);
assert_eq!(empty_ecc.len(), 3, "apply_rs_ecc(empty): nc = 0/2 + 3 = 3");
let data = [1u16, 2];
let ecc = apply_rs_ecc(&data);
assert_eq!(ecc.len(), 6, "data.len=2 → out.len = 2 + 4 = 6");
assert_eq!(&ecc[..2], &data[..], "prefix is data verbatim");
let data6 = [10u16, 20, 30, 40, 50, 60];
let ecc6 = apply_rs_ecc(&data6);
assert_eq!(ecc6.len(), 12, "data.len=6 → nc=6, out.len=12");
assert_eq!(&ecc6[..6], &data6[..], "6-element prefix");
let data = [42u16, 100, 7];
let ecc_default = apply_rs_ecc(&data);
let ecc_zero = apply_rs_ecc_with_leading(0, &data);
assert_eq!(
ecc_default, ecc_zero,
"apply_rs_ecc delegates to apply_rs_ecc_with_leading(0, …)"
);
let data = [5u16, 10, 15];
let ecc_0 = apply_rs_ecc_with_leading(0, &data);
let ecc_1 = apply_rs_ecc_with_leading(1, &data);
let ecc_2 = apply_rs_ecc_with_leading(2, &data);
let ecc_3 = apply_rs_ecc_with_leading(3, &data);
assert_eq!(&ecc_0[..3], &data[..]);
assert_eq!(&ecc_1[..3], &data[..]);
assert_eq!(&ecc_2[..3], &data[..]);
assert_eq!(&ecc_3[..3], &data[..]);
let suf_0 = &ecc_0[3..];
let suf_1 = &ecc_1[3..];
let suf_2 = &ecc_2[3..];
let suf_3 = &ecc_3[3..];
assert_ne!(suf_0, suf_1, "leading 0 vs 1 must produce different ECC");
assert_ne!(suf_0, suf_2);
assert_ne!(suf_0, suf_3);
assert_ne!(suf_1, suf_2);
assert_ne!(suf_1, suf_3);
assert_ne!(suf_2, suf_3);
for &v in &ecc_3[3..] {
assert!(v < 113, "ECC codeword {v} must be < 113 (GF(113))");
}
for n in [0usize, 1, 2, 3, 5, 8, 13, 20] {
let d: Vec<u16> = (0..n as u16).collect();
let ecc = apply_rs_ecc(&d);
let expected_nc = n / 2 + 3;
assert_eq!(
ecc.len(),
n + expected_nc,
"data.len={n} → out.len = n + (n/2+3) = {n} + {expected_nc}"
);
assert_eq!(&ecc[..n], &d[..], "prefix preserved for n={n}");
}
}
#[test]
fn parse_dotcode_input_parsefnc_off_and_escape_sequences() {
assert_eq!(
parse_dotcode_input(b"hello", false).unwrap(),
vec![104i16, 101, 108, 108, 111],
"parsefnc=off: bytes pass through as i16"
);
assert_eq!(
parse_dotcode_input(b"a^b", false).unwrap(),
vec![97i16, 94, 98],
"parsefnc=off: '^' is byte 94"
);
assert_eq!(
parse_dotcode_input(b"hello", true).unwrap(),
vec![104i16, 101, 108, 108, 111],
"parsefnc=on, no '^': pass-through"
);
assert_eq!(
parse_dotcode_input(b"^^", true).unwrap(),
vec![94i16],
"^^ → [94] (literal caret)"
);
assert_eq!(
parse_dotcode_input(b"x^^y", true).unwrap(),
vec![120i16, 94, 121],
"x^^y → [120, 94, 121]"
);
assert_eq!(
parse_dotcode_input(b"^FNC1", true).unwrap(),
vec![FN1],
"^FNC1 → [FN1]"
);
assert_eq!(
parse_dotcode_input(b"^FNC3", true).unwrap(),
vec![FN3],
"^FNC3 → [FN3]"
);
assert_eq!(
parse_dotcode_input(b"^FNC1^FNC3", true).unwrap(),
vec![FN1, FN3],
"^FNC1^FNC3 → [FN1, FN3]"
);
let result = parse_dotcode_input(b"^ECI000123", true).unwrap();
assert_eq!(
result,
vec![FN2, 48i16, 48, 48, 49, 50, 51],
"^ECInnnnnn → FN2 + 6 digit bytes"
);
let mixed = parse_dotcode_input(b"AB^FNC1CD", true).unwrap();
assert_eq!(
mixed,
vec![65i16, 66, FN1, 67, 68],
"AB + ^FNC1 + CD → [65, 66, FN1, 67, 68]"
);
match parse_dotcode_input(b"^", true) {
Err(crate::error::Error::InvalidData(msg)) => {
assert!(
msg.contains("DotCode parsefnc:"),
"lone-caret arm: missing `DotCode parsefnc:` prefix: {msg}"
);
assert!(
msg.contains("caret character truncated"),
"lone-caret arm: missing `caret character truncated` predicate: {msg}"
);
}
other => panic!("lone '^' should reject as InvalidData, got {other:?}"),
}
match parse_dotcode_input(b"^A", true) {
Err(crate::error::Error::InvalidData(msg)) => {
assert!(
msg.contains("DotCode parsefnc:"),
"^A arm: missing prefix: {msg}"
);
assert!(
msg.contains("function character truncated"),
"^A arm: missing `function character truncated` predicate: {msg}"
);
assert!(
!msg.contains("caret character truncated"),
"^A arm: lone-caret diagnostic leaked: {msg}"
);
}
other => panic!("^A should reject as InvalidData, got {other:?}"),
}
match parse_dotcode_input(b"^XXXX", true) {
Err(crate::error::Error::InvalidData(msg)) => {
assert!(
msg.contains("DotCode parsefnc:"),
"^XXXX arm: missing prefix: {msg}"
);
assert!(
msg.contains("unknown function character"),
"^XXXX arm: missing predicate: {msg}"
);
assert!(msg.contains("XXXX"), "^XXXX arm: missing tag echo: {msg}");
}
other => panic!("^XXXX should reject as InvalidData, got {other:?}"),
}
match parse_dotcode_input(b"^FNC2", true) {
Err(crate::error::Error::InvalidData(msg)) => {
assert!(
msg.contains("DotCode parsefnc:"),
"^FNC2 arm: missing prefix: {msg}"
);
assert!(
msg.contains("unknown function character"),
"^FNC2 arm: missing predicate: {msg}"
);
assert!(
msg.contains("FNC2"),
"^FNC2 arm: missing tag echo (kills `_ if tag == b\"FNC2\"` mutations that accept FNC2): {msg}"
);
}
other => panic!("^FNC2 should reject as InvalidData, got {other:?}"),
}
match parse_dotcode_input(b"^ECI12345", true) {
Err(crate::error::Error::InvalidData(msg)) => {
assert!(
msg.contains("DotCode parsefnc:"),
"^ECI12345 arm: missing prefix: {msg}"
);
assert!(
msg.contains("ECI truncated"),
"^ECI12345 arm: missing `ECI truncated` predicate: {msg}"
);
assert!(
!msg.contains("000000 to 999999"),
"^ECI12345 arm: non-digit diagnostic leaked: {msg}"
);
}
other => panic!("^ECI12345 should reject as InvalidData, got {other:?}"),
}
match parse_dotcode_input(b"^ECI00012A", true) {
Err(crate::error::Error::InvalidData(msg)) => {
assert!(
msg.contains("DotCode parsefnc:"),
"^ECI00012A arm: missing prefix: {msg}"
);
assert!(
msg.contains("ECI must be 000000 to 999999"),
"^ECI00012A arm: missing full ECI range predicate: {msg}"
);
assert!(
!msg.contains("ECI truncated"),
"^ECI00012A arm: truncated diagnostic leaked: {msg}"
);
}
other => panic!("^ECI00012A should reject as InvalidData, got {other:?}"),
}
match parse_dotcode_input(b"^ECIabcdef", true) {
Err(crate::error::Error::InvalidData(msg)) => {
assert!(
msg.contains("DotCode parsefnc:"),
"^ECIabcdef arm: missing prefix: {msg}"
);
assert!(
msg.contains("ECI must be 000000 to 999999"),
"^ECIabcdef arm: missing full ECI range predicate: {msg}"
);
}
other => panic!("^ECIabcdef should reject as InvalidData, got {other:?}"),
}
assert!(
parse_dotcode_input(b"", false).unwrap().is_empty(),
"empty + parsefnc=off → empty"
);
assert!(
parse_dotcode_input(b"", true).unwrap().is_empty(),
"empty + parsefnc=on → empty"
);
assert_eq!(
parse_dotcode_input(&[0xC3, 0xA9], false).unwrap(),
vec![0xC3i16, 0xA9],
"high-bit bytes pass through"
);
}
#[test]
fn crlf_in_mode_c_shift_to_b_does_not_panic() {
let _ = encode(b"-\r\n\x00\x00\x00\x00\xd7\xd5");
let _ = encode(b"1234567890\r\nabc");
let _ = encode(b"\r\n");
let _ = encode(b"00\r\n00\r\n00");
}
}