1#[cfg(not(feature = "std"))]
14#[allow(unused_imports)]
15use alloc::{borrow::ToOwned, string::String, vec, vec::Vec};
16
17use crate::ParseError;
18
19#[derive(Clone, Copy, Debug)]
21enum Len {
22 Fixed(usize),
24 Variable,
26}
27
28struct AiSpec {
30 ai: &'static str,
32 desc: &'static str,
34 len: Len,
36}
37
38#[rustfmt::skip]
40const AI_TABLE: &[AiSpec] = &[
41 AiSpec { ai: "00", desc: "SSCC (serial shipping container code)", len: Len::Fixed(18) },
42 AiSpec { ai: "01", desc: "GTIN (global trade item number)", len: Len::Fixed(14) },
43 AiSpec { ai: "02", desc: "GTIN of contained items", len: Len::Fixed(14) },
44 AiSpec { ai: "10", desc: "Batch or lot number", len: Len::Variable },
45 AiSpec { ai: "11", desc: "Production date (YYMMDD)", len: Len::Fixed(6) },
46 AiSpec { ai: "13", desc: "Packaging date (YYMMDD)", len: Len::Fixed(6) },
47 AiSpec { ai: "15", desc: "Best-before date (YYMMDD)", len: Len::Fixed(6) },
48 AiSpec { ai: "16", desc: "Sell-by date (YYMMDD)", len: Len::Fixed(6) },
49 AiSpec { ai: "17", desc: "Expiration date (YYMMDD)", len: Len::Fixed(6) },
50 AiSpec { ai: "20", desc: "Internal product variant", len: Len::Fixed(2) },
51 AiSpec { ai: "21", desc: "Serial number", len: Len::Variable },
52 AiSpec { ai: "22", desc: "Consumer product variant", len: Len::Variable },
53 AiSpec { ai: "30", desc: "Count of items (variable measure)", len: Len::Variable },
54 AiSpec { ai: "37", desc: "Count of items contained", len: Len::Variable },
55 AiSpec { ai: "240", desc: "Additional item identification", len: Len::Variable },
56 AiSpec { ai: "241", desc: "Customer part number", len: Len::Variable },
57 AiSpec { ai: "242", desc: "Made-to-order variation number", len: Len::Variable },
58 AiSpec { ai: "243", desc: "Packaging component number", len: Len::Variable },
59 AiSpec { ai: "244", desc: "Ground crew badge number", len: Len::Variable },
60 AiSpec { ai: "400", desc: "Customer order number", len: Len::Variable },
61 AiSpec { ai: "401", desc: "Consignment number", len: Len::Variable },
62 AiSpec { ai: "402", desc: "Global shipment identification number", len: Len::Fixed(17) },
63 AiSpec { ai: "403", desc: "Routing code", len: Len::Variable },
64 AiSpec { ai: "410", desc: "Ship to / deliver to GLN", len: Len::Fixed(13) },
65 AiSpec { ai: "411", desc: "Bill to / invoice to GLN", len: Len::Fixed(13) },
66 AiSpec { ai: "412", desc: "Purchased from GLN", len: Len::Fixed(13) },
67 AiSpec { ai: "413", desc: "Ship for / deliver for GLN", len: Len::Fixed(13) },
68 AiSpec { ai: "414", desc: "Identification of a physical location (GLN)", len: Len::Fixed(13) },
69 AiSpec { ai: "415", desc: "GLN of the invoicing party", len: Len::Fixed(13) },
70 AiSpec { ai: "416", desc: "GLN of the physical location", len: Len::Fixed(13) },
71 AiSpec { ai: "420", desc: "Ship to / deliver to postal code", len: Len::Variable },
72 AiSpec { ai: "421", desc: "Ship to / deliver to postal code (with ISO country prefix)", len: Len::Variable },
73 AiSpec { ai: "422", desc: "Country of origin (ISO 3166)", len: Len::Fixed(3) },
74 AiSpec { ai: "423", desc: "Country of initial processing", len: Len::Variable },
75 AiSpec { ai: "425", desc: "Country of disassembly", len: Len::Variable },
76 AiSpec { ai: "426", desc: "Country of processing (ISO 3166)", len: Len::Fixed(3) },
77 AiSpec { ai: "427", desc: "Country subdivision (ISO 3166-2)", len: Len::Variable },
78];
79
80struct AiMatch {
83 len: usize,
85 desc: &'static str,
87 length: Len,
89}
90
91fn is_measure_family(d: u32) -> bool {
97 (310..=316).contains(&d)
98 || (320..=326).contains(&d)
99 || (334..=337).contains(&d)
100 || (340..=357).contains(&d)
101 || (360..=369).contains(&d)
102}
103
104fn digits(data: &[u8], n: usize) -> Option<u32> {
107 if data.len() < n {
108 return None;
109 }
110 let mut v = 0u32;
111 for &b in &data[..n] {
112 if !b.is_ascii_digit() {
113 return None;
114 }
115 v = v * 10 + u32::from(b - b'0');
116 }
117 Some(v)
118}
119
120fn match_ai(data: &[u8]) -> Option<AiMatch> {
123 if let Some(d3) = digits(data, 3)
125 && digits(data, 4).is_some()
126 && is_measure_family(d3)
127 {
128 return Some(AiMatch { len: 4, desc: "Trade item measure (GS1 310n–369n family)", length: Len::Fixed(6) });
129 }
130 for &n in &[4usize, 3, 2] {
132 if data.len() >= n {
133 let Ok(prefix) = core::str::from_utf8(&data[..n]) else {
134 continue;
135 };
136 if let Some(spec) = AI_TABLE.iter().find(|s| s.ai == prefix) {
137 return Some(AiMatch { len: n, desc: spec.desc, length: spec.len });
138 }
139 }
140 }
141 None
142}
143
144#[non_exhaustive]
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct Gs1Element {
148 ai: String,
150 value: Vec<u8>,
152 description: &'static str,
154}
155
156impl Gs1Element {
157 #[must_use]
159 pub fn ai(&self) -> &str {
160 &self.ai
161 }
162
163 #[must_use]
165 pub fn value(&self) -> &[u8] {
166 &self.value
167 }
168
169 #[must_use]
171 pub fn description(&self) -> &'static str {
172 self.description
173 }
174}
175
176#[non_exhaustive]
178#[derive(Debug, Clone, PartialEq, Eq)]
179pub struct Gs1Result {
180 elements: Vec<Gs1Element>,
182 raw_data: Vec<u8>,
184}
185
186impl Gs1Result {
187 pub fn parse(data: &[u8]) -> Result<Self, ParseError> {
199 if data.is_empty() {
200 return Ok(Self { elements: Vec::new(), raw_data: Vec::new() });
201 }
202 if !data[0].is_ascii_digit() {
203 return Err(ParseError::InvalidFormat);
204 }
205
206 let mut elements = Vec::new();
207 let mut i = 0;
208 while i < data.len() {
209 let rest = &data[i..];
210 let (ai_len, desc, length) = match match_ai(rest) {
211 Some(m) => (m.len, m.desc, m.length),
212 None => {
213 let ai_len = rest.iter().take(2).take_while(|b| b.is_ascii_digit()).count().max(1);
216 let ai_len = ai_len.min(rest.len());
217 let value_end = sep_or_end(rest, ai_len);
218 elements.push(Gs1Element {
219 ai: core::str::from_utf8(&rest[..ai_len]).unwrap_or("").to_owned(),
220 value: rest[ai_len..value_end].to_vec(),
221 description: "Unknown application identifier",
222 });
223 i += value_end + usize::from(rest.get(value_end) == Some(&0x1D));
224 continue;
225 }
226 };
227
228 let ai = core::str::from_utf8(&rest[..ai_len]).unwrap_or("").to_owned();
229 i += ai_len;
230 let value_end = match length {
231 Len::Fixed(n) => (i + n).min(data.len()),
232 Len::Variable => sep_or_end(data, i),
233 };
234 elements.push(Gs1Element { ai, value: data[i..value_end].to_vec(), description: desc });
235 i = value_end;
236 if matches!(length, Len::Variable) && data.get(i) == Some(&0x1D) {
238 i += 1;
239 }
240 }
241
242 Ok(Self { elements, raw_data: data.to_vec() })
243 }
244
245 #[must_use]
247 pub fn elements(&self) -> &[Gs1Element] {
248 &self.elements
249 }
250
251 #[must_use]
253 pub fn raw_data(&self) -> &[u8] {
254 &self.raw_data
255 }
256}
257
258fn sep_or_end(data: &[u8], start: usize) -> usize {
261 data[start..].iter().position(|&b| b == 0x1D).map_or(data.len(), |p| start + p)
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn parses_gtin_dates_batch_serial() {
270 let mut data = Vec::new();
272 data.extend_from_slice(b"0104912345123459"); data.extend_from_slice(b"15970331"); data.extend_from_slice(b"10BATCH123"); data.push(0x1D);
276 data.extend_from_slice(b"21SERIAL9"); let r = Gs1Result::parse(&data).unwrap();
279 let e = r.elements();
280 assert_eq!(e.len(), 4);
281 assert_eq!(e[0].ai(), "01");
282 assert_eq!(e[0].value(), b"04912345123459");
283 assert!(e[0].description().contains("GTIN"));
284 assert_eq!(e[1].ai(), "15");
285 assert_eq!(e[1].value(), b"970331");
286 assert_eq!(e[2].ai(), "10");
287 assert_eq!(e[2].value(), b"BATCH123");
288 assert_eq!(e[3].ai(), "21");
289 assert_eq!(e[3].value(), b"SERIAL9");
290 assert_eq!(r.raw_data(), &data[..]);
291 }
292
293 #[test]
294 fn parses_measure_family() {
295 let data = b"3100001234";
297 let r = Gs1Result::parse(data).unwrap();
298 let e = r.elements();
299 assert_eq!(e.len(), 1);
300 assert_eq!(e[0].ai(), "3100");
301 assert_eq!(e[0].value(), b"001234");
302 assert!(e[0].description().contains("measure"));
303 }
304
305 #[test]
306 fn count_ai_not_misread_as_measure() {
307 let mut data = Vec::new();
309 data.extend_from_slice(b"0112345678901237"); data.extend_from_slice(b"3712"); let r = Gs1Result::parse(&data).unwrap();
312 let e = r.elements();
313 assert_eq!(e[1].ai(), "37");
314 assert_eq!(e[1].value(), b"12");
315 }
316
317 #[test]
318 fn unknown_ai_is_surfaced() {
319 let r = Gs1Result::parse(b"91ABCDEF").unwrap();
321 let e = r.elements();
322 assert_eq!(e.len(), 1);
323 assert_eq!(e[0].ai(), "91");
324 assert_eq!(e[0].value(), b"ABCDEF");
325 assert_eq!(e[0].description(), "Unknown application identifier");
326 }
327
328 #[test]
329 fn empty_input_yields_no_elements() {
330 let r = Gs1Result::parse(b"").unwrap();
331 assert!(r.elements().is_empty());
332 }
333
334 #[test]
335 fn non_digit_start_errors() {
336 assert_eq!(Gs1Result::parse(b"X123"), Err(ParseError::InvalidFormat));
337 }
338
339 #[test]
340 fn gln_fixed_length() {
341 let r = Gs1Result::parse(b"4101234567890128").unwrap();
343 let e = r.elements();
344 assert_eq!(e.len(), 1);
345 assert_eq!(e[0].ai(), "410");
346 assert_eq!(e[0].value(), b"1234567890128");
347 }
348}