Skip to content

Commit 9c40f7b

Browse files
authored
fix(core): use unique symbol to indicate tags for cbor serialization (#1457)
* fix(core): use unique symbol to indicate tags for cbor serialization * formatting
1 parent 7f0bf78 commit 9c40f7b

File tree

8 files changed

+85
-7
lines changed

8 files changed

+85
-7
lines changed

.changeset/sweet-planets-give.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@smithy/core": patch
3+
---
4+
5+
make CBOR tags more distinct in JS

packages/core/src/submodules/cbor/cbor-decode.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
specialNull,
2727
specialTrue,
2828
specialUndefined,
29+
tag,
2930
Uint8,
3031
Uint32,
3132
Uint64,
@@ -122,7 +123,7 @@ export function decode(at: Uint32, to: Uint32): CborValueType {
122123
const valueOffset = _offset;
123124

124125
_offset = offset + valueOffset;
125-
return { tag: castBigInt(unsignedInt), value };
126+
return tag({ tag: castBigInt(unsignedInt), value });
126127
}
127128
case majorUtf8String:
128129
case majorMap:

packages/core/src/submodules/cbor/cbor-encode.ts

+13
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import {
99
majorMap,
1010
majorNegativeInt64,
1111
majorSpecial,
12+
majorTag,
1213
majorUint64,
1314
majorUnstructuredByteString,
1415
majorUtf8String,
1516
specialFalse,
1617
specialNull,
1718
specialTrue,
19+
tagSymbol,
1820
Uint64,
1921
} from "./cbor-types";
2022
import { alloc } from "./cbor-types";
@@ -179,6 +181,17 @@ export function encode(_input: any): void {
179181
cursor += input.byteLength;
180182
continue;
181183
} else if (typeof input === "object") {
184+
if (input[tagSymbol]) {
185+
if ("tag" in input && "value" in input) {
186+
encodeStack.push(input.value);
187+
encodeHeader(majorTag, input.tag);
188+
continue;
189+
} else {
190+
throw new Error(
191+
"tag encountered with missing fields, need 'tag' and 'value', found: " + JSON.stringify(input)
192+
);
193+
}
194+
}
182195
const keys = Object.keys(input);
183196
for (let i = keys.length - 1; i >= 0; --i) {
184197
const key = keys[i];

packages/core/src/submodules/cbor/cbor-types.ts

+24
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export type CborItemType =
1010
export type CborTagType = {
1111
tag: Uint64 | number;
1212
value: CborValueType;
13+
[tagSymbol]: true;
1314
};
1415
export type CborUnstructuredByteStringType = Uint8Array;
1516
export type CborListType<T = any> = Array<T>;
@@ -66,3 +67,26 @@ export const minorIndefinite = 31; // 0b11111
6667
export function alloc(size: number) {
6768
return typeof Buffer !== "undefined" ? Buffer.alloc(size) : new Uint8Array(size);
6869
}
70+
71+
/**
72+
* @public
73+
*
74+
* The presence of this symbol as an object key indicates it should be considered a tag
75+
* for CBOR serialization purposes.
76+
*
77+
* The object must also have the properties "tag" and "value".
78+
*/
79+
export const tagSymbol = Symbol("@smithy/core/cbor::tagSymbol");
80+
81+
/**
82+
* @public
83+
* Applies the tag symbol to the object.
84+
*/
85+
export function tag(data: { tag: number | bigint; value: any; [tagSymbol]?: true }): {
86+
tag: number | bigint;
87+
value: any;
88+
[tagSymbol]: true;
89+
} {
90+
data[tagSymbol] = true;
91+
return data as typeof data & { [tagSymbol]: true };
92+
}

packages/core/src/submodules/cbor/cbor.spec.ts

+29-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { describe, expect, test as it } from "vitest";
55

66
import { cbor } from "./cbor";
77
import { bytesToFloat16 } from "./cbor-decode";
8+
import { tagSymbol } from "./cbor-types";
9+
import { dateToTag } from "./parseCborBody";
810

911
// syntax is ESM but the test target is CJS.
1012
const here = __dirname;
@@ -179,6 +181,18 @@ describe("cbor", () => {
179181
161, 103, 109, 101, 115, 115, 97, 103, 101, 108, 104, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100,
180182
]),
181183
},
184+
{
185+
name: "date=0",
186+
data: dateToTag(new Date(0)),
187+
// major tag (6 or 110), minor 1 (timestamp)
188+
cbor: allocByteArray([0b11000001, 0]),
189+
},
190+
{
191+
name: "date=turn of the millenium",
192+
data: dateToTag(new Date(946684799999)),
193+
// major tag (6 or 110), minor 1 (timestamp)
194+
cbor: allocByteArray([0b11000001, 251, 65, 204, 54, 161, 191, 255, 223, 59]),
195+
},
182196
{
183197
name: "complex object",
184198
data: {
@@ -202,7 +216,7 @@ describe("cbor", () => {
202216
];
203217

204218
const toBytes = (hex: string) => {
205-
const bytes = [];
219+
const bytes = [] as number[];
206220
hex.replace(/../g, (substr: string): string => {
207221
bytes.push(parseInt(substr, 16));
208222
return substr;
@@ -211,6 +225,19 @@ describe("cbor", () => {
211225
};
212226

213227
describe("locally curated scenarios", () => {
228+
it("should throw an error if serializing a tag with missing properties", () => {
229+
expect(() =>
230+
cbor.serialize({
231+
myTag: {
232+
[tagSymbol]: true,
233+
tag: 1,
234+
// value: undefined
235+
},
236+
})
237+
).toThrowError("tag encountered with missing fields, need 'tag' and 'value', found: {\"tag\":1}");
238+
cbor.resizeEncodingBuffer(0);
239+
});
240+
214241
for (const { name, data, cbor: cbor_representation } of examples) {
215242
it(`should encode for ${name}`, async () => {
216243
const serialized = cbor.serialize(data);
@@ -292,6 +319,7 @@ describe("cbor", () => {
292319
return {
293320
tag: id,
294321
value: translateTestData(tagValue),
322+
[tagSymbol]: true,
295323
};
296324
default:
297325
throw new Error(`Unrecognized test scenario <expect> type ${type}.`);

packages/core/src/submodules/cbor/cbor.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,13 @@ export const cbor = {
1717
return decode(0, payload.length);
1818
},
1919
serialize(input: any) {
20-
encode(input);
21-
return toUint8Array();
20+
try {
21+
encode(input);
22+
return toUint8Array();
23+
} catch (e) {
24+
toUint8Array(); // resets cursor.
25+
throw e;
26+
}
2227
},
2328
/**
2429
* @public
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { cbor } from "./cbor";
22
export * from "./parseCborBody";
3+
export { tagSymbol, tag } from "./cbor-types";

packages/core/src/submodules/cbor/parseCborBody.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { HeaderBag as __HeaderBag, HttpResponse, SerdeContext as __SerdeContext,
44
import { calculateBodyLength } from "@smithy/util-body-length-browser";
55

66
import { cbor } from "./cbor";
7+
import { tag, tagSymbol } from "./cbor-types";
78

89
/**
910
* @internal
@@ -27,11 +28,11 @@ export const parseCborBody = (streamBody: any, context: SerdeContext): any => {
2728
/**
2829
* @internal
2930
*/
30-
export const dateToTag = (date: Date): { tag: 1; value: number } => {
31-
return {
31+
export const dateToTag = (date: Date): { tag: number | bigint; value: any; [tagSymbol]: true } => {
32+
return tag({
3233
tag: 1,
3334
value: date.getTime() / 1000,
34-
};
35+
});
3536
};
3637

3738
/**

0 commit comments

Comments
 (0)