Skip to content

Fix UTF-8 astral encoding, Crockford decode, and a CI-breaking analyzer info#11

Merged
dipu-bd merged 4 commits into
masterfrom
claude/last-2-commits-afvxbf
Jul 22, 2026
Merged

Fix UTF-8 astral encoding, Crockford decode, and a CI-breaking analyzer info#11
dipu-bd merged 4 commits into
masterfrom
claude/last-2-commits-afvxbf

Conversation

@dipu-bd

@dipu-bd dipu-bd commented Jul 22, 2026

Copy link
Copy Markdown
Member

A repo-wide audit surfaced three actionable issues. Each is a separate commit, all verified with the full preflight (format, dart analyze --fatal-infos, dart test on vm and node, integration smoke test) and 100% line coverage on lib/ (911/911).

Observable-output changes are filed under a # _next_ CHANGELOG heading so the owner can pick the version number (both are semver-relevant per the operating manual).

1. UTF-8 encoder emitted invalid bytes for scalar code points ≥ U+10000

UTF8Encoder.convert's 3-byte branch had no upper bound, so a scalar code point in U+10000..U+10FFFF (a documented source: 32 input) was force-fit into 3 bytes and produced invalid UTF-8 — e.g. convert([0x1F600]) emitted [0xFF, 0x98, 0x80] (0xFF is never a valid UTF-8 byte) instead of [0xF0, 0x9F, 0x98, 0x80]. The 4-byte path was only reachable via a UTF-16 surrogate pair, never a scalar astral value.

  • Public toUtf8(String) was never affected — UTF-16 strings deliver astral characters as surrogate pairs, which already hit the correct 4-byte path. The bug only bit direct UTF8Encoder().convert([scalar]) callers.
  • Regression test asserts convert([scalar]) matches dart:convert for U+10000, U+1F600, U+1D11E, U+10FFFF, and round-trips.
  • Review follow-up: the scalar and surrogate-pair 4-byte cases were merged into one shared emit block.

2. Crockford Base-32 decoder was not case-insensitive / spec-compliant

The reverse table was upper-only and marked I/L/O invalid, so lowercase Crockford strings were undecodable and the spec's ambiguous-letter substitutions were missing — contradicting fromBase32's own dartdoc. Now, per Crockford's spec, decoding is case-insensitive with I/i/L/l1 and O/o0 (U/u stays outside the alphabet). Encoding output is unchanged.

  • Table regenerated by tool/alphabet_maker.dart (its rev helper gained an alias map); never hand-typed.
  • Decode differential tests against package:base_codecs' base32Crockford (uppercase, lowercase, ambiguous letters) plus a U/u-rejection check.

3. benchmark/bit.dart failed dart analyze --fatal-infos

Public fields exposed the private _BitEncoder/_BitDecoder types (library_private_types_in_public_api), failing the analyzer step CI and the documented preflight both run. It slipped through because the Test workflow's paths: filter excludes benchmark/**. Fields are now typed as the public base classes. Also alphabetized the core/bit.dart export and corrected stale scripts/alphabet_maker.py references (the tool is tool/alphabet_maker.dart).


Scope note

This PR does not touch the crypt / PHC code. A separate feature-gap roadmap (Base58, Base85/Z85, Bech32, streaming, constantTimeEquals, …) was produced alongside the audit.

claude added 3 commits July 22, 2026 07:27
…e U+FFFF

The `UTF8Encoder.convert` 3-byte branch had no upper bound, so a scalar code
point in `U+10000..U+10FFFF` (a documented `source: 32` input) was force-fit
into a 3-byte sequence and produced invalid UTF-8 — for example `U+1F600`
emitted the byte `0xFF`, which is never a valid UTF-8 byte. The 4-byte path was
only reachable through a UTF-16 surrogate pair, never a scalar astral value.

Gate the 3-byte branch with `x <= 0xFFFF` and add a 4-byte branch for
non-surrogate scalars. The public `toUtf8(String)` path was never affected,
since UTF-16 strings deliver astral characters as surrogate pairs.

Adds a regression group asserting `UTF8Encoder().convert([scalar])` matches
`dart:convert` for `U+10000`, `U+1F600`, `U+1D11E`, and `U+10FFFF`, and that
each round-trips back through the decoder.
The Crockford reverse table was upper-only and marked `I`, `L`, `O` as invalid,
so lowercase Crockford strings could not be decoded and the specification's
ambiguous-letter substitutions were missing. This contradicted `fromBase32`'s
own dartdoc, which promises it handles both upper and lower case.

Per Crockford's specification, decoding is now case-insensitive and accepts the
ambiguous letters as digits: `I`/`i`/`L`/`l` decode as `1`, and `O`/`o` decode
as `0`. `U`/`u` remains outside the alphabet. The encoding alphabet and its
output are unchanged.

The table is regenerated by `tool/alphabet_maker.dart`, whose `rev` helper now
accepts an optional alias map for such substitutions. Adds decode differential
tests against `package:base_codecs`' `base32Crockford` (uppercase, lowercase,
and the ambiguous letters), plus a check that `U`/`u` is rejected.
`benchmark/bit.dart` exposed the private `_BitEncoder`/`_BitDecoder` types
through public fields, tripping `library_private_types_in_public_api` and
failing `dart analyze --fatal-infos` (which CI and the documented preflight
both run). The Test workflow's `paths:` filter excludes `benchmark/**`, so it
went unnoticed. Annotate the fields with the public `BitEncoder`/`BitDecoder`
base types.

Also alphabetize the `core/bit.dart` export in `lib/src/codecs_base.dart`, and
correct the `alphabet_maker` references in `AGENTS.md` and the `new-codec`
skill: the tool is `tool/alphabet_maker.dart` (run with `dart run`), not the
non-existent `scripts/alphabet_maker.py`.

@dipu-bd dipu-bd left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

check if you want to fix these. otherwise present your reason for denial. i will consider to approve or not

Comment thread lib/src/codecs/utf8.dart
Comment thread tool/alphabet_maker.dart Outdated
- Fold the scalar and surrogate-pair 4-byte cases in `UTF8Encoder.convert`
  into a single branch: the surrogate pair is combined into the scalar `x`,
  then both share one emit block. Removes the now-unused `c` local. Output is
  byte-identical (vm + node green, coverage still 100%).
- Remove the verbose comments added to `rev` in `tool/alphabet_maker.dart`.
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

Files with missing lines Coverage Δ
lib/src/codecs/base32.dart 100.00% <ø> (ø)
lib/src/codecs/utf8.dart 100.00% <100.00%> (ø)

@dipu-bd
dipu-bd merged commit d9cbfd0 into master Jul 22, 2026
18 checks passed
@dipu-bd
dipu-bd deleted the claude/last-2-commits-afvxbf branch July 22, 2026 15:27
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.

3 participants