Skip to content

feat(core)!: zero-allocation no_std encoding (0.2.0)#3

Merged
ashaffah merged 16 commits into
mainfrom
feat/zero-alloc
Jul 6, 2026
Merged

feat(core)!: zero-allocation no_std encoding (0.2.0)#3
ashaffah merged 16 commits into
mainfrom
feat/zero-alloc

Conversation

@ashaffah

@ashaffah ashaffah commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Summary

Reworks the entire crate to be zero-allocation: the default build is now
pure no_std and does not link alloc at all, so any stray heap
allocation becomes a compile error.

Key changes

  • BarcodeEncoder::encode_into(input, &mut [bool]) -> Encoded — all 14
    symbologies (linear, EAN/UPC, GS1, postal, PDF417, Data Matrix, Aztec, QR)
    write their modules into a caller-provided buffer using fixed stack scratch.
  • common::buffer::SliceWriter — bounds-checked slice writer
    (EncodeError::BufferTooSmall).
  • Allocation-free EncodeError (&'static str / char payloads, no
    String/format!).
  • common::svg — renders SVG into any core::fmt::Write sink, no heap.
  • Optional alloc feature gates the owned encode() -> BarcodeOutput
    convenience and to_svg_string(); std implies alloc; image implies std.

Breaking changes (→ 0.2.0)

  • encode() now requires the alloc feature.
  • The trait's associated type Error is removed; all encoders use EncodeError.
  • PDF417 codeword patterns changed so every codeword is a valid 17-module
    pattern (constant row width) — this alters the PDF417 visual pattern, which
    was already a "representative" (non-spec) encoding.

Verification

Configuration Build Tests Clippy -D warnings Miri (UB + leaks)
--no-default-features (no heap) 127 ✅ 127, 0 leaks/UB ✅
--features alloc 145, 0 leaks/UB ✅
--all-features 156 lib + 23 doctests ✅

cargo fmt --all --check ✅. #![forbid(unsafe_code)] retained in all 29 source
files. CI gains build + test + clippy jobs for --no-default-features.

ashaffah added 3 commits July 6, 2026 12:44
Redesign the whole crate around a caller-provided module buffer so the
default build performs no heap allocation and does not even link `alloc`
(any accidental allocation is now a compile error).

- BarcodeEncoder now requires `encode_into(input, &mut [bool]) -> Encoded`;
  every symbology (linear, EAN/UPC, GS1, postal, 2D, QR) streams its
  modules into the caller buffer using fixed stack scratch and the new
  `common::buffer::SliceWriter`.
- EncodeError is now allocation-free (`&'static str` / `char` payloads,
  plus `BufferTooSmall`); `common::svg` renders SVG into any
  `core::fmt::Write` sink without allocating.
- `alloc` becomes an optional feature gating the owned `encode()` ->
  BarcodeOutput convenience and `to_svg_string()`; `std` implies `alloc`;
  `image` implies `std`.

BREAKING CHANGE: `encode()` now requires the `alloc` feature and the trait
associated `Error` type is removed in favour of `EncodeError`.

Verified: builds/tests/clippy green with no features, `alloc`, and all
features; Miri clean (no UB, no leaks) on the no-alloc and alloc paths.
Add clippy + build + test jobs for --no-default-features so CI proves the
default configuration compiles and passes without linking alloc.
Describe the no-heap default, the feature matrix (alloc/std/image), and a
stack-buffer encode_into example; bump versions to 0.2.
Copilot AI review requested due to automatic review settings July 6, 2026 06:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the barcodes crate to make encoding zero-allocation by default and fully no_std without linking alloc, introducing an encode_into() API that writes modules into a caller-provided &mut [bool] and returns an allocation-free Encoded shape descriptor. It also adds allocation-free SVG streaming via core::fmt::Write, and gates the owned encode() / to_svg_string() conveniences behind an alloc feature.

Changes:

  • Replaces per-symbology encode() -> BarcodeOutput implementations with encode_into() -> Encoded, using fixed-capacity stack scratch + common::buffer::SliceWriter.
  • Introduces allocation-free Encoded + EncodeError and adds streaming SVG writers in common::svg.
  • Updates crate docs/README, feature flags (alloc, std implies alloc), and CI to validate --no-default-features (no heap).

Reviewed changes

