#[cfg(not(feature = "std"))]
#[allow(unused_imports)]
use alloc::{borrow::ToOwned, string::String, vec, vec::Vec};
use crate::ParseError;
#[derive(Clone, Copy, Debug)]
enum Len {
Fixed(usize),
Variable,
}
struct AiSpec {
ai: &'static str,
desc: &'static str,
len: Len,
}
#[rustfmt::skip]
const AI_TABLE: &[AiSpec] = &[
AiSpec { ai: "00", desc: "SSCC (serial shipping container code)", len: Len::Fixed(18) },
AiSpec { ai: "01", desc: "GTIN (global trade item number)", len: Len::Fixed(14) },
AiSpec { ai: "02", desc: "GTIN of contained items", len: Len::Fixed(14) },
AiSpec { ai: "10", desc: "Batch or lot number", len: Len::Variable },
AiSpec { ai: "11", desc: "Production date (YYMMDD)", len: Len::Fixed(6) },
AiSpec { ai: "13", desc: "Packaging date (YYMMDD)", len: Len::Fixed(6) },
AiSpec { ai: "15", desc: "Best-before date (YYMMDD)", len: Len::Fixed(6) },
AiSpec { ai: "16", desc: "Sell-by date (YYMMDD)", len: Len::Fixed(6) },
AiSpec { ai: "17", desc: "Expiration date (YYMMDD)", len: Len::Fixed(6) },
AiSpec { ai: "20", desc: "Internal product variant", len: Len::Fixed(2) },
AiSpec { ai: "21", desc: "Serial number", len: Len::Variable },
AiSpec { ai: "22", desc: "Consumer product variant", len: Len::Variable },
AiSpec { ai: "30", desc: "Count of items (variable measure)", len: Len::Variable },
AiSpec { ai: "37", desc: "Count of items contained", len: Len::Variable },
AiSpec { ai: "240", desc: "Additional item identification", len: Len::Variable },
AiSpec { ai: "241", desc: "Customer part number", len: Len::Variable },
AiSpec { ai: "242", desc: "Made-to-order variation number", len: Len::Variable },
AiSpec { ai: "243", desc: "Packaging component number", len: Len::Variable },
AiSpec { ai: "244", desc: "Ground crew badge number", len: Len::Variable },
AiSpec { ai: "400", desc: "Customer order number", len: Len::Variable },
AiSpec { ai: "401", desc: "Consignment number", len: Len::Variable },
AiSpec { ai: "402", desc: "Global shipment identification number", len: Len::Fixed(17) },
AiSpec { ai: "403", desc: "Routing code", len: Len::Variable },
AiSpec { ai: "410", desc: "Ship to / deliver to GLN", len: Len::Fixed(13) },
AiSpec { ai: "411", desc: "Bill to / invoice to GLN", len: Len::Fixed(13) },
AiSpec { ai: "412", desc: "Purchased from GLN", len: Len::Fixed(13) },
AiSpec { ai: "413", desc: "Ship for / deliver for GLN", len: Len::Fixed(13) },
AiSpec { ai: "414", desc: "Identification of a physical location (GLN)", len: Len::Fixed(13) },
AiSpec { ai: "415", desc: "GLN of the invoicing party", len: Len::Fixed(13) },
AiSpec { ai: "416", desc: "GLN of the physical location", len: Len::Fixed(13) },
AiSpec { ai: "420", desc: "Ship to / deliver to postal code", len: Len::Variable },
AiSpec { ai: "421", desc: "Ship to / deliver to postal code (with ISO country prefix)", len: Len::Variable },
AiSpec { ai: "422", desc: "Country of origin (ISO 3166)", len: Len::Fixed(3) },
AiSpec { ai: "423", desc: "Country of initial processing", len: Len::Variable },
AiSpec { ai: "425", desc: "Country of disassembly", len: Len::Variable },
AiSpec { ai: "426", desc: "Country of processing (ISO 3166)", len: Len::Fixed(3) },
AiSpec { ai: "427", desc: "Country subdivision (ISO 3166-2)", len: Len::Variable },
];
struct AiMatch {
len: usize,
desc: &'static str,
length: Len,
}
fn is_measure_family(d: u32) -> bool {
(310..=316).contains(&d)
|| (320..=326).contains(&d)
|| (334..=337).contains(&d)
|| (340..=357).contains(&d)
|| (360..=369).contains(&d)
}
fn digits(data: &[u8], n: usize) -> Option<u32> {
if data.len() < n {
return None;
}
let mut v = 0u32;
for &b in &data[..n] {
if !b.is_ascii_digit() {
return None;
}
v = v * 10 + u32::from(b - b'0');
}
Some(v)
}
fn match_ai(data: &[u8]) -> Option<AiMatch> {
if let Some(d3) = digits(data, 3)
&& digits(data, 4).is_some()
&& is_measure_family(d3)
{
return Some(AiMatch { len: 4, desc: "Trade item measure (GS1 310n–369n family)", length: Len::Fixed(6) });
}
for &n in &[4usize, 3, 2] {
if data.len() >= n {
let Ok(prefix) = core::str::from_utf8(&data[..n]) else {
continue;
};
if let Some(spec) = AI_TABLE.iter().find(|s| s.ai == prefix) {
return Some(AiMatch { len: n, desc: spec.desc, length: spec.len });
}
}
}
None
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Gs1Element {
ai: String,
value: Vec<u8>,
description: &'static str,
}
impl Gs1Element {
#[must_use]
pub fn ai(&self) -> &str {
&self.ai
}
#[must_use]
pub fn value(&self) -> &[u8] {
&self.value
}
#[must_use]
pub fn description(&self) -> &'static str {
self.description
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Gs1Result {
elements: Vec<Gs1Element>,
raw_data: Vec<u8>,
}
impl Gs1Result {
pub fn parse(data: &[u8]) -> Result<Self, ParseError> {
if data.is_empty() {
return Ok(Self { elements: Vec::new(), raw_data: Vec::new() });
}
if !data[0].is_ascii_digit() {
return Err(ParseError::InvalidFormat);
}
let mut elements = Vec::new();
let mut i = 0;
while i < data.len() {
let rest = &data[i..];
let (ai_len, desc, length) = match match_ai(rest) {
Some(m) => (m.len, m.desc, m.length),
None => {
let ai_len = rest.iter().take(2).take_while(|b| b.is_ascii_digit()).count().max(1);
let ai_len = ai_len.min(rest.len());
let value_end = sep_or_end(rest, ai_len);
elements.push(Gs1Element {
ai: core::str::from_utf8(&rest[..ai_len]).unwrap_or("").to_owned(),
value: rest[ai_len..value_end].to_vec(),
description: "Unknown application identifier",
});
i += value_end + usize::from(rest.get(value_end) == Some(&0x1D));
continue;
}
};
let ai = core::str::from_utf8(&rest[..ai_len]).unwrap_or("").to_owned();
i += ai_len;
let value_end = match length {
Len::Fixed(n) => (i + n).min(data.len()),
Len::Variable => sep_or_end(data, i),
};
elements.push(Gs1Element { ai, value: data[i..value_end].to_vec(), description: desc });
i = value_end;
if matches!(length, Len::Variable) && data.get(i) == Some(&0x1D) {
i += 1;
}
}
Ok(Self { elements, raw_data: data.to_vec() })
}
#[must_use]
pub fn elements(&self) -> &[Gs1Element] {
&self.elements
}
#[must_use]
pub fn raw_data(&self) -> &[u8] {
&self.raw_data
}
}
fn sep_or_end(data: &[u8], start: usize) -> usize {
data[start..].iter().position(|&b| b == 0x1D).map_or(data.len(), |p| start + p)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_gtin_dates_batch_serial() {
let mut data = Vec::new();
data.extend_from_slice(b"0104912345123459"); data.extend_from_slice(b"15970331"); data.extend_from_slice(b"10BATCH123"); data.push(0x1D);
data.extend_from_slice(b"21SERIAL9");
let r = Gs1Result::parse(&data).unwrap();
let e = r.elements();
assert_eq!(e.len(), 4);
assert_eq!(e[0].ai(), "01");
assert_eq!(e[0].value(), b"04912345123459");
assert!(e[0].description().contains("GTIN"));
assert_eq!(e[1].ai(), "15");
assert_eq!(e[1].value(), b"970331");
assert_eq!(e[2].ai(), "10");
assert_eq!(e[2].value(), b"BATCH123");
assert_eq!(e[3].ai(), "21");
assert_eq!(e[3].value(), b"SERIAL9");
assert_eq!(r.raw_data(), &data[..]);
}
#[test]
fn parses_measure_family() {
let data = b"3100001234";
let r = Gs1Result::parse(data).unwrap();
let e = r.elements();
assert_eq!(e.len(), 1);
assert_eq!(e[0].ai(), "3100");
assert_eq!(e[0].value(), b"001234");
assert!(e[0].description().contains("measure"));
}
#[test]
fn count_ai_not_misread_as_measure() {
let mut data = Vec::new();
data.extend_from_slice(b"0112345678901237"); data.extend_from_slice(b"3712"); let r = Gs1Result::parse(&data).unwrap();
let e = r.elements();
assert_eq!(e[1].ai(), "37");
assert_eq!(e[1].value(), b"12");
}
#[test]
fn unknown_ai_is_surfaced() {
let r = Gs1Result::parse(b"91ABCDEF").unwrap();
let e = r.elements();
assert_eq!(e.len(), 1);
assert_eq!(e[0].ai(), "91");
assert_eq!(e[0].value(), b"ABCDEF");
assert_eq!(e[0].description(), "Unknown application identifier");
}
#[test]
fn empty_input_yields_no_elements() {
let r = Gs1Result::parse(b"").unwrap();
assert!(r.elements().is_empty());
}
#[test]
fn non_digit_start_errors() {
assert_eq!(Gs1Result::parse(b"X123"), Err(ParseError::InvalidFormat));
}
#[test]
fn gln_fixed_length() {
let r = Gs1Result::parse(b"4101234567890128").unwrap();
let e = r.elements();
assert_eq!(e.len(), 1);
assert_eq!(e[0].ai(), "410");
assert_eq!(e[0].value(), b"1234567890128");
}
}