#![forbid(unsafe_code)]
extern crate alloc;
use alloc::{string::String, vec::Vec};
use crate::common::{
errors::EncodeError,
traits::BarcodeEncoder,
types::{BarcodeOutput, LinearBarcode},
};
use crate::linear::code128::{FNC1, START_B, START_C, STOP, compute_check, symbols_to_bars};
fn is_fixed_length_ai(ai: &str) -> bool {
matches!(
ai,
"00" | "01"
| "02"
| "03"
| "04"
| "11"
| "12"
| "13"
| "14"
| "15"
| "16"
| "17"
| "18"
| "19"
| "20"
| "31"
| "32"
| "33"
| "34"
| "35"
| "36"
| "41"
)
}
pub struct Gs1_128;
impl BarcodeEncoder for Gs1_128 {
type Input = str;
type Error = EncodeError;
fn encode(input: &str) -> Result<BarcodeOutput, EncodeError> {
if input.trim().is_empty() {
return Err(EncodeError::InvalidInput(
"GS1-128 input must not be empty".into(),
));
}
let segments = parse_gs1(input.trim())?;
let bars = build_barcode(&segments);
let text = build_text_representation(&segments);
Ok(BarcodeOutput::Linear(LinearBarcode {
bars,
height: 50,
text: Some(text),
}))
}
fn symbology_name() -> &'static str {
"GS1-128"
}
}
struct AiSegment {
ai: String,
data: String,
}
fn parse_gs1(input: &str) -> Result<Vec<AiSegment>, EncodeError> {
let mut segments: Vec<AiSegment> = Vec::new();
let bytes = input.as_bytes();
let mut pos = 0;
while pos < bytes.len() {
if bytes[pos] != b'(' {
return Err(EncodeError::InvalidInput(alloc::format!(
"expected '(' at position {pos}, got '{}'",
bytes[pos] as char
)));
}
pos += 1;
let ai_start = pos;
while pos < bytes.len() && bytes[pos] != b')' {
if !bytes[pos].is_ascii_digit() {
return Err(EncodeError::InvalidInput(
"AI must contain only digits".into(),
));
}
pos += 1;
}
if pos >= bytes.len() {
return Err(EncodeError::InvalidInput(
"unclosed '(' in AI specification".into(),
));
}
let ai = core::str::from_utf8(&bytes[ai_start..pos])
.map_err(|_| EncodeError::InvalidInput("invalid UTF-8 in AI".into()))?;
let ai = String::from(ai);
pos += 1;
let data_start = pos;
while pos < bytes.len() && bytes[pos] != b'(' {
pos += 1;
}
let data = core::str::from_utf8(&bytes[data_start..pos])
.map_err(|_| EncodeError::InvalidInput("invalid UTF-8 in AI data".into()))?;
if data.is_empty() {
return Err(EncodeError::InvalidInput(alloc::format!(
"AI ({ai}) has no data"
)));
}
segments.push(AiSegment {
ai,
data: String::from(data),
});
}
if segments.is_empty() {
return Err(EncodeError::InvalidInput(
"no valid AIs found in input".into(),
));
}
Ok(segments)
}
fn build_barcode(segments: &[AiSegment]) -> Vec<bool> {
let mut symbols: Vec<u8> = Vec::new();
let all_numeric = segments
.iter()
.all(|s| s.data.chars().all(|c| c.is_ascii_digit()));
let start = if all_numeric { START_C } else { START_B };
symbols.push(start);
symbols.push(FNC1);
for (i, seg) in segments.iter().enumerate() {
for byte in seg.ai.bytes() {
symbols.push(byte - 0x20); }
let data_bytes = seg.data.as_bytes();
if all_numeric
&& data_bytes.iter().all(|b| b.is_ascii_digit())
&& data_bytes.len() % 2 == 0
&& start == START_C
{
let mut j = 0;
while j + 1 < data_bytes.len() {
let tens = data_bytes[j] - b'0';
let units = data_bytes[j + 1] - b'0';
symbols.push(tens * 10 + units);
j += 2;
}
if j < data_bytes.len() {
symbols.push(data_bytes[j] - 0x20);
}
} else {
for &byte in data_bytes {
if !(0x20..=0x7E).contains(&byte) {
continue;
}
symbols.push(byte - 0x20);
}
}
if i + 1 < segments.len() && !is_fixed_length_ai(&seg.ai) {
symbols.push(FNC1);
}
}
let check = compute_check(&symbols);
symbols.push(check);
symbols.push(STOP);
symbols_to_bars(&symbols)
}
fn build_text_representation(segments: &[AiSegment]) -> String {
let mut s = String::new();
for seg in segments {
s.push('(');
s.push_str(&seg.ai);
s.push(')');
s.push_str(&seg.data);
}
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_single_ai() {
let out = Gs1_128::encode("(01)12345678901231").unwrap();
assert!(matches!(out, BarcodeOutput::Linear(_)));
}
#[test]
fn test_encode_multiple_ai() {
let out = Gs1_128::encode("(01)12345678901231(10)ABC123").unwrap();
assert!(matches!(out, BarcodeOutput::Linear(_)));
}
#[test]
fn test_parse_ai_digits_only() {
let segs = parse_gs1("(01)12345678901231").unwrap();
assert_eq!(segs.len(), 1);
assert_eq!(segs[0].ai, "01");
assert_eq!(segs[0].data, "12345678901231");
}
#[test]
fn test_parse_multiple_ais() {
let segs = parse_gs1("(01)12345678901231(10)LOT123").unwrap();
assert_eq!(segs.len(), 2);
assert_eq!(segs[0].ai, "01");
assert_eq!(segs[1].ai, "10");
assert_eq!(segs[1].data, "LOT123");
}
#[test]
fn test_invalid_no_parens() {
assert!(Gs1_128::encode("0112345678901231").is_err());
}
#[test]
fn test_empty_input() {
assert!(Gs1_128::encode("").is_err());
}
#[test]
fn test_symbology_name() {
assert_eq!(Gs1_128::symbology_name(), "GS1-128");
}
#[test]
fn test_svg_output() {
let svg = Gs1_128::encode("(01)12345678901231")
.unwrap()
.to_svg_string();
assert!(svg.starts_with("<svg "));
}
#[test]
fn test_fixed_length_ai() {
assert!(is_fixed_length_ai("01"));
assert!(is_fixed_length_ai("02"));
assert!(!is_fixed_length_ai("10"));
assert!(!is_fixed_length_ai("21"));
}
}