forked from
mary.my.id/atcute
a collection of lightweight TypeScript packages for AT Protocol, the protocol powering Bluesky
1import { Buffer as NodeBuffer } from 'node:buffer';
2import {
3 hash as _hash,
4 randomFillSync as _randomFillSync,
5 timingSafeEqual as _timingSafeEqual,
6} from 'node:crypto';
7
8const _alloc = /*#__PURE__*/ NodeBuffer.alloc;
9const _allocUnsafe = /*#__PURE__*/ NodeBuffer.allocUnsafe;
10const _concat = /*#__PURE__*/ NodeBuffer.concat;
11const _from = /*#__PURE__*/ NodeBuffer.from;
12
13const _byteLength = /*#__PURE__*/ NodeBuffer.byteLength;
14
15const _compare = /*#__PURE__*/ NodeBuffer.prototype.compare;
16const _equals = /*#__PURE__*/ NodeBuffer.prototype.equals;
17const _utf8Slice = /*#__PURE__*/ NodeBuffer.prototype.utf8Slice;
18const _utf8Write = /*#__PURE__*/ NodeBuffer.prototype.utf8Write;
19
20const toUint8Array = (buffer: NodeBuffer) => {
21 return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
22};
23
24export const alloc = (size: number): Uint8Array<ArrayBuffer> => {
25 return toUint8Array(_alloc(size)) as Uint8Array<ArrayBuffer>;
26};
27
28export const allocUnsafe = (size: number): Uint8Array<ArrayBuffer> => {
29 return toUint8Array(_allocUnsafe(size)) as Uint8Array<ArrayBuffer>;
30};
31
32export const compare = (a: Uint8Array, b: Uint8Array): number => {
33 return _compare.call(a, b);
34};
35
36export const equals = (a: Uint8Array, b: Uint8Array): boolean => {
37 return _equals.call(a, b);
38};
39
40export const timingSafeEquals = (a: Uint8Array, b: Uint8Array): boolean => {
41 return _timingSafeEqual(a, b);
42};
43
44export const concat = (arrays: Uint8Array[], size?: number): Uint8Array<ArrayBuffer> => {
45 return toUint8Array(_concat(arrays, size)) as Uint8Array<ArrayBuffer>;
46};
47
48export const encodeUtf8 = (str: string): Uint8Array<ArrayBuffer> => {
49 return toUint8Array(_from(str, 'utf8')) as Uint8Array<ArrayBuffer>;
50};
51
52export const encodeUtf8Into = (to: Uint8Array, str: string, offset?: number, length?: number): number => {
53 return _utf8Write.call(to, str, offset, length);
54};
55
56const _fromCharCode = String.fromCharCode;
57
58// fully unrolled short string decoder, inspired by cbor-x
59// returns null if non-ASCII byte encountered, signaling fallback to utf8Slice
60const _shortString = (from: Uint8Array, p: number, length: number): string | null => {
61 if (length < 4) {
62 if (length < 2) {
63 if (length === 0) return '';
64 const a = from[p];
65 if (a & 0x80) return null;
66 return _fromCharCode(a);
67 }
68 const a = from[p];
69 const b = from[p + 1];
70 if ((a | b) & 0x80) return null;
71 if (length === 2) return _fromCharCode(a, b);
72 const c = from[p + 2];
73 if (c & 0x80) return null;
74 return _fromCharCode(a, b, c);
75 }
76 const a = from[p];
77 const b = from[p + 1];
78 const c = from[p + 2];
79 const d = from[p + 3];
80 if ((a | b | c | d) & 0x80) return null;
81 if (length < 8) {
82 if (length === 4) return _fromCharCode(a, b, c, d);
83 const e = from[p + 4];
84 if (e & 0x80) return null;
85 if (length === 5) return _fromCharCode(a, b, c, d, e);
86 const f = from[p + 5];
87 if (f & 0x80) return null;
88 if (length === 6) return _fromCharCode(a, b, c, d, e, f);
89 const g = from[p + 6];
90 if (g & 0x80) return null;
91 return _fromCharCode(a, b, c, d, e, f, g);
92 }
93 const e = from[p + 4];
94 const f = from[p + 5];
95 const g = from[p + 6];
96 const h = from[p + 7];
97 if ((e | f | g | h) & 0x80) return null;
98 if (length < 12) {
99 if (length === 8) return _fromCharCode(a, b, c, d, e, f, g, h);
100 const i = from[p + 8];
101 if (i & 0x80) return null;
102 if (length === 9) return _fromCharCode(a, b, c, d, e, f, g, h, i);
103 const j = from[p + 9];
104 if (j & 0x80) return null;
105 if (length === 10) return _fromCharCode(a, b, c, d, e, f, g, h, i, j);
106 const k = from[p + 10];
107 if (k & 0x80) return null;
108 return _fromCharCode(a, b, c, d, e, f, g, h, i, j, k);
109 }
110 const i = from[p + 8];
111 const j = from[p + 9];
112 const k = from[p + 10];
113 const l = from[p + 11];
114 if ((i | j | k | l) & 0x80) return null;
115 if (length === 12) return _fromCharCode(a, b, c, d, e, f, g, h, i, j, k, l);
116 const m = from[p + 12];
117 if (m & 0x80) return null;
118 if (length === 13) return _fromCharCode(a, b, c, d, e, f, g, h, i, j, k, l, m);
119 const n = from[p + 13];
120 if (n & 0x80) return null;
121 if (length === 14) return _fromCharCode(a, b, c, d, e, f, g, h, i, j, k, l, m, n);
122 const o = from[p + 14];
123 if (o & 0x80) return null;
124 return _fromCharCode(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o);
125};
126
127/**
128 * decodes a UTF-8 string from a given buffer
129 * @param from source buffer
130 * @param offset byte offset to start reading from
131 * @param length number of bytes to read
132 * @returns decoded string
133 */
134export const decodeUtf8From = (
135 from: Uint8Array,
136 offset: number = 0,
137 length: number = from.length,
138): string => {
139 if (length <= 15) {
140 const result = _shortString(from, offset, length);
141 if (result !== null) return result;
142 }
143 return _utf8Slice.call(from, offset, offset + length);
144};
145
146/**
147 * calculates the UTF-8 byte length of a string
148 * @param str string to measure
149 * @returns byte length when encoded as UTF-8
150 */
151export const getUtf8Length = (str: string): number => {
152 return _byteLength(str, 'utf8');
153};
154
155/**
156 * checks if a string's UTF-8 byte length is within a given range
157 * @param str string to measure
158 * @param min minimum byte length (inclusive)
159 * @param max maximum byte length (inclusive)
160 * @returns true if byte length is within [min, max]
161 */
162export const isUtf8LengthInRange = (str: string, min: number, max: number): boolean => {
163 const len = str.length;
164
165 // fast path: if max possible UTF-8 length is below min, fail
166 if (len * 3 < min) {
167 return false;
168 }
169
170 // fast path: if UTF-16 length satisfies min and max possible satisfies max
171 if (len >= min && len * 3 <= max) {
172 return true;
173 }
174
175 const utf8len = _byteLength(str, 'utf8');
176 return utf8len >= min && utf8len <= max;
177};
178
179export const toSha256 = async (buffer: Uint8Array): Promise<Uint8Array<ArrayBuffer>> => {
180 return toUint8Array(_hash('sha256', buffer, 'buffer')) as Uint8Array<ArrayBuffer>;
181};
182
183/**
184 * generates cryptographically secure random bytes
185 * @param size number of bytes to generate
186 * @returns buffer filled with random bytes
187 */
188export const randomBytes = (size: number): Uint8Array<ArrayBuffer> => {
189 return _randomFillSync(toUint8Array(_allocUnsafe(size))) as Uint8Array<ArrayBuffer>;
190};