Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
Nov 21, 2024
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
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