Releases: nao1215/yabase
Releases · nao1215/yabase
v0.22.0
Added
intid.encode_int_base16_compact/1: leading-zero-dropping companion to the existing byte-alignedintid.encode_int_base16/1, so theencode_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-alignedencode_int_base16/1is unchanged.intid.decode_int_base16/1now also accepts odd-length input (internally zero-padded on the left), sodecode_int_base16(encode_int_base16_compact(n) |> result.unwrap_or("")) == Ok(n)round-trips for every non-negativeInt; the low-levelbase16.decode/1keeps its strict even-length contract. (#99)
Changed
- BREAKING: every
intid.encode_int_*function now returnsResult(String, CodecError)instead ofString, and rejects negative inputs with the newError(NegativeValue(value))variant onCodecError. The previous behaviour silently absolutized negatives viaint.absolute_value, which broke thedecode(encode(n)) == nround-trip whenevern < 0and let two distinct inputs (-1and1) collide on the same encoded string. Callers that fed only non-negative values intoencode_int_*need to unwrap the newResult(eitherlet assert Ok(s) = ...for known-safe call sites, orresult.map/casefor ones that already accept negatives at the boundary). The newNegativeValue(value: Int)variant onCodecErroris exported via the existingyabase/core/errorand theintid.CodecErrorre-export. Closed #84; regressed in #100. (#100) - BREAKING:
intid.decode_int_base58(anddecode_int_base58_bounded) now enforces canonical wire form. Inputs that decode to a valuenbut are not byte-equal toencode_int_base58(n)— the previously-tolerated leading-"1"aliases such as"15Q","115Q","1115Q"— returnError(NonCanonical)instead ofOk(255)."1"alone remains the canonical encoding of0and decodes toOk(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 otherdecode_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. TheNonCanonicalvariant onCodecErroris the existing one already used bybase16.decode_strict/base32/rfc4648.decode_strict/base64/standard.decode_strict, so callers that already pattern-matchError(NonCanonical)for those strict decoders inherit the same shape here without a new variant to handle. Closes #101. (#101)
v0.21.0
Documentation
- README's
decode_base64_strict("TR==")example now annotates the result asError(NonCanonical)instead ofError(InvalidPadding). TheCodecErrortype has noInvalidPaddingconstructor, 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
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 mirrorsyabase.encode/yabase.decode
for runtime codec selection. Picks the right per-base helper from
anEncodingvalue 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 newUnsupportedForInt
error variant onCodecErrorfor byte-only encodings (Base2,
Base8, Base64 family, Base85 family, Base91, Bech32, the
remaining Base32 variants). The per-baseencode_int_*/
decode_int_*/decode_int_*_boundedhelpers stay for callers
who pick the codec at compile time — no breaking change. (#93)
v0.19.0
Added
yabase/intid.encode_int_base16/decode_int_base16/
decode_int_base16_bounded: closes the API symmetry gap where
every other base inintid(base10, base32 RFC 4648 / Crockford,
base36, base58 Bitcoin / Flickr / check, base62) hadencode_int_*
anddecode_int_*helpers but base16 was missing. The new helpers
route throughyabase/base16and 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 existingdecode_strictonbase32/rfc4648
andbase64/standard. ReturnsError(NonCanonical)for input
that decodes successfully but is not byte-equal toencode/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 fromdecode/1. Closes #87.
Documentation
yabase/intidnegative-input handling: everyencode_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 absolutizedsubsection 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
everyencode_int_*function's one-line docstring to read
"Negative inputs are normalized toint.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 fromencode/1's output are
rejected withError(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
## Strictnesssection between## Quick start
and## Integer IDs. The new section names the lenient-by-
defaultdecode_base32/decode_base64posture, shows the
decode_base64("TR==")repro for non-canonical pad bits (RFC
4648 §3.5), points at the_strictsiblings 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_boundedclose 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
onencode_int_base10") and forced callers to special-case
decimal withint.to_string/int.parseplus a hand-rolled
bounds check. The new helpers route throughyabase/base10
for symmetry with the rest of the family. (#78)- Property-based round-trip tests using
metamon covering every
encoding'sdecode(encode(data)) == Ok(data)invariant. Lives
intest/yabase_metamon_test.gleamand 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 withLengthNotMultipleOf4per the existing strict
variants and is exercised by the per-encoding test files.
v0.18.0
Documentation
- README gains a
## Strictnesssection between## Quick start
and## Integer IDs. The new section names the lenient-by-
defaultdecode_base32/decode_base64posture, shows the
decode_base64("TR==")repro for non-canonical pad bits (RFC
4648 §3.5), points at the_strictsiblings 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_boundedclose 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
onencode_int_base10") and forced callers to special-case
decimal withint.to_string/int.parseplus a hand-rolled
bounds check. The new helpers route throughyabase/base10
for symmetry with the rest of the family. (#78)- Property-based round-trip tests using
metamon covering every
encoding'sdecode(encode(data)) == Ok(data)invariant. Lives
intest/yabase_metamon_test.gleamand 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 withLengthNotMultipleOf4per the existing strict
variants and is exercised by the per-encoding test files.
v0.17.0
Added
yabase/intidgains checksum-bearing_checkvariants 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 byte0(Bitcoin mainnet P2PKH); callers who need a
different version still drop toyabase/base58checkdirectly. (#73)yabase/intidand the top-levelyabasemodule now re-export
CodecErroras a public type alias, so callers who only
import yabase/intid(or onlyimport yabase) can type-annotate a
wrapper arounddecode_int_*/encode/decodewithout reaching
intoyabase/core/error. The alias preserves type identity (it
resolves to the sameCodecErrorthe underlying functions already
return), so existing code keeps working unchanged. (#74)
v0.16.0
Tests
- New
test/empty_input_round_trip_test.gleampins the empty-input
round-trip contract uniformly across all 32 facade pairs in
yabase/facade. For every totalencode_*function, the test
assertsdecode(encode(<<>>)) == Ok(<<>>). For the twoResult-
returning encoders (Z85, RFC 1924 Base85) the same round-trip is
pinned vialet assert Ok(encoded) = encode(<<>>). The RFC 4648
family additionally pinsdecode("") == 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
Added
- Checksum-bearing encodings now fit the
EncodingADT. New
smart constructorsencoding.base58_check(version: Int),
encoding.bech32(hrp: String), andencoding.bech32m(hrp: String)
returnEncodingvalues that the unifiedyabase.encodeand
yabase.decodefamily dispatches alongside the plain codecs.
yabase.decoderejects any wire whose embedded checksum-bearing
metadata (Base58Check version, Bech32 HRP / variant) does not
match what the caller declared on theEncodingvalue:- 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 consumesEncoding(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/bech32decoders remain available
for callers that need to inspect the embedded metadata
directly. Six round-trip tests in
test/checksum_bearing_adt_test.gleamlock the new dispatch
(positive controls + version / HRP / variant mismatch
rejection). (#65)
- Base58Check version mismatch →
Documentation
- README: new "Codec ergonomics" section spells out the
per-moduleencodereturn-type asymmetry (Stringvs
Result(String, CodecError)) that arises from genuine encode-
time preconditions inz85,rfc1924_base85,base58check,
andbech32. The section names every codec by category and
recommends the unifiedyabase.encodeAPI for callers that
need a uniformResultshape (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 newyabase/core/guard.assert_byte_alignedhelper, which
panics when the inputBitArray's total bit length is not a
multiple of 8. Previously the byte-walker pattern
<<byte:int, rest:bits>>used inbase16.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 shorterBitArraythan the caller supplied. A
reproducer likebase16.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
encodecontract, so the change is API-compatible for every
caller that was already passing byte-alignedBitArrays.
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
Fixed
- base91:
decodenow silently ignores ASCII whitespace
(,\t,\n,\r, and the CR+LF cluster) instead of
rejecting it asInvalidCharacter. 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.replaceshim. (#60) - ascii85:
decodenow skips ASCII whitespace at group
boundaries and inside groups, mirroring the existing
adobe_ascii85posture (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
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 priorlet assert/
glinterassert_ok_pattern = "error"explanation, and the pointer
to theResult-shaped unifiedyabase.encode, are moved verbatim
into that later section so first-time readers see the facade success
path before the policy detail. (#55)