Skip to main content

qrcode_rs/
structured_append.rs

1//! ISO/IEC 18004 §7.4 Structured Append — splitting one payload across
2//! 2..=16 QR symbols.
3//!
4//! Each symbol in a Structured Append sequence carries a 20-bit header (see
5//! [`Bits::push_structured_append_header`]) at the very start of its bit
6//! stream, before the data mode indicator, so a spec-aware decoder can
7//! reassemble the original message in order and verify it via the shared
8//! parity byte.
9//!
10//! Encoding is one-way — a [`QrCode`] does not retain its input payload — so
11//! this module only *encodes* the split. To recombine symbols a decoder has
12//! scanned, use [`reassemble`] with the per-symbol metadata that decoder
13//! supplies.
14
15#[cfg(not(feature = "std"))]
16#[allow(unused_imports)]
17use alloc::vec::Vec;
18
19use core::cmp::min;
20use core::fmt::{Display, Error, Formatter};
21
22use crate::QrCode;
23use crate::bits::{self, Bits};
24use crate::optimize::{Optimizer, Parser, Segment, total_encoded_len};
25use crate::types::{EcLevel, QrError, QrResult, Version};
26
27/// A Structured Append sequence builder: splits one payload across 2..=16 QR
28/// symbols.
29///
30/// Construct with [`StructuredAppend::new`], then call
31/// [`StructuredAppend::encode`] to emit the symbols. Every emitted symbol is a
32/// complete, independently-scannable QR code that also carries the Structured
33/// Append header, so a compliant reader knows how the symbols belong together.
34#[non_exhaustive]
35#[derive(Debug, Clone, Copy)]
36pub struct StructuredAppend<'a> {
37    /// Number of symbols in the sequence (`2..=16`).
38    symbols: u8,
39    /// The original, un-split payload.
40    payload: &'a [u8],
41    /// XOR of every payload byte — the Structured Append parity, stored
42    /// identically in every emitted symbol.
43    parity: u8,
44}
45
46impl<'a> StructuredAppend<'a> {
47    /// Creates a builder that will split `payload` across `symbols` QR symbols.
48    ///
49    /// `symbols` must be in `2..=16` (per ISO/IEC 18004 §7.4 a Structured
50    /// Append sequence has at least two symbols). The parity byte (the XOR of
51    /// every payload byte) is computed once here and reused for every symbol.
52    ///
53    /// # Errors
54    ///
55    /// Returns [`QrError::InvalidStructuredAppend`] if `symbols` is not in
56    /// `2..=16`.
57    ///
58    /// ```
59    /// use qrcode_rs::structured_append::StructuredAppend;
60    /// use qrcode_rs::EcLevel;
61    ///
62    /// let sa = StructuredAppend::new(3, b"split across three symbols")?;
63    /// let codes = sa.encode(EcLevel::M)?;
64    /// assert_eq!(codes.len(), 3);
65    /// # Ok::<(), qrcode_rs::QrError>(())
66    /// ```
67    pub fn new(symbols: u8, payload: &'a [u8]) -> QrResult<Self> {
68        if !(2..=16).contains(&symbols) {
69            return Err(QrError::InvalidStructuredAppend { value: symbols });
70        }
71        let parity = payload.iter().fold(0u8, |acc, &byte| acc ^ byte);
72        Ok(Self { symbols, payload, parity })
73    }
74
75    /// The number of symbols the payload will be split across.
76    #[must_use]
77    pub const fn symbols(&self) -> u8 {
78        self.symbols
79    }
80
81    /// The Structured Append parity byte (the XOR of every payload byte),
82    /// stored identically in every emitted symbol so a reader can verify it has
83    /// reassembled the right group.
84    #[must_use]
85    pub const fn parity(&self) -> u8 {
86        self.parity
87    }
88
89    /// The payload being split.
90    #[must_use]
91    pub fn payload(&self) -> &'a [u8] {
92        self.payload
93    }
94
95    /// Encodes the payload into [`Self::symbols`] QR codes.
96    ///
97    /// The payload is split as evenly as possible (the last symbol may be
98    /// shorter). Each symbol is encoded at the smallest version that fits its
99    /// chunk plus the 20-bit Structured Append header, at error-correction
100    /// level `ec`. Symbols in a sequence may therefore differ in version — this
101    /// is permitted by the standard.
102    ///
103    /// # Errors
104    ///
105    /// Returns [`QrError::DataTooLong`] if any chunk cannot fit even version 40
106    /// at the requested error-correction level.
107    pub fn encode(&self, ec: EcLevel) -> QrResult<Vec<QrCode>> {
108        let n = usize::from(self.symbols);
109        let chunk = self.payload.len().div_ceil(n);
110        let mut codes = Vec::with_capacity(n);
111        for i in 0..n {
112            // Clamp `start` so a payload shorter than `n * chunk` yields empty
113            // trailing slices (valid `&[len..len]`) rather than panicking.
114            let start = min(i * chunk, self.payload.len());
115            let end = min((i + 1) * chunk, self.payload.len());
116            let piece = &self.payload[start..end];
117            let code = encode_one_symbol(piece, i as u8 + 1, self.symbols, self.parity, ec)?;
118            codes.push(code);
119        }
120        Ok(codes)
121    }
122}
123
124/// Encodes a single payload chunk as one QR symbol carrying its Structured
125/// Append header, picking the smallest version that fits the chunk plus the
126/// 20-bit header overhead.
127///
128/// Mirrors [`bits::encode_auto`](crate::bits::encode_auto)'s tier-based search:
129/// the segment split is constant within a character-count tier (the per-segment
130/// length is tier-invariant), so we optimize once per tier (V9 / V26 / V40) and
131/// let [`bits::find_min_version`] pick the smallest fitting version. This runs
132/// the optimizer ~3× per chunk instead of ~40×. The constant 20-bit header is
133/// added to the encoded length before the capacity check.
134fn encode_one_symbol(data: &[u8], position: u8, total: u8, parity: u8, ec: EcLevel) -> QrResult<QrCode> {
135    let segments = Parser::new(data).collect::<Vec<Segment>>();
136    for &checkpoint in &[Version::Normal(9), Version::Normal(26), Version::Normal(40)] {
137        let opt = Optimizer::new(segments.iter().copied(), checkpoint).collect::<Vec<_>>();
138        // +20 bits: the Structured Append header (4-bit mode + 8-bit sequence
139        // indicator + 8-bit parity) prepended before the data mode indicator.
140        let total_len = total_encoded_len(&opt, checkpoint) + 20;
141        if total_len <= bits::data_capacity_bits(checkpoint, ec)? {
142            let version = bits::find_min_version(total_len, ec);
143            let mut bits = Bits::new(version);
144            bits.reserve(total_len);
145            bits.push_structured_append_header(position, total, parity)?;
146            bits.push_segments(data, opt.into_iter())?;
147            bits.push_terminator(ec)?;
148            return QrCode::with_bits(bits, ec);
149        }
150    }
151    Err(QrError::DataTooLong)
152}
153
154/// Errors returned by [`reassemble`] when decoded Structured Append symbols
155/// cannot be recombined.
156///
157/// The enum is `#[non_exhaustive]`: future versions may add variants, so
158/// external callers should match with a `_` arm.
159#[non_exhaustive]
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum SaError {
162    /// Fewer symbols were supplied than the total count requires.
163    Incomplete,
164    /// Two symbols claim the same position. Carries the duplicated position.
165    DuplicatePosition(u8),
166    /// The symbols disagree on the total symbol count.
167    CountMismatch,
168    /// The symbols disagree on the parity byte.
169    ParityMismatch,
170    /// A total count or position is outside its valid range (`2..=16` /
171    /// `1..=total`). Carries the offending value.
172    OutOfRange(u8),
173    /// The decoded bit stream does not begin with the Structured Append mode
174    /// indicator (`0011`) — the symbol is not part of a Structured Append
175    /// sequence (or the wrong bytes were supplied).
176    NotStructuredAppend,
177    /// The bit stream was truncated or otherwise malformed while parsing a
178    /// Structured Append header or data segment.
179    MalformedStream,
180}
181
182impl Display for SaError {
183    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
184        match self {
185            Self::Incomplete => f.write_str("incomplete Structured Append sequence (symbols missing)"),
186            Self::DuplicatePosition(position) => {
187                write!(f, "duplicate Structured Append position {position}")
188            }
189            Self::CountMismatch => f.write_str("Structured Append symbols disagree on the total count"),
190            Self::ParityMismatch => f.write_str("Structured Append symbols disagree on the parity byte"),
191            Self::OutOfRange(value) => {
192                write!(f, "Structured Append value {value} out of range (total 2..=16, position 1..=total)")
193            }
194            Self::NotStructuredAppend => f.write_str("not a Structured Append symbol (no `0011` mode indicator)"),
195            Self::MalformedStream => f.write_str("malformed Structured Append bit stream"),
196        }
197    }
198}
199
200impl ::core::error::Error for SaError {}
201
202/// One decoded Structured Append symbol's metadata, ready for [`reassemble`].
203///
204/// Build this from whatever your decoder exposes: the position and total count
205/// (the high and low nibbles of the 8-bit symbol-sequence indicator), the
206/// parity byte, and that symbol's decoded payload bytes. The fields are public
207/// so it can be constructed with a struct literal.
208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
209pub struct SaSymbol<'a> {
210    /// 1-based position of this symbol within the sequence (`1..=total`).
211    pub position: u8,
212    /// Total number of symbols in the sequence (`2..=16`).
213    pub total: u8,
214    /// The parity byte (identical in every symbol of the sequence).
215    pub parity: u8,
216    /// This symbol's decoded payload bytes.
217    pub data: &'a [u8],
218}
219
220/// Reassembles the original message from decoded Structured Append symbols.
221///
222/// Validates that every symbol agrees on the total count and parity, that the
223/// positions form a complete `1..=total` set (no gaps, no duplicates), then
224/// orders them by position and concatenates their data.
225///
226/// The parity byte each symbol carries is the XOR of the *original* full
227/// message, not of any one symbol — so `reassemble` only checks that all
228/// symbols report the *same* parity. To confirm the reassembled bytes match a
229/// known original, XOR them yourself and compare to that shared parity.
230///
231/// # Errors
232///
233/// Returns [`SaError`] if `parts` is empty, disagrees on the total count or
234/// parity, holds an out-of-range value, is incomplete, or repeats a position.
235pub fn reassemble(parts: &[SaSymbol<'_>]) -> Result<Vec<u8>, SaError> {
236    let Some(first) = parts.first() else { return Err(SaError::Incomplete) };
237    if !(2..=16).contains(&first.total) {
238        return Err(SaError::OutOfRange(first.total));
239    }
240    let total = first.total;
241    let parity = first.parity;
242    for p in parts {
243        if p.total != total {
244            return Err(SaError::CountMismatch);
245        }
246        if p.parity != parity {
247            return Err(SaError::ParityMismatch);
248        }
249        if !(1..=total).contains(&p.position) {
250            return Err(SaError::OutOfRange(p.position));
251        }
252    }
253    if parts.len() != usize::from(total) {
254        return Err(SaError::Incomplete);
255    }
256    let mut seen = [false; 16];
257    for p in parts {
258        let idx = usize::from(p.position - 1);
259        if seen[idx] {
260            return Err(SaError::DuplicatePosition(p.position));
261        }
262        seen[idx] = true;
263    }
264    let mut ordered: Vec<&SaSymbol<'_>> = parts.iter().collect();
265    ordered.sort_by_key(|s| s.position);
266    let mut out = Vec::new();
267    for s in ordered {
268        out.extend_from_slice(s.data);
269    }
270    Ok(out)
271}
272
273#[cfg(test)]
274mod tests {
275    use super::StructuredAppend;
276    use crate::types::{EcLevel, QrError};
277    use alloc::{vec, vec::Vec};
278
279    #[test]
280    fn test_new_rejects_out_of_range() {
281        assert_eq!(StructuredAppend::new(1, b"x").err(), Some(QrError::InvalidStructuredAppend { value: 1 }));
282        assert_eq!(StructuredAppend::new(17, b"x").err(), Some(QrError::InvalidStructuredAppend { value: 17 }));
283    }
284
285    #[test]
286    fn test_new_accepts_bounds() {
287        assert!(StructuredAppend::new(2, b"x").is_ok());
288        assert!(StructuredAppend::new(16, b"x").is_ok());
289    }
290
291    #[test]
292    fn test_parity_xor() {
293        assert_eq!(StructuredAppend::new(3, &[0x01, 0x02, 0x03]).unwrap().parity(), 0x00);
294        assert_eq!(StructuredAppend::new(2, &[0xff, 0x0f]).unwrap().parity(), 0xf0);
295        // 0..=255 each value appears once → XOR is 0.
296        let bytes: Vec<u8> = (0u8..=255).collect();
297        assert_eq!(StructuredAppend::new(2, &bytes).unwrap().parity(), 0);
298    }
299
300    #[test]
301    fn test_parity_empty() {
302        assert_eq!(StructuredAppend::new(2, b"").unwrap().parity(), 0);
303    }
304
305    #[test]
306    fn test_encode_count_and_versions() {
307        let payload = b"Split this payload across several QR symbols for resilience.";
308        let codes = StructuredAppend::new(3, payload).unwrap().encode(EcLevel::M).unwrap();
309        assert_eq!(codes.len(), 3);
310        for code in &codes {
311            assert!(!code.info().version().is_micro(), "Structured Append must be Normal QR");
312        }
313    }
314
315    #[test]
316    fn test_encode_all_normal_across_counts() {
317        let payload = b"the quick brown fox jumps over the lazy dog";
318        for n in 2..=16u8 {
319            let codes = StructuredAppend::new(n, payload).unwrap().encode(EcLevel::L).unwrap();
320            assert_eq!(codes.len(), usize::from(n));
321            assert!(codes.iter().all(|c| !c.info().version().is_micro()), "n={n} produced a Micro QR");
322        }
323    }
324
325    #[test]
326    fn test_encode_empty_payload() {
327        // Degenerate but well-formed: each symbol carries only header + terminator.
328        let codes = StructuredAppend::new(2, b"").unwrap().encode(EcLevel::M).unwrap();
329        assert_eq!(codes.len(), 2);
330        assert!(codes.iter().all(|c| !c.info().version().is_micro()));
331    }
332
333    #[test]
334    fn test_encode_deterministic() {
335        let payload = b"deterministic encoding";
336        let a = StructuredAppend::new(3, payload).unwrap().encode(EcLevel::M).unwrap();
337        let b = StructuredAppend::new(3, payload).unwrap().encode(EcLevel::M).unwrap();
338        // Identical inputs → identical module grids.
339        for (a, b) in a.iter().zip(b.iter()) {
340            assert_eq!(a.to_colors(), b.to_colors());
341        }
342    }
343
344    #[test]
345    fn test_encode_too_long() {
346        // 16 symbols × 4000 bytes > version-40-H capacity → each chunk overflows.
347        let payload = vec![0u8; 16 * 4000];
348        let result = StructuredAppend::new(16, &payload).unwrap().encode(EcLevel::H);
349        assert_eq!(result.err(), Some(QrError::DataTooLong));
350    }
351}
352
353#[cfg(test)]
354mod reassemble_tests {
355    use super::{SaError, SaSymbol, reassemble};
356    use alloc::vec::Vec;
357
358    fn sym(position: u8, total: u8, parity: u8, data: &[u8]) -> SaSymbol<'_> {
359        SaSymbol { position, total, parity, data }
360    }
361
362    #[test]
363    fn test_reassemble_ok() {
364        let parts = [sym(1, 3, 0x5a, b"hel"), sym(2, 3, 0x5a, b"lo "), sym(3, 3, 0x5a, b"world")];
365        assert_eq!(reassemble(&parts).unwrap(), b"hello world");
366    }
367
368    #[test]
369    fn test_reassemble_out_of_order() {
370        // Supplied as 3, 1, 2 — reassemble must order by position.
371        let parts = [sym(3, 3, 0x5a, b"wor"), sym(1, 3, 0x5a, b"hel"), sym(2, 3, 0x5a, b"lo")];
372        assert_eq!(reassemble(&parts).unwrap(), b"hellowor");
373    }
374
375    #[test]
376    fn test_reassemble_empty() {
377        assert_eq!(reassemble(&[]), Err(SaError::Incomplete));
378    }
379
380    #[test]
381    fn test_reassemble_incomplete() {
382        let parts = [sym(1, 3, 0x5a, b"a"), sym(2, 3, 0x5a, b"b")];
383        assert_eq!(reassemble(&parts), Err(SaError::Incomplete));
384    }
385
386    #[test]
387    fn test_reassemble_duplicate() {
388        let parts = [sym(1, 3, 0x5a, b"a"), sym(1, 3, 0x5a, b"b"), sym(3, 3, 0x5a, b"c")];
389        assert_eq!(reassemble(&parts), Err(SaError::DuplicatePosition(1)));
390    }
391
392    #[test]
393    fn test_reassemble_count_mismatch() {
394        let parts = [sym(1, 3, 0x5a, b"a"), sym(2, 4, 0x5a, b"b")];
395        assert_eq!(reassemble(&parts), Err(SaError::CountMismatch));
396    }
397
398    #[test]
399    fn test_reassemble_parity_mismatch() {
400        let parts = [sym(1, 2, 0x5a, b"a"), sym(2, 2, 0x5b, b"b")];
401        assert_eq!(reassemble(&parts), Err(SaError::ParityMismatch));
402    }
403
404    #[test]
405    fn test_reassemble_out_of_range_total() {
406        assert_eq!(reassemble(&[sym(1, 1, 0, b"a")]), Err(SaError::OutOfRange(1)));
407        assert_eq!(reassemble(&[sym(1, 17, 0, b"a")]), Err(SaError::OutOfRange(17)));
408    }
409
410    #[test]
411    fn test_reassemble_out_of_range_position() {
412        let parts = [sym(0, 2, 0x5a, b"a"), sym(2, 2, 0x5a, b"b")];
413        assert_eq!(reassemble(&parts), Err(SaError::OutOfRange(0)));
414        let parts = [sym(1, 2, 0x5a, b"a"), sym(3, 2, 0x5a, b"b")];
415        assert_eq!(reassemble(&parts), Err(SaError::OutOfRange(3)));
416    }
417
418    #[test]
419    fn test_reassemble_max_sequence() {
420        // 16 symbols (the spec maximum): positions 1..=16, each carrying one byte.
421        let bytes: Vec<u8> = (1u8..=16).collect();
422        let parts: Vec<SaSymbol<'_>> = bytes
423            .iter()
424            .enumerate()
425            .map(|(i, _)| SaSymbol {
426                position: u8::try_from(i + 1).unwrap(),
427                total: 16,
428                parity: 0xff,
429                data: &bytes[i..=i],
430            })
431            .collect();
432        assert_eq!(reassemble(&parts).unwrap(), bytes);
433    }
434}