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  let chunk = [];
41  for (let i = 0; i < bytes.length; i++) {
42    chunk.push(bytes[i]);
43    if (chunk.length >= 4096) {
44      binary += String.fromCharCode.apply(null, chunk);
45      chunk.length = 0;
46    }
47  }
48  if (chunk.length > 0) {
49    binary += String.fromCharCode.apply(null, chunk);
50  }
51  return globalThis.btoa(binary);
52}
53
54function base64Decode(str) {
55  const binary = globalThis.atob(str);
56  const bytes = new Uint8Array(binary.length);
57  for (let i = 0; i < binary.length; i++) {
58    bytes[i] = binary.charCodeAt(i);
59  }
60  return bytes;
61}
62
63function latin1Encode(str) {
64  const bytes = new Uint8Array(str.length);
65  for (let i = 0; i < str.length; i++) {
66    bytes[i] = str.charCodeAt(i) & 0xFF;
67  }
68  return bytes;
69}
70
71function latin1Decode(bytes, start, end) {
72  let out = '';
73  let chunk = [];
74  const len = end - start;
75  for (let i = 0; i < len; i++) {
76    chunk.push(bytes[start + i]);
77    if (chunk.length >= 4096) {
78      out += String.fromCharCode.apply(null, chunk);
79      chunk.length = 0;
80    }
81  }
82  if (chunk.length > 0) {
83    out += String.fromCharCode.apply(null, chunk);
84  }
85  return out;
86}
87
88function normalizeEncoding(enc) {
89  if (!enc || enc === 'utf8' || enc === 'utf-8') return 'utf8';
90  const lower = enc.toLowerCase();
91  if (lower === 'utf8' || lower === 'utf-8') return 'utf8';
92  if (lower === 'hex') return 'hex';
93  if (lower === 'base64') return 'base64';
94  if (lower === 'ascii' || lower === 'binary' || lower === 'latin1') return 'latin1';
95  return 'utf8';
96}
97
98function encodeString(str, encoding) {
99  switch (normalizeEncoding(encoding)) {
100    case 'hex': return hexDecode(str);
101    case 'base64': return base64Decode(str);
102    case 'latin1': return latin1Encode(str);
103    default: return utf8Encode(str);
104  }
105}
106
107function decodeBytes(bytes, encoding, start, end) {
108  start = start || 0;
109  end = end != null ? end : bytes.length;
110  switch (normalizeEncoding(encoding)) {
111    case 'hex': return hexEncode(bytes.subarray(start, end));
112    case 'base64': return base64Encode(bytes.subarray(start, end));
113    case 'latin1': return latin1Decode(bytes, start, end);
114    default: return utf8Decode(bytes, start, end);
115  }
116}
117
118function normalizeSearchOffset(length, byteOffset) {
119  if (byteOffset == null) return 0;
120  const number = Number(byteOffset);
121  if (Number.isNaN(number)) return 0;
122  if (number === Infinity) return length;
123  if (number === -Infinity) return 0;
124  const offset = Math.trunc(number);
125  if (offset < 0) return Math.max(length + offset, 0);
126  if (offset > length) return length;
127  return offset;
128}
129
130// ─── Buffer class ────────────────────────────────────────────────────────────
131
132class Buffer extends Uint8Array {
133  // ── Static factory methods ─────────────────────────────────────────────
134  static from(input, encodingOrOffset, length) {
135    if (typeof input === 'string') {
136      const bytes = encodeString(input, encodingOrOffset);
137      const buf = new Buffer(bytes.length);
138      buf.set(bytes);
139      return buf;
140    }
141    if (input instanceof ArrayBuffer || input instanceof SharedArrayBuffer) {
142      const offset = encodingOrOffset || 0;
143      const len = length != null ? length : input.byteLength - offset;
144      return new Buffer(input, offset, len);
145    }
146    if (ArrayBuffer.isView(input)) {
147      const buf = new Buffer(input.length);
148      buf.set(input);
149      return buf;
150    }
151    if (Array.isArray(input)) {
152      const buf = new Buffer(input.length);
153      buf.set(input);
154      return buf;
155    }
156    if (input && typeof input === 'object' && typeof input.length === 'number') {
157      const buf = new Buffer(input.length);
158      for (let i = 0; i < input.length; i++) buf[i] = input[i] & 0xFF;
159      return buf;
160    }
161    throw new TypeError('The first argument must be a string, Buffer, ArrayBuffer, Array, or array-like object.');
162  }
163
164  static alloc(size, fill, encoding) {
165    const buf = new Buffer(size);
166    if (fill !== undefined && fill !== 0) {
167      buf.fill(fill, 0, size, encoding);
168    }
169    return buf;
170  }
171
172  static allocUnsafe(size) {
173    return new Buffer(size);
174  }
175
176  static allocUnsafeSlow(size) {
177    return new Buffer(size);
178  }
179
180  static isBuffer(obj) {
181    return obj instanceof Buffer;
182  }
183
184  static isEncoding(encoding) {
185    return ['utf8', 'utf-8', 'hex', 'base64', 'ascii', 'binary', 'latin1']
186      .includes((encoding || '').toLowerCase());
187  }
188
189  static byteLength(string, encoding) {
190    if (typeof string !== 'string') {
191      if (ArrayBuffer.isView(string)) return string.byteLength;
192      if (string instanceof ArrayBuffer) return string.byteLength;
193      throw new TypeError('The "string" argument must be a string, Buffer, or ArrayBuffer');
194    }
195    return encodeString(string, encoding).length;
196  }
197
198  static concat(list, totalLength) {
199    if (!Array.isArray(list)) throw new TypeError('"list" argument must be an Array of Buffers');
200    if (list.length === 0) return Buffer.alloc(0);
201    const total = totalLength != null
202      ? totalLength
203      : list.reduce((acc, b) => acc + b.length, 0);
204    const result = Buffer.alloc(total);
205    let offset = 0;
206    for (const buf of list) {
207      const src = buf instanceof Uint8Array ? buf : Buffer.from(buf);
208      const copyLen = Math.min(src.length, total - offset);
209      result.set(src.subarray(0, copyLen), offset);
210      offset += copyLen;
211      if (offset >= total) break;
212    }
213    return result;
214  }
215
216  static compare(a, b) {
217    if (!(a instanceof Uint8Array) || !(b instanceof Uint8Array)) {
218      throw new TypeError('Arguments must be Buffers');
219    }
220    const len = Math.min(a.length, b.length);
221    for (let i = 0; i < len; i++) {
222      if (a[i] < b[i]) return -1;
223      if (a[i] > b[i]) return 1;
224    }
225    if (a.length < b.length) return -1;
226    if (a.length > b.length) return 1;
227    return 0;
228  }
229
230  // ── Instance methods ───────────────────────────────────────────────────
231  toString(encoding, start, end) {
232    return decodeBytes(this, encoding, start, end);
233  }
234
235  write(string, offset, length, encoding) {
236    if (typeof offset === 'string') { encoding = offset; offset = 0; length = this.length; }
237    else if (typeof length === 'string') { encoding = length; length = this.length - (offset || 0); }
238    offset = offset || 0;
239    length = length != null ? length : this.length - offset;
240    const bytes = encodeString(string, encoding);
241    const writeLen = Math.min(bytes.length, length, this.length - offset);
242    this.set(bytes.subarray(0, writeLen), offset);
243    return writeLen;
244  }
245
246  toJSON() {
247    return { type: 'Buffer', data: Array.from(this) };
248  }
249
250  equals(other) {
251    if (!(other instanceof Uint8Array)) throw new TypeError('Argument must be a Buffer');
252    return Buffer.compare(this, other) === 0;
253  }
254
255  compare(other, targetStart, targetEnd, sourceStart, sourceEnd) {
256    if (!(other instanceof Uint8Array)) throw new TypeError('Argument must be a Buffer');
257    targetStart = targetStart || 0;
258    targetEnd = targetEnd != null ? targetEnd : other.length;
259    sourceStart = sourceStart || 0;
260    sourceEnd = sourceEnd != null ? sourceEnd : this.length;
261    return Buffer.compare(
262      this.subarray(sourceStart, sourceEnd),
263      other.subarray(targetStart, targetEnd)
264    );
265  }
266
267  copy(target, targetStart, sourceStart, sourceEnd) {
268    targetStart = targetStart || 0;
269    sourceStart = sourceStart || 0;
270    sourceEnd = sourceEnd != null ? sourceEnd : this.length;
271    const len = Math.min(sourceEnd - sourceStart, target.length - targetStart);
272    target.set(this.subarray(sourceStart, sourceStart + len), targetStart);
273    return len;
274  }
275
276  indexOf(value, byteOffset, encoding) {
277    let offset = normalizeSearchOffset(this.length, byteOffset);
278    let searchEncoding = encoding;
279    if (typeof byteOffset === 'string') {
280      offset = 0;
281      searchEncoding = byteOffset;
282    }
283    if (typeof value === 'number') {
284      for (let i = offset; i < this.length; i++) {
285        if (this[i] === (value & 0xFF)) return i;
286      }
287      return -1;
288    }
289    if (typeof value === 'string') {
290      value = Buffer.from(value, searchEncoding);
291    }
292    if (value instanceof Uint8Array) {
293      if (value.length === 0) return offset;
294      outer: for (let i = offset; i <= this.length - value.length; i++) {
295        for (let j = 0; j < value.length; j++) {
296          if (this[i + j] !== value[j]) continue outer;
297        }
298        return i;
299      }
300    }
301    return -1;
302  }
303
304  includes(value, byteOffset, encoding) {
305    return this.indexOf(value, byteOffset, encoding) !== -1;
306  }
307
308  fill(value, offset, end, encoding) {
309    offset = offset || 0;
310    end = end != null ? end : this.length;
311    if (typeof value === 'number') {
312      for (let i = offset; i < end; i++) this[i] = value & 0xFF;
313    } else if (typeof value === 'string') {
314      const bytes = encodeString(value, encoding);
315      if (bytes.length === 0) return this;
316      for (let i = offset; i < end; i++) {
317        this[i] = bytes[(i - offset) % bytes.length];
318      }
319    } else if (value instanceof Uint8Array) {
320      if (value.length === 0) return this;
321      for (let i = offset; i < end; i++) {
322        this[i] = value[(i - offset) % value.length];
323      }
324    }
325    return this;
326  }
327
328  slice(start, end) {
329    return this.subarray(start, end);
330  }
331
332  subarray(start, end) {
333    // Override to return a Buffer, not a plain Uint8Array
334    const sub = super.subarray(start, end);
335    Object.setPrototypeOf(sub, Buffer.prototype);
336    return sub;
337  }
338
339  swap16() {
340    if (this.length % 2 !== 0) throw new RangeError('Buffer size must be a multiple of 16-bits');
341    for (let i = 0; i < this.length; i += 2) {
342      const t = this[i]; this[i] = this[i + 1]; this[i + 1] = t;
343    }
344    return this;
345  }
346
347  swap32() {
348    if (this.length % 4 !== 0) throw new RangeError('Buffer size must be a multiple of 32-bits');
349    for (let i = 0; i < this.length; i += 4) {
350      const t0 = this[i], t1 = this[i + 1];
351      this[i] = this[i + 3]; this[i + 1] = this[i + 2];
352      this[i + 2] = t1; this[i + 3] = t0;
353    }
354    return this;
355  }
356
357  // Read/write integers (LE/BE) — commonly used by extensions
358  readUInt8(offset) {
359    offset = offset >>> 0;
360    if (offset >= this.length) throw new RangeError('Index out of range');
361    return this[offset];
362  }
363
364  readUInt16LE(offset) {
365    offset = offset >>> 0;
366    if (offset + 2 > this.length) throw new RangeError('Index out of range');
367    return this[offset] | (this[offset + 1] << 8);
368  }
369
370  readUInt16BE(offset) {
371    offset = offset >>> 0;
372    if (offset + 2 > this.length) throw new RangeError('Index out of range');
373    return (this[offset] << 8) | this[offset + 1];
374  }
375
376  readUInt32LE(offset) {
377    offset = offset >>> 0;
378    if (offset + 4 > this.length) throw new RangeError('Index out of range');
379    return (this[offset] | (this[offset+1] << 8) | (this[offset+2] << 16)) + (this[offset+3] * 0x1000000);
380  }
381
382  readUInt32BE(offset) {
383    offset = offset >>> 0;
384    if (offset + 4 > this.length) throw new RangeError('Index out of range');
385    return (this[offset] * 0x1000000) + ((this[offset+1] << 16) | (this[offset+2] << 8) | this[offset+3]);
386  }
387
388  readInt8(offset) {
389    offset = offset >>> 0;
390    if (offset >= this.length) throw new RangeError('Index out of range');
391    const v = this[offset];
392    return v > 127 ? v - 256 : v;
393  }
394
395  readInt16LE(offset) {
396    offset = offset >>> 0;
397    if (offset + 2 > this.length) throw new RangeError('Index out of range');
398    const v = this[offset] | (this[offset + 1] << 8);
399    return v > 32767 ? v - 65536 : v;
400  }
401
402  readInt16BE(offset) {
403    offset = offset >>> 0;
404    if (offset + 2 > this.length) throw new RangeError('Index out of range');
405    const v = (this[offset] << 8) | this[offset + 1];
406    return v > 32767 ? v - 65536 : v;
407  }
408
409  readInt32LE(offset) {
410    offset = offset >>> 0;
411    if (offset + 4 > this.length) throw new RangeError('Index out of range');
412    return this[offset] | (this[offset+1] << 8) | (this[offset+2] << 16) | (this[offset+3] << 24);
413  }
414
415  readInt32BE(offset) {
416    offset = offset >>> 0;
417    if (offset + 4 > this.length) throw new RangeError('Index out of range');
418    return (this[offset] << 24) | (this[offset+1] << 16) | (this[offset+2] << 8) | this[offset+3];
419  }
420
421  writeUInt8(value, offset) {
422    offset = offset >>> 0;
423    if (offset >= this.length) throw new RangeError('Index out of range');
424    this[offset] = value & 0xFF;
425    return offset + 1;
426  }
427
428  writeInt8(value, offset) {
429    return this.writeUInt8(value, offset);
430  }
431
432  writeUInt16LE(value, offset) {
433    offset = offset >>> 0;
434    if (offset + 2 > this.length) throw new RangeError('Index out of range');
435    this[offset] = value & 0xFF;
436    this[offset + 1] = (value >>> 8) & 0xFF;
437    return offset + 2;
438  }
439
440  writeInt16LE(value, offset) {
441    return this.writeUInt16LE(value, offset);
442  }
443
444  writeUInt16BE(value, offset) {
445    offset = offset >>> 0;
446    if (offset + 2 > this.length) throw new RangeError('Index out of range');
447    this[offset] = (value >>> 8) & 0xFF;
448    this[offset + 1] = value & 0xFF;
449    return offset + 2;
450  }
451
452  writeInt16BE(value, offset) {
453    return this.writeUInt16BE(value, offset);
454  }
455
456  writeUInt32LE(value, offset) {
457    offset = offset >>> 0;
458    if (offset + 4 > this.length) throw new RangeError('Index out of range');
459    this[offset] = value & 0xFF;
460    this[offset+1] = (value >>> 8) & 0xFF;
461    this[offset+2] = (value >>> 16) & 0xFF;
462    this[offset+3] = (value >>> 24) & 0xFF;
463    return offset + 4;
464  }
465
466  writeInt32LE(value, offset) {
467    return this.writeUInt32LE(value, offset);
468  }
469
470  writeUInt32BE(value, offset) {
471    offset = offset >>> 0;
472    if (offset + 4 > this.length) throw new RangeError('Index out of range');
473    this[offset] = (value >>> 24) & 0xFF;
474    this[offset+1] = (value >>> 16) & 0xFF;
475    this[offset+2] = (value >>> 8) & 0xFF;
476    this[offset+3] = value & 0xFF;
477    return offset + 4;
478  }
479
480  writeInt32BE(value, offset) {
481    return this.writeUInt32BE(value, offset);
482  }
483}
484
485// Make Buffer available globally (Node.js compatibility)
486globalThis.Buffer = Buffer;
487
488export { Buffer };
489export default { Buffer };
490"#;