diff --git a/.changeset/sweet-planets-give.md b/.changeset/sweet-planets-give.md new file mode 100644 index 00000000000..0330d959c2c --- /dev/null +++ b/.changeset/sweet-planets-give.md @@ -0,0 +1,5 @@ +--- +"@smithy/core": patch +--- + +make CBOR tags more distinct in JS diff --git a/packages/core/src/submodules/cbor/cbor-decode.ts b/packages/core/src/submodules/cbor/cbor-decode.ts index 1c0a0629fc8..1931e23c064 100644 --- a/packages/core/src/submodules/cbor/cbor-decode.ts +++ b/packages/core/src/submodules/cbor/cbor-decode.ts @@ -26,6 +26,7 @@ import { specialNull, specialTrue, specialUndefined, + tag, Uint8, Uint32, Uint64, @@ -122,7 +123,7 @@ export function decode(at: Uint32, to: Uint32): CborValueType { const valueOffset = _offset; _offset = offset + valueOffset; - return { tag: castBigInt(unsignedInt), value }; + return tag({ tag: castBigInt(unsignedInt), value }); } case majorUtf8String: case majorMap: diff --git a/packages/core/src/submodules/cbor/cbor-encode.ts b/packages/core/src/submodules/cbor/cbor-encode.ts index 8aac902846d..6ee27f65b65 100644 --- a/packages/core/src/submodules/cbor/cbor-encode.ts +++ b/packages/core/src/submodules/cbor/cbor-encode.ts @@ -9,12 +9,14 @@ import { majorMap, majorNegativeInt64, majorSpecial, + majorTag, majorUint64, majorUnstructuredByteString, majorUtf8String, specialFalse, specialNull, specialTrue, + tagSymbol, Uint64, } from "./cbor-types"; import { alloc } from "./cbor-types"; @@ -179,6 +181,17 @@ export function encode(_input: any): void { cursor += input.byteLength; continue; } else if (typeof input === "object") { + if (input[tagSymbol]) { + if ("tag" in input && "value" in input) { + encodeStack.push(input.value); + encodeHeader(majorTag, input.tag); + continue; + } else { + throw new Error( + "tag encountered with missing fields, need 'tag' and 'value', found: " + JSON.stringify(input) + ); + } + } const keys = Object.keys(input); for (let i = keys.length - 1; i >= 0; --i) { const key = keys[i]; diff --git a/packages/core/src/submodules/cbor/cbor-types.ts b/packages/core/src/submodules/cbor/cbor-types.ts index 9708fab96cd..cef352d394d 100644 --- a/packages/core/src/submodules/cbor/cbor-types.ts +++ b/packages/core/src/submodules/cbor/cbor-types.ts @@ -10,6 +10,7 @@ export type CborItemType = export type CborTagType = { tag: Uint64 | number; value: CborValueType; + [tagSymbol]: true; }; export type CborUnstructuredByteStringType = Uint8Array; export type CborListType = Array; @@ -66,3 +67,26 @@ export const minorIndefinite = 31; // 0b11111 export function alloc(size: number) { return typeof Buffer !== "undefined" ? Buffer.alloc(size) : new Uint8Array(size); } + +/** + * @public + * + * The presence of this symbol as an object key indicates it should be considered a tag + * for CBOR serialization purposes. + * + * The object must also have the properties "tag" and "value". + */ +export const tagSymbol = Symbol("@smithy/core/cbor::tagSymbol"); + +/** + * @public + * Applies the tag symbol to the object. + */ +export function tag(data: { tag: number | bigint; value: any; [tagSymbol]?: true }): { + tag: number | bigint; + value: any; + [tagSymbol]: true; +} { + data[tagSymbol] = true; + return data as typeof data & { [tagSymbol]: true }; +} diff --git a/packages/core/src/submodules/cbor/cbor.spec.ts b/packages/core/src/submodules/cbor/cbor.spec.ts index 82d6bc92721..09a59b652c2 100644 --- a/packages/core/src/submodules/cbor/cbor.spec.ts +++ b/packages/core/src/submodules/cbor/cbor.spec.ts @@ -5,6 +5,8 @@ import { describe, expect, test as it } from "vitest"; import { cbor } from "./cbor"; import { bytesToFloat16 } from "./cbor-decode"; +import { tagSymbol } from "./cbor-types"; +import { dateToTag } from "./parseCborBody"; // syntax is ESM but the test target is CJS. const here = __dirname; @@ -179,6 +181,18 @@ describe("cbor", () => { 161, 103, 109, 101, 115, 115, 97, 103, 101, 108, 104, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, ]), }, + { + name: "date=0", + data: dateToTag(new Date(0)), + // major tag (6 or 110), minor 1 (timestamp) + cbor: allocByteArray([0b11000001, 0]), + }, + { + name: "date=turn of the millenium", + data: dateToTag(new Date(946684799999)), + // major tag (6 or 110), minor 1 (timestamp) + cbor: allocByteArray([0b11000001, 251, 65, 204, 54, 161, 191, 255, 223, 59]), + }, { name: "complex object", data: { @@ -202,7 +216,7 @@ describe("cbor", () => { ]; const toBytes = (hex: string) => { - const bytes = []; + const bytes = [] as number[]; hex.replace(/../g, (substr: string): string => { bytes.push(parseInt(substr, 16)); return substr; @@ -211,6 +225,19 @@ describe("cbor", () => { }; describe("locally curated scenarios", () => { + it("should throw an error if serializing a tag with missing properties", () => { + expect(() => + cbor.serialize({ + myTag: { + [tagSymbol]: true, + tag: 1, + // value: undefined + }, + }) + ).toThrowError("tag encountered with missing fields, need 'tag' and 'value', found: {\"tag\":1}"); + cbor.resizeEncodingBuffer(0); + }); + for (const { name, data, cbor: cbor_representation } of examples) { it(`should encode for ${name}`, async () => { const serialized = cbor.serialize(data); @@ -292,6 +319,7 @@ describe("cbor", () => { return { tag: id, value: translateTestData(tagValue), + [tagSymbol]: true, }; default: throw new Error(`Unrecognized test scenario type ${type}.`); diff --git a/packages/core/src/submodules/cbor/cbor.ts b/packages/core/src/submodules/cbor/cbor.ts index a9e8c8a2f40..51fc3b9ca64 100644 --- a/packages/core/src/submodules/cbor/cbor.ts +++ b/packages/core/src/submodules/cbor/cbor.ts @@ -17,8 +17,13 @@ export const cbor = { return decode(0, payload.length); }, serialize(input: any) { - encode(input); - return toUint8Array(); + try { + encode(input); + return toUint8Array(); + } catch (e) { + toUint8Array(); // resets cursor. + throw e; + } }, /** * @public diff --git a/packages/core/src/submodules/cbor/index.ts b/packages/core/src/submodules/cbor/index.ts index 444b9cbfdbf..0910d274e31 100644 --- a/packages/core/src/submodules/cbor/index.ts +++ b/packages/core/src/submodules/cbor/index.ts @@ -1,2 +1,3 @@ export { cbor } from "./cbor"; export * from "./parseCborBody"; +export { tagSymbol, tag } from "./cbor-types"; diff --git a/packages/core/src/submodules/cbor/parseCborBody.ts b/packages/core/src/submodules/cbor/parseCborBody.ts index 40cc72302f0..d0605fd1bdb 100644 --- a/packages/core/src/submodules/cbor/parseCborBody.ts +++ b/packages/core/src/submodules/cbor/parseCborBody.ts @@ -4,6 +4,7 @@ import { HeaderBag as __HeaderBag, HttpResponse, SerdeContext as __SerdeContext, import { calculateBodyLength } from "@smithy/util-body-length-browser"; import { cbor } from "./cbor"; +import { tag, tagSymbol } from "./cbor-types"; /** * @internal @@ -27,11 +28,11 @@ export const parseCborBody = (streamBody: any, context: SerdeContext): any => { /** * @internal */ -export const dateToTag = (date: Date): { tag: 1; value: number } => { - return { +export const dateToTag = (date: Date): { tag: number | bigint; value: any; [tagSymbol]: true } => { + return tag({ tag: 1, value: date.getTime() / 1000, - }; + }); }; /**