Skip to content

Commit

Permalink
fix(core): use unique symbol to indicate tags for cbor serialization (s…
Browse files Browse the repository at this point in the history
…mithy-lang#1457)

* fix(core): use unique symbol to indicate tags for cbor serialization

* formatting
  • Loading branch information
kuhe authored Nov 21, 2024
1 parent 7f0bf78 commit 9c40f7b
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/sweet-planets-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@smithy/core": patch
---

make CBOR tags more distinct in JS
3 changes: 2 additions & 1 deletion packages/core/src/submodules/cbor/cbor-decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
specialNull,
specialTrue,
specialUndefined,
tag,
Uint8,
Uint32,
Uint64,
Expand Down Expand Up @@ -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:
Expand Down
13 changes: 13 additions & 0 deletions packages/core/src/submodules/cbor/cbor-encode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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];
Expand Down
24 changes: 24 additions & 0 deletions packages/core/src/submodules/cbor/cbor-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type CborItemType =
export type CborTagType = {
tag: Uint64 | number;
value: CborValueType;
[tagSymbol]: true;
};
export type CborUnstructuredByteStringType = Uint8Array;
export type CborListType<T = any> = Array<T>;
Expand Down Expand Up @@ -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 };
}
30 changes: 29 additions & 1 deletion packages/core/src/submodules/cbor/cbor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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: {
Expand All @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -292,6 +319,7 @@ describe("cbor", () => {
return {
tag: id,
value: translateTestData(tagValue),
[tagSymbol]: true,
};
default:
throw new Error(`Unrecognized test scenario <expect> type ${type}.`);
Expand Down
9 changes: 7 additions & 2 deletions packages/core/src/submodules/cbor/cbor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/submodules/cbor/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { cbor } from "./cbor";
export * from "./parseCborBody";
export { tagSymbol, tag } from "./cbor-types";
7 changes: 4 additions & 3 deletions packages/core/src/submodules/cbor/parseCborBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
};
});
};

/**
Expand Down

0 comments on commit 9c40f7b

Please sign in to comment.