Copilot reviewed 28 out of 29 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/twod/pdf417.rs Converts PDF417 to encode_into() + stack scratch + slice writer output.
src/twod/datamatrix.rs Converts Data Matrix to encode_into() + fixed scratch buffers and direct grid writes.
src/twod/aztec.rs Converts Aztec to encode_into() with fixed-capacity grids/bits and no heap.
src/qrcode.rs Ports BarcodeEncoder impl to encode_into() with stack scratch buffers and caller module buffer.
src/postal/rm4scc.rs Converts RM4SCC to encode_into() with fixed input/state buffers and SliceWriter.
src/postal/imb.rs Converts IMb to encode_into() with fixed digit buffer and SliceWriter.
src/linear/itf.rs Converts ITF to encode_into() and eliminates padding allocation via virtual leading zero.
src/linear/code93.rs Converts Code 93 to encode_into() with fixed value buffer and SliceWriter.
src/linear/code39.rs Converts Code 39 to encode_into() with SliceWriter-based pattern expansion.
src/linear/code128.rs Converts Code 128 to encode_into() with fixed symbol buffer and SliceWriter.
src/linear/codabar.rs Converts Codabar to encode_into() with SliceWriter-based pattern expansion.
src/lib.rs Makes crate #![no_std] by default and gates alloc/std extern crates behind features.
src/gs1/gs1_128.rs Removes allocation in GS1-128 parsing/encoding by borrowing slices and fixed buffers.
src/gs1/databar.rs Converts DataBar to encode_into() and replaces Vec-based expansion with SliceWriter.
src/ean_upc/upce.rs Converts UPC-E to encode_into() with SliceWriter and alloc-free errors.
src/ean_upc/upca.rs Converts UPC-A to encode_into() with SliceWriter and alloc-free errors.
src/ean_upc/ean8.rs Converts EAN-8 to encode_into() with SliceWriter and alloc-free errors.
src/ean_upc/ean13.rs Converts EAN-13 to encode_into() with SliceWriter and alloc-free errors.
src/common/types.rs Introduces Encoded and gates owned output types behind alloc.
src/common/traits.rs Replaces trait contract with encode_into() and adds alloc-gated default encode().
src/common/svg.rs Adds allocation-free SVG rendering that streams to any core::fmt::Write.
src/common/output.rs Reworks owned BarcodeOutput::to_svg_string() as an alloc wrapper over streaming SVG.
src/common/mod.rs Exposes new buffer/svg modules and gates output behind alloc.
src/common/errors.rs Makes EncodeError allocation-free and adds BufferTooSmall + InvalidCharacter.
src/common/buffer.rs Adds SliceWriter for bounds-checked writing into caller-provided module buffers.
README.md Documents zero-allocation core, new feature matrix, and updates examples/version.
Cargo.toml Bumps to 0.2.0 and introduces alloc feature; makes std imply alloc.
Cargo.lock Updates package version to 0.2.0.
.github/workflows/ci.yml Adds clippy/build/test jobs for --no-default-features to enforce “no heap”.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/twod/pdf417.rs Outdated
Comment on lines +306 to +308
// Step 2: Determine rows and columns from the total codeword count.
let ec_count = 1usize << (ec_level + 1);
let total_data = data_codewords.len();
let total_codewords = total_data + ec_count;
let total_codewords = data_len + ec_count;
Comment thread src/twod/pdf417.rs Outdated
Comment on lines +329 to +333
// Assemble: length descriptor + data padded to (capacity - ec_count).
let padded_data_len = (capacity - ec_count).max(data_len);
let mut n = 0;
all_codewords[n] = (total_codewords + 1) as u16; // length descriptor
n += 1;
Comment thread src/gs1/gs1_128.rs Outdated
Comment on lines +217 to +222
continue;
}
symbols.push(byte - 0x20);
push!(byte - 0x20);
Comment thread src/common/svg.rs
Comment on lines +20 to +25
pub fn write_svg<W: Write>(encoded: Encoded, buf: &[bool], out: &mut W) -> fmt::Result {
match encoded {
Encoded::Linear { len, height } => write_linear(&buf[..len], height, out),
Encoded::Matrix { width, height } => write_matrix(&buf[..width * height], width, out),
}
}
Comment thread src/common/traits.rs
Comment on lines +57 to +61
return Ok(BarcodeOutput::Linear(LinearBarcode {
bars: buf,
height,
text: None,
}));
ashaffah added 13 commits July 6, 2026 18:54
Port the standard ISO/IEC 16022 ECC 200 fixes to the zero-allocation core:
standard symbol-character placement (utah + corner cases), correct
finder/timing tracks, 253-state padding, and the multi-region sizes
32x32-48x48 (up to 174 data codewords) — all using fixed stack scratch,
no heap.

