| 1 | /** |
| 2 | * Low-level protobuf wire format encoding utilities. |
| 3 | * |
| 4 | * Protobuf wire types: |
| 5 | * - 0: Varint (int32, int64, uint32, uint64, sint32, sint64, bool, enum) |
| 6 | * - 1: 64-bit (fixed64, sfixed64, double) |
| 7 | * - 2: Length-delimited (string, bytes, embedded messages, packed repeated fields) |
| 8 | * - 5: 32-bit (fixed32, sfixed32, float) |
| 9 | */ |
| 10 | |
| 11 | const WireType = { |
| 12 | Varint: 0, |
| 13 | Fixed64: 1, |
| 14 | LengthDelimited: 2, |
| 15 | Fixed32: 5 |
| 16 | } as const |
| 17 | type WireType = typeof WireType[keyof typeof WireType] |
| 18 | |
| 19 | /** |
| 20 | * Encodes a field tag (field number + wire type) |
| 21 | */ |
| 22 | const encodeTag = (fieldNumber: number, wireType: WireType): number => (fieldNumber << 3) | wireType |
| 23 | |
| 24 | /** |
| 25 | * Encodes a varint (variable-length integer) |
| 26 | * |
| 27 | * @internal |
| 28 | */ |
| 29 | export const encodeVarint = (value: number | bigint): Uint8Array => { |
| 30 | const bytes: Array<number> = [] |
| 31 | let n = typeof value === "bigint" ? value : BigInt(value) |
| 32 | if (n < BigInt(0)) { |
| 33 | n = BigInt.asUintN(64, n) |
| 34 | } |
| 35 | while (n > BigInt(127)) { |
| 36 | bytes.push(Number(n & BigInt(127)) | 0x80) |
| 37 | n >>= BigInt(7) |
| 38 | } |
| 39 | bytes.push(Number(n)) |
| 40 | return new Uint8Array(bytes) |
| 41 | } |
| 42 | |
| 43 | /** |
| 44 | * Encodes a 64-bit fixed value (little-endian) |
| 45 | * |
| 46 | * @internal |
| 47 | */ |
| 48 | export const encodeFixed64 = (value: bigint): Uint8Array => { |
| 49 | const bytes = new Uint8Array(8) |
| 50 | const view = new DataView(bytes.buffer) |
| 51 | view.setBigUint64(0, value, true) |
| 52 | return bytes |
| 53 | } |
| 54 | |
| 55 | /** |
| 56 | * Encodes a 32-bit fixed value (little-endian) |
| 57 | * |
| 58 | * @internal |
| 59 | */ |
| 60 | export const encodeFixed32 = (value: number): Uint8Array => { |
| 61 | const bytes = new Uint8Array(4) |
| 62 | const view = new DataView(bytes.buffer) |
| 63 | view.setUint32(0, value, true) |
| 64 | return bytes |
| 65 | } |
| 66 | |
| 67 | /** |
| 68 | * Encodes a double (64-bit float, little-endian) |
| 69 | * |
| 70 | * @internal |
| 71 | */ |
| 72 | export const encodeDouble = (value: number): Uint8Array => { |
| 73 | const bytes = new Uint8Array(8) |
| 74 | const view = new DataView(bytes.buffer) |
| 75 | view.setFloat64(0, value, true) |
| 76 | return bytes |
| 77 | } |
| 78 | |
| 79 | /** |
| 80 | * Encodes a string to UTF-8 bytes |
| 81 | * |
| 82 | * @internal |
| 83 | */ |
| 84 | export const encodeString = (value: string): Uint8Array => new TextEncoder().encode(value) |
| 85 | |
| 86 | /** |
| 87 | * Encodes bytes as a hex string to Uint8Array |
| 88 | * |
| 89 | * @internal |
| 90 | */ |
| 91 | export const encodeHexBytes = (hex: string): Uint8Array => { |
| 92 | const bytes = new Uint8Array(hex.length / 2) |
| 93 | for (let i = 0; i < hex.length; i += 2) { |
| 94 | bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16) |
| 95 | } |
| 96 | return bytes |
| 97 | } |
| 98 | |
| 99 | /** |
| 100 | * Concatenates multiple Uint8Arrays |
| 101 | * |
| 102 | * @internal |
| 103 | */ |
| 104 | export const concat = (...arrays: Array<Uint8Array>): Uint8Array => { |
| 105 | const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0) |
| 106 | const result = new Uint8Array(totalLength) |
| 107 | let offset = 0 |
| 108 | for (const arr of arrays) { |
| 109 | result.set(arr, offset) |
| 110 | offset += arr.length |
| 111 | } |
| 112 | return result |
| 113 | } |
| 114 | |
| 115 | // Field encoders |
| 116 | |
| 117 | /** |
| 118 | * Encodes a varint field |
| 119 | * |
| 120 | * @internal |
| 121 | */ |
| 122 | export const varintField = (fieldNumber: number, value: number | bigint): Uint8Array => |
| 123 | concat( |
| 124 | encodeVarint(encodeTag(fieldNumber, WireType.Varint)), |
| 125 | encodeVarint(value) |
| 126 | ) |
| 127 | |
| 128 | /** |
| 129 | * Encodes a bool field |
| 130 | * |
| 131 | * @internal |
| 132 | */ |
| 133 | export const boolField = (fieldNumber: number, value: boolean): Uint8Array => varintField(fieldNumber, value ? 1 : 0) |
| 134 | |
| 135 | /** |
| 136 | * Encodes a fixed64 field |
| 137 | * |
| 138 | * @internal |
| 139 | */ |
| 140 | export const fixed64Field = (fieldNumber: number, value: bigint): Uint8Array => |
| 141 | concat( |
| 142 | encodeVarint(encodeTag(fieldNumber, WireType.Fixed64)), |
| 143 | encodeFixed64(value) |
| 144 | ) |
| 145 | |
| 146 | /** |
| 147 | * Encodes a fixed32 field |
| 148 | * |
| 149 | * @internal |
| 150 | */ |
| 151 | export const fixed32Field = (fieldNumber: number, value: number): Uint8Array => |
| 152 | concat( |
| 153 | encodeVarint(encodeTag(fieldNumber, WireType.Fixed32)), |
| 154 | encodeFixed32(value) |
| 155 | ) |
| 156 | |
| 157 | /** |
| 158 | * Encodes a double field |
| 159 | * |
| 160 | * @internal |
| 161 | */ |
| 162 | export const doubleField = (fieldNumber: number, value: number): Uint8Array => |
| 163 | concat( |
| 164 | encodeVarint(encodeTag(fieldNumber, WireType.Fixed64)), |
| 165 | encodeDouble(value) |
| 166 | ) |
| 167 | |
| 168 | /** |
| 169 | * Encodes a length-delimited field (bytes, string, embedded message) |
| 170 | * |
| 171 | * @internal |
| 172 | */ |
| 173 | export const lengthDelimitedField = (fieldNumber: number, value: Uint8Array): Uint8Array => |
| 174 | concat( |
| 175 | encodeVarint(encodeTag(fieldNumber, WireType.LengthDelimited)), |
| 176 | encodeVarint(value.length), |
| 177 | value |
| 178 | ) |
| 179 | |
| 180 | /** |
| 181 | * Encodes a string field |
| 182 | * |
| 183 | * @internal |
| 184 | */ |
| 185 | export const stringField = (fieldNumber: number, value: string): Uint8Array => |
| 186 | lengthDelimitedField(fieldNumber, encodeString(value)) |
| 187 | |
| 188 | /** |
| 189 | * Encodes a bytes field from hex string |
| 190 | * |
| 191 | * @internal |
| 192 | */ |
| 193 | export const bytesFieldFromHex = (fieldNumber: number, hex: string): Uint8Array => |
| 194 | lengthDelimitedField(fieldNumber, encodeHexBytes(hex)) |
| 195 | |
| 196 | /** |
| 197 | * Encodes an embedded message field |
| 198 | * |
| 199 | * @internal |
| 200 | */ |
| 201 | export const messageField = (fieldNumber: number, message: Uint8Array): Uint8Array => |
| 202 | lengthDelimitedField(fieldNumber, message) |
| 203 | |
| 204 | /** |
| 205 | * Encodes repeated fields |
| 206 | * |
| 207 | * @internal |
| 208 | */ |
| 209 | export const repeatedField = <T>( |
| 210 | fieldNumber: number, |
| 211 | values: ReadonlyArray<T>, |
| 212 | encode: (value: T) => Uint8Array |
| 213 | ): Uint8Array => concat(...values.map((v) => messageField(fieldNumber, encode(v)))) |
| 214 | |
| 215 | /** |
| 216 | * Helper to conditionally encode a string field if non-empty |
| 217 | * |
| 218 | * @internal |
| 219 | */ |
| 220 | export const optionalStringField = ( |
| 221 | fieldNumber: number, |
| 222 | value: string | undefined |
| 223 | ): Uint8Array => value !== undefined && value !== "" ? stringField(fieldNumber, value) : new Uint8Array(0) |