use crate::encoding::{BitMatrix, LinearPattern};
use crate::error::Error;
use crate::options::Options;
#[cfg(test)]
fn validate_gtin14(data: &str) -> Result<String, Error> {
let trimmed = data.trim();
let body = trimmed
.strip_prefix("(01)")
.or_else(|| trimmed.strip_prefix("01"))
.unwrap_or(trimmed);
if body.len() != 14 || !body.chars().all(|c| c.is_ascii_digit()) {
return Err(Error::InvalidData(format!(
"GS1 DataBar: expected 14 digits (with optional `(01)` AI prefix), got {data:?}"
)));
}
let body_chars: Vec<u32> = body.chars().map(|c| c.to_digit(10).unwrap()).collect();
let mut sum = 0u32;
for (i, &d) in body_chars[..13].iter().rev().enumerate() {
sum += if i % 2 == 0 { d * 3 } else { d };
}
let expected = (10 - sum % 10) % 10;
if expected != body_chars[13] {
return Err(Error::InvalidData(format!(
"GS1 DataBar: GTIN-14 check digit mismatch (got {}, expected {expected})",
body_chars[13]
)));
}
Ok(body.to_string())
}
pub fn encode_omni(data: &str, opts: &Options) -> Result<LinearPattern, Error> {
render_omni(data, opts)
}
#[rustfmt::skip]
const TAB164: &[u32] = &[
160, 0, 12, 4, 8, 1, 161, 1,
960, 161, 10, 6, 6, 3, 80, 10,
2014, 961, 8, 8, 4, 5, 31, 34,
2714, 2015, 6, 10, 3, 6, 10, 70,
2840, 2715, 4, 12, 1, 8, 1, 126,
];
#[rustfmt::skip]
const TAB154: &[u32] = &[
335, 0, 5, 10, 2, 7, 4, 84,
1035, 336, 7, 8, 4, 5, 20, 35,
1515, 1036, 9, 6, 6, 3, 48, 10,
1596, 1516, 11, 4, 8, 1, 81, 1,
];
#[rustfmt::skip]
const CHECK_WEIGHTS: &[u32] = &[
1, 3, 9, 27, 2, 6, 18, 54, 58, 72, 24, 8, 29, 36, 12, 4,
74, 51, 17, 32, 37, 65, 48, 16, 64, 34, 23, 69, 49, 68, 46, 59,
];
#[rustfmt::skip]
const FINDER_WIDTHS: &[u8] = &[
3, 8, 2, 1, 1,
3, 5, 5, 1, 1,
3, 3, 7, 1, 1,
3, 1, 9, 1, 1,
2, 7, 4, 1, 1,
2, 5, 6, 1, 1,
2, 3, 8, 1, 1,
1, 5, 7, 1, 1,
1, 3, 9, 1, 1,
];
pub(super) fn get_rss_widths(mut val: i64, mut nm: i64, mw: i64, el: i64, oe: bool) -> Vec<u8> {
let el_usize = el as usize;
let mut out = vec![0u8; el_usize];
let mut mask: u64 = 0;
for bar in 0..(el - 1) {
let mut ew: i64 = 1;
mask |= 1u64 << bar;
let mut sval;
loop {
sval = ncr_bwipp(nm - ew - 1, el - bar - 2);
if oe && mask == 0 && (nm - ew - el * 2 + bar * 2) >= -2 {
sval -= ncr_bwipp(nm - ew - el + bar, el - bar - 2);
}
if (el - bar) > 2 {
let mut lval: i64 = 0;
let mut k = nm - ew - el + bar + 2;
while k > mw {
lval += ncr_bwipp(nm - k - ew - 1, el - bar - 3);
k -= 1;
}
sval -= lval * (el - bar - 1);
} else if (nm - ew) > mw {
sval -= 1;
}
val -= sval;
if val < 0 {
break;
}
ew += 1;
mask &= !(1u64 << bar);
}
val += sval;
nm -= ew;
out[bar as usize] = ew as u8;
}
out[el_usize - 1] = nm as u8;
out
}
fn ncr_bwipp(n: i64, r: i64) -> i64 {
let v = r.max(n - r);
let smaller = r.min(n - r);
let mut product: i64 = 1;
let mut counter: i64 = 1;
let mut k = n;
while k > v {
product *= k;
if counter <= smaller {
product /= counter;
counter += 1;
}
k -= 1;
}
while counter <= smaller {
product /= counter;
counter += 1;
}
product
}
fn lookup_group(tab: &[u32], d: u32) -> Option<[u32; 7]> {
let mut i = 0;
while i < tab.len() {
if d <= tab[i] {
let mut g = [0u32; 7];
g.copy_from_slice(&tab[i + 1..i + 8]);
return Some(g);
}
i += 8;
}
None
}
pub(crate) fn omni_widths(data: &str) -> Result<([u8; 32], u32), Error> {
omni_widths_with_linkage(data, false)
}
pub(crate) fn omni_widths_with_linkage(
data: &str,
linkage: bool,
) -> Result<([u8; 32], u32), Error> {
let body = validate_gtin14_or_13(data)?;
debug_assert_eq!(body.len(), 13);
let mut binval: [u32; 14] = [0; 14];
binval[0] = u32::from(linkage);
for (i, b) in body.bytes().enumerate() {
binval[i + 1] = (b - b'0') as u32;
}
let modulus: u64 = 4_537_077;
for i in 0..13 {
let next = binval[i + 1] as u64 + (binval[i] as u64 % modulus) * 10;
binval[i + 1] = next as u32;
binval[i] = (binval[i] as u64 / modulus) as u32;
}
let right: u32 = binval[13] % modulus as u32;
binval[13] = (binval[13] as u64 / modulus) as u32;
let mut left: u64 = 0;
let mut first = true;
for (j, &val) in binval.iter().enumerate() {
if val == 0 && first {
continue;
}
first = false;
left += val as u64 * 10u64.pow((13 - j) as u32);
}
let left = left as u32;
let d1 = left / 1597;
let d2 = left % 1597;
let d3 = right / 1597;
let d4 = right % 1597;
let g1 =
lookup_group(TAB164, d1).ok_or_else(|| Error::InvalidData("d1 out of tab164".into()))?;
let g2 =
lookup_group(TAB154, d2).ok_or_else(|| Error::InvalidData("d2 out of tab154".into()))?;
let g3 =
lookup_group(TAB164, d3).ok_or_else(|| Error::InvalidData("d3 out of tab164".into()))?;
let g4 =
lookup_group(TAB154, d4).ok_or_else(|| Error::InvalidData("d4 out of tab154".into()))?;
let (d1gs, d1elo, d1ele, d1mwo, d1mwe, _d1to, d1te) =
(g1[0], g1[1], g1[2], g1[3], g1[4], g1[5], g1[6]);
let (d2gs, d2elo, d2ele, d2mwo, d2mwe, d2to, _d2te) =
(g2[0], g2[1], g2[2], g2[3], g2[4], g2[5], g2[6]);
let (d3gs, d3elo, d3ele, d3mwo, d3mwe, _d3to, d3te) =
(g3[0], g3[1], g3[2], g3[3], g3[4], g3[5], g3[6]);
let (d4gs, d4elo, d4ele, d4mwo, d4mwe, d4to, _d4te) =
(g4[0], g4[1], g4[2], g4[3], g4[4], g4[5], g4[6]);
let d1_val = (d1 - d1gs) as i64;
let d2_val = (d2 - d2gs) as i64;
let d3_val = (d3 - d3gs) as i64;
let d4_val = (d4 - d4gs) as i64;
let d1wo = get_rss_widths(d1_val / d1te as i64, d1elo as i64, d1mwo as i64, 4, false);
let d1we = get_rss_widths(d1_val % d1te as i64, d1ele as i64, d1mwe as i64, 4, true);
let d2wo = get_rss_widths(d2_val % d2to as i64, d2elo as i64, d2mwo as i64, 4, true);
let d2we = get_rss_widths(d2_val / d2to as i64, d2ele as i64, d2mwe as i64, 4, false);
let d3wo = get_rss_widths(d3_val / d3te as i64, d3elo as i64, d3mwo as i64, 4, false);
let d3we = get_rss_widths(d3_val % d3te as i64, d3ele as i64, d3mwe as i64, 4, true);
let d4wo = get_rss_widths(d4_val % d4to as i64, d4elo as i64, d4mwo as i64, 4, true);
let d4we = get_rss_widths(d4_val / d4to as i64, d4ele as i64, d4mwe as i64, 4, false);
let mut widths = [0u8; 32];
for i in 0..4 {
widths[i * 2] = d1wo[i];
widths[i * 2 + 1] = d1we[i];
widths[8 + 7 - i * 2] = d2wo[i];
widths[8 + 6 - i * 2] = d2we[i];
widths[16 + 7 - i * 2] = d3wo[i];
widths[16 + 6 - i * 2] = d3we[i];
widths[24 + i * 2] = d4wo[i];
widths[24 + i * 2 + 1] = d4we[i];
}
let mut csum: u32 = 0;
for i in 0..32 {
csum += widths[i] as u32 * CHECK_WEIGHTS[i];
}
let mut csum = csum % 79;
if csum >= 8 {
csum += 1;
}
if csum >= 72 {
csum += 1;
}
Ok((widths, csum))
}
pub(crate) fn omni_sbs_with_linkage(data: &str, linkage: bool) -> Result<[u8; 45], Error> {
let (widths, csum) = omni_widths_with_linkage(data, linkage)?;
Ok(omni_sbs(&widths, csum))
}
fn omni_sbs(widths: &[u8; 32], csum: u32) -> [u8; 45] {
let (d1w, rest) = widths.split_at(8);
let (d2w, rest) = rest.split_at(8);
let (d3w, d4w) = rest.split_at(8);
let checklt_start = (csum as usize / 9) * 5;
let checkrt_start = (csum as usize % 9) * 5;
let checklt = &FINDER_WIDTHS[checklt_start..checklt_start + 5];
let mut checkrt = [0u8; 5];
for (i, slot) in checkrt.iter_mut().enumerate() {
*slot = FINDER_WIDTHS[checkrt_start + 4 - i];
}
let mut sbs = [0u8; 45];
sbs[0] = 1; sbs[1..9].copy_from_slice(d1w);
sbs[9..14].copy_from_slice(checklt);
sbs[14..22].copy_from_slice(d2w);
sbs[22..30].copy_from_slice(d4w);
sbs[30..35].copy_from_slice(&checkrt);
sbs[35..43].copy_from_slice(d3w);
sbs[43] = 1; sbs[44] = 1; sbs
}
pub(super) fn expand_pairs_to_modules(widths: &[u8], start_bar: bool) -> Vec<u8> {
let total: usize = widths.iter().map(|&w| w as usize).sum();
let mut out = Vec::with_capacity(total);
let polarity = u8::from(start_bar);
for (i, &w) in widths.iter().enumerate() {
let bit = ((i % 2) as u8) ^ polarity;
for _ in 0..w {
out.push(bit);
}
}
out
}
pub(crate) fn stacked_top_bot(widths: &[u8; 32], csum: u32) -> ([u8; 50], [u8; 50]) {
let (d1w, rest) = widths.split_at(8);
let (d2w, rest) = rest.split_at(8);
let (d3w, d4w) = rest.split_at(8);
let checklt_start = (csum as usize / 9) * 5;
let checkrt_start = (csum as usize % 9) * 5;
let checklt = &FINDER_WIDTHS[checklt_start..checklt_start + 5];
let mut checkrt = [0u8; 5];
for (i, slot) in checkrt.iter_mut().enumerate() {
*slot = FINDER_WIDTHS[checkrt_start + 4 - i];
}
let mut top_widths = [0u8; 26];
top_widths[0] = 1;
top_widths[1] = 1;
top_widths[2..10].copy_from_slice(d1w);
top_widths[10..15].copy_from_slice(checklt);
top_widths[15..23].copy_from_slice(d2w);
top_widths[23] = 1;
top_widths[24] = 1;
top_widths[25] = 0;
let mut bot_widths = [0u8; 26];
bot_widths[0] = 1;
bot_widths[1] = 1;
bot_widths[2..10].copy_from_slice(d4w);
bot_widths[10..15].copy_from_slice(&checkrt);
bot_widths[15..23].copy_from_slice(d3w);
bot_widths[23] = 1;
bot_widths[24] = 1;
bot_widths[25] = 0;
let top_vec = expand_pairs_to_modules(&top_widths, false);
let bot_vec = expand_pairs_to_modules(&bot_widths, true);
debug_assert_eq!(top_vec.len(), 50);
debug_assert_eq!(bot_vec.len(), 50);
let mut top = [0u8; 50];
let mut bot = [0u8; 50];
top.copy_from_slice(&top_vec);
bot.copy_from_slice(&bot_vec);
(top, bot)
}
pub(crate) fn stacked_sep(top: &[u8; 50], bot: &[u8; 50]) -> [u8; 50] {
let mut sep = [0u8; 50];
for i in 1..50 {
sep[i] = if top[i] == bot[i] {
1 - top[i]
} else {
1 - sep[i - 1]
};
}
for slot in &mut sep[0..4] {
*slot = 0;
}
for slot in &mut sep[46..50] {
*slot = 0;
}
sep
}
pub fn encode_stacked(data: &str, _opts: &Options) -> Result<BitMatrix, Error> {
let (widths, csum) = omni_widths(data)?;
let (top, bot) = stacked_top_bot(&widths, csum);
let sep = stacked_sep(&top, &bot);
let mut bm = BitMatrix::new(50, 13);
paint_module_rows(&mut bm, &[(&top, 5), (&sep, 1), (&bot, 7)]);
Ok(bm)
}
pub(crate) type StackedOmniRows = ([u8; 50], [u8; 50], [u8; 50], [u8; 50], [u8; 50]);
pub(crate) fn stackedomni_logical_rows(
data: &str,
linkage: bool,
) -> Result<StackedOmniRows, Error> {
let (widths, csum) = omni_widths_with_linkage(data, linkage)?;
let (top, bot) = stacked_top_bot(&widths, csum);
let mut sep1 = [0u8; 50];
for i in 0..50 {
sep1[i] = 1 - top[i];
}
for slot in &mut sep1[0..4] {
*slot = 0;
}
for slot in &mut sep1[46..50] {
*slot = 0;
}
for i in 18..=30 {
sep1[i] = u8::from(top[i] == 0 && (top[i - 1] == 1 || sep1[i - 1] == 0));
}
let mut sep2 = [0u8; 50];
for i in 0..21 {
sep2[4 + i * 2 + 1] = 1;
}
let mut sep3 = [0u8; 50];
for i in 0..50 {
sep3[i] = 1 - bot[i];
}
for slot in &mut sep3[0..4] {
*slot = 0;
}
for slot in &mut sep3[46..50] {
*slot = 0;
}
for i in 19..=31 {
sep3[i] = u8::from(bot[i] == 0 && (bot[i - 1] == 1 || sep3[i - 1] == 0));
}
const F3PAT: [u8; 13] = [1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1];
const FINDERSEP: [u8; 13] = [0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0];
if bot[19..32] == F3PAT {
sep3[19..32].copy_from_slice(&FINDERSEP);
}
Ok((top, sep1, sep2, sep3, bot))
}
pub fn encode_stackedomni(data: &str, _opts: &Options) -> Result<BitMatrix, Error> {
let (top, sep1, sep2, sep3, bot) = stackedomni_logical_rows(data, false)?;
let mut bm = BitMatrix::new(50, 69);
paint_module_rows(
&mut bm,
&[(&top, 33), (&sep1, 1), (&sep2, 1), (&sep3, 1), (&bot, 33)],
);
Ok(bm)
}
fn paint_module_rows(bm: &mut BitMatrix, rows: &[(&[u8; 50], usize)]) {
let mut y = 0;
for &(modules, mult) in rows {
for _ in 0..mult {
for (x, &bit) in modules.iter().enumerate() {
if bit != 0 {
bm.set(x, y, true);
}
}
y += 1;
}
}
debug_assert_eq!(y, bm.height());
}
pub fn render_omni(data: &str, opts: &Options) -> Result<LinearPattern, Error> {
let linkage = check_databaromni_opts(opts)?;
let (widths, csum) = omni_widths_with_linkage(data, linkage)?;
let sbs = omni_sbs(&widths, csum);
let mut bars = Vec::with_capacity(sbs.len() + 1);
bars.push(0);
bars.extend_from_slice(&sbs);
let body = validate_gtin14_or_13(data)?;
let full = format!("(01){body}{}", gtin14_check_digit(&body));
Ok(LinearPattern {
bars,
text: Some(full),
})
}
fn gtin14_check_digit(body_13: &str) -> char {
let mut sum = 0u32;
for (i, c) in body_13.chars().enumerate() {
let d = c.to_digit(10).unwrap_or(0);
sum += if i % 2 == 0 { d * 3 } else { d };
}
let check = (10 - sum % 10) % 10;
char::from_digit(check, 10).unwrap_or('0')
}
fn validate_gtin14_or_13(data: &str) -> Result<String, Error> {
let trimmed = data.trim();
let body = trimmed
.strip_prefix("(01)")
.or_else(|| trimmed.strip_prefix("01"))
.unwrap_or(trimmed);
if !body.chars().all(|c| c.is_ascii_digit()) {
return Err(Error::InvalidData(format!(
"GS1 DataBar Omnidirectional: non-digit in payload {data:?}"
)));
}
let chars: Vec<u32> = body.chars().map(|c| c.to_digit(10).unwrap()).collect();
let body_13: Vec<u32> = match chars.len() {
13 => chars,
14 => {
let mut sum = 0u32;
for (i, &d) in chars[..13].iter().enumerate() {
sum += if i % 2 == 0 { d * 3 } else { d };
}
let expected = (10 - sum % 10) % 10;
if expected != chars[13] {
return Err(Error::InvalidData(format!(
"GS1 DataBar: GTIN-14 check digit mismatch (got {}, expected {expected})",
chars[13]
)));
}
chars[..13].to_vec()
}
_ => {
return Err(Error::InvalidData(format!(
"GS1 DataBar Omnidirectional: expected 13 or 14 digits, got {}",
body.len()
)))
}
};
Ok(body_13
.into_iter()
.map(|d| char::from_digit(d, 10).unwrap())
.collect())
}
pub fn encode_truncated(data: &str, opts: &Options) -> Result<LinearPattern, Error> {
render_omni(data, opts)
}
#[rustfmt::skip]
const LIMITED_TAB267: &[u32] = &[
183063, 0, 17, 9, 6, 3, 6538, 28,
820063, 183064, 13, 13, 5, 4, 875, 728,
1000775, 820064, 9, 17, 3, 6, 28, 6454,
1491020, 1000776, 15, 11, 5, 4, 2415, 203,
1979844, 1491021, 11, 15, 4, 5, 203, 2408,
1996938, 1979845, 19, 7, 8, 1, 17094, 1,
2013570, 1996939, 7, 19, 1, 8, 1, 16632,
];
#[rustfmt::skip]
const LIMITED_CHECK_WEIGHTS: &[u32] = &[
1, 3, 9, 27, 81, 65, 17, 51, 64, 14, 42, 37, 22, 66,
20, 60, 2, 6, 18, 54, 73, 41, 34, 13, 39, 28, 84, 74,
];
#[rustfmt::skip]
const LIMITED_CHECK_SEQ: &[u32] = &[
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
42, 43, 45, 52, 57, 63, 64, 65, 66, 73, 74, 75, 76, 77,
78, 79, 82, 126, 127, 128, 129, 130, 132, 141, 142, 143,
144, 145, 146, 210, 211, 212, 213, 214, 215, 216, 217, 220,
316, 317, 318, 319, 320, 322, 323, 326, 337,
];
fn lookup_group_limited(d: u32) -> Option<[u32; 7]> {
let tab = LIMITED_TAB267;
let mut i = 0;
while i < tab.len() {
if d <= tab[i] {
let mut g = [0u32; 7];
g.copy_from_slice(&tab[i + 1..i + 8]);
return Some(g);
}
i += 8;
}
None
}
const LIMITED_LINKVAL: [u32; 13] = [2, 0, 1, 5, 1, 3, 3, 5, 3, 1, 0, 9, 6];
pub(crate) fn limited_widths_with_linkage(
data: &str,
linkage: bool,
) -> Result<([u8; 28], [u8; 14], u32), Error> {
let body = validate_gtin14_or_13(data)?;
if !matches!(body.as_bytes()[0], b'0' | b'1') {
return Err(Error::InvalidData(
"GS1 DataBar Limited must begin with 0 or 1".into(),
));
}
debug_assert_eq!(body.len(), 13);
let mut binval: [u32; 13] = [0; 13];
for (i, b) in body.bytes().enumerate() {
binval[i] = (b - b'0') as u32;
}
if linkage {
for (i, slot) in binval.iter_mut().enumerate() {
*slot += LIMITED_LINKVAL[i];
}
}
let modulus: u64 = 2_013_571;
for i in 0..12 {
let next = binval[i + 1] as u64 + (binval[i] as u64 % modulus) * 10;
binval[i + 1] = next as u32;
binval[i] = (binval[i] as u64 / modulus) as u32;
}
let d2 = binval[12] % modulus as u32;
binval[12] = (binval[12] as u64 / modulus) as u32;
let mut d1: u64 = 0;
let mut first = true;
for (j, &val) in binval.iter().enumerate() {
if val == 0 && first {
continue;
}
first = false;
d1 += val as u64 * 10u64.pow((12 - j) as u32);
}
let d1 = d1 as u32;
let g1 =
lookup_group_limited(d1).ok_or_else(|| Error::InvalidData("d1 out of tab267".into()))?;
let g2 =
lookup_group_limited(d2).ok_or_else(|| Error::InvalidData("d2 out of tab267".into()))?;
let (d1gs, d1elo, d1ele, d1mwo, d1mwe, _d1to, d1te) =
(g1[0], g1[1], g1[2], g1[3], g1[4], g1[5], g1[6]);
let (d2gs, d2elo, d2ele, d2mwo, d2mwe, _d2to, d2te) =
(g2[0], g2[1], g2[2], g2[3], g2[4], g2[5], g2[6]);
let d1_off = (d1 - d1gs) as i64;
let d2_off = (d2 - d2gs) as i64;
let d1wo = get_rss_widths(d1_off / d1te as i64, d1elo as i64, d1mwo as i64, 7, false);
let d1we = get_rss_widths(d1_off % d1te as i64, d1ele as i64, d1mwe as i64, 7, true);
let d2wo = get_rss_widths(d2_off / d2te as i64, d2elo as i64, d2mwo as i64, 7, false);
let d2we = get_rss_widths(d2_off % d2te as i64, d2ele as i64, d2mwe as i64, 7, true);
let mut d1w = [0u8; 14];
let mut d2w = [0u8; 14];
for i in 0..7 {
d1w[i * 2] = d1wo[i];
d1w[i * 2 + 1] = d1we[i];
d2w[i * 2] = d2wo[i];
d2w[i * 2 + 1] = d2we[i];
}
let mut widths = [0u8; 28];
widths[..14].copy_from_slice(&d1w);
widths[14..].copy_from_slice(&d2w);
let mut csum: u32 = 0;
for i in 0..28 {
csum += widths[i] as u32 * LIMITED_CHECK_WEIGHTS[i];
}
let csum_mod = csum % 89;
let seq = LIMITED_CHECK_SEQ[csum_mod as usize];
let swidths = get_rss_widths((seq / 21) as i64, 8, 3, 6, false);
let bwidths = get_rss_widths((seq % 21) as i64, 8, 3, 6, false);
let mut checkwidths = [0u8; 14];
for i in 0..6 {
checkwidths[i * 2] = swidths[i];
checkwidths[i * 2 + 1] = bwidths[i];
}
checkwidths[12] = 1;
checkwidths[13] = 1;
let mut full = [0u8; 28];
full[..14].copy_from_slice(&d1w);
full[14..].copy_from_slice(&d2w);
Ok((full, checkwidths, csum_mod))
}
fn limited_sbs(widths: &[u8; 28], checkwidths: &[u8; 14]) -> [u8; 46] {
let mut sbs = [0u8; 46];
sbs[0] = 1;
sbs[1..15].copy_from_slice(&widths[..14]);
sbs[15..29].copy_from_slice(checkwidths);
sbs[29..43].copy_from_slice(&widths[14..]);
sbs[43] = 1;
sbs[44] = 1;
sbs[45] = 5;
sbs
}
fn check_databaromni_opts(opts: &Options) -> Result<bool, Error> {
if let Some(v) = opts.get("linkage") {
return match v {
"false" => Ok(false),
"true" => Ok(true),
_ => Err(Error::InvalidOption(format!(
"databaromni: linkage={v:?} must be \"true\" or \"false\""
))),
};
}
Ok(false)
}
fn check_databarlimited_opts(opts: &Options) -> Result<bool, Error> {
if let Some(v) = opts.get("linkage") {
return match v {
"false" => Ok(false),
"true" => Ok(true),
_ => Err(Error::InvalidOption(format!(
"databarlimited: linkage={v:?} must be \"true\" or \"false\""
))),
};
}
Ok(false)
}
pub fn encode_limited(data: &str, opts: &Options) -> Result<LinearPattern, Error> {
let linkage = check_databarlimited_opts(opts)?;
let (widths, checkwidths, _csum) = limited_widths_with_linkage(data, linkage)?;
let sbs = limited_sbs(&widths, &checkwidths);
let mut bars = Vec::with_capacity(sbs.len() + 1);
bars.push(0);
bars.extend_from_slice(&sbs);
let body = validate_gtin14_or_13(data)?;
let full = format!("(01){body}{}", gtin14_check_digit(&body));
Ok(LinearPattern {
bars,
text: Some(full),
})
}
pub(crate) fn limited_sbs_with_linkage(data: &str, linkage: bool) -> Result<[u8; 46], Error> {
let (widths, checkwidths, _csum) = limited_widths_with_linkage(data, linkage)?;
Ok(limited_sbs(&widths, &checkwidths))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stackedomni_2nd_golden_fingerprint_pinned() {
let bm = encode_stackedomni("(01)00000000000383", &Options::default())
.expect("encode_stackedomni((01)00000000000383, default) must succeed");
assert_eq!(bm.width(), 50);
assert_eq!(bm.height(), 69);
let want_top = "01010100100000000100011111000001011111110010101010";
let want_sep1 = "00001011011111111010100000101010100000001101010000";
let want_sep2 = "00000101010101010101010101010101010101010101010000";
let want_sep3 = "00000011011101111010010101010000100000000100100000";
let want_bot = "10101100100010000101100000000111011111111011010101";
for y in 0..33 {
assert_eq!(row_of(&bm, y), want_top, "top row {y} mismatch");
}
assert_eq!(row_of(&bm, 33), want_sep1, "sep1 row");
assert_eq!(row_of(&bm, 34), want_sep2, "sep2 row");
assert_eq!(
row_of(&bm, 35),
want_sep3,
"sep3 row (kills L543 bot[i-1]->bot[i+1])"
);
for y in 36..69 {
assert_eq!(row_of(&bm, y), want_bot, "bot row {y} mismatch");
}
}
#[test]
fn stackedomni_separator_div_mutants_are_equivalent() {
fn sep1_orig(top: &[u8; 50]) -> [u8; 50] {
let mut s = [0u8; 50];
for i in 0..50 {
s[i] = 1 - top[i];
}
for x in &mut s[0..4] {
*x = 0;
}
for x in &mut s[46..50] {
*x = 0;
}
for i in 18..=30 {
s[i] = u8::from(top[i] == 0 && (top[i - 1] == 1 || s[i - 1] == 0));
}
s
}
fn sep1_mut(top: &[u8; 50]) -> [u8; 50] {
let mut s = [0u8; 50];
for i in 0..50 {
s[i] = 1 - top[i];
}
for x in &mut s[0..4] {
*x = 0;
}
for x in &mut s[46..50] {
*x = 0;
}
for i in 18..=30 {
s[i] = u8::from(top[i] == 0 && (top[i] == 1 || s[i - 1] == 0));
}
s
}
fn sep3_orig(bot: &[u8; 50]) -> [u8; 50] {
let mut s = [0u8; 50];
for i in 0..50 {
s[i] = 1 - bot[i];
}
for x in &mut s[0..4] {
*x = 0;
}
for x in &mut s[46..50] {
*x = 0;
}
for i in 19..=31 {
s[i] = u8::from(bot[i] == 0 && (bot[i - 1] == 1 || s[i - 1] == 0));
}
s
}
fn sep3_mut(bot: &[u8; 50]) -> [u8; 50] {
let mut s = [0u8; 50];
for i in 0..50 {
s[i] = 1 - bot[i];
}
for x in &mut s[0..4] {
*x = 0;
}
for x in &mut s[46..50] {
*x = 0;
}
for i in 19..=31 {
s[i] = u8::from(bot[i] == 0 && (bot[i] == 1 || s[i - 1] == 0));
}
s
}
for n in 0u64..100_000 {
let body = format!("{n:013}");
let cd = gtin14_check_digit(&body);
let gtin = format!("{body}{cd}");
let (widths, csum) = omni_widths_with_linkage(>in, false).unwrap();
let (top, bot) = stacked_top_bot(&widths, csum);
let s1 = sep1_orig(&top);
let s3 = sep3_orig(&bot);
for k in 0..50 {
assert!(
!(s1[k] == 1 && top[k] == 1),
"sep1 invariant broken at k={k} for {gtin}"
);
assert!(
!(s3[k] == 1 && bot[k] == 1),
"sep3 invariant broken at k={k} for {gtin}"
);
}
assert_eq!(
s1,
sep1_mut(&top),
"sep1 L519 /1 mutant diverged for {gtin}"
);
assert_eq!(
s3,
sep3_mut(&bot),
"sep3 L543 /1 mutant diverged for {gtin}"
);
}
}
#[test]
fn databar_equivalence_notes() {
fn ncr_exact(n: i64, r: i64) -> i64 {
if r > n || r <= 0 {
return 1;
}
let r = r.min(n - r);
let mut num: i128 = 1;
let mut den: i128 = 1;
for i in 0..r {
num *= (n - i) as i128;
den *= (i + 1) as i128;
}
(num / den) as i64
}
for n in 0..=26i64 {
for r in 0..=n {
assert_eq!(
ncr_bwipp(n, r),
ncr_exact(n, r),
"ncr_bwipp reordering changed result at n={n} r={r}"
);
let v = r.max(n - r);
let smaller = r.min(n - r);
let mut counter: i64 = 1;
let mut k = n;
let mut max_prod: i128 = 1;
let mut prod: i128 = 1;
while k > v {
prod *= k as i128;
if prod > max_prod {
max_prod = prod;
}
if counter <= smaller {
prod /= counter as i128;
counter += 1;
}
k -= 1;
}
assert!(
counter > smaller,
"ncr_bwipp trailing loop is reachable at n={n} r={r} (counter={counter})"
);
assert!(
max_prod < i64::MAX as i128,
"ncr_bwipp intermediate product overflows i64 at n={n} r={r}: {max_prod}"
);
}
}
fn readout_orig(binval: &[u32], pow_base: usize) -> u64 {
let mut acc: u64 = 0;
let mut first = true;
for (j, &val) in binval.iter().enumerate() {
if val == 0 && first {
continue;
}
first = false;
acc += val as u64 * 10u64.pow((pow_base - j) as u32);
}
acc
}
fn readout_mut(binval: &[u32], pow_base: usize) -> u64 {
let mut acc: u64 = 0;
let mut first = true;
for (j, &val) in binval.iter().enumerate() {
if val != 0 && first {
continue;
}
first = false;
acc += val as u64 * 10u64.pow((pow_base - j) as u32);
}
acc
}
for n in 0u64..50_000 {
let body = format!("{n:013}");
let cd = gtin14_check_digit(&body);
let gtin = format!("{body}{cd}");
let mut binval: [u32; 14] = [0; 14];
for (i, b) in body.bytes().enumerate() {
binval[i + 1] = (b - b'0') as u32;
}
let modulus: u64 = 4_537_077;
for i in 0..13 {
let next = binval[i + 1] as u64 + (binval[i] as u64 % modulus) * 10;
binval[i + 1] = next as u32;
binval[i] = (binval[i] as u64 / modulus) as u32;
}
binval[13] = (binval[13] as u64 / modulus) as u32;
assert_eq!(binval[0], 0, "omni binval[0] != 0 for {gtin}");
assert_eq!(
readout_orig(&binval, 13),
readout_mut(&binval, 13),
"omni L262 == → != read-out diverged for {gtin}"
);
if !matches!(body.as_bytes()[0], b'0' | b'1') {
continue;
}
let mut lbin: [u32; 13] = [0; 13];
for (i, b) in body.bytes().enumerate() {
lbin[i] = (b - b'0') as u32;
}
let lmod: u64 = 2_013_571;
for i in 0..12 {
let next = lbin[i + 1] as u64 + (lbin[i] as u64 % lmod) * 10;
lbin[i + 1] = next as u32;
lbin[i] = (lbin[i] as u64 / lmod) as u32;
}
lbin[12] = (lbin[12] as u64 / lmod) as u32;
assert_eq!(lbin[0], 0, "limited binval[0] != 0 for {gtin}");
assert_eq!(
readout_orig(&lbin, 12),
readout_mut(&lbin, 12),
"limited L803 == → != read-out diverged for {gtin}"
);
}
}
#[test]
fn accepts_canonical_gtin_with_ai_prefix() {
let g = validate_gtin14("(01)24012345678905").expect(
"validate_gtin14(\"(01)24012345678905\") (DataBar canonical AI=01 GTIN-14 with valid mod-10 check; prefix-strip path) must succeed",
);
assert_eq!(g, "24012345678905");
}
#[test]
fn accepts_bare_gtin14() {
let g = validate_gtin14("24012345678905").expect(
"validate_gtin14(\"24012345678905\") (DataBar bare 14-digit GTIN-14 without AI=01 prefix; must accept as-is) must succeed",
);
assert_eq!(g, "24012345678905");
}
#[test]
fn rejects_wrong_length() {
match validate_gtin14("(01)1234") {
Err(Error::InvalidData(msg)) => {
assert!(
msg.contains("GS1 DataBar:"),
"missing `GS1 DataBar:` prefix: {msg}"
);
assert!(
msg.contains("expected 14 digits"),
"missing length predicate: {msg}"
);
assert!(
msg.contains("(01)1234"),
"missing input-echo of `(01)1234`: {msg}"
);
assert!(
!msg.contains("check digit mismatch"),
"wrong arm — check-digit diagnostic leaked: {msg}"
);
}
other => panic!("`(01)1234` should reject as InvalidData, got {other:?}"),
}
}
#[test]
fn rejects_bad_check_digit() {
match validate_gtin14("(01)24012345678900") {
Err(Error::InvalidData(msg)) => {
assert!(
msg.contains("GS1 DataBar:"),
"missing `GS1 DataBar:` prefix: {msg}"
);
assert!(
msg.contains("GTIN-14 check digit mismatch"),
"missing check-digit predicate: {msg}"
);
assert!(
msg.contains("got 0"),
"missing supplied-check echo `got 0`: {msg}"
);
assert!(
msg.contains("expected 5"),
"missing computed-check echo `expected 5`: {msg}"
);
assert!(
!msg.contains("expected 14 digits"),
"wrong arm — length diagnostic leaked: {msg}"
);
}
other => panic!("`(01)24012345678900` should reject as InvalidData, got {other:?}"),
}
}
#[test]
fn omni_rejects_invalid_input() {
match encode_omni("not a gtin", &Options::default()) {
Err(Error::InvalidData(msg)) => {
assert!(
msg.contains("GS1 DataBar Omnidirectional:"),
"missing `GS1 DataBar Omnidirectional:` prefix: {msg}"
);
assert!(
msg.contains("non-digit in payload"),
"missing `non-digit in payload` predicate: {msg}"
);
assert!(
msg.contains("\"not a gtin\""),
"missing input echo `\"not a gtin\"`: {msg}"
);
assert!(
!msg.contains("expected 13 or 14 digits"),
"wrong arm — length diagnostic leaked: {msg}"
);
assert!(
!msg.contains("check digit mismatch"),
"wrong arm — check-digit diagnostic leaked: {msg}"
);
}
other => panic!("`not a gtin` should reject as InvalidData, got {other:?}"),
}
}
#[test]
fn omni_rendered_sbs_matches_bwip_js() {
let cases: &[(&str, [u8; 45])] = &[
(
"(01)24012345678905",
[
1, 1, 1, 4, 1, 2, 1, 3, 3, 2, 5, 6, 1, 1, 4, 3, 1, 1, 1, 2, 2, 1, 2, 1, 1, 2,
1, 1, 5, 2, 1, 1, 5, 5, 3, 1, 2, 1, 5, 1, 1, 1, 4, 1, 1,
],
),
(
"(01)00012345678905",
[
1, 1, 1, 1, 1, 2, 1, 8, 1, 2, 7, 4, 1, 1, 3, 2, 1, 1, 2, 1, 4, 1, 3, 2, 1, 1,
1, 1, 2, 4, 1, 1, 7, 3, 3, 2, 2, 2, 4, 1, 3, 1, 1, 1, 1,
],
),
(
"(01)12345678901231",
[
1, 1, 3, 1, 1, 2, 1, 6, 1, 2, 3, 8, 1, 1, 1, 4, 1, 1, 5, 1, 1, 1, 2, 1, 1, 3,
2, 2, 2, 2, 1, 1, 9, 3, 1, 1, 2, 1, 2, 3, 3, 3, 1, 1, 1,
],
),
];
for &(input, want_sbs) in cases {
let pat = render_omni(input, &Options::default()).unwrap_or_else(|e| {
panic!("render_omni({input:?}) failed: {e:?}");
});
assert_eq!(pat.bars[0], 0, "expected zero-width-bar prefix");
assert_eq!(&pat.bars[1..], &want_sbs, "sbs mismatch for {input:?}",);
assert_eq!(pat.total_width(), 95, "DataBar Omni is 95 modules wide");
}
}
#[test]
fn omni_widths_with_linkage_differs_from_standalone() {
let (no_link, csum_no) = omni_widths_with_linkage("(01)24012345678905", false).expect(
"omni_widths_with_linkage(\"(01)24012345678905\", linkage=false) (DataBar Omni linkage=false baseline; csum<79) must succeed",
);
let (with_link, csum_with) = omni_widths_with_linkage("(01)24012345678905", true).expect(
"omni_widths_with_linkage(\"(01)24012345678905\", linkage=true) (DataBar Omni linkage=true path; must differ from linkage=false widths) must succeed",
);
assert_ne!(
no_link, with_link,
"linkage should change the encoded widths",
);
assert!(csum_no < 79);
assert!(csum_with < 79);
}
#[test]
fn render_omni_linkage_true_differs_from_default() {
let default = render_omni("(01)24012345678905", &Options::default()).expect(
"render_omni(\"(01)24012345678905\", default) (DataBar Omni render-linkage default baseline for linkage=true distinctness) must succeed",
);
let linked = render_omni(
"(01)24012345678905",
&Options::default().with("linkage", "true"),
)
.expect(
"render_omni(\"(01)24012345678905\", linkage=true) (DataBar Omni render-linkage=true path; must differ from default) must succeed",
);
assert_ne!(
default.bars, linked.bars,
"linkage should change the encoded sbs",
);
}
#[test]
fn render_omni_linkage_false_equivalent_to_default() {
let a = render_omni("(01)24012345678905", &Options::default()).expect(
"render_omni(\"(01)24012345678905\", default) (DataBar Omni no-option baseline for linkage=false equivalence cross-check) must succeed",
);
let b = render_omni(
"(01)24012345678905",
&Options::default().with("linkage", "false"),
)
.expect(
"render_omni(\"(01)24012345678905\", linkage=false) (DataBar Omni explicit linkage=false; must equal default output) must succeed",
);
assert_eq!(a.bars, b.bars);
}
#[test]
fn render_omni_rejects_invalid_linkage_value() {
let err = render_omni(
"(01)24012345678905",
&Options::default().with("linkage", "maybe"),
)
.unwrap_err();
match err {
Error::InvalidOption(msg) => {
assert!(
msg.contains("databaromni:"),
"must carry databaromni prefix; got {msg}"
);
assert!(
msg.contains("linkage=\"maybe\""),
"must Debug-echo the offending value; got {msg}"
);
assert!(
msg.contains("must be"),
"must carry the predicate; got {msg}"
);
assert!(
msg.contains("\"true\"") && msg.contains("\"false\""),
"must name BOTH valid values; got {msg}"
);
assert!(
!msg.contains("databarlimited"),
"must NOT leak databarlimited prefix; got {msg}"
);
}
other => panic!("expected InvalidOption, got {other:?}"),
}
}
#[test]
fn encode_limited_linkage_true_matches_bwip_js() {
let opts = Options::default().with("linkage", "true");
let p = encode_limited("(01)15012345678907", &opts).expect("linkage=true encodes");
let want: [u8; 46] = [
1, 1, 1, 3, 1, 1, 1, 2, 4, 1, 4, 1, 1, 2, 3, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1, 2, 2, 1, 1,
2, 1, 2, 1, 1, 2, 3, 2, 1, 3, 2, 2, 2, 2, 1, 1, 5,
];
assert_eq!(p.bars[0], 0, "leading bar should be 0-width spacer");
assert_eq!(&p.bars[1..], &want[..]);
}
#[test]
fn encode_limited_linkage_false_equivalent_to_default() {
let a = encode_limited("(01)15012345678907", &Options::default()).expect(
"encode_limited(\"(01)15012345678907\", default) (DataBar Limited no-option baseline for linkage=false equivalence cross-check) must succeed",
);
let b = encode_limited(
"(01)15012345678907",
&Options::default().with("linkage", "false"),
)
.expect(
"encode_limited(\"(01)15012345678907\", linkage=false) (DataBar Limited explicit linkage=false; must equal default output) must succeed",
);
assert_eq!(a.bars, b.bars);
}
#[test]
fn encode_limited_rejects_invalid_linkage_value() {
let err = encode_limited(
"(01)15012345678907",
&Options::default().with("linkage", "maybe"),
)
.unwrap_err();
match err {
Error::InvalidOption(msg) => {
assert!(
msg.contains("databarlimited:"),
"must carry databarlimited prefix; got {msg}"
);
assert!(
msg.contains("linkage=\"maybe\""),
"must Debug-echo the offending value; got {msg}"
);
assert!(
msg.contains("must be"),
"must carry the predicate; got {msg}"
);
assert!(
msg.contains("\"true\"") && msg.contains("\"false\""),
"must name BOTH valid values; got {msg}"
);
assert!(
!msg.contains("databaromni"),
"must NOT leak databaromni prefix; got {msg}"
);
}
other => panic!("expected InvalidOption, got {other:?}"),
}
}
#[test]
fn limited_sbs_with_linkage_matches_bwip_js() {
let sbs = limited_sbs_with_linkage("(01)15012345678907", true).expect(
"limited_sbs_with_linkage(\"(01)15012345678907\", linkage=true) (DataBar Limited byte-for-byte 46-element 78-module SBS bwip-js raw oracle) must succeed",
);
let want: [u8; 46] = [
1, 1, 1, 3, 1, 1, 1, 2, 4, 1, 4, 1, 1, 2, 3, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1, 2, 2, 1, 1,
2, 1, 2, 1, 1, 2, 3, 2, 1, 3, 2, 2, 2, 2, 1, 1, 5,
];
assert_eq!(sbs, want);
}
#[test]
fn omni_widths_match_bwip_js_oracle() {
let cases: &[(&str, [u8; 32], u32)] = &[
(
"(01)24012345678905",
[
1, 1, 4, 1, 2, 1, 3, 3, 4, 3, 1, 1, 1, 2, 2, 1, 1, 2, 1, 5, 1, 1, 1, 4, 2, 1,
1, 2, 1, 1, 5, 2,
],
46,
),
(
"(01)00012345678905",
[
1, 1, 1, 1, 2, 1, 8, 1, 3, 2, 1, 1, 2, 1, 4, 1, 2, 2, 2, 4, 1, 3, 1, 1, 3, 2,
1, 1, 1, 1, 2, 4,
],
38,
),
(
"(01)12345678901231",
[
1, 3, 1, 1, 2, 1, 6, 1, 1, 4, 1, 1, 5, 1, 1, 1, 1, 2, 1, 2, 3, 3, 3, 1, 2, 1,
1, 3, 2, 2, 2, 2,
],
62,
),
];
for (input, want_widths, want_csum) in cases {
let (widths, csum) = omni_widths(input)
.unwrap_or_else(|e| panic!("omni_widths({input:?}) failed: {e:?}"));
assert_eq!(&widths, want_widths, "widths mismatch for {input:?}");
assert_eq!(csum, *want_csum, "checksum mismatch for {input:?}");
}
}
#[test]
fn omni_widths_handles_13_digit_input() {
let (w_with, c_with) = omni_widths("(01)24012345678905").unwrap();
let (w_without, c_without) = omni_widths("(01)2401234567890").unwrap();
assert_eq!(w_with, w_without);
assert_eq!(c_with, c_without);
}
#[test]
fn limited_rendered_sbs_matches_bwip_js() {
let cases: &[(&str, [u8; 46])] = &[
(
"(01)00012345678905",
[
1, 1, 2, 1, 1, 1, 2, 2, 1, 1, 1, 5, 1, 6, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 3,
1, 1, 1, 1, 1, 1, 3, 2, 2, 2, 3, 1, 1, 5, 1, 1, 2, 1, 1, 5,
],
),
(
"(01)15012345678907",
[
1, 3, 2, 2, 2, 3, 2, 1, 2, 1, 1, 1, 1, 2, 3, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1, 2,
2, 1, 1, 2, 1, 2, 1, 1, 2, 3, 2, 1, 3, 2, 2, 2, 2, 1, 1, 5,
],
),
(
"(01)09521234567899",
[
1, 1, 1, 4, 1, 2, 2, 1, 1, 1, 1, 3, 4, 1, 3, 1, 2, 1, 2, 1, 1, 1, 1, 2, 1, 2,
1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 3, 1, 1, 4, 2, 1, 4, 1, 1, 5,
],
),
];
for &(input, want_sbs) in cases {
let pat = encode_limited(input, &Options::default())
.unwrap_or_else(|e| panic!("encode_limited({input:?}) failed: {e:?}"));
assert_eq!(pat.bars[0], 0, "expected zero-width-bar prefix");
assert_eq!(&pat.bars[1..], &want_sbs, "sbs mismatch for {input:?}");
}
}
#[test]
fn limited_rejects_bad_leading_digit() {
match encode_limited("(01)24012345678905", &Options::default()) {
Err(Error::InvalidData(msg)) => {
assert!(
msg.contains("GS1 DataBar Limited"),
"missing `GS1 DataBar Limited` prefix: {msg}"
);
assert!(
msg.contains("must begin with 0 or 1"),
"missing leading-digit predicate: {msg}"
);
assert!(
!msg.contains("expected 14 digits"),
"wrong arm — length diagnostic leaked: {msg}"
);
assert!(
!msg.contains("check digit mismatch"),
"wrong arm — check-digit diagnostic leaked: {msg}"
);
}
other => panic!(
"`(01)24012345678905` (leading-digit 2) should reject as InvalidData, got {other:?}"
),
}
}
fn row_of(bm: &BitMatrix, y: usize) -> String {
(0..bm.width())
.map(|x| if bm.get(x, y) { '1' } else { '0' })
.collect()
}
#[test]
fn stacked_matches_bwip_js_pixs() {
let bm = encode_stacked("(01)24012345678905", &Options::default()).expect(
"encode_stacked(\"(01)24012345678905\", default) (DataBar Stacked 50×13 pixs golden: 3 rows × 50 cols scaled by rowmult=[5,1,7]) must succeed",
);
assert_eq!(bm.width(), 50);
assert_eq!(bm.height(), 13);
let want_top = "01010000100100011100111110000001011110001010011010";
let want_sep = "00001011010010101010000011111010100101010101000000";
let want_bot = "10110100101111100101111100000111011011111010111101";
for y in 0..5 {
assert_eq!(row_of(&bm, y), want_top, "top row {y} mismatch");
}
assert_eq!(row_of(&bm, 5), want_sep, "sep row");
for y in 6..13 {
assert_eq!(row_of(&bm, y), want_bot, "bot row {y} mismatch");
}
}
#[test]
fn stackedomni_matches_bwip_js_pixs() {
let bm = encode_stackedomni("(01)24012345678905", &Options::default()).expect(
"encode_stackedomni(\"(01)24012345678905\", default) (DataBar Stacked Omnidirectional 50×69 pixs golden: 5 modules rowmult=[33,1,1,1,33]) must succeed",
);
assert_eq!(bm.width(), 50);
assert_eq!(bm.height(), 69);
let want_top = "01010000100100011100111110000001011110001010011010";
let want_sep1 = "00001111011011100010000001010100100001110101100000";
let want_sep2 = "00000101010101010101010101010101010101010101010000";
let want_sep3 = "00001011010000011010000010101000100100000101000000";
let want_bot = "10110100101111100101111100000111011011111010111101";
for y in 0..33 {
assert_eq!(row_of(&bm, y), want_top, "top row {y} mismatch");
}
assert_eq!(row_of(&bm, 33), want_sep1, "sep1 row");
assert_eq!(row_of(&bm, 34), want_sep2, "sep2 row");
assert_eq!(row_of(&bm, 35), want_sep3, "sep3 row");
for y in 36..69 {
assert_eq!(row_of(&bm, y), want_bot, "bot row {y} mismatch");
}
}
#[test]
fn ncr_bwipp_quirks_at_boundaries() {
assert_eq!(ncr_bwipp(3, 5), 1);
assert_eq!(ncr_bwipp(0, 1), 1);
assert_eq!(ncr_bwipp(5, 0), 1);
assert_eq!(ncr_bwipp(5, 5), 1);
assert_eq!(ncr_bwipp(0, 0), 1);
assert_eq!(ncr_bwipp(5, 1), 5);
assert_eq!(ncr_bwipp(10, 1), 10);
assert_eq!(ncr_bwipp(5, 4), 5);
assert_eq!(ncr_bwipp(5, 2), 10);
assert_eq!(ncr_bwipp(6, 3), 20);
assert_eq!(ncr_bwipp(10, 5), 252);
}
#[test]
fn gtin14_check_digit_known_values() {
assert_eq!(gtin14_check_digit("0123456789012"), '8');
assert_eq!(gtin14_check_digit("0401234567890"), '1');
assert_eq!(gtin14_check_digit("0000000000000"), '0');
}
#[test]
fn lookup_group_table_walk_boundaries_and_no_match() {
let tab: [u32; 24] = [
4, 10, 11, 12, 13, 14, 15, 16, 9, 20, 21, 22, 23, 24, 25, 26, 15, 30, 31, 32, 33, 34, 35, 36, ];
assert_eq!(lookup_group(&tab, 0), Some([10, 11, 12, 13, 14, 15, 16]));
assert_eq!(lookup_group(&tab, 4), Some([10, 11, 12, 13, 14, 15, 16]));
assert_eq!(lookup_group(&tab, 5), Some([20, 21, 22, 23, 24, 25, 26]));
assert_eq!(lookup_group(&tab, 9), Some([20, 21, 22, 23, 24, 25, 26]));
assert_eq!(lookup_group(&tab, 10), Some([30, 31, 32, 33, 34, 35, 36]));
assert_eq!(lookup_group(&tab, 15), Some([30, 31, 32, 33, 34, 35, 36]));
assert_eq!(lookup_group(&tab, 16), None);
assert_eq!(lookup_group(&tab, u32::MAX), None);
assert_eq!(lookup_group(&[], 0), None);
}
#[test]
fn expand_pairs_to_modules_polarity_and_run_lengths() {
let out = expand_pairs_to_modules(&[1, 2, 1, 3], true);
assert_eq!(out, vec![1, 0, 0, 1, 0, 0, 0], "start_bar=true expansion");
let out = expand_pairs_to_modules(&[1, 2, 1, 3], false);
assert_eq!(out, vec![0, 1, 1, 0, 1, 1, 1], "start_bar=false expansion");
assert!(expand_pairs_to_modules(&[], true).is_empty());
assert!(expand_pairs_to_modules(&[], false).is_empty());
assert_eq!(
expand_pairs_to_modules(&[5], true),
vec![1, 1, 1, 1, 1],
"single bar run"
);
assert_eq!(
expand_pairs_to_modules(&[5], false),
vec![0, 0, 0, 0, 0],
"single space run"
);
let widths = &[2u8, 1, 3, 1, 2];
let a = expand_pairs_to_modules(widths, true);
let b = expand_pairs_to_modules(widths, false);
assert_eq!(a.len(), b.len(), "polarity must not change length");
for (i, (&ba, &bb)) in a.iter().zip(b.iter()).enumerate() {
assert_eq!(
ba ^ bb,
1,
"module {i}: opposite-polarity outputs must differ in every bit"
);
}
}
#[test]
fn stacked_top_bot_opposite_polarity_and_check_placement() {
let mut widths = [2u8; 32];
widths[15] = 1; widths[23] = 1; let (top, bot) = stacked_top_bot(&widths, 0);
assert_eq!(top.len(), 50);
assert_eq!(bot.len(), 50);
assert_eq!(top[0], 0, "top[0] polarity=false → bit 0");
assert_eq!(top[1], 1, "top[1] polarity=false → bit 1");
assert_eq!(bot[0], 1, "bot[0] polarity=true → bit 1");
assert_eq!(bot[1], 0, "bot[1] polarity=true → bit 0");
assert_eq!(top[48], 1, "top[48] from i=23 odd polarity=0 → bit 1");
assert_eq!(top[49], 0, "top[49] from i=24 even polarity=0 → bit 0");
assert_eq!(bot[48], 0, "bot[48] from i=23 odd polarity=1 → bit 0");
assert_eq!(bot[49], 1, "bot[49] from i=24 even polarity=1 → bit 1");
assert_eq!(top[18], 0, "top[18] checklt i=10 w=3 polarity=0 → 0");
assert_eq!(top[19], 0, "top[19] in checklt[0] run of 3");
assert_eq!(top[20], 0, "top[20] last of checklt[0]=3");
assert_eq!(bot[18], 1, "bot[18] checkrt i=10 w=1 polarity=1 → 1");
assert_eq!(bot[19], 0, "bot[19] checkrt i=11 w=1 polarity=1 → 0");
assert_eq!(top[22], 1, "top[22] checklt i=11 w=8 polarity=0 → 1");
assert_eq!(bot[22], 0, "bot[22] checkrt i=13 w=8 polarity=1 → 0");
assert_ne!(top, bot, "top and bot must differ overall");
}
#[test]
fn omni_sbs_layout_with_d3_d4_swap_and_checkrt_reversal() {
let widths: [u8; 32] = std::array::from_fn(|i| (i + 10) as u8);
let csum = 11u32;
let sbs = omni_sbs(&widths, csum);
assert_eq!(sbs[0], 1, "sbs[0] leading quiet");
assert_eq!(&sbs[1..9], &widths[0..8], "sbs[1..9] = d1w");
assert_eq!(&sbs[9..14], &[3u8, 5, 5, 1, 1], "sbs[9..14] = checklt");
assert_eq!(&sbs[14..22], &widths[8..16], "sbs[14..22] = d2w");
assert_eq!(
&sbs[22..30],
&widths[24..32],
"sbs[22..30] = d4w (4th group, placed BEFORE d3)"
);
assert_eq!(
&sbs[30..35],
&[1u8, 1, 7, 3, 3],
"sbs[30..35] = REVERSED checkrt"
);
assert_eq!(
&sbs[35..43],
&widths[16..24],
"sbs[35..43] = d3w (3rd group, placed AFTER d4)"
);
assert_eq!(sbs[43], 1, "sbs[43] trailing bar");
assert_eq!(sbs[44], 1, "sbs[44] trailing space");
}
#[test]
fn limited_sbs_layout_with_distinct_region_values() {
let widths: [u8; 28] = std::array::from_fn(|i| (i + 1) as u8);
let checkwidths: [u8; 14] = std::array::from_fn(|i| (i + 50) as u8);
let sbs = limited_sbs(&widths, &checkwidths);
assert_eq!(sbs[0], 1, "sbs[0] leading-quiet sentinel");
for i in 0..14 {
assert_eq!(sbs[1 + i], (i + 1) as u8, "sbs[{}] left widths", 1 + i);
}
for i in 0..14 {
assert_eq!(sbs[15 + i], (i + 50) as u8, "sbs[{}] checkwidths", 15 + i);
}
for i in 0..14 {
assert_eq!(sbs[29 + i], (i + 15) as u8, "sbs[{}] right widths", 29 + i);
}
assert_eq!(sbs[43], 1, "sbs[43] trailing bar");
assert_eq!(sbs[44], 1, "sbs[44] extra bar");
assert_eq!(sbs[45], 5, "sbs[45] trailing quiet (=5, distinct)");
}
#[test]
fn expand_pairs_to_modules_alternates_with_polarity() {
assert_eq!(
expand_pairs_to_modules(&[1, 2, 1], false),
vec![0u8, 1, 1, 0]
);
assert_eq!(
expand_pairs_to_modules(&[1, 2, 1], true),
vec![1u8, 0, 0, 1]
);
assert_eq!(expand_pairs_to_modules(&[3], false), vec![0u8, 0, 0]);
assert_eq!(expand_pairs_to_modules(&[3], true), vec![1u8, 1, 1]);
assert_eq!(expand_pairs_to_modules(&[], false), Vec::<u8>::new());
assert_eq!(expand_pairs_to_modules(&[], true), Vec::<u8>::new());
assert_eq!(
expand_pairs_to_modules(&[0, 5], false),
vec![1u8, 1, 1, 1, 1]
);
assert_eq!(
expand_pairs_to_modules(&[2, 0, 3], true),
vec![1u8, 1, 1, 1, 1]
);
assert_eq!(
expand_pairs_to_modules(&[1, 1, 1, 2], false),
vec![0u8, 1, 0, 1, 1]
);
}
#[test]
fn lookup_group_limited_table_walks_with_inclusive_max() {
assert_eq!(
lookup_group_limited(0),
Some([0, 17, 9, 6, 3, 6538, 28]),
"d=0 → row 0 group"
);
assert_eq!(
lookup_group_limited(183063),
Some([0, 17, 9, 6, 3, 6538, 28]),
"d=183063 exact max; `<=` boundary"
);
assert_eq!(
lookup_group_limited(183064),
Some([183064, 13, 13, 5, 4, 875, 728]),
"d=183064 just past row 0 → row 1"
);
assert_eq!(
lookup_group_limited(820063),
Some([183064, 13, 13, 5, 4, 875, 728]),
"d=820063 exact row 1 max"
);
assert_eq!(
lookup_group_limited(2013570),
Some([1996939, 7, 19, 1, 8, 1, 16632]),
"d=2013570 last row max"
);
assert_eq!(
lookup_group_limited(2013571),
None,
"d past last row max → None (no OOB)"
);
assert_eq!(lookup_group_limited(u32::MAX), None, "u32::MAX → None");
}
#[test]
fn lookup_group_walks_8_stride_with_inclusive_boundary() {
let tab: [u32; 24] = [
10, 100, 200, 300, 400, 500, 600, 700, 20, 110, 210, 310, 410, 510, 610, 710, 30, 120, 220, 320, 420, 520, 620, 720, ];
assert_eq!(
lookup_group(&tab, 5),
Some([100, 200, 300, 400, 500, 600, 700])
);
assert_eq!(
lookup_group(&tab, 10),
Some([100, 200, 300, 400, 500, 600, 700]),
"d=10 exactly == row 0 max; `<=` boundary"
);
assert_eq!(
lookup_group(&tab, 11),
Some([110, 210, 310, 410, 510, 610, 710])
);
assert_eq!(
lookup_group(&tab, 20),
Some([110, 210, 310, 410, 510, 610, 710])
);
assert_eq!(
lookup_group(&tab, 21),
Some([120, 220, 320, 420, 520, 620, 720])
);
assert_eq!(
lookup_group(&tab, 30),
Some([120, 220, 320, 420, 520, 620, 720])
);
assert_eq!(lookup_group(&tab, 31), None);
assert_eq!(
lookup_group(&tab, 0),
Some([100, 200, 300, 400, 500, 600, 700])
);
assert_eq!(lookup_group(&[], 5), None);
}
#[test]
fn paint_module_rows_replicates_each_row_and_sets_set_bits() {
let mut row_a = [0u8; 50];
for i in (0..50).step_by(2) {
row_a[i] = 1;
}
let mut row_b = [0u8; 50];
for i in (1..50).step_by(2) {
row_b[i] = 1;
}
let mut bm = BitMatrix::new(50, 4);
paint_module_rows(&mut bm, &[(&row_a, 2), (&row_b, 2)]);
for x in 0..50 {
let expect = x % 2 == 0;
assert_eq!(bm.get(x, 0), expect, "(x={x},y=0) row_a mismatch");
assert_eq!(bm.get(x, 1), expect, "(x={x},y=1) row_a mult mismatch");
}
for x in 0..50 {
let expect = x % 2 == 1;
assert_eq!(bm.get(x, 2), expect, "(x={x},y=2) row_b mismatch");
assert_eq!(bm.get(x, 3), expect, "(x={x},y=3) row_b mult mismatch");
}
}
#[test]
fn stacked_sep_per_position_rules_and_seppad() {
let top = [1u8; 50];
let bot = [1u8; 50];
let sep = stacked_sep(&top, &bot);
assert!(sep.iter().all(|&v| v == 0), "all-1 equal → all zero");
let top = [0u8; 50];
let bot = [0u8; 50];
let sep = stacked_sep(&top, &bot);
for i in 0..4 {
assert_eq!(sep[i], 0, "front pad pos {i}");
}
for i in 4..46 {
assert_eq!(sep[i], 1, "middle pos {i} should be 1");
}
for i in 46..50 {
assert_eq!(sep[i], 0, "back pad pos {i}");
}
let top = [0u8; 50];
let bot = [1u8; 50];
let sep = stacked_sep(&top, &bot);
for i in 0..4 {
assert_eq!(sep[i], 0, "front pad pos {i}");
}
assert_eq!(sep[4], 0, "alternation i=4 even → 0");
assert_eq!(sep[5], 1, "alternation i=5 odd → 1");
assert_eq!(sep[6], 0, "alternation i=6 even → 0");
assert_eq!(sep[45], 1, "alternation i=45 odd → 1");
for i in 46..50 {
assert_eq!(sep[i], 0, "back pad pos {i}");
}
}
#[test]
fn databar_linkage_option_parsing() {
assert!(!check_databaromni_opts(&Options::default()).unwrap());
assert!(!check_databaromni_opts(&Options::default().with("linkage", "false")).unwrap());
assert!(check_databaromni_opts(&Options::default().with("linkage", "true")).unwrap());
let err = check_databaromni_opts(&Options::default().with("linkage", "maybe")).unwrap_err();
let Error::InvalidOption(msg) = err else {
panic!("databaromni linkage=maybe must yield InvalidOption; got {err:?}");
};
assert!(
msg.contains("databaromni:"),
"databaromni diagnostic must carry its own prefix; got {msg:?}"
);
assert!(
msg.contains("\"maybe\""),
"databaromni diagnostic must echo the offending value via {{v:?}}; got {msg:?}"
);
assert!(
msg.contains("must be \"true\" or \"false\""),
"databaromni diagnostic must carry the must-be tail; got {msg:?}"
);
assert!(
!msg.contains("databarlimited:"),
"databaromni diagnostic must not leak the databarlimited prefix; got {msg:?}"
);
assert!(!check_databarlimited_opts(&Options::default()).unwrap());
assert!(!check_databarlimited_opts(&Options::default().with("linkage", "false")).unwrap());
assert!(check_databarlimited_opts(&Options::default().with("linkage", "true")).unwrap());
let err =
check_databarlimited_opts(&Options::default().with("linkage", "maybe")).unwrap_err();
let Error::InvalidOption(msg) = err else {
panic!("databarlimited linkage=maybe must yield InvalidOption; got {err:?}");
};
assert!(
msg.contains("databarlimited:"),
"databarlimited diagnostic must carry its own prefix; got {msg:?}"
);
assert!(
msg.contains("\"maybe\""),
"databarlimited diagnostic must echo the offending value via {{v:?}}; got {msg:?}"
);
assert!(
msg.contains("must be \"true\" or \"false\""),
"databarlimited diagnostic must carry the must-be tail; got {msg:?}"
);
assert!(
!msg.contains("databaromni:"),
"databarlimited diagnostic must not leak the databaromni prefix; got {msg:?}"
);
}
#[test]
fn stacked_sep_complement_alternate_with_seppad() {
let top = [0u8; 50];
let bot = [0u8; 50];
let sep = stacked_sep(&top, &bot);
assert_eq!(sep.len(), 50, "sep is always 50 cells");
assert_eq!(&sep[..4], &[0, 0, 0, 0], "seppad: sep[0..4] = 0");
for i in 4..46 {
assert_eq!(
sep[i], 1,
"all-zero top+bot, equal arm complements to 1 at i={i}"
);
}
assert_eq!(&sep[46..], &[0, 0, 0, 0], "seppad: sep[46..50] = 0");
let top = [1u8; 50];
let bot = [1u8; 50];
let sep = stacked_sep(&top, &bot);
assert_eq!(
sep, [0u8; 50],
"all-one top+bot → complement to 0 + seppad = all-zero"
);
let mut top = [0u8; 50];
top[10] = 1;
let bot = [0u8; 50];
let sep = stacked_sep(&top, &bot);
assert_eq!(&sep[..4], &[0, 0, 0, 0], "seppad start");
for i in 4..10 {
assert_eq!(sep[i], 1, "equal arm pre-diverge: sep[{i}] = 1");
}
assert_eq!(
sep[10], 0,
"i=10 differ arm: 1 - sep[9] = 1 - 1 = 0 (catches `1 -` drop)"
);
for i in 11..46 {
assert_eq!(sep[i], 1, "equal arm post-diverge: sep[{i}] = 1");
}
assert_eq!(&sep[46..], &[0, 0, 0, 0], "seppad end");
let top: [u8; 50] = std::array::from_fn(|i| (i % 2) as u8);
let bot: [u8; 50] = std::array::from_fn(|i| 1 - (i % 2) as u8);
let sep = stacked_sep(&top, &bot);
assert_eq!(&sep[..4], &[0, 0, 0, 0], "seppad start");
for i in 4..46 {
assert_eq!(
sep[i],
(i % 2) as u8,
"differ arm: alternating sep[{i}] = i%2 (pins `1 - sep[i-1]`)"
);
}
assert_eq!(&sep[46..], &[0, 0, 0, 0], "seppad end");
let zero_top = [0u8; 50];
let zero_bot = [0u8; 50];
let sep = stacked_sep(&zero_top, &zero_bot);
assert_eq!(
sep[3], 0,
"seppad must cover sep[3] (catches `0..3` boundary)"
);
assert_eq!(
sep[46], 0,
"seppad must cover sep[46] (catches `47..50` boundary)"
);
}
#[test]
fn validate_gtin14_or_13_accepts_13_and_14_with_check_and_prefixes() {
assert_eq!(
validate_gtin14_or_13("1234567890123").unwrap(),
"1234567890123",
"13 digits no prefix"
);
assert_eq!(
validate_gtin14_or_13("(01)1234567890123").unwrap(),
"1234567890123",
"(01) + 13 digits"
);
assert_eq!(
validate_gtin14_or_13("011234567890123").unwrap(),
"1234567890123",
"bare 01 + 13 digits"
);
assert_eq!(
validate_gtin14_or_13("12345678901231").unwrap(),
"1234567890123",
"14 digits with correct check; returns 13-digit body"
);
assert_eq!(
validate_gtin14_or_13("(01)12345678901231").unwrap(),
"1234567890123",
"(01) + 14 digits with correct check"
);
assert_eq!(
validate_gtin14_or_13("90012345678908").unwrap(),
"9001234567890",
"GTIN-14 90012345678908: returns 13-digit body 9001234567890"
);
match validate_gtin14_or_13("12345678901232").unwrap_err() {
Error::InvalidData(msg) => {
assert!(
msg.contains("GS1 DataBar:") && msg.contains("GTIN-14 check digit mismatch"),
"wrong-check diagnostic must carry GS1 DataBar prefix + predicate; got {msg}"
);
assert!(
msg.contains("got 2") && msg.contains("expected 1"),
"wrong-check diagnostic must echo actual=2 + expected=1; got {msg}"
);
}
other => panic!("expected InvalidData for wrong check, got {other:?}"),
}
assert_eq!(
validate_gtin14_or_13("00000000000000").unwrap(),
"0000000000000",
"all-zero GTIN-14: check = 0 via outer mod"
);
match validate_gtin14_or_13("123456789012").unwrap_err() {
Error::InvalidData(msg) => {
assert!(
msg.contains("GS1 DataBar Omnidirectional:")
&& msg.contains("expected 13 or 14 digits"),
"12-digit diagnostic must carry length predicate; got {msg}"
);
assert!(
msg.contains("got 12"),
"12-digit diagnostic must echo actual length=12; got {msg}"
);
}
other => panic!("expected InvalidData for 12 digits, got {other:?}"),
}
match validate_gtin14_or_13("123456789012345").unwrap_err() {
Error::InvalidData(msg) => {
assert!(
msg.contains("expected 13 or 14 digits") && msg.contains("got 15"),
"15-digit diagnostic must echo actual length=15; got {msg}"
);
}
other => panic!("expected InvalidData for 15 digits, got {other:?}"),
}
match validate_gtin14_or_13("123456789012A").unwrap_err() {
Error::InvalidData(msg) => {
assert!(
msg.contains("GS1 DataBar Omnidirectional:")
&& msg.contains("non-digit in payload"),
"non-digit diagnostic must carry the predicate; got {msg}"
);
assert!(
msg.contains("\"123456789012A\""),
"non-digit diagnostic must Debug-echo the raw payload; got {msg}"
);
assert!(
!msg.contains("expected 13 or 14 digits"),
"non-digit diagnostic must NOT leak length-arm wording; got {msg}"
);
}
other => panic!("expected InvalidData for non-digit, got {other:?}"),
}
match validate_gtin14_or_13("").unwrap_err() {
Error::InvalidData(msg) => {
assert!(
msg.contains("expected 13 or 14 digits") && msg.contains("got 0"),
"empty-payload diagnostic must echo length=0; got {msg}"
);
}
other => panic!("expected InvalidData for empty, got {other:?}"),
}
assert_eq!(
validate_gtin14_or_13(" 1234567890123 ").unwrap(),
"1234567890123",
"leading/trailing whitespace must be trimmed"
);
assert_eq!(
validate_gtin14_or_13("\t90012345678908\n").unwrap(),
"9001234567890",
"tab+newline whitespace must be trimmed"
);
}
#[test]
fn stackedomni_logical_rows_sep2_literal_and_margin_invariants() {
let (_top, sep1, sep2, sep3, _bot) =
stackedomni_logical_rows("(01)24012345678905", false).unwrap();
let mut expected_sep2 = [0u8; 50];
for i in 0..21 {
expected_sep2[4 + i * 2 + 1] = 1;
}
assert_eq!(sep2, expected_sep2, "sep2 literal pattern broken");
let expected_ones: Vec<usize> = (5..=45).step_by(2).collect();
let actual_ones: Vec<usize> = sep2
.iter()
.enumerate()
.filter_map(|(i, &v)| (v == 1).then_some(i))
.collect();
assert_eq!(
actual_ones, expected_ones,
"sep2 ones must be at exactly {{5,7,…,45}}; got {actual_ones:?}"
);
assert_eq!(sep2.iter().filter(|&&v| v == 1).count(), 21);
for i in 0..4 {
assert_eq!(sep1[i], 0, "sep1[{i}] (front margin) must be 0");
}
for i in 46..50 {
assert_eq!(sep1[i], 0, "sep1[{i}] (back margin) must be 0");
}
for i in 0..4 {
assert_eq!(sep3[i], 0, "sep3[{i}] (front margin) must be 0");
}
for i in 46..50 {
assert_eq!(sep3[i], 0, "sep3[{i}] (back margin) must be 0");
}
let sep1_mid_range = 30 - 18 + 1;
let sep3_mid_range = 31 - 19 + 1;
assert_eq!(
sep1_mid_range, 13,
"sep1 mid-strip must span 13 cells (18..=30)"
);
assert_eq!(
sep3_mid_range, 13,
"sep3 mid-strip must span 13 cells (19..=31)"
);
}
#[test]
fn databar_stackedomni_2nd_golden_fingerprint_pinned() {
fn fp_bm(bm: &BitMatrix) -> (usize, usize, u64) {
let mut s: u64 = 0;
for y in 0..bm.height() {
for x in 0..bm.width() {
let bit = u64::from(bm.get(x, y));
s = s.wrapping_add(
bit.wrapping_mul(
((y as u64).wrapping_mul(50).wrapping_add(x as u64))
.wrapping_add(1)
.wrapping_mul(2_654_435_761),
),
);
}
}
(bm.width(), bm.height(), s)
}
let bm = encode_stackedomni("(01)00000000000017", &Options::default()).expect(
"encode_stackedomni(\"(01)00000000000017\", default) (DataBar Stacked Omnidirectional 2nd golden — distinct top/bot pattern for L519/L543 separator-bit mutant kill) must succeed",
);
let got = fp_bm(&bm);
assert_eq!(got, FP_DB_STK_2ND);
}
const FP_DB_STK_2ND: (usize, usize, u64) = (50, 69, 8971398278569536);
}