Initial Commit
This commit is contained in:
111
packages/foundation/flow/cbor/src/encoder/encoder.edge.test.ts
Normal file
111
packages/foundation/flow/cbor/src/encoder/encoder.edge.test.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { encode } from "./encoder.js";
|
||||
import { tryEncodeFloat16 } from "./half-float.js";
|
||||
|
||||
const hex = (b: Uint8Array): string =>
|
||||
Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
||||
|
||||
describe("encoder — bigint range branches", () => {
|
||||
it("encodes -1n as major-1 minor=0", { timeout: 5000 }, () => {
|
||||
// -1n is in [-u64, -1]: encoded as major 1, value = -(-1) - 1 = 0
|
||||
expect(hex(encode(-1n))).toBe("20");
|
||||
});
|
||||
|
||||
it("encodes -100n via major-1", { timeout: 5000 }, () => {
|
||||
// -100n encodes as major 1 minor=24 0x63 (= -1 - 99 = -100)
|
||||
expect(hex(encode(-100n))).toBe("3863");
|
||||
});
|
||||
|
||||
it("encodes negative on the u64 boundary", { timeout: 5000 }, () => {
|
||||
// -2^64 in range? value >= -0x10000000000000000n is true for value=-2^64
|
||||
// -value - 1n = 2^64 - 1 = 0xffffffffffffffff fits in 8 bytes
|
||||
expect(hex(encode(-(2n ** 64n)))).toBe("3bffffffffffffffff");
|
||||
});
|
||||
});
|
||||
|
||||
describe("encoder — half-float edge branches", () => {
|
||||
it("subnormal reconstruction mismatch returns undefined", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
// 1.5 * 2^-24 — mant rounds to 2, reconstructed = 2^-23 ≠ original
|
||||
expect(tryEncodeFloat16(1.5 * 2 ** -24)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when rollup overflows exponent", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
// Value such that mantissa rounds up to 1024, pushing biasedExp from
|
||||
// 30 (0x1e) to 31 (0x1f) — overflow → undefined.
|
||||
// 65520 has mantissa-bits 1023.5 which rounds to 1024.
|
||||
expect(tryEncodeFloat16(65520)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when normal-path reconstruction mismatch", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
// 1.0001 — fraction rounds to a mantissa whose reconstructed value
|
||||
// differs from the input by a tiny amount.
|
||||
expect(tryEncodeFloat16(1.0001)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("encoder — Map with Tagged keys", () => {
|
||||
it("deterministic mode sorts mixed-type keys by canonical bytes", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
// Keys: "b" (62), 1 (01), 0xff (18ff)
|
||||
const m = new Map<unknown, number>([
|
||||
["b", 1],
|
||||
[1, 2],
|
||||
[255, 3],
|
||||
]);
|
||||
// Canonical-byte sort: 01 < 18ff < 6162. Expected order: 1, 255, "b"
|
||||
const out = hex(encode(m, { deterministic: true }));
|
||||
expect(out.startsWith("a3")).toBe(true);
|
||||
// Body: 0102 18ff03 616201
|
||||
expect(out).toBe("a3010218ff036162 01".replace(/\s/g, ""));
|
||||
});
|
||||
});
|
||||
|
||||
describe("encoder — depth boundary", () => {
|
||||
it("encodes at exact maxDepth without throw", { timeout: 5000 }, () => {
|
||||
let nested: unknown = 1;
|
||||
for (let i = 0; i < 5; i++) nested = [nested];
|
||||
// maxDepth=5 should accept depth-5; depth-6 throws
|
||||
expect(() => encode(nested, { maxDepth: 5 })).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("encoder — depth on bigint outside u64", () => {
|
||||
it("encodes large positive bignum via tag 2", { timeout: 5000 }, () => {
|
||||
// 2^128 → tag 2 + 16-byte payload
|
||||
expect(hex(encode(2n ** 128n)).startsWith("c25")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("encoder — float branch coverage", () => {
|
||||
it("deterministic mode encodes NaN via float32 (NaN-NaN equality path)", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const out = encode(Number.NaN, { deterministic: true });
|
||||
expect(out[0]).toBe(0xfa); // float32
|
||||
});
|
||||
|
||||
it("preferFloat16=true falls through to float32 when f16 not exact", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
// 1 + 2^-23 — float32-exact; f16 cannot represent the fractional bit.
|
||||
const value = 1 + 2 ** -23;
|
||||
const out = encode(value, { preferFloat16: true });
|
||||
expect(out[0]).toBe(0xfa); // float32
|
||||
expect(out.length).toBe(5);
|
||||
});
|
||||
|
||||
it("default float path emits float64 when not exact in float32", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const out = encode(0.1);
|
||||
expect(out[0]).toBe(0xfb); // float64
|
||||
expect(out.length).toBe(9);
|
||||
});
|
||||
});
|
||||
89
packages/foundation/flow/cbor/src/encoder/encoder.md
Normal file
89
packages/foundation/flow/cbor/src/encoder/encoder.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# `encoder`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs --> - half-float — Encodes a JavaScript `number` as IEEE……
|
||||
- [half-float](./half-float.md) — Encodes a JavaScript `number` as IEEE 754 binary16 (CBOR major 7 / value 25) when the round-trip is exact, and otherwis…
|
||||
- half-float — Encodes a JavaScript `number` as IEEE……
|
||||
|
||||
- [half-float](./half-float.md) — Encodes a JavaScript `number` as IEEE 754 binary16 (CBOR major 7 / value 25) when the round-trip is exact, and otherwis…
|
||||
|
||||
- half-float — Encodes a JavaScript `number` as IEEE……
|
||||
|
||||
- [half-float](./half-float.md) — Encodes a JavaScript `number` as IEEE 754 binary16 (CBOR major 7 / value 25) when the round-trip is exact, and otherwis…
|
||||
|
||||
- half-float — Encodes a JavaScript `number` as IEEE…
|
||||
|
||||
- [half-float](./half-float.md) — Encodes a JavaScript `number` as IEEE 754 binary16 (CBOR major 7 / value 25) when the round-trip is exact, and otherwis…
|
||||
- [half-float](./half-float.md) — Encodes a JavaScript `number` as IEEE 754 binary16 (CBOR major 7 / value 25) when the round-trip is exact, and otherwis…
|
||||
|
||||
## Purpose
|
||||
|
||||
The CBOR encoder serialises an arbitrary JavaScript value into RFC 8949 binary
|
||||
form. It supports every CBOR major type (0–7) and applies preferred
|
||||
serialisation rules — shortest-form integers always-on, shortest-exact float
|
||||
width by default. It also exposes a deterministic-encoding mode (RFC 8949
|
||||
§4.2) for callers that need byte-stable output across runs (e.g. signature
|
||||
generation, content addressing).
|
||||
|
||||
## Features
|
||||
|
||||
- All seven major types: unsigned, negative, byte string, text string, array,
|
||||
map, tag, and major-7 (simple values + floats).
|
||||
- Shortest-form integers per RFC 8949 §4.2.1 — always-on, regardless of mode.
|
||||
- Shortest-exact float selection among `{float16 (opt-in), float32, float64}`.
|
||||
- Deterministic mode: sorted map keys (lexicographic over canonical encoded
|
||||
key bytes), no indefinite-length items, no half-float, rejects `undefined`.
|
||||
- BigInt support: ranges within `±u64` use major 0/1; outside that range, tag
|
||||
2 (positive) or tag 3 (negative) bignum byte-string.
|
||||
- `Tagged<T>` round-trip — encode emits tag-then-inner per §3.4.
|
||||
- `Map<K,V>` and plain object literals both serialise to major 5 maps.
|
||||
- Configurable `maxDepth` (default 64) and `maxItems` (default 1_000_000)
|
||||
guard against pathological inputs.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { encode } from "@suaveplan/cbor/encoder";
|
||||
|
||||
const bytes = encode({ a: 1, b: [true, "two"] });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { encode } from "@suaveplan/cbor/encoder";
|
||||
import { Tagged } from "@suaveplan/cbor/tags";
|
||||
|
||||
// Deterministic encoding for signing
|
||||
const claims = new Map<number, unknown>([
|
||||
[1, "issuer"],
|
||||
[4, new Tagged(1, 1700000000)], // exp claim with epoch tag
|
||||
]);
|
||||
const signedBytes = encode(claims, { deterministic: true });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `encode(value: unknown, options?: EncoderOptions): Uint8Array`
|
||||
- `EncoderOptions`:
|
||||
- `deterministic?: boolean` — default `false`. Enforces RFC 8949 §4.2.
|
||||
- `preferFloat16?: boolean` — default `false`. Try binary16 when exact.
|
||||
Ignored when `deterministic` is `true`.
|
||||
- `maxDepth?: number` — default 64. Throws `CBOR.NESTED_TOO_DEEP` on overflow.
|
||||
- `maxItems?: number` — default 1_000_000. Throws `CBOR.LENGTH_OVERFLOW`.
|
||||
|
||||
Throws `CborError` with codes `UNEXPECTED_TYPE` (functions, symbols),
|
||||
`NON_DETERMINISTIC` (e.g. `undefined` under deterministic), `NESTED_TOO_DEEP`,
|
||||
and `LENGTH_OVERFLOW`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The encoder accumulates output as an array of `Uint8Array` chunks and
|
||||
concatenates at the end. This avoids the O(n²) cost of repeatedly grow-and-
|
||||
copying a single buffer. Map-key sorting in deterministic mode encodes each
|
||||
key first (yielding the canonical bytes), then sorts the entry list by
|
||||
lexicographic byte order before emitting headers and bodies. BigInt encoding
|
||||
uses native `DataView.setBigUint64` for the in-range case and a hand-rolled
|
||||
big-endian byte builder for tag 2/3 bignums to avoid pulling in any external
|
||||
dependency.
|
||||
205
packages/foundation/flow/cbor/src/encoder/encoder.test.ts
Normal file
205
packages/foundation/flow/cbor/src/encoder/encoder.test.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { Tagged } from "../tags/tagged.js";
|
||||
import { encode } from "./encoder.js";
|
||||
|
||||
const hex = (bytes: Uint8Array): string =>
|
||||
Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
||||
|
||||
describe("encode — RFC 8949 Appendix A vectors", () => {
|
||||
it.each([
|
||||
[0, "00"],
|
||||
[1, "01"],
|
||||
[10, "0a"],
|
||||
[23, "17"],
|
||||
[24, "1818"],
|
||||
[25, "1819"],
|
||||
[100, "1864"],
|
||||
[1000, "1903e8"],
|
||||
[1000000, "1a000f4240"],
|
||||
[1000000000000, "1b000000e8d4a51000"],
|
||||
[-1, "20"],
|
||||
[-10, "29"],
|
||||
[-100, "3863"],
|
||||
[-1000, "3903e7"],
|
||||
])("integer %p → %s", { timeout: 5000 }, (input, expected) => {
|
||||
expect(hex(encode(input))).toBe(expected);
|
||||
});
|
||||
|
||||
it("BigInt within u64 range encodes as major 0", { timeout: 5000 }, () => {
|
||||
expect(hex(encode(18446744073709551615n))).toBe("1bffffffffffffffff");
|
||||
});
|
||||
|
||||
it("BigInt outside u64 → tag 2 bignum", { timeout: 5000 }, () => {
|
||||
expect(hex(encode(18446744073709551616n))).toBe(
|
||||
"c249010000000000000000"
|
||||
);
|
||||
});
|
||||
|
||||
it("BigInt outside negative u64 → tag 3 bignum", { timeout: 5000 }, () => {
|
||||
expect(hex(encode(-18446744073709551617n))).toBe(
|
||||
"c349010000000000000000"
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[false, "f4"],
|
||||
[true, "f5"],
|
||||
[null, "f6"],
|
||||
[undefined, "f7"],
|
||||
])("simple value %p → %s", { timeout: 5000 }, (input, expected) => {
|
||||
expect(hex(encode(input))).toBe(expected);
|
||||
});
|
||||
|
||||
it("empty byte string", { timeout: 5000 }, () => {
|
||||
expect(hex(encode(new Uint8Array()))).toBe("40");
|
||||
});
|
||||
|
||||
it("byte string [0x01,0x02,0x03,0x04]", { timeout: 5000 }, () => {
|
||||
expect(hex(encode(new Uint8Array([1, 2, 3, 4])))).toBe("4401020304");
|
||||
});
|
||||
|
||||
it("empty string", { timeout: 5000 }, () => {
|
||||
expect(hex(encode(""))).toBe("60");
|
||||
});
|
||||
|
||||
it("'a' → 6161", { timeout: 5000 }, () => {
|
||||
expect(hex(encode("a"))).toBe("6161");
|
||||
});
|
||||
|
||||
it("'IETF'", { timeout: 5000 }, () => {
|
||||
expect(hex(encode("IETF"))).toBe("6449455446");
|
||||
});
|
||||
|
||||
it("multibyte UTF-8 'ü'", { timeout: 5000 }, () => {
|
||||
expect(hex(encode("ü"))).toBe("62c3bc");
|
||||
});
|
||||
|
||||
it("empty array", { timeout: 5000 }, () => {
|
||||
expect(hex(encode([]))).toBe("80");
|
||||
});
|
||||
|
||||
it("array [1,2,3]", { timeout: 5000 }, () => {
|
||||
expect(hex(encode([1, 2, 3]))).toBe("83010203");
|
||||
});
|
||||
|
||||
it("nested array [1,[2,3],[4,5]]", { timeout: 5000 }, () => {
|
||||
expect(hex(encode([1, [2, 3], [4, 5]]))).toBe("8301820203820405");
|
||||
});
|
||||
|
||||
it("empty map", { timeout: 5000 }, () => {
|
||||
expect(hex(encode(new Map()))).toBe("a0");
|
||||
});
|
||||
|
||||
it("map {1:2,3:4}", { timeout: 5000 }, () => {
|
||||
const m = new Map<number, number>([
|
||||
[1, 2],
|
||||
[3, 4],
|
||||
]);
|
||||
expect(hex(encode(m))).toBe("a201020304");
|
||||
});
|
||||
|
||||
it("Tagged(0, '2013-03-21T20:04:00Z')", { timeout: 5000 }, () => {
|
||||
const tagged = new Tagged(0, "2013-03-21T20:04:00Z");
|
||||
expect(hex(encode(tagged))).toBe(
|
||||
"c074323031332d30332d32315432303a30343a30305a"
|
||||
);
|
||||
});
|
||||
|
||||
it("Tagged(1, 1363896240) epoch time", { timeout: 5000 }, () => {
|
||||
expect(hex(encode(new Tagged(1, 1363896240)))).toBe("c11a514b67b0");
|
||||
});
|
||||
});
|
||||
|
||||
describe("encode — deterministic mode", () => {
|
||||
it("sorts map keys lexicographically by canonical bytes", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const m = new Map<number, number>([
|
||||
[10, 1],
|
||||
[1, 2],
|
||||
[100, 3],
|
||||
]);
|
||||
const out = encode(m, { deterministic: true });
|
||||
// Keys should be 01 (1), 0a (10), 1864 (100)
|
||||
expect(hex(out).startsWith("a3")).toBe(true);
|
||||
expect(hex(out)).toBe("a301020a01186403");
|
||||
});
|
||||
|
||||
it("rejects undefined under deterministic", { timeout: 5000 }, () => {
|
||||
expect(() => encode(undefined, { deterministic: true })).toThrow(
|
||||
/CBOR\.NON_DETERMINISTIC/
|
||||
);
|
||||
});
|
||||
|
||||
it("uses shortest-exact float (no float16) under deterministic", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
// 1.5 fits exactly in float32; deterministic mode disables float16 but
|
||||
// keeps shortest-exact among {float32, float64}.
|
||||
const out = encode(1.5, { deterministic: true });
|
||||
expect(out[0]).toBe(0xfa); // major 7 / minor 26 = float32
|
||||
expect(out.length).toBe(5);
|
||||
});
|
||||
|
||||
it("uses float64 when value not representable in float32 (deterministic)", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const out = encode(0.1, { deterministic: true });
|
||||
expect(out[0]).toBe(0xfb);
|
||||
expect(out.length).toBe(9);
|
||||
});
|
||||
});
|
||||
|
||||
describe("encode — bounds", () => {
|
||||
it("throws NESTED_TOO_DEEP past maxDepth", { timeout: 5000 }, () => {
|
||||
let nested: unknown = 1;
|
||||
for (let i = 0; i < 65; i++) nested = [nested];
|
||||
expect(() => encode(nested, { maxDepth: 64 })).toThrow(
|
||||
/NESTED_TOO_DEEP/
|
||||
);
|
||||
});
|
||||
|
||||
it("throws LENGTH_OVERFLOW past maxItems", { timeout: 5000 }, () => {
|
||||
const big = Array.from({ length: 11 }, (_, i) => i);
|
||||
expect(() => encode(big, { maxItems: 10 })).toThrow(/LENGTH_OVERFLOW/);
|
||||
});
|
||||
|
||||
it("rejects function", { timeout: 5000 }, () => {
|
||||
expect(() => encode(() => undefined)).toThrow(/UNEXPECTED_TYPE/);
|
||||
});
|
||||
|
||||
it("rejects symbol", { timeout: 5000 }, () => {
|
||||
expect(() => encode(Symbol("x"))).toThrow(/UNEXPECTED_TYPE/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("encode — float16", () => {
|
||||
it("preferFloat16 emits 2-byte float for exactly-representable", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const out = encode(1.5, { preferFloat16: true });
|
||||
expect(out[0]).toBe(0xf9);
|
||||
expect(out.length).toBe(3);
|
||||
});
|
||||
|
||||
it("falls back to float32 when float16 not exact", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
// 0.1 is not representable in any binary float exactly, but float32 is closer than float16
|
||||
const out = encode(0.1, { preferFloat16: true });
|
||||
// Should select float64 because no narrower form is exact
|
||||
expect(out[0]).toBe(0xfb);
|
||||
});
|
||||
});
|
||||
|
||||
describe("encode — object literal", () => {
|
||||
it("encodes plain object as map", { timeout: 5000 }, () => {
|
||||
const out = encode({ a: 1, b: 2 });
|
||||
expect(hex(out)).toBe("a26161016162 02".replace(/\s/g, ""));
|
||||
});
|
||||
|
||||
it("deterministic-sorts object keys", { timeout: 5000 }, () => {
|
||||
const out = encode({ b: 2, a: 1 }, { deterministic: true });
|
||||
expect(hex(out)).toBe("a26161016162 02".replace(/\s/g, ""));
|
||||
});
|
||||
});
|
||||
349
packages/foundation/flow/cbor/src/encoder/encoder.ts
Normal file
349
packages/foundation/flow/cbor/src/encoder/encoder.ts
Normal file
@@ -0,0 +1,349 @@
|
||||
import { CBOR_CODES, cborError } from "../errors/error-codes.js";
|
||||
import { Tagged } from "../tags/tagged.js";
|
||||
import {
|
||||
DEFAULT_ENCODER_OPTIONS,
|
||||
type EncoderOptions,
|
||||
} from "./encoder.types.js";
|
||||
import { selectFloatWidth, tryEncodeFloat16 } from "./half-float.js";
|
||||
|
||||
const TEXT_ENCODER = new TextEncoder();
|
||||
|
||||
/** Major-type prefixes (top 3 bits). */
|
||||
const MT_UNSIGNED = 0x00;
|
||||
const MT_NEGATIVE = 0x20;
|
||||
const MT_BYTES = 0x40;
|
||||
const MT_TEXT = 0x60;
|
||||
const MT_ARRAY = 0x80;
|
||||
const MT_MAP = 0xa0;
|
||||
const MT_TAG = 0xc0;
|
||||
const MT_SIMPLE = 0xe0;
|
||||
|
||||
interface EncodeContext {
|
||||
readonly options: Required<EncoderOptions>;
|
||||
depth: number;
|
||||
items: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode `value` to a CBOR byte sequence per RFC 8949. Throws `CborError` for
|
||||
* unsupported values (e.g. `function`, `Symbol`), depth-overflow, and
|
||||
* deterministic-mode violations.
|
||||
*/
|
||||
export function encode(
|
||||
value: unknown,
|
||||
options: EncoderOptions = {}
|
||||
): Uint8Array {
|
||||
const ctx: EncodeContext = {
|
||||
options: { ...DEFAULT_ENCODER_OPTIONS, ...options },
|
||||
depth: 0,
|
||||
items: 0,
|
||||
};
|
||||
const chunks: Uint8Array[] = [];
|
||||
encodeValue(value, ctx, chunks);
|
||||
return concat(chunks);
|
||||
}
|
||||
|
||||
function encodeValue(
|
||||
value: unknown,
|
||||
ctx: EncodeContext,
|
||||
out: Uint8Array[]
|
||||
): void {
|
||||
ctx.items += 1;
|
||||
if (ctx.items > ctx.options.maxItems) {
|
||||
throw cborError(
|
||||
CBOR_CODES.LENGTH_OVERFLOW,
|
||||
`encoded item count exceeds ${ctx.options.maxItems}`
|
||||
);
|
||||
}
|
||||
if (ctx.depth > ctx.options.maxDepth) {
|
||||
throw cborError(
|
||||
CBOR_CODES.NESTED_TOO_DEEP,
|
||||
`encoded depth exceeds ${ctx.options.maxDepth}`
|
||||
);
|
||||
}
|
||||
if (value === null) {
|
||||
out.push(new Uint8Array([0xf6]));
|
||||
return;
|
||||
}
|
||||
if (value === undefined) {
|
||||
if (ctx.options.deterministic) {
|
||||
throw cborError(
|
||||
CBOR_CODES.NON_DETERMINISTIC,
|
||||
"deterministic mode forbids `undefined`"
|
||||
);
|
||||
}
|
||||
out.push(new Uint8Array([0xf7]));
|
||||
return;
|
||||
}
|
||||
if (typeof value === "boolean") {
|
||||
out.push(new Uint8Array([value ? 0xf5 : 0xf4]));
|
||||
return;
|
||||
}
|
||||
if (typeof value === "number") {
|
||||
encodeNumber(value, ctx, out);
|
||||
return;
|
||||
}
|
||||
if (typeof value === "bigint") {
|
||||
encodeBigInt(value, out);
|
||||
return;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
encodeString(value, out);
|
||||
return;
|
||||
}
|
||||
if (value instanceof Uint8Array) {
|
||||
encodeBytes(value, out);
|
||||
return;
|
||||
}
|
||||
if (value instanceof Tagged) {
|
||||
encodeTagged(value, ctx, out);
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
encodeArray(value, ctx, out);
|
||||
return;
|
||||
}
|
||||
if (value instanceof Map) {
|
||||
encodeMap(value, ctx, out);
|
||||
return;
|
||||
}
|
||||
if (typeof value === "object") {
|
||||
encodeObject(value as Record<string, unknown>, ctx, out);
|
||||
return;
|
||||
}
|
||||
throw cborError(
|
||||
CBOR_CODES.UNEXPECTED_TYPE,
|
||||
`cannot encode value of type ${typeof value}`
|
||||
);
|
||||
}
|
||||
|
||||
function encodeHead(
|
||||
major: number,
|
||||
n: number | bigint,
|
||||
out: Uint8Array[]
|
||||
): void {
|
||||
const v = typeof n === "bigint" ? n : BigInt(n);
|
||||
if (v < 24n) {
|
||||
out.push(new Uint8Array([major | Number(v)]));
|
||||
return;
|
||||
}
|
||||
if (v < 0x100n) {
|
||||
out.push(new Uint8Array([major | 24, Number(v)]));
|
||||
return;
|
||||
}
|
||||
if (v < 0x10000n) {
|
||||
const num = Number(v);
|
||||
out.push(new Uint8Array([major | 25, (num >> 8) & 0xff, num & 0xff]));
|
||||
return;
|
||||
}
|
||||
if (v < 0x100000000n) {
|
||||
const num = Number(v);
|
||||
out.push(
|
||||
new Uint8Array([
|
||||
major | 26,
|
||||
(num >> 24) & 0xff,
|
||||
(num >> 16) & 0xff,
|
||||
(num >> 8) & 0xff,
|
||||
num & 0xff,
|
||||
])
|
||||
);
|
||||
return;
|
||||
}
|
||||
const buf = new Uint8Array(9);
|
||||
buf[0] = major | 27;
|
||||
const view = new DataView(buf.buffer);
|
||||
view.setBigUint64(1, v, false);
|
||||
out.push(buf);
|
||||
}
|
||||
|
||||
function encodeNumber(
|
||||
value: number,
|
||||
ctx: EncodeContext,
|
||||
out: Uint8Array[]
|
||||
): void {
|
||||
if (
|
||||
Number.isInteger(value) &&
|
||||
value >= Number.MIN_SAFE_INTEGER &&
|
||||
value <= Number.MAX_SAFE_INTEGER
|
||||
) {
|
||||
if (value >= 0) {
|
||||
encodeHead(MT_UNSIGNED, value, out);
|
||||
} else {
|
||||
encodeHead(MT_NEGATIVE, -value - 1, out);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Float — pick narrowest exact width
|
||||
if (ctx.options.deterministic) {
|
||||
// Deterministic: half-float disabled per the proposal's design choice.
|
||||
// Pick the shortest exact width among {float32, float64} so the encoding
|
||||
// is still deterministic AND minimal.
|
||||
const f32buf = new ArrayBuffer(4);
|
||||
const view32 = new DataView(f32buf);
|
||||
view32.setFloat32(0, value, false);
|
||||
const round32 = view32.getFloat32(0, false);
|
||||
const exact32 =
|
||||
round32 === value || (Number.isNaN(value) && Number.isNaN(round32));
|
||||
if (exact32) {
|
||||
out.push(new Uint8Array([MT_SIMPLE | 26]));
|
||||
out.push(new Uint8Array(f32buf));
|
||||
return;
|
||||
}
|
||||
const f64buf = new ArrayBuffer(8);
|
||||
new DataView(f64buf).setFloat64(0, value, false);
|
||||
out.push(new Uint8Array([MT_SIMPLE | 27]));
|
||||
out.push(new Uint8Array(f64buf));
|
||||
return;
|
||||
}
|
||||
if (ctx.options.preferFloat16) {
|
||||
const f16 = tryEncodeFloat16(value);
|
||||
if (f16) {
|
||||
out.push(new Uint8Array([MT_SIMPLE | 25]));
|
||||
out.push(f16);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const { width, bytes } = selectFloatWidth(value);
|
||||
const head = width === 4 ? MT_SIMPLE | 26 : MT_SIMPLE | 27;
|
||||
out.push(new Uint8Array([head]));
|
||||
out.push(bytes);
|
||||
}
|
||||
|
||||
function encodeBigInt(value: bigint, out: Uint8Array[]): void {
|
||||
if (value >= 0n && value <= 0xffffffffffffffffn) {
|
||||
encodeHead(MT_UNSIGNED, value, out);
|
||||
return;
|
||||
}
|
||||
if (value < 0n && value >= -0x10000000000000000n) {
|
||||
encodeHead(MT_NEGATIVE, -value - 1n, out);
|
||||
return;
|
||||
}
|
||||
// Big-num via tag 2 / 3
|
||||
const tag = value >= 0n ? 2 : 3;
|
||||
const positive = value >= 0n ? value : -value - 1n;
|
||||
const bytes = bigIntToBytes(positive);
|
||||
encodeHead(MT_TAG, tag, out);
|
||||
encodeHead(MT_BYTES, bytes.length, out);
|
||||
out.push(bytes);
|
||||
}
|
||||
|
||||
function bigIntToBytes(v: bigint): Uint8Array {
|
||||
// Caller never passes 0n: encodeBigInt routes 0n through the unsigned
|
||||
// major-0 fast path, and the bignum path is only reached for values
|
||||
// strictly outside the ±u64 range.
|
||||
let n = v;
|
||||
const bytes: number[] = [];
|
||||
while (n > 0n) {
|
||||
bytes.push(Number(n & 0xffn));
|
||||
n >>= 8n;
|
||||
}
|
||||
bytes.reverse();
|
||||
return new Uint8Array(bytes);
|
||||
}
|
||||
|
||||
function encodeString(value: string, out: Uint8Array[]): void {
|
||||
const bytes = TEXT_ENCODER.encode(value);
|
||||
encodeHead(MT_TEXT, bytes.length, out);
|
||||
out.push(bytes);
|
||||
}
|
||||
|
||||
function encodeBytes(value: Uint8Array, out: Uint8Array[]): void {
|
||||
encodeHead(MT_BYTES, value.length, out);
|
||||
out.push(value);
|
||||
}
|
||||
|
||||
function encodeArray(
|
||||
value: readonly unknown[],
|
||||
ctx: EncodeContext,
|
||||
out: Uint8Array[]
|
||||
): void {
|
||||
encodeHead(MT_ARRAY, value.length, out);
|
||||
ctx.depth += 1;
|
||||
for (const item of value) encodeValue(item, ctx, out);
|
||||
ctx.depth -= 1;
|
||||
}
|
||||
|
||||
function encodeMap(
|
||||
value: Map<unknown, unknown>,
|
||||
ctx: EncodeContext,
|
||||
out: Uint8Array[]
|
||||
): void {
|
||||
const entries: Array<[Uint8Array, Uint8Array]> = [];
|
||||
for (const [k, v] of value) {
|
||||
const keyChunks: Uint8Array[] = [];
|
||||
const valChunks: Uint8Array[] = [];
|
||||
ctx.depth += 1;
|
||||
encodeValue(k, ctx, keyChunks);
|
||||
encodeValue(v, ctx, valChunks);
|
||||
ctx.depth -= 1;
|
||||
entries.push([concat(keyChunks), concat(valChunks)]);
|
||||
}
|
||||
if (ctx.options.deterministic)
|
||||
entries.sort((a, b) => compareBytes(a[0], b[0]));
|
||||
encodeHead(MT_MAP, entries.length, out);
|
||||
for (const [k, v] of entries) {
|
||||
out.push(k);
|
||||
out.push(v);
|
||||
}
|
||||
}
|
||||
|
||||
function encodeObject(
|
||||
value: Record<string, unknown>,
|
||||
ctx: EncodeContext,
|
||||
out: Uint8Array[]
|
||||
): void {
|
||||
const keys = Object.keys(value);
|
||||
const entries: Array<[Uint8Array, Uint8Array]> = [];
|
||||
for (const k of keys) {
|
||||
const keyChunks: Uint8Array[] = [];
|
||||
const valChunks: Uint8Array[] = [];
|
||||
ctx.depth += 1;
|
||||
encodeValue(k, ctx, keyChunks);
|
||||
encodeValue(value[k], ctx, valChunks);
|
||||
ctx.depth -= 1;
|
||||
entries.push([concat(keyChunks), concat(valChunks)]);
|
||||
}
|
||||
if (ctx.options.deterministic)
|
||||
entries.sort((a, b) => compareBytes(a[0], b[0]));
|
||||
encodeHead(MT_MAP, entries.length, out);
|
||||
for (const [k, v] of entries) {
|
||||
out.push(k);
|
||||
out.push(v);
|
||||
}
|
||||
}
|
||||
|
||||
function encodeTagged(
|
||||
value: Tagged,
|
||||
ctx: EncodeContext,
|
||||
out: Uint8Array[]
|
||||
): void {
|
||||
encodeHead(MT_TAG, value.tag, out);
|
||||
ctx.depth += 1;
|
||||
encodeValue(value.value, ctx, out);
|
||||
ctx.depth -= 1;
|
||||
}
|
||||
|
||||
function compareBytes(a: Uint8Array, b: Uint8Array): number {
|
||||
// Canonical CBOR encodings are prefix-free (each major type's head byte
|
||||
// pins both type and length), and Map semantics rule out duplicate keys,
|
||||
// so two distinct CBOR-encoded keys always differ at some byte index in
|
||||
// [0, min(a.length, b.length)). Returning at the first differing index is
|
||||
// both correct AND total — every reachable input exits via the inner
|
||||
// `return`.
|
||||
const len = Math.min(a.length, b.length);
|
||||
let i = 0;
|
||||
while (i < len && a[i] === b[i]) i++;
|
||||
return (a[i] as number) - (b[i] as number);
|
||||
}
|
||||
|
||||
function concat(chunks: readonly Uint8Array[]): Uint8Array {
|
||||
let total = 0;
|
||||
for (const c of chunks) total += c.length;
|
||||
const out = new Uint8Array(total);
|
||||
let off = 0;
|
||||
for (const c of chunks) {
|
||||
out.set(c, off);
|
||||
off += c.length;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
40
packages/foundation/flow/cbor/src/encoder/encoder.types.ts
Normal file
40
packages/foundation/flow/cbor/src/encoder/encoder.types.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Caller-supplied encoder options. Every field is optional; defaults match
|
||||
* RFC 8949 §4.1 preferred-serialization rules.
|
||||
*/
|
||||
export interface EncoderOptions {
|
||||
/**
|
||||
* Enforce RFC 8949 §4.2 deterministic encoding. Implies sorted map keys
|
||||
* (lexicographic over canonical encoded key bytes), shortest-form integers
|
||||
* (always-on regardless), no indefinite-length items, and no half-float
|
||||
* shortcuts. When `true`, attempts to emit indefinite items throw
|
||||
* `CBOR.NON_DETERMINISTIC`.
|
||||
*/
|
||||
deterministic?: boolean;
|
||||
|
||||
/**
|
||||
* Try to encode floats as binary16 when round-trip is exact. Off by default;
|
||||
* when `deterministic` is `true`, this flag is ignored (always off).
|
||||
*/
|
||||
preferFloat16?: boolean;
|
||||
|
||||
/**
|
||||
* Maximum nesting depth before the encoder throws `CBOR.NESTED_TOO_DEEP`.
|
||||
* Default: 64. Guards against stack overflow on adversarial inputs.
|
||||
*/
|
||||
maxDepth?: number;
|
||||
|
||||
/**
|
||||
* Maximum total item count emitted before the encoder throws
|
||||
* `CBOR.LENGTH_OVERFLOW`. Default: 1_000_000.
|
||||
*/
|
||||
maxItems?: number;
|
||||
}
|
||||
|
||||
/** Default encoder options applied at the boundary. */
|
||||
export const DEFAULT_ENCODER_OPTIONS: Required<EncoderOptions> = {
|
||||
deterministic: false,
|
||||
preferFloat16: false,
|
||||
maxDepth: 64,
|
||||
maxItems: 1_000_000,
|
||||
};
|
||||
58
packages/foundation/flow/cbor/src/encoder/half-float.md
Normal file
58
packages/foundation/flow/cbor/src/encoder/half-float.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# `half-float` (encoder side)
|
||||
|
||||
## Purpose
|
||||
|
||||
Encodes a JavaScript `number` as IEEE 754 binary16 (CBOR major 7 / value 25)
|
||||
when the round-trip is exact, and otherwise indicates non-representability so
|
||||
the caller can pick a wider format. Also exports `selectFloatWidth`, which
|
||||
chooses between float32 and float64 based on exact-representability — the
|
||||
encoder's deterministic path uses this to skip half-float without giving up
|
||||
shortest-exact behaviour at the float32 / float64 boundary.
|
||||
|
||||
## Features
|
||||
|
||||
- Full IEEE 754 binary16 coverage: ±0, ±Infinity, NaN, normal values,
|
||||
subnormals, smallest-normal / largest-normal boundaries.
|
||||
- `tryEncodeFloat16(value)` returns the 2-byte big-endian sequence on exact
|
||||
round-trip; returns `undefined` on overflow / underflow / inexact-precision.
|
||||
- `selectFloatWidth(value)` picks float32 (4 bytes) when `getFloat32` round-
|
||||
trips the value exactly, otherwise float64 (8 bytes). NaN is treated as
|
||||
float32-exact since both float32 and float64 represent NaN.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { tryEncodeFloat16 } from "@suaveplan/cbor/encoder";
|
||||
|
||||
const bytes = tryEncodeFloat16(1.5); // Uint8Array(2) [0x3e, 0x00]
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { selectFloatWidth, tryEncodeFloat16 } from "@suaveplan/cbor/encoder";
|
||||
|
||||
function pickFloat(value: number): { tag: number; bytes: Uint8Array } {
|
||||
const f16 = tryEncodeFloat16(value);
|
||||
if (f16) return { tag: 0xf9, bytes: f16 };
|
||||
const { width, bytes } = selectFloatWidth(value);
|
||||
return { tag: width === 4 ? 0xfa : 0xfb, bytes };
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `tryEncodeFloat16(value: number): Uint8Array | undefined` — exact-only.
|
||||
- `selectFloatWidth(value: number): { width: 4 | 8; bytes: Uint8Array }`.
|
||||
- `type FloatWidth = 4 | 8`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The exact-only contract is critical for deterministic encoding: a lossy
|
||||
half-float emission would produce different bytes for inputs that look
|
||||
"close" but differ in the last bit of the mantissa. The implementation
|
||||
verifies round-trip via reconstruction (`(1 + mant/1024) * 2^(exp-15)` for
|
||||
normals, `mant * 2^-24` for subnormals) and rejects when the rebuild does
|
||||
not equal the input under `===`. NaN is hard-coded to `7e00` (the canonical
|
||||
quiet-NaN form) — RFC 8949 §3.3 explicitly permits losing NaN payload bits,
|
||||
which JavaScript would do anyway since `Number` collapses payloads.
|
||||
91
packages/foundation/flow/cbor/src/encoder/half-float.test.ts
Normal file
91
packages/foundation/flow/cbor/src/encoder/half-float.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { selectFloatWidth, tryEncodeFloat16 } from "./half-float.js";
|
||||
|
||||
const hex = (b: Uint8Array | undefined): string =>
|
||||
b
|
||||
? Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("")
|
||||
: "<undef>";
|
||||
|
||||
describe("tryEncodeFloat16 — IEEE 754 binary16", () => {
|
||||
it("encodes +0", { timeout: 5000 }, () => {
|
||||
expect(hex(tryEncodeFloat16(0))).toBe("0000");
|
||||
});
|
||||
|
||||
it("encodes -0", { timeout: 5000 }, () => {
|
||||
expect(hex(tryEncodeFloat16(-0))).toBe("8000");
|
||||
});
|
||||
|
||||
it("encodes 1.0", { timeout: 5000 }, () => {
|
||||
expect(hex(tryEncodeFloat16(1))).toBe("3c00");
|
||||
});
|
||||
|
||||
it("encodes 1.5", { timeout: 5000 }, () => {
|
||||
expect(hex(tryEncodeFloat16(1.5))).toBe("3e00");
|
||||
});
|
||||
|
||||
it("encodes -1.0", { timeout: 5000 }, () => {
|
||||
expect(hex(tryEncodeFloat16(-1))).toBe("bc00");
|
||||
});
|
||||
|
||||
it("encodes +Infinity", { timeout: 5000 }, () => {
|
||||
expect(hex(tryEncodeFloat16(Number.POSITIVE_INFINITY))).toBe("7c00");
|
||||
});
|
||||
|
||||
it("encodes -Infinity", { timeout: 5000 }, () => {
|
||||
expect(hex(tryEncodeFloat16(Number.NEGATIVE_INFINITY))).toBe("fc00");
|
||||
});
|
||||
|
||||
it("encodes NaN", { timeout: 5000 }, () => {
|
||||
expect(hex(tryEncodeFloat16(Number.NaN))).toBe("7e00");
|
||||
});
|
||||
|
||||
it("encodes smallest positive subnormal 2^-24", { timeout: 5000 }, () => {
|
||||
expect(hex(tryEncodeFloat16(2 ** -24))).toBe("0001");
|
||||
});
|
||||
|
||||
it("encodes largest subnormal", { timeout: 5000 }, () => {
|
||||
expect(hex(tryEncodeFloat16((2 ** 10 - 1) * 2 ** -24))).toBe("03ff");
|
||||
});
|
||||
|
||||
it("returns undefined for value too large for f16", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(tryEncodeFloat16(70000)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for non-exact float (0.1)", { timeout: 5000 }, () => {
|
||||
expect(tryEncodeFloat16(0.1)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for too-small subnormal underflow", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
// Smaller than smallest subnormal — should return undefined
|
||||
expect(tryEncodeFloat16(2 ** -30)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectFloatWidth", () => {
|
||||
it("returns float32 for exactly representable values", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const r = selectFloatWidth(1.5);
|
||||
expect(r.width).toBe(4);
|
||||
expect(r.bytes.length).toBe(4);
|
||||
});
|
||||
|
||||
it("returns float64 for non-float32-exact values", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const r = selectFloatWidth(0.1);
|
||||
expect(r.width).toBe(8);
|
||||
expect(r.bytes.length).toBe(8);
|
||||
});
|
||||
|
||||
it("returns float32 for NaN (NaN-NaN equality path)", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const r = selectFloatWidth(Number.NaN);
|
||||
expect(r.width).toBe(4);
|
||||
});
|
||||
});
|
||||
86
packages/foundation/flow/cbor/src/encoder/half-float.ts
Normal file
86
packages/foundation/flow/cbor/src/encoder/half-float.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* IEEE-754 binary16 (half-precision) encoder for CBOR major type 7 / value 25.
|
||||
*
|
||||
* Reference: RFC 8949 §3.3 + Appendix D and §A test vectors. The encoder takes
|
||||
* a JavaScript `number` and either returns the 2-byte big-endian binary16
|
||||
* representation OR `undefined` when the value cannot be represented exactly
|
||||
* (loses precision relative to the input float64). Callers fall back to
|
||||
* float32 / float64 in the latter case so deterministic encoding never
|
||||
* silently degrades precision.
|
||||
*/
|
||||
|
||||
const FLOAT16_BIAS = 15;
|
||||
const FLOAT16_MANT_BITS = 10;
|
||||
const _FLOAT16_EXP_BITS = 5;
|
||||
|
||||
/**
|
||||
* Try to encode `value` as binary16. Returns the 2 big-endian bytes when the
|
||||
* round-trip is exact; returns `undefined` otherwise so the caller can pick a
|
||||
* wider format.
|
||||
*/
|
||||
export function tryEncodeFloat16(value: number): Uint8Array | undefined {
|
||||
if (Number.isNaN(value)) return new Uint8Array([0x7e, 0x00]);
|
||||
if (value === Number.POSITIVE_INFINITY) return new Uint8Array([0x7c, 0x00]);
|
||||
if (value === Number.NEGATIVE_INFINITY) return new Uint8Array([0xfc, 0x00]);
|
||||
if (value === 0) {
|
||||
const sign = Object.is(value, -0) ? 0x80 : 0x00;
|
||||
return new Uint8Array([sign, 0x00]);
|
||||
}
|
||||
const sign = value < 0 ? 1 : 0;
|
||||
const abs = Math.abs(value);
|
||||
const exp = Math.floor(Math.log2(abs));
|
||||
const biasedExp = exp + FLOAT16_BIAS;
|
||||
if (biasedExp >= 0x1f) return undefined; // overflow → caller uses wider format
|
||||
let mantissaBits: number;
|
||||
let storedExp: number;
|
||||
if (biasedExp <= 0) {
|
||||
// Subnormal: exp stored as 0; value = mant * 2^-24 with mant ∈ [1, 1023].
|
||||
const mant = Math.round(abs * 2 ** 24);
|
||||
if (mant <= 0 || mant >= 1024) return undefined;
|
||||
const reconstructed = mant * 2 ** -24;
|
||||
if (reconstructed !== abs) return undefined;
|
||||
storedExp = 0;
|
||||
mantissaBits = mant;
|
||||
} else {
|
||||
const fraction = abs / 2 ** exp - 1;
|
||||
mantissaBits = Math.round(fraction * 2 ** FLOAT16_MANT_BITS);
|
||||
// When the rounded mantissa hits 2^10, the value would round up into
|
||||
// the next exponent. Reject — the only abs that round-trips through
|
||||
// this path is `abs === 2^(exp+1)` which would have been classified
|
||||
// at exp+1 from the start, so the rollup case never produces a valid
|
||||
// half-float.
|
||||
if (mantissaBits === 1 << FLOAT16_MANT_BITS) return undefined;
|
||||
storedExp = biasedExp;
|
||||
const reconstructed =
|
||||
(1 + mantissaBits / 2 ** FLOAT16_MANT_BITS) * 2 ** exp;
|
||||
if (reconstructed !== abs) return undefined;
|
||||
}
|
||||
const bits = (sign << 15) | (storedExp << FLOAT16_MANT_BITS) | mantissaBits;
|
||||
return new Uint8Array([(bits >> 8) & 0xff, bits & 0xff]);
|
||||
}
|
||||
|
||||
/** Float-bit-width selector used by the encoder hot path. */
|
||||
export type FloatWidth = 4 | 8;
|
||||
|
||||
/**
|
||||
* Pick the narrowest exact float width for `value` among `{float32, float64}`.
|
||||
* Float16 is excluded — opt-in via `tryEncodeFloat16` from the call site so
|
||||
* deterministic-mode behaviour stays predictable.
|
||||
*/
|
||||
export function selectFloatWidth(value: number): {
|
||||
width: FloatWidth;
|
||||
bytes: Uint8Array;
|
||||
} {
|
||||
const f32buf = new ArrayBuffer(4);
|
||||
const f32view = new DataView(f32buf);
|
||||
f32view.setFloat32(0, value, false);
|
||||
if (
|
||||
f32view.getFloat32(0, false) === value ||
|
||||
(Number.isNaN(value) && Number.isNaN(f32view.getFloat32(0, false)))
|
||||
) {
|
||||
return { width: 4, bytes: new Uint8Array(f32buf) };
|
||||
}
|
||||
const f64buf = new ArrayBuffer(8);
|
||||
new DataView(f64buf).setFloat64(0, value, false);
|
||||
return { width: 8, bytes: new Uint8Array(f64buf) };
|
||||
}
|
||||
10
packages/foundation/flow/cbor/src/encoder/index.ts
Normal file
10
packages/foundation/flow/cbor/src/encoder/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export { encode } from "./encoder.js";
|
||||
export {
|
||||
DEFAULT_ENCODER_OPTIONS,
|
||||
type EncoderOptions,
|
||||
} from "./encoder.types.js";
|
||||
export {
|
||||
type FloatWidth,
|
||||
selectFloatWidth,
|
||||
tryEncodeFloat16,
|
||||
} from "./half-float.js";
|
||||
Reference in New Issue
Block a user