Verified with libdmtx (dmtxread): sizes 10x10-48x48 decode back to the
exact input. Round-trip / ISO RS vector / 253-state padding tests run under
both the no-alloc and alloc configurations.
Audit with real decoders (ZXing, zbar) found several encoders produced
unscannable output due to corrupted lookup tables:

- EAN L_CODE digits 6-9 had the wrong final module (0 instead of 1),
  breaking EAN-13, EAN-8 and UPC-E (UPC-A only used digits 0-5 so it
  happened to work).
- UPC-E parity table was the exact complement of the ECC 200 standard.
- The Code 39 pattern table was wrong for 34 of 43 characters.

All corrected against the standards and verified end-to-end: EAN-13,
EAN-8, UPC-A, UPC-E and Code 39 now decode back to the exact input with
ZXingReader and zbarimg.

These are pre-existing bugs (also present in 0.1.x).
Port the Code-B GS1-128 fix to the zero-allocation core: encode the whole
message in Code Set B with a leading FNC1 instead of starting in Code C
with Code-B AI values (which a reader misread as numeric pairs).

Verified with ZXingReader. With this, feat/zero-alloc has all the
scannability fixes (EAN/UPC/Code 39 via a7f4ad9, GS1-128 here).
Replace the non-conformant 'representative' encoder with a real
implementation: byte compaction, Reed-Solomon over GF(929) using the
standard EC coefficient tables, auto EC level, and the standard low-level
codeword patterns (embedded from the ISO/IEC 15438 table). Each row is
emitted at 3x height so square-module rendering scans.

Verified with the ZXing decoder (ZXingReader): text, mixed and URL inputs
decode back to the exact input.

Codeword pattern table and EC coefficients sourced from ZXing (Apache-2.0).
…24724)

Replace the 'simplified' encoder with a real RSS-14 implementation: the
standard combinatorial element-width generation, group/checksum tables, and
finder patterns, producing the 96-module DataBar Omnidirectional pattern.

Verified with ZXing and zbar: 13/14-digit GTINs decode back to the correct
(01) GTIN-14. Tables/algorithm ported from zint (BSD-3-Clause).
Replace the 'simplified' encoder with a real implementation: Binary Shift
high-level encoding, Reed-Solomon over the Aztec Galois fields (GF(64/256/
1024) + GF(16) for the mode message), bit stuffing, and the standard
bull's-eye / mode-message / spiral layout with alignment grid. Compact
(1-4 layers) and full-range (1-12 layers) symbols.

Verified with ZXing (ZXingReader): text and URL inputs decode back to the
exact input. Layout/RS ported from ZXing (Apache-2.0).
Rewrite the postal encoders to their real specifications, emitting a
3-row matrix (ascender / tracker / descender) instead of a placeholder
linear pattern.

- IMb (USPS-B-3200): u128 field accumulation, 11-bit CRC frame check,
  base-636/1365 codeword conversion and Appendix D character/bar tables.
  Verified bit-for-bit against the canonical DAFT reference vector.
- RM4SCC: KRSET alphabet, per-character 4-bar states, top/bottom
  check-character derivation, start/stop bars. Tables ported from zint.

All 147 tests pass; clippy clean on --no-default-features and
--all-features; rustfmt clean.
The shared check_digit routine weighted digits from the left, which only
yields the GS1-correct result when the data length is even (EAN-13's 12
digits). For UPC-A and UPC-E (11 data digits) the rightmost digit received
weight 1 instead of 3, producing an invalid check digit — UPC-A symbols
failed to scan (verified with ZXing and zbar).

Weight from the right instead (rightmost data digit ×3), which is the
length-independent GS1 rule and leaves EAN-13/EAN-8 output unchanged.
Add a UPC-A regression test (03600029145 -> check 2).
Add a package `exclude` so `examples/` (a dev-only generation harness that
needs the `image` feature) and `generated_barcodes/` (throwaway PNGs) are
never shipped to crates.io, and gitignore the generated images.
Reproducible example that renders a PNG for every symbology (for manual
decoder testing). Gated with required-features = ["image"] so it is
skipped by default/no-default-features builds, and excluded from the
published crate.
@ashaffah
ashaffah merged commit 0824bfb into main Jul 6, 2026
@ashaffah
ashaffah deleted the feat/zero-alloc branch July 6, 2026 19:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants