Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ the table layout.
backticks around identifiers: `Add constructor to ByteCollector abstract class`,
`Introduce \`ByteCollector\` abstract class`. One logical change per commit.

**Pull requests.** Do not append the "Generated with Claude Code" footer, or any
other bot attribution, to PR bodies.

**CHANGELOG.md.** Newest first, `# X.Y.Z` heading, `-` bullets. Breaking changes
lead with `**Breaking Changes**:` and list renames as `` `old` -> `new` ``.
Every released version has an entry; the top entry is extracted verbatim into
Expand Down
97 changes: 34 additions & 63 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,66 +1,37 @@
# _next_

- Fix the UTF-8 encoder producing invalid output for scalar code points in
`U+10000..U+10FFFF`. The `UTF8Encoder.convert` 3-byte branch had no upper
bound, so an astral scalar (e.g. `U+1F600`, a documented `source: 32` input)
was force-fit into 3 bytes and emitted invalid UTF-8 (including the byte
`0xFF`). It now emits the correct 4-byte sequence, verified against
`dart:convert`. The public `toUtf8(String)` path was never affected, since
UTF-16 strings deliver astral characters as surrogate pairs. **Observable
output change** for direct `UTF8Encoder().convert([scalar])` callers.
- Make the Crockford Base-32 decoder spec-compliant: it is now case-insensitive
and substitutes the ambiguous letters `I`/`i`/`L`/`l` -> `1` and `O`/`o` ->
`0` (the letter `U`/`u` is still not part of the alphabet). Previously
lowercase Crockford strings and the ambiguous letters were rejected. Verified
against `package:base_codecs`. Encoding output is unchanged. **Observable
change**: inputs that previously threw now decode.
- Fix `benchmark/bit.dart` exposing the private `_BitEncoder`/`_BitDecoder`
types through public fields, which failed `dart analyze --fatal-infos`
(benchmark tooling only; no library code or output is affected).
- Alphabetize the `core/bit.dart` export in `lib/src/codecs_base.dart`.

# 3.5.2

- Renames internal abstract class `CipherlibConverter` -> `BitConverter`.
- Add missing dartdoc to the `Base32Encoder`, `Base32Decoder`, `Base64Encoder`,
and `Base64Decoder` constructors and the `UTF8Encoder` and `UTF8Decoder`
classes.
- Speed up `fromBase32` and `fromBase64` by returning the decoder output
directly instead of copying it into a fresh `Uint8List`.
- Speed up the generic `AlphabetEncoder` by fusing the alphabet lookup into the
bit-regrouping loop and writing straight into a single correctly-sized buffer,
removing an extra allocation and two full passes over the output. Output is
byte-identical (verified against `dart:convert` and RFC 4648 vectors).
- Export the `ByteEncoder`, `ByteDecoder`, `AlphabetEncoder`, and
`AlphabetDecoder` base classes, which were part of the public API surface but
not reachable through `package:convertlib/convertlib.dart`.
- Move `BitEncoder` and `BitDecoder` into the same file.
- `AlphabetDecoder` extends `ByteDecoder` now intead of `BitDecoder`.
- Fix `ByteCollector.number` truncating values wider than 32 bits on the web,
where bitwise operators are limited to 32 bits. It now accumulates with
multiplication and is exact up to `2^53` on the web (a web `int` cannot
represent larger values). Output on the VM is unchanged.
- Allow empty parameter values in the PHC/crypt string format, as permitted by
the specification (e.g. `$id$data=`). `CryptData.validate` no longer rejects
an empty parameter value; salt and hash must still be non-empty.
- Fix the PHC decoder mis-classifying a comma-containing `v=...` segment (e.g.
`v=19,m=8`) as the version; it is now parsed as a parameters segment.
- Add value-based `==` and `hashCode` to `CryptData`.
- `BigIntCodec` decoders now return a `Uint8List`; the `BigIntDecoder` typedef
narrows from `Converter<BigInt, Iterable<int>>` to `Converter<BigInt,
Uint8List>`, and `fromBigInt` no longer copies the decoded bytes.
- Tighten `ByteCollector.isEqual` parameter type from `dynamic` to `Object?`.
- Standardize the `AlphabetDecoder` invalid-bit-length error on
`ArgumentError.value`, matching `AlphabetEncoder`.
- Add `benchmark/alphabet.dart` covering the generic `AlphabetEncoder` and
`AlphabetDecoder` engine.
- Add `benchmark/bit.dart` covering the generic `BitEncoder` and `BitDecoder`.
- Improve the benchmark harness (`benchmark/_base.dart`): it now reports the
median per-iteration time sampled across ~25ms batches instead of the
arithmetic mean, making results robust against GC pauses and keeping each
batch well above the coarsened web timer resolution. Benchmark tooling only;
no library code or output is affected.
- Add tests from official / well-known sources and cover corner cases.
# 3.6.0

