a collection of lightweight TypeScript packages for AT Protocol, the protocol powering Bluesky
at trunk 190 lines 6.2 kB view raw
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};