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
11const WireType = {
12 Varint: 0,
13 Fixed64: 1,
14 LengthDelimited: 2,
15 Fixed32: 5
16} as const
17type WireType = typeof WireType[keyof typeof WireType]
18
19/**
20 * Encodes a field tag (field number + wire type)
21 */
22const encodeTag = (fieldNumber: number, wireType: WireType): number => (fieldNumber << 3) | wireType
23
24/**
25 * Encodes a varint (variable-length integer)
26 *
27 * @internal
28 */
29export 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 */
48export 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 */
60export 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 */
72export 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 */
84export const encodeString = (value: string): Uint8Array => new TextEncoder().encode(value)
85
86/**
87 * Encodes bytes as a hex string to Uint8Array
88 *
89 * @internal
90 */
91export 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 */
104export 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 */
122export 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 */
133export const boolField = (fieldNumber: number, value: boolean): Uint8Array => varintField(fieldNumber, value ? 1 : 0)
134
135/**
136 * Encodes a fixed64 field
137 *
138 * @internal
139 */
140export 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 */
151export 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 */
162export 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 */
173export 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 */
185export 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 */
193export const bytesFieldFromHex = (fieldNumber: number, hex: string): Uint8Array =>
194 lengthDelimitedField(fieldNumber, encodeHexBytes(hex))
195
196/**
197 * Encodes an embedded message field
198 *
199 * @internal
200 */
201export const messageField = (fieldNumber: number, message: Uint8Array): Uint8Array =>
202 lengthDelimitedField(fieldNumber, message)
203
204/**
205 * Encodes repeated fields
206 *
207 * @internal
208 */
209export 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 */
220export const optionalStringField = (
221 fieldNumber: number,
222 value: string | undefined
223): Uint8Array => value !== undefined && value !== "" ? stringField(fieldNumber, value) : new Uint8Array(0)