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
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,20 @@ jobs:

- name: Check dependency hygiene
run: bun run check:deps

rust-validation:
name: Cross-validate vectors against the Rust reference
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

# The runner image ships a stable Rust toolchain
- name: Validate against dcbor (default build)
working-directory: tests/rust-validation
run: cargo run --release --locked -- ../vectors

- name: Validate against dcbor (num-bigint build)
working-directory: tests/rust-validation
run: cargo run --release --locked --features bignum -- ../vectors
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ node_modules
# Build artifacts
dist
coverage
# Generated TypeDoc output; retain handwritten architecture notes.
# Generated TypeDoc output
/docs/*
*.tsbuildinfo

Expand Down
4 changes: 2 additions & 2 deletions .size-limit.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
{
"name": "ESM entry (import *), minified + gzipped",
"path": "dist/index.mjs",
"limit": "10 kB"
"limit": "11.5 kB"
},
{
"name": "decode-only ({ decodeCbor }), minified + gzipped",
"path": "dist/index.mjs",
"import": "{ decodeCbor }",
"limit": "6.5 kB"
"limit": "8 kB"
}
]
79 changes: 77 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,80 @@
# Changelog

## Unreleased

### Changed

- **Decoding keeps a leading U+FEFF** in a text string, as the reference's
`String::from_utf8` does; the WHATWG `TextDecoder` default stripped it, so
`64efbbbf61` did not round-trip.
- **`cbor(string)` keeps the string as given; encoding normalizes to NFC**
(the reference's `cbor_data` does the same). `expectText`, `cborEquals`,
`diagnostic` and `hexAnnotated` now see the original string; the wire
bytes are unchanged. Byte-string annotations in `hexAnnotated` keep astral
characters (`"😀"`) instead of dotting them.
- **`InvalidUtf8` messages mirror `core::str::Utf8Error`**
(`invalid utf-8 sequence of 1 bytes from index 3`, `incomplete utf-8 byte
sequence from index 0`) instead of the host decoder's text.
- **Float heads are validated with the reference's `validate_canonical_*`
predicates** and decode to the node its `From<f32>`/`From<f64>` build. A
whole-valued f32 head at or beyond 2^31 (or an f64 head at or beyond
2^63) is accepted and reduces to an integer where one fits: `fa4f000001`
decodes to `2147483904` and re-encodes as `1a80000100`; `fa4f800000`
stays a float. Before, every such head was `NonCanonicalNumeric`.
- **Float diagnostics round exact decimal ties up**, as Rust's `{:?}` does
(`f9000a` prints `5.960464477539063e-7`, not `…062e-7`); `simpleName`
prints `inf`/`-inf`/`42.0` like `Simple`'s `Debug`.
- **`CborDate` holds whole seconds plus nanoseconds**, the reference's
`chrono::DateTime` model. A parsed leap second displays as `:60`
(`2023-12-25T10:30:60Z`), `equals`/`compare` distinguish it from the next
second, a sub-second fraction can no longer move the displayed second, and
the range check applies to the truncated whole seconds (`MIN - 0.5` is
`MIN`). `fromUntaggedCbor`/`fromTaggedCbor` return a new instance.
- **`CborDate.fromEpochSeconds(NaN)` and a decoded tag-1 `NaN` are the
epoch** (`c100`, `1970-01-01`), as the reference's saturating cast makes
them; ±Infinity is still `InvalidDate` (the reference panics).
- **`WrongTag` errors name both tags as the reference does.** `CborDate`'s
expected tag carries the global store's name for tag 1 (`date` once
`registerStandardTags()` has run, else `1`); `taggedValue(tag, …)` keeps a
named `Tag`'s name on the node (`CborTaggedType.tagName`, never on the
wire) so the actual tag is named too; `expectTaggedContent` accepts a
`Tag` and keeps its name.
- **`cborEquals` is structural** (`PartialEq for CBOR`): a whole-valued
float node is not equal to the integer it encodes as, a decomposed string
is not equal to its composed form, `NaN` equals `NaN`, tags compare by
value, maps entry by entry. It compared encodings before.
- **`registerStandardTags` registers unconditionally** through
`registerAll`, as `insert_all` does: it moves each standard name back to
its standard value instead of skipping a store that already had the name.
`registerAll` accepts any `Iterable<Tag>`.
- **Tags are frozen values.** `Tag.from` returns a frozen object and a
`TagsStore` keeps frozen tags by identity (an unfrozen literal is copied).
- **One global tags store per process.** `getGlobalTagsStore()` keeps the
store on `globalThis` under `Symbol.for("@blockchaincommons/dcbor/global-tags-store@1")`,
so the ESM and CommonJS builds share it, as the reference's `GLOBAL_TAGS`.

### Added

- `expectUnsigned(cbor, { width, wrapNegative })` extracts into a fixed
width like `u8`…`u64::try_from`, including the reference's wrap of a
negative integer (`-1` → `255` at width 8; RUST_DIVERGENCES.md §1.1).
Without options the behaviour is unchanged.
- `TagsStore.clone()` mirrors `#[derive(Clone)]`: an independent copy that
shares frozen tags and summarizer functions.
- Harness: `format-vectors.json`, `date-vectors.json` and
`uint-vectors.json`; decode rejections pin the reference's error message;
a `bignum` feature runs the reference's `num-bigint` build; both builds
run in CI. Guard tests for the engine's Unicode version and a 1,000-deep
array.

### Removed

- The `TAG_*` constants with no counterpart in the reference (`TAG_UUID`,
`TAG_ENCODED_CBOR`, `TAG_SET`, `TAG_SELF_DESCRIBE_CBOR`, …; the reference
defines only `TAG_DATE`, `TAG_POSITIVE_BIGNUM` and `TAG_NEGATIVE_BIGNUM`).
`TAG_EPOCH_DATE_TIME` was a duplicate of `TAG_DATE`. `TAG_BINARY_UUID`
named IANA tag 257, which is the binary MIME message tag.

## 1.0.0-beta.2 - 2026-09-13

The review against `dcbor` 0.25.2 aligned diagnostic formatting, date
Expand Down Expand Up @@ -43,9 +118,9 @@ validation, and optional bignum tag registration.
stored timestamp is the reference's `timestamp()` arithmetic (whole
seconds plus nanoseconds over 10⁹), so a fraction encodes to the same
bytes on both sides; `fromDate` computes a JS `Date`'s milliseconds the
same way. 57 `datestr/*` vectors run every form through the reference.
same way. 62 `datestr/*` vectors run every form through the reference.
- **`CborDate.fromYmd` / `fromYmdHms` validate their components** - year
within ±262143, a calendar month and day, a time within 23:59:59 - and
within −262143…+262142, a calendar month and day, a time within 23:59:59 - and
throw `InvalidDate` where the reference's `with_ymd_and_hms(…).unwrap()`
panics. They used to roll over (`2023-13-01` became 2024-01-01) and mapped
years 0–99 to 1900–1999.
Expand Down
59 changes: 22 additions & 37 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,27 @@

`@blockchaincommons/dcbor` is the redesigned successor to `@bcts/dcbor`.

## TL;DR checklist

- [ ] Replace the dependency `@bcts/dcbor` with `@blockchaincommons/dcbor`; update imports.
- [ ] Import `diagnostic`/`hexAnnotated` from `@blockchaincommons/dcbor/diagnostic` and
`walk` & friends from `@blockchaincommons/dcbor/walk` (they left the root).
- [ ] `cborData(v)` → `encodeCbor(v)`; `toTaggedValue(t, v)` → `taggedValue(t, v)`.
- [ ] Replace `Cbor` *value* usages (`Cbor.from`, `Cbor.tryFromData`,
`Cbor.True`…) - the type survives, the namespace value is gone.
- [ ] Move instance-method calls to free functions (`c.isMap()` → `isMap(c)`,
`c.toText()` → `expectText(c)`, …) - the value keeps only
`toData()`/`toHex()`/`toString()`.
- [ ] Errors: `errorToString(e)`/`errorMsg(e)` → `e.message`;
`e.errorType.type` → `e.code`; `e.errorType.<field>` → `e.details.<field>`;
`new CborError({ type })` → `CborError.<factory>()`.
- [ ] Containers: `CborMap.new()/insert/containsKey/len` →
`new CborMap()/set/has/size`; `CborSet.insert/contains/fromArray` →
`add/has/from`; note `CborMap.get` now returns the stored `Cbor` node.
- [ ] Fix the two shapes that now THROW: plain `{tag, value}` object
literals and `taggedCbor()`-only objects (see below).
- [ ] *(optional)* adopt `tryDecode()` (non-throwing) and
`decodeWith(bytes, codec)` (typed decode, `@beta`).
## Summary

- Replace the dependency `@bcts/dcbor` with `@blockchaincommons/dcbor`; update imports.
- Import `diagnostic`/`hexAnnotated` from `@blockchaincommons/dcbor/diagnostic` and
`walk` & friends from `@blockchaincommons/dcbor/walk` (they left the root).
- `cborData(v)` → `encodeCbor(v)`; `toTaggedValue(t, v)` → `taggedValue(t, v)`.
- Replace `Cbor` *value* usages (`Cbor.from`, `Cbor.tryFromData`,
`Cbor.True`…) - the type survives, the namespace value is gone.
- Move instance-method calls to free functions (`c.isMap()` → `isMap(c)`,
`c.toText()` → `expectText(c)`, …) - the value keeps only
`toData()`/`toHex()`/`toString()`.
- Errors: `errorToString(e)`/`errorMsg(e)` → `e.message`;
`e.errorType.type` → `e.code`; `e.errorType.<field>` → `e.details.<field>`;
`new CborError({ type })` → `CborError.<factory>()`.
- Containers: `CborMap.new()/insert/containsKey/len` →
`new CborMap()/set/has/size`; `CborSet.insert/contains/fromArray` →
`add/has/from`; `CborMap.get` returns the stored `Cbor` node.
- Fix the two shapes that throw: plain `{tag, value}` object literals and
`taggedCbor()`-only objects (see below).
- Optionally adopt `tryDecode()` (non-throwing) and
`decodeWith(bytes, codec)` (typed decode, `@beta`).

---

Expand All @@ -41,7 +41,7 @@ and their options types), `@blockchaincommons/dcbor/walk` (`walk`, `Visitor`, `W
`@blockchaincommons/dcbor/debug` (`installDebugHooks()` - opt-in diagnostic-flavored console
output). `bytesToHex`/`hexToBytes` stay at the root.

## 2. Two input shapes now throw (transitional, one rc cycle)
## 2. Two input shapes now throw

These previously mapped to tagged values SILENTLY; changing that quietly
would corrupt bytes, so they throw a directive `CborError` (code `Custom`):
Expand Down Expand Up @@ -246,18 +246,3 @@ precedent). Tagged types keep `cborTags()` / `untaggedCbor()` /
The prefix grammar is policy (CONTRIBUTING.md): `is*` = narrowing guard,
`as*` = `T | undefined`, `expect*` = `T` or throw `CborError`,
`try*` = returns `Result`, never throws.

## 11. What did NOT change

The wire format (byte-for-byte, including every decoder rejection and its
error code); the accessor names that already followed the grammar
(`isMap`, `asText`, `expectArray`, `hasTag`, `getTaggedContent`,
`expectTaggedContent`, `tagValue`, `tagContent`, `arrayItem`, `arrayLength`,
`mapKeys`, `mapValues`, `mapSize`, …); `extractCbor` (now typed
`CborNative`); the bignum functions (tags 2/3); `encodeVarInt`/`decodeVarInt`;
`sortArrayByCborEncoding`; `cborEquals`; `bytesToHex`/`hexToBytes` names;
`getGlobalTagsStore` and all lookup names; the standard `TAG_*` constants;
`CborDate`'s `fromYmd`/`fromYmdHms`/`fromString`/`now`/`add`/`subtract`/
`difference`/`equals`/`compare`/`toString`/`toJSON`; `CborSet`'s algebra
(`union`/`intersection`/`difference`/`isSubsetOf`/`isSupersetOf`); and
`walk`'s state-cloning visitor semantics (`@beta`).
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Runnable examples live in the [`examples/`](https://github.com/BlockchainCommons

### Version History

- **Unreleased** - Closes every observable difference from `dcbor` 0.25.2: decoding keeps a leading U+FEFF; `cbor(string)` keeps the string and the encoder normalizes to NFC; `InvalidUtf8` messages mirror `core::str::Utf8Error`; float heads follow the reference's canonicality predicates (a whole f32 head at or beyond 2^31 decodes to an integer); float diagnostics round exact decimal ties like Rust; `CborDate` holds seconds plus nanoseconds (leap seconds display as `:60`, `NaN` is the epoch); `WrongTag` names both tags; `cborEquals` is structural; `registerStandardTags` registers unconditionally, tags are frozen and the global store is one per process across ESM and CJS; `expectUnsigned` gains fixed-width extraction and `TagsStore.clone()` is added. The Rust harness runs two builds over encode, decode (with messages), format, date and unsigned vectors, in CI. See `CHANGELOG.md` and `RUST_DIVERGENCES.md`.
- **1.0.0-beta.2 (September 13, 2026)** - Diagnostic line breaking measures strings in UTF-8 bytes as the reference does; dates outside the reference's representable range are `InvalidDate` (an integer `f64` cannot hold is `OutOfRange` on decode) instead of a late `RangeError`; `registerStandardTags` names the bignum tags only on request, as the reference names them only under `num-bigint`; a tag-registration conflict is a `CborError`; `CborDate.fromString` parses exactly as `Date::from_string` (nanosecond fractions, leap seconds, chrono's bare-date forms), the component constructors validate instead of rolling over, and `toString()` prints years outside 0–9999 as the reference does.
- **1.0.0-beta.1 (July 21, 2026)** - Initial beta implementation.

Expand Down
Loading