- [**Breaking Changes**]
- `CipherlibConverter` -> `BitConverter` (the exported bit-converter base class).
- `BigIntDecoder` narrows from `Converter<BigInt, Iterable<int>>` to
`Converter<BigInt, Uint8List>`; `BigIntCodec` decoders now return a `Uint8List`.
- Export the `ByteEncoder`, `ByteDecoder`, `AlphabetEncoder`, and `AlphabetDecoder`
base classes, previously part of the API surface but not reachable through
`package:convertlib/convertlib.dart`.
- Add `constantTimeEquals`, a top-level constant-time byte comparison for
verifying MACs and digests without wrapping them in a `ByteCollector`.
- Add `toHexBytes`, `toBinaryBytes`, `toOctalBytes`, `toBase32Bytes`, and
`toBase64Bytes`, returning the encoded ASCII output as a `Uint8List` without
the intermediate `String`.
- Add non-throwing decoders `tryFromHex`, `tryFromBinary`, `tryFromOctal`,
`tryFromBase32`, `tryFromBase64`, `tryFromUtf8`, and `tryFromBigInt`, which
return `null` instead of throwing a `FormatException` on invalid input.
- Fix the UTF-8 encoder emitting invalid bytes for scalar code points in
`U+10000..U+10FFFF`; `UTF8Encoder.convert` now uses the 4-byte form (the public
`toUtf8` was unaffected). Verified against `dart:convert`.
- Make the Crockford Base-32 decoder case-insensitive and decode the ambiguous
letters `I`/`i`/`L`/`l` as `1` and `O`/`o` as `0`. Encoding output is unchanged.
- Fix `ByteCollector.number` truncating values wider than 32 bits on the web; it
now accumulates with multiplication and is exact up to `2^53` (VM unchanged).
- `ByteCollector.isEqual` returns `false` for a [String] that is not valid
hexadecimal instead of throwing a `FormatException`, matching its contract.
- Add value-based `==` and `hashCode` to `CryptData`; a null and an empty
`params` map now compare equal (both encode to the same string).
- PHC/crypt: allow empty parameter values (e.g. `$id$data=`), and fix the decoder
mis-classifying a comma-containing `v=...` segment as the version.
- Speed up `fromBase32`/`fromBase64` decoding and the generic `AlphabetEncoder` by
removing extra allocations and passes; output is byte-identical.
- Clarify the `FormatException` message for non-canonical encoded input:
`'Invalid length'` is now `'Invalid length or non-zero trailing bits'`.

# 3.5.1

Expand Down
35 changes: 35 additions & 0 deletions lib/src/base16.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,25 @@ String toHex(
return String.fromCharCodes(out);
}

/// Converts 8-bit integer sequence to Base-16 and returns the ASCII bytes.
///
/// This is the same as [toHex] but returns the encoded characters as a
/// [Uint8List] of ASCII codes, skipping the intermediate [String].
///
/// Parameters:
/// - [input] is a sequence of 8-bit integers.
/// - If [upper] is true, the uppercase standard alphabet is used.
/// - [codec] is the [Base16Codec] to use. It is derived from the other
/// parameters if not provided.
Uint8List toHexBytes(
List<int> input, {
Base16Codec? codec,
bool upper = false,
}) {
codec ??= _codecFromParameters(upper: upper);
return codec.encoder.convert(input);
}

