Skip to main content

pi/
buffer_shim.rs

1//! Node.js `Buffer` shim — pure-JS implementation for the QuickJS extension runtime.
2//!
3//! Provides a `Buffer` class that extends `Uint8Array` with Node.js-compatible
4//! encoding/decoding (utf8, base64, hex, ascii, latin1), static factory methods
5//! (`from`, `alloc`, `concat`, `isBuffer`, `byteLength`), and instance methods
6//! (`toString`, `write`, `slice`, `copy`, `compare`, `equals`, `indexOf`,
7//! `includes`, `fill`, `toJSON`).
8
9/// The JS source for the `node:buffer` virtual module.
10pub const NODE_BUFFER_JS: &str = r#"
11// ─── Encoding helpers ────────────────────────────────────────────────────────
12
13function utf8Encode(str) {
14  return new TextEncoder().encode(str);
15}
16
17function utf8Decode(bytes, start, end) {
18  return new TextDecoder().decode(bytes.subarray(start, end));
19}
20
21function hexEncode(bytes) {
22  let out = '';
23  for (let i = 0; i < bytes.length; i++) {
24    out += bytes[i].toString(16).padStart(2, '0');
25  }
26  return out;
27}
28
29function hexDecode(str) {
30  const len = str.length >>> 1;
31  const bytes = new Uint8Array(len);
32  for (let i = 0; i < len; i++) {
33    bytes[i] = parseInt(str.slice(i * 2, i * 2 + 2), 16);
34  }
35  return bytes;
36}
37
38function base64Encode(bytes) {
39  let binary = '';
40  for (let i = 0; i < bytes.length; i++) {
41    binary += String.fromCharCode(bytes[i]);
42  }
43  return globalThis.btoa(binary);
44}
45
46function base64Decode(str) {
47  const binary = globalThis.atob(str);
48  const bytes = new Uint8Array(binary.length);
49  for (let i = 0; i < binary.length; i++) {
50    bytes[i] = binary.charCodeAt(i);
51  }
52  return bytes;
53}
54
55function latin1Encode(str) {
56  const bytes = new Uint8Array(str.length);
57  for (let i = 0; i < str.length; i++) {
58    bytes[i] = str.charCodeAt(i) & 0xFF;
59  }
60  return bytes;
61}
62
63function latin1Decode(bytes, start, end) {
64  let out = '';
65  for (let i = start; i < end; i++) {
66    out += String.fromCharCode(bytes[i]);
67  }
68  return out;
69}
70
71function normalizeEncoding(enc) {
72  if (!enc || enc === 'utf8' || enc === 'utf-8') return 'utf8';
73  const lower = enc.toLowerCase();
74  if (lower === 'utf8' || lower === 'utf-8') return 'utf8';
75  if (lower === 'hex') return 'hex';
76  if (lower === 'base64') return 'base64';
77  if (lower === 'ascii' || lower === 'binary' || lower === 'latin1') return 'latin1';
78  return 'utf8';
79}
80
81function encodeString(str, encoding) {
82  switch (normalizeEncoding(encoding)) {
83    case 'hex': return hexDecode(str);
84    case 'base64': return base64Decode(str);
85    case 'latin1': return latin1Encode(str);
86    default: return utf8Encode(str);
87  }
88}
89
90function decodeBytes(bytes, encoding, start, end) {
91  start = start || 0;
92  end = end != null ? end : bytes.length;
93  switch (normalizeEncoding(encoding)) {
94    case 'hex': return hexEncode(bytes.subarray(start, end));
95    case 'base64': return base64Encode(bytes.subarray(start, end));
96    case 'latin1': return latin1Decode(bytes, start, end);
97    default: return utf8Decode(bytes, start, end);
98  }
99}
100
101// ─── Buffer class ────────────────────────────────────────────────────────────
102
103class Buffer extends Uint8Array {
104  // ── Static factory methods ─────────────────────────────────────────────
105  static from(input, encodingOrOffset, length) {
106    if (typeof input === 'string') {
107      const bytes = encodeString(input, encodingOrOffset);
108      const buf = new Buffer(bytes.length);
109      buf.set(bytes);
110      return buf;
111    }
112    if (input instanceof ArrayBuffer || input instanceof SharedArrayBuffer) {
113      const offset = encodingOrOffset || 0;
114      const len = length != null ? length : input.byteLength - offset;
115      return new Buffer(input, offset, len);
116    }
117    if (ArrayBuffer.isView(input)) {
118      const buf = new Buffer(input.length);
119      buf.set(input);
120      return buf;
121    }
122    if (Array.isArray(input)) {
123      const buf = new Buffer(input.length);
124      buf.set(input);
125      return buf;
126    }
127    if (input && typeof input === 'object' && typeof input.length === 'number') {
128      const buf = new Buffer(input.length);
129      for (let i = 0; i < input.length; i++) buf[i] = input[i] & 0xFF;
130      return buf;
131    }
132    throw new TypeError('The first argument must be a string, Buffer, ArrayBuffer, Array, or array-like object.');
133  }
134
135  static alloc(size, fill, encoding) {
136    const buf = new Buffer(size);
137    if (fill !== undefined && fill !== 0) {
138      buf.fill(fill, 0, size, encoding);
139    }
140    return buf;
141  }
142
143  static allocUnsafe(size) {
144    return new Buffer(size);
145  }
146
147  static allocUnsafeSlow(size) {
148    return new Buffer(size);
149  }
150
151  static isBuffer(obj) {
152    return obj instanceof Buffer;
153  }
154
155  static isEncoding(encoding) {
156    return ['utf8', 'utf-8', 'hex', 'base64', 'ascii', 'binary', 'latin1']
157      .includes((encoding || '').toLowerCase());
158  }
159
160  static byteLength(string, encoding) {
161    if (typeof string !== 'string') {
162      if (ArrayBuffer.isView(string)) return string.byteLength;
163      if (string instanceof ArrayBuffer) return string.byteLength;
164      throw new TypeError('The "string" argument must be a string, Buffer, or ArrayBuffer');
165    }
166    return encodeString(string, encoding).length;
167  }
168
169  static concat(list, totalLength) {
170    if (!Array.isArray(list)) throw new TypeError('"list" argument must be an Array of Buffers');
171    if (list.length === 0) return Buffer.alloc(0);
172    const total = totalLength != null
173      ? totalLength
174      : list.reduce((acc, b) => acc + b.length, 0);
175    const result = Buffer.alloc(total);
176    let offset = 0;
177    for (const buf of list) {
178      const src = buf instanceof Uint8Array ? buf : Buffer.from(buf);
179      const copyLen = Math.min(src.length, total - offset);
180      result.set(src.subarray(0, copyLen), offset);
181      offset += copyLen;
182      if (offset >= total) break;
183    }
184    return result;
185  }
186
187  static compare(a, b) {
188    if (!(a instanceof Uint8Array) || !(b instanceof Uint8Array)) {
189      throw new TypeError('Arguments must be Buffers');
190    }
191    const len = Math.min(a.length, b.length);
192    for (let i = 0; i < len; i++) {
193      if (a[i] < b[i]) return -1;
194      if (a[i] > b[i]) return 1;
195    }
196    if (a.length < b.length) return -1;
197    if (a.length > b.length) return 1;
198    return 0;
199  }
200
201  // ── Instance methods ───────────────────────────────────────────────────
202  toString(encoding, start, end) {
203    return decodeBytes(this, encoding, start, end);
204  }
205
206  write(string, offset, length, encoding) {
207    if (typeof offset === 'string') { encoding = offset; offset = 0; length = this.length; }
208    else if (typeof length === 'string') { encoding = length; length = this.length - (offset || 0); }
209    offset = offset || 0;
210    length = length != null ? length : this.length - offset;
211    const bytes = encodeString(string, encoding);
212    const writeLen = Math.min(bytes.length, length, this.length - offset);
213    this.set(bytes.subarray(0, writeLen), offset);
214    return writeLen;
215  }
216
217  toJSON() {
218    return { type: 'Buffer', data: Array.from(this) };
219  }
220
221  equals(other) {
222    if (!(other instanceof Uint8Array)) throw new TypeError('Argument must be a Buffer');
223    return Buffer.compare(this, other) === 0;
224  }
225
226  compare(other, targetStart, targetEnd, sourceStart, sourceEnd) {
227    if (!(other instanceof Uint8Array)) throw new TypeError('Argument must be a Buffer');
228    targetStart = targetStart || 0;
229    targetEnd = targetEnd != null ? targetEnd : other.length;
230    sourceStart = sourceStart || 0;
231    sourceEnd = sourceEnd != null ? sourceEnd : this.length;
232    return Buffer.compare(
233      this.subarray(sourceStart, sourceEnd),
234      other.subarray(targetStart, targetEnd)
235    );
236  }
237
238  copy(target, targetStart, sourceStart, sourceEnd) {
239    targetStart = targetStart || 0;
240    sourceStart = sourceStart || 0;
241    sourceEnd = sourceEnd != null ? sourceEnd : this.length;
242    const len = Math.min(sourceEnd - sourceStart, target.length - targetStart);
243    target.set(this.subarray(sourceStart, sourceStart + len), targetStart);
244    return len;
245  }
246
247  indexOf(value, byteOffset, encoding) {
248    if (typeof value === 'number') {
249      byteOffset = byteOffset || 0;
250      for (let i = byteOffset; i < this.length; i++) {
251        if (this[i] === (value & 0xFF)) return i;
252      }
253      return -1;
254    }
255    if (typeof value === 'string') {
256      value = Buffer.from(value, encoding);
257    }
258    if (value instanceof Uint8Array) {
259      byteOffset = byteOffset || 0;
260      if (value.length === 0) return byteOffset <= this.length ? byteOffset : -1;
261      outer: for (let i = byteOffset; i <= this.length - value.length; i++) {
262        for (let j = 0; j < value.length; j++) {
263          if (this[i + j] !== value[j]) continue outer;
264        }
265        return i;
266      }
267    }
268    return -1;
269  }
270
271  includes(value, byteOffset, encoding) {
272    return this.indexOf(value, byteOffset, encoding) !== -1;
273  }
274
275  fill(value, offset, end, encoding) {
276    offset = offset || 0;
277    end = end != null ? end : this.length;
278    if (typeof value === 'number') {
279      for (let i = offset; i < end; i++) this[i] = value & 0xFF;
280    } else if (typeof value === 'string') {
281      const bytes = encodeString(value, encoding);
282      if (bytes.length === 0) return this;
283      for (let i = offset; i < end; i++) {
284        this[i] = bytes[(i - offset) % bytes.length];
285      }
286    }
287    return this;
288  }
289
290  slice(start, end) {
291    const sliced = super.slice(start, end);
292    const buf = new Buffer(sliced.length);
293    buf.set(sliced);
294    return buf;
295  }
296
297  subarray(start, end) {
298    // Override to return a Buffer, not a plain Uint8Array
299    const sub = super.subarray(start, end);
300    Object.setPrototypeOf(sub, Buffer.prototype);
301    return sub;
302  }
303
304  swap16() {
305    if (this.length % 2 !== 0) throw new RangeError('Buffer size must be a multiple of 16-bits');
306    for (let i = 0; i < this.length; i += 2) {
307      const t = this[i]; this[i] = this[i + 1]; this[i + 1] = t;
308    }
309    return this;
310  }
311
312  swap32() {
313    if (this.length % 4 !== 0) throw new RangeError('Buffer size must be a multiple of 32-bits');
314    for (let i = 0; i < this.length; i += 4) {
315      const t0 = this[i], t1 = this[i + 1];
316      this[i] = this[i + 3]; this[i + 1] = this[i + 2];
317      this[i + 2] = t1; this[i + 3] = t0;
318    }
319    return this;
320  }
321
322  // Read/write integers (LE/BE) — commonly used by extensions
323  readUInt8(offset) {
324    offset = offset >>> 0;
325    if (offset >= this.length) throw new RangeError('Index out of range');
326    return this[offset];
327  }
328
329  readUInt16LE(offset) {
330    offset = offset >>> 0;
331    if (offset + 2 > this.length) throw new RangeError('Index out of range');
332    return this[offset] | (this[offset + 1] << 8);
333  }
334
335  readUInt16BE(offset) {
336    offset = offset >>> 0;
337    if (offset + 2 > this.length) throw new RangeError('Index out of range');
338    return (this[offset] << 8) | this[offset + 1];
339  }
340
341  readUInt32LE(offset) {
342    offset = offset >>> 0;
343    if (offset + 4 > this.length) throw new RangeError('Index out of range');
344    return (this[offset] | (this[offset+1] << 8) | (this[offset+2] << 16)) + (this[offset+3] * 0x1000000);
345  }
346
347  readUInt32BE(offset) {
348    offset = offset >>> 0;
349    if (offset + 4 > this.length) throw new RangeError('Index out of range');
350    return (this[offset] * 0x1000000) + ((this[offset+1] << 16) | (this[offset+2] << 8) | this[offset+3]);
351  }
352
353  readInt8(offset) {
354    offset = offset >>> 0;
355    if (offset >= this.length) throw new RangeError('Index out of range');
356    const v = this[offset];
357    return v > 127 ? v - 256 : v;
358  }
359
360  writeUInt8(value, offset) {
361    offset = offset >>> 0;
362    if (offset >= this.length) throw new RangeError('Index out of range');
363    this[offset] = value & 0xFF;
364    return offset + 1;
365  }
366
367  writeUInt16LE(value, offset) {
368    offset = offset >>> 0;
369    if (offset + 2 > this.length) throw new RangeError('Index out of range');
370    this[offset] = value & 0xFF;
371    this[offset + 1] = (value >>> 8) & 0xFF;
372    return offset + 2;
373  }
374
375  writeUInt16BE(value, offset) {
376    offset = offset >>> 0;
377    if (offset + 2 > this.length) throw new RangeError('Index out of range');
378    this[offset] = (value >>> 8) & 0xFF;
379    this[offset + 1] = value & 0xFF;
380    return offset + 2;
381  }
382
383  writeUInt32LE(value, offset) {
384    offset = offset >>> 0;
385    if (offset + 4 > this.length) throw new RangeError('Index out of range');
386    this[offset] = value & 0xFF;
387    this[offset+1] = (value >>> 8) & 0xFF;
388    this[offset+2] = (value >>> 16) & 0xFF;
389    this[offset+3] = (value >>> 24) & 0xFF;
390    return offset + 4;
391  }
392
393  writeUInt32BE(value, offset) {
394    offset = offset >>> 0;
395    if (offset + 4 > this.length) throw new RangeError('Index out of range');
396    this[offset] = (value >>> 24) & 0xFF;
397    this[offset+1] = (value >>> 16) & 0xFF;
398    this[offset+2] = (value >>> 8) & 0xFF;
399    this[offset+3] = value & 0xFF;
400    return offset + 4;
401  }
402}
403
404// Make Buffer available globally (Node.js compatibility)
405globalThis.Buffer = Buffer;
406
407export { Buffer };
408export default { Buffer };
409"#;