Skip to content

Releases: nao1215/yabase

v0.22.0

21 May 10:30
18259b0

Choose a tag to compare

Added

  • intid.encode_int_base16_compact/1: leading-zero-dropping companion to the existing byte-aligned intid.encode_int_base16/1, so the encode_int_* family now has a uniform compact variant across base10/base16/base36/base58/base62 (encode_int_base16_compact(1) == Ok("1"), encode_int_base16_compact(2025) == Ok("7E9")); the byte-aligned encode_int_base16/1 is unchanged. intid.decode_int_base16/1 now also accepts odd-length input (internally zero-padded on the left), so decode_int_base16(encode_int_base16_compact(n) |> result.unwrap_or("")) == Ok(n) round-trips for every non-negative Int; the low-level base16.decode/1 keeps its strict even-length contract. (#99)

Changed

  • BREAKING: every intid.encode_int_* function now returns Result(String, CodecError) instead of String, and rejects negative inputs with the new Error(NegativeValue(value)) variant on CodecError. The previous behaviour silently absolutized negatives via int.absolute_value, which broke the decode(encode(n)) == n round-trip whenever n < 0 and let two distinct inputs (-1 and 1) collide on the same encoded string. Callers that fed only non-negative values into encode_int_* need to unwrap the new Result (either let assert Ok(s) = ... for known-safe call sites, or result.map / case for ones that already accept negatives at the boundary). The new NegativeValue(value: Int) variant on CodecError is exported via the existing yabase/core/error and the intid.CodecError re-export. Closed #84; regressed in #100. (#100)
  • BREAKING: intid.decode_int_base58 (and decode_int_base58_bounded) now enforces canonical wire form. Inputs that decode to a value n but are not byte-equal to encode_int_base58(n) — the previously-tolerated leading-"1" aliases such as "15Q", "115Q", "1115Q" — return Error(NonCanonical) instead of Ok(255). "1" alone remains the canonical encoding of 0 and decodes to Ok(0); only extra leading "1" characters are rejected. This closes the silent-duplicate-ID surface for ID callers (URL shorteners, idempotency keys, database lookups, cache keys) where two distinct wire strings used to name the same row, breaking deduplication and cache invariants. The other decode_int_* codecs (base10 / base16 / base32 RFC 4648 / Crockford / Crockford-Check / base36 / base58 Flickr / base62) still accept leading zero characters leniently; applying the same canonical-form check to them consistently is tracked as a follow-up. The NonCanonical variant on CodecError is the existing one already used by base16.decode_strict / base32/rfc4648.decode_strict / base64/standard.decode_strict, so callers that already pattern-match Error(NonCanonical) for those strict decoders inherit the same shape here without a new variant to handle. Closes #101. (#101)

v0.21.0

18 May 12:09
62ee860

Choose a tag to compare

Documentation

  • README's decode_base64_strict("TR==") example now annotates the result as Error(NonCanonical) instead of Error(InvalidPadding). The CodecError type has no InvalidPadding constructor, so pattern-matching the README's snippet failed to compile. The strict decoders' own doc-strings already used the correct variant name; this fix aligns the README with both the type and the docstrings. (#96)

v0.20.0

12 May 13:13
f727f3b

Choose a tag to compare

Added

  • intid.encode_int(encoding:, value:),
    intid.decode_int(encoding:, value:), and
    intid.decode_int_bounded(encoding:, value:, max:): the generic
    integer-side facade that mirrors yabase.encode / yabase.decode
    for runtime codec selection. Picks the right per-base helper from
    an Encoding value so callers writing their own dispatch
    (case enc { Base58 -> intid.encode_int_base58(n); ... }) can
    delete the boilerplate. Supports the same integer-domain codecs
    the per-base helpers already cover (Base10, Base16, Base32
    RFC4648 / Crockford / CrockfordCheck, Base36, Base58 Bitcoin /
    Flickr, Base58Check, Base62). Surfaces a new UnsupportedForInt
    error variant on CodecError for byte-only encodings (Base2,
    Base8, Base64 family, Base85 family, Base91, Bech32, the
    remaining Base32 variants). The per-base encode_int_* /
    decode_int_* / decode_int_*_bounded helpers stay for callers
    who pick the codec at compile time — no breaking change. (#93)

v0.19.0

10 May 02:05
bfa4551

Choose a tag to compare

Added

  • yabase/intid.encode_int_base16 / decode_int_base16 /
    decode_int_base16_bounded
    : closes the API symmetry gap where
    every other base in intid (base10, base32 RFC 4648 / Crockford,
    base36, base58 Bitcoin / Flickr / check, base62) had encode_int_*
    and decode_int_* helpers but base16 was missing. The new helpers
    route through yabase/base16 and use the canonical RFC 4648 §8
    uppercase output. Lenient case-insensitive read on the decode side,
    matching the rest of the family. Adds 9 regression tests covering
    zero, single byte, canonical uppercase, empty input rejection,
    round-trip, case-insensitive read, invalid character, and the
    bounded variants. Closes #85.
  • yabase/base16.decode_strict/1: canonical-form check for hex
    digests, mirroring the existing decode_strict on base32/rfc4648
    and base64/standard. Returns Error(NonCanonical) for input
    that decodes successfully but is not byte-equal to encode/1's
    output — i.e. lowercase (encode_lowercase/1's output), mixed
    case, or any other deviation from the RFC 4648 §8 canonical
    uppercase form. Useful for HMAC/TOTP/WebAuthn/content-addressable
    storage workflows where the encoded string itself is part of the
    contract. Other failure modes (InvalidCharacter,
    InvalidLength) surface unchanged from decode/1. Closes #87.

Documentation

  • yabase/intid negative-input handling: every encode_int_*
    function silently normalizes negative inputs to
    int.absolute_value. The previous module docstring mentioned this
    in passing — easy to miss because it was buried inside a long
    paragraph and the per-function one-liners said "non-negative
    Int" without noting what happens when callers pass a negative.
    Move the negative-input note to a dedicated ## Negative inputs are silently absolutized subsection at the top of the module
    doc, calling it out as an intentional but footgun-prone design
    choice and showing the recommended boundary-check pattern. Update
    every encode_int_* function's one-line docstring to read
    "Negative inputs are normalized to int.absolute_value; see the
    module note ..." so callers can't miss it. Behaviour itself is
    unchanged — the contract is the same, just made more visible.
    Closes #84.

Fixed

  • Security / Functional Suitability: base32/rfc4648.decode_strict
    was upper-casing the input before comparing to the canonical
    re-encoding (encode(bytes) == string.uppercase(input)), which
    silently accepted lowercase and mixed-case input — exactly the
    axis the strict path exists to gate. The check is now byte-equal
    (encode(bytes) == input), so lowercase, mixed-case, missing
    padding, and any other deviation from encode/1's output are
    rejected with Error(NonCanonical). This closes the replay-attack
    surface where two distinct wire forms (e.g. MZXW6=== and
    mzxw6===) both validated as canonical for the same bytes —
    important for HMAC / TOTP / WebAuthn / content-addressable-storage
    use cases that were the entire reason for the strict path.
    The docstring is updated to state the contract clearly. The
    pre-existing test that asserted the buggy behaviour
    (rfc4648_decode_strict_lowercase_canonical_passes_test) is
    rewritten to assert the new contract, and two new regression tests
    cover mixed case and missing-padding cases. Closes #86.

Documentation

  • README gains a ## Strictness section between ## Quick start
    and ## Integer IDs. The new section names the lenient-by-
    default decode_base32 / decode_base64 posture, shows the
    decode_base64("TR==") repro for non-canonical pad bits (RFC
    4648 §3.5), points at the _strict siblings on the facade,
    cross-references the per-encoding strict variants for URL-safe
    / hex / nopadding, and gives the lenient-vs-strict default
    recommendation (strict for attacker-controlled input, lenient
    for friendly producers). Closes the discoverability gap from
    #79 — the strict variants shipped in #39 but the README never
    mentioned them. (#79)

Added

  • yabase/intid: encode_int_base10 /
    decode_int_base10 / decode_int_base10_bounded close the
    decimal gap in the integer-id surface. Every other base
    (base32 RFC 4648 / Crockford, base36, base58 Bitcoin /
    Flickr, base62) had encode/decode/bounded-decode helpers; the
    base10 omission broke switch-case bench harnesses ("swap
    base58 for base62 to compare ID lengths, hit a compile error
    on encode_int_base10") and forced callers to special-case
    decimal with int.to_string / int.parse plus a hand-rolled
    bounds check. The new helpers route through yabase/base10
    for symmetry with the rest of the family. (#78)
  • Property-based round-trip tests using
    metamon covering every
    encoding's decode(encode(data)) == Ok(data) invariant. Lives
    in test/yabase_metamon_test.gleam and pins the documented
    round-trip law for: base16 (upper / lowercase), base32 RFC 4648
    / hex / Crockford / Clockwork, base64 standard / urlsafe /
    nopadding, base10, base36, base45, base58 Bitcoin, base62,
    base91, ascii85 (4-byte-aligned inputs), z85 (4-byte-aligned
    inputs). The 4-byte-aligned generators stay inside ascii85 /
    z85's documented length contract; non-conforming length is
    rejected with LengthNotMultipleOf4 per the existing strict
    variants and is exercised by the per-encoding test files.

v0.18.0

09 May 22:22
5a926bc

Choose a tag to compare

Documentation

  • README gains a ## Strictness section between ## Quick start
    and ## Integer IDs. The new section names the lenient-by-
    default decode_base32 / decode_base64 posture, shows the
    decode_base64("TR==") repro for non-canonical pad bits (RFC
    4648 §3.5), points at the _strict siblings on the facade,
    cross-references the per-encoding strict variants for URL-safe
    / hex / nopadding, and gives the lenient-vs-strict default
    recommendation (strict for attacker-controlled input, lenient
    for friendly producers). Closes the discoverability gap from
    #79 — the strict variants shipped in #39 but the README never
    mentioned them. (#79)

Added

  • yabase/intid: encode_int_base10 /
    decode_int_base10 / decode_int_base10_bounded close the
    decimal gap in the integer-id surface. Every other base
    (base32 RFC 4648 / Crockford, base36, base58 Bitcoin /
    Flickr, base62) had encode/decode/bounded-decode helpers; the
    base10 omission broke switch-case bench harnesses ("swap
    base58 for base62 to compare ID lengths, hit a compile error
    on encode_int_base10") and forced callers to special-case
    decimal with int.to_string / int.parse plus a hand-rolled
    bounds check. The new helpers route through yabase/base10
    for symmetry with the rest of the family. (#78)
  • Property-based round-trip tests using
    metamon covering every
    encoding's decode(encode(data)) == Ok(data) invariant. Lives
    in test/yabase_metamon_test.gleam and pins the documented
    round-trip law for: base16 (upper / lowercase), base32 RFC 4648
    / hex / Crockford / Clockwork, base64 standard / urlsafe /
    nopadding, base10, base36, base45, base58 Bitcoin, base62,
    base91, ascii85 (4-byte-aligned inputs), z85 (4-byte-aligned
    inputs). The 4-byte-aligned generators stay inside ascii85 /
    z85's documented length contract; non-conforming length is
    rejected with LengthNotMultipleOf4 per the existing strict
    variants and is exercised by the per-encoding test files.

v0.17.0

09 May 11:44
409365e

Choose a tag to compare

Added

  • yabase/intid gains checksum-bearing _check variants for the
    Crockford Base32 and Base58Check codecs, removing the
    Int → BitArray → encode_check / decode_check → BitArray → Int
    dance every caller previously reimplemented. New helpers:
    encode_int_base32_crockford_check,
    decode_int_base32_crockford_check[_bounded],
    encode_int_base58check,
    decode_int_base58check[_bounded]. The decoders surface
    Error(InvalidChecksum) on a mistyped input — the whole reason
    callers reach for the checksummed variant. Base58Check is fixed at
    version byte 0 (Bitcoin mainnet P2PKH); callers who need a
    different version still drop to yabase/base58check directly. (#73)
  • yabase/intid and the top-level yabase module now re-export
    CodecError
    as a public type alias, so callers who only
    import yabase/intid (or only import yabase) can type-annotate a
    wrapper around decode_int_* / encode / decode without reaching
    into yabase/core/error. The alias preserves type identity (it
    resolves to the same CodecError the underlying functions already
    return), so existing code keeps working unchanged. (#74)

v0.16.0

08 May 00:10
86e65a3

Choose a tag to compare

Tests

  • New test/empty_input_round_trip_test.gleam pins the empty-input
    round-trip contract uniformly across all 32 facade pairs in
    yabase/facade. For every total encode_* function, the test
    asserts decode(encode(<<>>)) == Ok(<<>>). For the two Result-
    returning encoders (Z85, RFC 1924 Base85) the same round-trip is
    pinned via let assert Ok(encoded) = encode(<<>>). The RFC 4648
    family additionally pins decode("") == Ok(<<>>) directly. A future
    refactor that flips the rule for one codec now surfaces here as a
    single-test diff. metamon is not a dev-dep so the property is
    exercised exhaustively over the facade rather than via
    forall_round_trip. (#70)

v0.15.0

07 May 04:41
71bd8bc

Choose a tag to compare

Added

  • Checksum-bearing encodings now fit the Encoding ADT. New
    smart constructors encoding.base58_check(version: Int),
    encoding.bech32(hrp: String), and encoding.bech32m(hrp: String)
    return Encoding values that the unified yabase.encode and
    yabase.decode family dispatches alongside the plain codecs.
    yabase.decode rejects any wire whose embedded checksum-bearing
    metadata (Base58Check version, Bech32 HRP / variant) does not
    match what the caller declared on the Encoding value:
    • Base58Check version mismatch → Error(InvalidChecksum) (the
      version is part of the checksummed payload, so a mismatch is a
      checksum-class failure on the wire).
    • Bech32 HRP mismatch → Error(InvalidHrp(_)) carrying the
      expected and observed HRP.
    • Bech32 / Bech32m variant mismatch → Error(InvalidChecksum)
      (the variants share the same wire shape but differ in the
      checksum constant).
      Property-test tooling that consumes Encoding (e.g.
      metamon's
      forall_round_trip) can now drive every codec — plain and
      checksum-bearing — through one ADT without forking by codec
      module. The README's "Checksum-bearing" section is rewritten to
      lead with the unified API while the per-module
      yabase/base58check, yabase/bech32 decoders remain available
      for callers that need to inspect the embedded metadata
      directly. Six round-trip tests in
      test/checksum_bearing_adt_test.gleam lock the new dispatch
      (positive controls + version / HRP / variant mismatch
      rejection). (#65)

Documentation

  • README: new "Codec ergonomics" section spells out the
    per-module encode return-type asymmetry (String vs
    Result(String, CodecError)) that arises from genuine encode-
    time preconditions in z85, rfc1924_base85, base58check,
    and bech32. The section names every codec by category and
    recommends the unified yabase.encode API for callers that
    need a uniform Result shape (e.g. property-test tooling).
    The asymmetry itself is unchanged — closing it would require
    a smart-constructor refactor across every Result-returning
    codec, which is a larger breaking change. (#63)

Fixed

  • Every encoder now rejects sub-byte input at the boundary
    via the new yabase/core/guard.assert_byte_aligned helper, which
    panics when the input BitArray's total bit length is not a
    multiple of 8. Previously the byte-walker pattern
    <<byte:int, rest:bits>> used in base16.encode,
    base2.encode, base8.encode, base64.standard.encode, and
    most other codecs only matched a full 8-bit prefix; any trailing
    fewer-than-8-bit segment fell through the catch-all branch and
    was silently dropped, producing a string that decoded to a
    strictly shorter BitArray than the caller supplied. A
    reproducer like base16.encode(<<0xFF, 0x80, 1:size(3)>>) lost
    the 3-bit tail without warning. The new guard turns that data
    loss into a loud crash with a diagnostic message
    ("yabase encode: input must be byte-aligned (a multiple of 8 bits); got N bits"). Sub-byte input has always been outside the
    encode contract, so the change is API-compatible for every
    caller that was already passing byte-aligned BitArrays.
    Affected codecs: base2, base8, base10, base16,
    base32/{rfc4648, hex, crockford, clockwork, zbase32}, base36,
    base45, base58/{bitcoin, flickr}, base62,
    base64/{standard, urlsafe, nopadding, urlsafe_nopadding, dq},
    base91, ascii85, adobe_ascii85. The Result-returning
    codecs (z85, rfc1924_base85, base58check, bech32)
    already rejected sub-byte input via their existing length checks
    and are unchanged. (#64)

v0.14.0

06 May 05:19
9d55012

Choose a tag to compare

Fixed

  • base91: decode now silently ignores ASCII whitespace
    ( , \t, \n, \r, and the CR+LF cluster) instead of
    rejecting it as InvalidCharacter. The basE91 reference
    implementation is explicitly lenient about non-alphabet bytes,
    and the reference encoder wraps long output at 76 chars, so any
    caller piping wrapped text used to need a hand-rolled
    string.replace shim. (#60)
  • ascii85: decode now skips ASCII whitespace at group
    boundaries and inside groups, mirroring the existing
    adobe_ascii85 posture (and the historical btoa reference
    decoders, which all ignore whitespace). Production btoa emitters
    routinely wrap output at column 76; rejecting whitespace forced
    every caller to strip it first. (#59)

v0.13.0

04 May 01:18
543efd3

Choose a tag to compare

Documentation

  • readme: Quick start now ends at the facade encode + decode happy
    path with a single sentence pointing at a new "Notes for production
    code" section near the bottom of the README. The prior let assert /
    glinter assert_ok_pattern = "error" explanation, and the pointer
    to the Result-shaped unified yabase.encode, are moved verbatim
    into that later section so first-time readers see the facade success
    path before the policy detail. (#55)