File size: 6,712 Bytes
80d7f0c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
import {
	CborError,
	type CborOptions,
	MAX_UINT32,
	type ResolvedCborOptions,
	resolveOptions,
	textDecoder,
	textEncoder,
	UINT32_BASE,
} from "./options.ts";

class CborWriter {
	private buffer: Uint8Array;
	private offset = 0;
	private readonly maxByteLength: number;

	constructor(maxByteLength: number) {
		this.maxByteLength = maxByteLength;
		this.buffer = new Uint8Array(Math.min(256, maxByteLength));
	}

	writeByte(value: number): void {
		this.ensureCapacity(1);
		this.buffer[this.offset] = value;
		this.offset++;
	}

	writeBytes(bytes: Uint8Array): void {
		this.ensureCapacity(bytes.byteLength);
		this.buffer.set(bytes, this.offset);
		this.offset += bytes.byteLength;
	}

	writeUint16(value: number): void {
		this.ensureCapacity(2);
		this.buffer[this.offset] = value >>> 8;
		this.buffer[this.offset + 1] = value;
		this.offset += 2;
	}

	writeUint32(value: number): void {
		this.ensureCapacity(4);
		this.buffer[this.offset] = value >>> 24;
		this.buffer[this.offset + 1] = value >>> 16;
		this.buffer[this.offset + 2] = value >>> 8;
		this.buffer[this.offset + 3] = value;
		this.offset += 4;
	}

	writeUint64(value: number): void {
		const high = Math.floor(value / UINT32_BASE);
		const low = value - high * UINT32_BASE;
		this.writeUint32(high);
		this.writeUint32(low);
	}

	writeFloat64(value: number): void {
		this.ensureCapacity(9);
		this.buffer[this.offset] = 0xfb;
		new DataView(this.buffer.buffer).setFloat64(this.offset + 1, value, false);
		this.offset += 9;
	}

	finish(): Uint8Array {
		return this.buffer.slice(0, this.offset);
	}

	private ensureCapacity(additionalBytes: number): void {
		const required = this.offset + additionalBytes;
		if (required > this.maxByteLength) {
			throw new CborError(`CBOR byte length exceeds configured limit of ${this.maxByteLength}`);
		}
		if (required <= this.buffer.byteLength) return;

		let capacity = Math.max(1, this.buffer.byteLength);
		while (capacity < required) capacity = Math.min(this.maxByteLength, Math.max(required, capacity * 2));
		const expanded = new Uint8Array(capacity);
		expanded.set(this.buffer);
		this.buffer = expanded;
	}
}

function writeArgument(writer: CborWriter, majorType: number, value: number): void {
	const prefix = majorType << 5;
	if (value < 24) {
		writer.writeByte(prefix | value);
	} else if (value <= 0xff) {
		writer.writeByte(prefix | 24);
		writer.writeByte(value);
	} else if (value <= 0xffff) {
		writer.writeByte(prefix | 25);
		writer.writeUint16(value);
	} else if (value <= MAX_UINT32) {
		writer.writeByte(prefix | 26);
		writer.writeUint32(value);
	} else {
		writer.writeByte(prefix | 27);
		writer.writeUint64(value);
	}
}

function isPlainObject(value: unknown): value is Record<string, unknown> {
	if (typeof value !== "object" || value === null) return false;
	const prototype = Object.getPrototypeOf(value);
	return prototype === Object.prototype || prototype === null;
}

function encodeText(writer: CborWriter, value: string, options: ResolvedCborOptions): void {
	const bytes = textEncoder.encode(value);
	if (bytes.byteLength > options.maxByteLength) {
		throw new CborError(`CBOR text string length exceeds configured limit of ${options.maxByteLength}`);
	}
	if (textDecoder.decode(bytes) !== value)
		throw new CborError("CBOR text strings must contain valid Unicode scalar values");
	writeArgument(writer, 3, bytes.byteLength);
	writer.writeBytes(bytes);
}

function encodeValue(
	writer: CborWriter,
	value: unknown,
	options: ResolvedCborOptions,
	depth: number,
	ancestors: Set<object>,
): void {
	if (depth > options.maxDepth)
		throw new CborError(`CBOR nesting depth exceeds configured limit of ${options.maxDepth}`);

	if (value === null) {
		writer.writeByte(0xf6);
		return;
	}
	if (typeof value === "boolean") {
		writer.writeByte(value ? 0xf5 : 0xf4);
		return;
	}
	if (typeof value === "number") {
		if (!Number.isFinite(value)) throw new CborError("CBOR numbers must be finite");
		if (Number.isInteger(value) && !Object.is(value, -0)) {
			if (!Number.isSafeInteger(value)) throw new CborError("CBOR integers must be safe JavaScript integers");
			if (value >= 0) writeArgument(writer, 0, value);
			else writeArgument(writer, 1, -1 - value);
		} else {
			writer.writeFloat64(value);
		}
		return;
	}
	if (typeof value === "string") {
		encodeText(writer, value, options);
		return;
	}
	if (value instanceof Uint8Array) {
		if (value.byteLength > options.maxByteLength) {
			throw new CborError(`CBOR byte string length exceeds configured limit of ${options.maxByteLength}`);
		}
		writeArgument(writer, 2, value.byteLength);
		writer.writeBytes(value);
		return;
	}
	if (Array.isArray(value)) {
		if (ancestors.has(value)) throw new CborError("CBOR values must not contain cycles");
		if (value.length > options.maxContainerLength) {
			throw new CborError(`CBOR array length exceeds configured limit of ${options.maxContainerLength}`);
		}
		ancestors.add(value);
		try {
			writeArgument(writer, 4, value.length);
			for (let index = 0; index < value.length; index++) {
				if (!Object.hasOwn(value, index) || value[index] === undefined) {
					throw new CborError("CBOR arrays must not contain holes or undefined values");
				}
				encodeValue(writer, value[index], options, depth + 1, ancestors);
			}
		} finally {
			ancestors.delete(value);
		}
		return;
	}
	if (isPlainObject(value)) {
		if (ancestors.has(value)) throw new CborError("CBOR values must not contain cycles");
		for (const symbol of Object.getOwnPropertySymbols(value)) {
			if (Object.prototype.propertyIsEnumerable.call(value, symbol)) {
				throw new CborError("CBOR map keys must be strings");
			}
		}
		const entries: Array<readonly [string, unknown]> = [];
		for (const key of Object.keys(value)) {
			const entryValue = value[key];
			if (entryValue !== undefined) entries.push([key, entryValue]);
		}
		if (entries.length > options.maxContainerLength) {
			throw new CborError(`CBOR map length exceeds configured limit of ${options.maxContainerLength}`);
		}
		ancestors.add(value);
		try {
			writeArgument(writer, 5, entries.length);
			for (const [key, entryValue] of entries) {
				encodeText(writer, key, options);
				encodeValue(writer, entryValue, options, depth + 1, ancestors);
			}
		} finally {
			ancestors.delete(value);
		}
		return;
	}

	throw new CborError(`Unsupported CBOR value type: ${typeof value}`);
}

/** Encodes the protocol's strict, definite-length RFC 8949 subset. */
export function encodeCbor(value: unknown, options?: CborOptions): Uint8Array {
	const resolved = resolveOptions(options);
	const writer = new CborWriter(resolved.maxByteLength);
	encodeValue(writer, value, resolved, 0, new Set<object>());
	return writer.finish();
}