/// Converts 4-bit Base-16 character sequence to 8-bit integer sequence.
///
/// Parameters:
Expand All @@ -51,3 +70,19 @@ Uint8List fromHex(
codec ??= _codecFromParameters();
return codec.decoder.convert(input.codeUnits);
}

/// Converts a Base-16 string to an 8-bit integer sequence, returning `null`
/// instead of throwing when the [input] is not valid.
///
/// This is the non-throwing counterpart of [fromHex]. See [fromHex] for the
/// meaning of [codec].
Uint8List? tryFromHex(
String input, {
Base16Codec? codec,
}) {
try {
return fromHex(input, codec: codec);
} on FormatException {
return null;
}
}
31 changes: 31 additions & 0 deletions lib/src/base2.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ String toBinary(
return String.fromCharCodes(out);
}

/// Converts 8-bit integer sequence to Base-2 and returns the ASCII bytes.
///
/// This is the same as [toBinary] but returns the encoded characters as a
/// [Uint8List] of ASCII codes, skipping the intermediate [String].
///
/// Parameters:
/// - [input] is a sequence of 8-bit integers.
/// - [codec] is the [Base2Codec] to use. Default: [Base2Codec.standard].
Uint8List toBinaryBytes(
List<int> input, {
Base2Codec codec = Base2Codec.standard,
}) {
return codec.encoder.convert(input);
}

/// Converts 2-bit Base-2 character sequence to 8-bit integer sequence.
///
/// Parameters:
Expand All @@ -46,3 +61,19 @@ Uint8List fromBinary(
}) {
return codec.decoder.convert(input.codeUnits);
}

/// Converts a Base-2 string to an 8-bit integer sequence, returning `null`
/// instead of throwing when the [input] is not valid.
///
/// This is the non-throwing counterpart of [fromBinary]. See [fromBinary] for
/// the meaning of [codec].
Uint8List? tryFromBinary(
String input, {
Base2Codec codec = Base2Codec.standard,
}) {
try {
return fromBinary(input, codec: codec);
} on FormatException {
return null;
}
}
47 changes: 47 additions & 0 deletions lib/src/base32.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,36 @@ String toBase32(
return String.fromCharCodes(out);
}

/// Converts 8-bit integer sequence to Base-32 and returns the ASCII bytes.
///
/// This is the same as [toBase32] but returns the encoded characters as a
/// [Uint8List] of ASCII codes, skipping the intermediate [String].
///
/// Parameters:
/// - [input] is a sequence of 8-bit integers
/// - If [lower] is true, the [Base32Codec.lowercase] alphabet is used.
/// - If [padding] is true, the output will have padding characters.
/// - [codec] is the [Base32Codec] to use. It is derived from the other
/// parameters if not provided.
Uint8List toBase32Bytes(
List<int> input, {
Base32Codec? codec,
bool lower = false,
bool padding = true,
}) {
codec ??= _codecFromParameters(
lower: lower,
padding: padding,
);
Uint8List out = codec.encoder.convert(input);
if (!padding && _codecsWithPadding.contains(codec)) {
out = Uint8List.fromList(
out.takeWhile((x) => x != codec!.encoder.padding).toList(),
);
}
return out;
}

/// Converts 5-bit Base-32 character sequence to 8-bit integer sequence.
///
/// Parameters:
Expand All @@ -78,3 +108,20 @@ Uint8List fromBase32(
codec ??= _codecFromParameters(padding: padding);
return codec.decoder.convert(input.codeUnits);
}

/// Converts a Base-32 string to an 8-bit integer sequence, returning `null`
/// instead of throwing when the [input] is not valid.
///
/// This is the non-throwing counterpart of [fromBase32]. See [fromBase32] for
/// the meaning of [codec] and [padding].
Uint8List? tryFromBase32(
String input, {
Base32Codec? codec,
bool padding = true,
}) {
try {
return fromBase32(input, codec: codec, padding: padding);
} on FormatException {
return null;
}
}
47 changes: 47 additions & 0 deletions lib/src/base64.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,36 @@ String toBase64(
return String.fromCharCodes(out);
}

/// Converts 8-bit integer sequence to Base-64 and returns the ASCII bytes.
///
/// This is the same as [toBase64] but returns the encoded characters as a
/// [Uint8List] of ASCII codes, skipping the intermediate [String].
///
/// Parameters:
/// - [input] is a sequence of 8-bit integers
/// - If [url] is true, URL and Filename-safe alphabet is used.
/// - If [padding] is true, the output will have padding characters.
/// - [codec] is the [Base64Codec] to use. It is derived from the other
/// parameters if not provided.
Uint8List toBase64Bytes(
List<int> input, {
Base64Codec? codec,
bool url = false,
bool padding = true,
}) {
codec ??= _codecFromParameters(
url: url,
padding: padding,
);
Uint8List out = codec.encoder.convert(input);
if (!padding && _codecsWithPadding.contains(codec)) {
out = Uint8List.fromList(
out.takeWhile((x) => x != codec!.encoder.padding).toList(),
);
}
return out;
}

/// Converts 6-bit Base-64 character sequence to 8-bit integer sequence.
///
/// Parameters:
Expand All @@ -75,3 +105,20 @@ Uint8List fromBase64(
codec ??= _codecFromParameters(padding: padding);
return codec.decoder.convert(input.codeUnits);
}

/// Converts a Base-64 string to an 8-bit integer sequence, returning `null`
/// instead of throwing when the [input] is not valid.
///
/// This is the non-throwing counterpart of [fromBase64]. See [fromBase64] for
/// the meaning of [codec] and [padding].
Uint8List? tryFromBase64(
String input, {
Base64Codec? codec,
bool padding = true,
}) {
try {
return fromBase64(input, codec: codec, padding: padding);
} on FormatException {
return null;
}
}
31 changes: 31 additions & 0 deletions lib/src/base8.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ String toOctal(
return String.fromCharCodes(out);
}

/// Converts 8-bit integer sequence to Base-8 and returns the ASCII bytes.
///
/// This is the same as [toOctal] but returns the encoded characters as a
/// [Uint8List] of ASCII codes, skipping the intermediate [String].
///
/// Parameters:
/// - [input] is a sequence of 8-bit integers.
/// - [codec] is the [Base8Codec] to use. Default: [Base8Codec.standard].
Uint8List toOctalBytes(
List<int> input, {
Base8Codec codec = Base8Codec.standard,
}) {
return codec.encoder.convert(input);
}

/// Converts 3-bit Base-8 character sequence to 8-bit integer sequence.
///
/// Parameters:
Expand All @@ -46,3 +61,19 @@ Uint8List fromOctal(
}) {
return codec.decoder.convert(input.codeUnits);
}

/// Converts a Base-8 string to an 8-bit integer sequence, returning `null`
/// instead of throwing when the [input] is not valid.
///
/// This is the non-throwing counterpart of [fromOctal]. See [fromOctal] for the
/// meaning of [codec].
Uint8List? tryFromOctal(
String input, {
Base8Codec codec = Base8Codec.standard,
}) {
try {
return fromOctal(input, codec: codec);
} on FormatException {
return null;
}
}
17 changes: 17 additions & 0 deletions lib/src/bigint.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,20 @@ Uint8List fromBigInt(
codec ??= _codecFromParameters(msbFirst: msbFirst);
return codec.decoder.convert(input);
}

/// Converts a non-negative [BigInt] to an 8-bit integer sequence, returning
/// `null` instead of throwing when the [input] is negative.
///
/// This is the non-throwing counterpart of [fromBigInt]. See [fromBigInt] for
/// the meaning of [codec] and [msbFirst].
Uint8List? tryFromBigInt(
BigInt input, {
BigIntCodec? codec,
bool msbFirst = false,
}) {
try {
return fromBigInt(input, codec: codec, msbFirst: msbFirst);
} on FormatException {
return null;
}
}
Loading