diff --git a/ext/node/polyfills/_brotli.js b/ext/node/polyfills/_brotli.js index 1524bf85cde152..ce5db323027c02 100644 --- a/ext/node/polyfills/_brotli.js +++ b/ext/node/polyfills/_brotli.js @@ -9,7 +9,10 @@ const { TypedArrayPrototypeSlice, TypedArrayPrototypeSubarray, TypedArrayPrototypeGetByteLength, + TypedArrayPrototypeGetByteOffset, DataViewPrototypeGetBuffer, + DataViewPrototypeGetByteLength, + DataViewPrototypeGetByteOffset, TypedArrayPrototypeGetBuffer, } = primordials; const { isTypedArray, isDataView, close } = core; @@ -38,9 +41,17 @@ const toU8 = (input) => { } if (isTypedArray(input)) { - return new Uint8Array(TypedArrayPrototypeGetBuffer(input)); + return new Uint8Array( + TypedArrayPrototypeGetBuffer(input), + TypedArrayPrototypeGetByteOffset(input), + TypedArrayPrototypeGetByteLength(input), + ); } else if (isDataView(input)) { - return new Uint8Array(DataViewPrototypeGetBuffer(input)); + return new Uint8Array( + DataViewPrototypeGetBuffer(input), + DataViewPrototypeGetByteOffset(input), + DataViewPrototypeGetByteLength(input), + ); } return input; diff --git a/ext/node/polyfills/_http_outgoing.ts b/ext/node/polyfills/_http_outgoing.ts index a6edc1144f09f8..41a1d844d70ba8 100644 --- a/ext/node/polyfills/_http_outgoing.ts +++ b/ext/node/polyfills/_http_outgoing.ts @@ -542,7 +542,7 @@ export class OutgoingMessage extends Stream { if (data instanceof Buffer) { data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength); } - if (data.buffer.byteLength > 0) { + if (data.byteLength > 0) { this._bodyWriter.write(data).then(() => { callback?.(); this.emit("drain"); diff --git a/ext/node/polyfills/internal/buffer.mjs b/ext/node/polyfills/internal/buffer.mjs index c3249455583f34..7c3f3ade2f425b 100644 --- a/ext/node/polyfills/internal/buffer.mjs +++ b/ext/node/polyfills/internal/buffer.mjs @@ -19,6 +19,7 @@ import { asciiToBytes, base64ToBytes, base64UrlToBytes, + base64Write, bytesToAscii, bytesToUtf16le, hexToBytes, @@ -42,6 +43,15 @@ import { Blob } from "ext:deno_web/09_file.js"; export { atob, Blob, btoa }; +class FastBuffer extends Uint8Array { + // Using an explicit constructor here is necessary to avoid relying on + // `Array.prototype[Symbol.iterator]`, which can be mutated by users. + // eslint-disable-next-line no-useless-constructor + constructor(bufferOrLength, byteOffset, length) { + super(bufferOrLength, byteOffset, length); + } +} + const utf8Encoder = new TextEncoder(); // Temporary buffers to convert numbers. @@ -72,6 +82,9 @@ export const constants = { MAX_STRING_LENGTH: kStringMaxLength, }; +FastBuffer.prototype.constructor = Buffer; +Buffer.prototype = FastBuffer.prototype; + Object.defineProperty(Buffer.prototype, "parent", { enumerable: true, get: function () { @@ -98,9 +111,7 @@ function createBuffer(length) { 'The value "' + length + '" is invalid for option "size"', ); } - const buf = new Uint8Array(length); - Object.setPrototypeOf(buf, Buffer.prototype); - return buf; + return new FastBuffer(length); } export function Buffer(arg, encodingOrOffset, length) { @@ -117,7 +128,32 @@ export function Buffer(arg, encodingOrOffset, length) { return _from(arg, encodingOrOffset, length); } -Buffer.poolSize = 8192; +Object.defineProperty(Buffer, Symbol.species, { + __proto__: null, + enumerable: false, + configurable: true, + get() { + return FastBuffer; + }, +}); + +Buffer.poolSize = 8 * 1024; +let poolSize, poolOffset, allocPool; + +function createPool() { + poolSize = Buffer.poolSize; + allocPool = new Uint8Array(poolSize).buffer; + poolOffset = 0; +} +createPool(); + +function alignPool() { + // Ensure aligned slices + if (poolOffset & 0x7) { + poolOffset |= 0x7; + poolOffset++; + } +} function _from(value, encodingOrOffset, length) { if (typeof value === "string") { @@ -204,6 +240,28 @@ Buffer.allocUnsafeSlow = function allocUnsafeSlow(size) { return _allocUnsafe(size); }; +function fromStringFast(string, ops) { + const length = ops.byteLength(string); + if (length >= (Buffer.poolSize >>> 1)) { + const data = ops.create(string); + Object.setPrototypeOf(data, Buffer.prototype); + return data; + } + + if (length > (poolSize - poolOffset)) { + createPool(); + } + let b = new FastBuffer(allocPool, poolOffset, length); + const actual = ops.write(b, string, 0, length); + if (actual != length) { + // byteLength() may overestimate. That's a rare case, though. + b = new FastBuffer(allocPool, poolOffset, actual); + } + poolOffset += actual; + alignPool(); + return b; +} + function fromString(string, encoding) { if (typeof encoding !== "string" || encoding === "") { encoding = "utf8"; @@ -211,19 +269,15 @@ function fromString(string, encoding) { if (!Buffer.isEncoding(encoding)) { throw new codes.ERR_UNKNOWN_ENCODING(encoding); } - const length = byteLength(string, encoding) | 0; - let buf = createBuffer(length); - const actual = buf.write(string, encoding); - if (actual !== length) { - buf = buf.slice(0, actual); + const ops = getEncodingOps(encoding); + if (ops === undefined) { + throw new codes.ERR_UNKNOWN_ENCODING(encoding); } - return buf; + return fromStringFast(string, ops); } function fromArrayLike(obj) { - const buf = new Uint8Array(obj); - Object.setPrototypeOf(buf, Buffer.prototype); - return buf; + return new FastBuffer(obj); } function fromObject(obj) { @@ -260,7 +314,7 @@ Object.setPrototypeOf(SlowBuffer.prototype, Uint8Array.prototype); Object.setPrototypeOf(SlowBuffer, Uint8Array); Buffer.isBuffer = function isBuffer(b) { - return b != null && b._isBuffer === true && b !== Buffer.prototype; + return b instanceof Buffer; }; Buffer.compare = function compare(a, b) { @@ -664,12 +718,12 @@ Buffer.prototype.base64Slice = function base64Slice( } }; -Buffer.prototype.base64Write = function base64Write( +Buffer.prototype.base64Write = function base64Write_( string, offset, length, ) { - return blitBuffer(base64ToBytes(string), this, offset, length); + return base64Write(string, this, offset, length); }; Buffer.prototype.base64urlSlice = function base64urlSlice( @@ -737,8 +791,8 @@ Buffer.prototype.ucs2Write = function ucs2Write(string, offset, length) { ); }; -Buffer.prototype.utf8Slice = function utf8Slice(string, offset, length) { - return _utf8Slice(this, string, offset, length); +Buffer.prototype.utf8Slice = function utf8Slice(offset, length) { + return _utf8Slice(this, offset, length); }; Buffer.prototype.utf8Write = function utf8Write(string, offset, length) { @@ -831,9 +885,7 @@ function fromArrayBuffer(obj, byteOffset, length) { } } - const buffer = new Uint8Array(obj, byteOffset, length); - Object.setPrototypeOf(buffer, Buffer.prototype); - return buffer; + return new FastBuffer(obj, byteOffset, length); } function _base64Slice(buf, start, end) { @@ -2105,6 +2157,7 @@ export const encodingOps = { dir, ), slice: (buf, start, end) => buf.asciiSlice(start, end), + create: (string) => asciiToBytes(string), write: (buf, string, offset, len) => buf.asciiWrite(string, offset, len), }, base64: { @@ -2119,6 +2172,7 @@ export const encodingOps = { encodingsMap.base64, dir, ), + create: (string) => base64ToBytes(string), slice: (buf, start, end) => buf.base64Slice(start, end), write: (buf, string, offset, len) => buf.base64Write(string, offset, len), }, @@ -2134,6 +2188,7 @@ export const encodingOps = { encodingsMap.base64url, dir, ), + create: (string) => base64UrlToBytes(string), slice: (buf, start, end) => buf.base64urlSlice(start, end), write: (buf, string, offset, len) => buf.base64urlWrite(string, offset, len), @@ -2150,6 +2205,7 @@ export const encodingOps = { encodingsMap.hex, dir, ), + create: (string) => hexToBytes(string), slice: (buf, start, end) => buf.hexSlice(start, end), write: (buf, string, offset, len) => buf.hexWrite(string, offset, len), }, @@ -2165,6 +2221,7 @@ export const encodingOps = { encodingsMap.latin1, dir, ), + create: (string) => asciiToBytes(string), slice: (buf, start, end) => buf.latin1Slice(start, end), write: (buf, string, offset, len) => buf.latin1Write(string, offset, len), }, @@ -2180,6 +2237,7 @@ export const encodingOps = { encodingsMap.utf16le, dir, ), + create: (string) => utf16leToBytes(string), slice: (buf, start, end) => buf.ucs2Slice(start, end), write: (buf, string, offset, len) => buf.ucs2Write(string, offset, len), }, @@ -2195,6 +2253,7 @@ export const encodingOps = { encodingsMap.utf8, dir, ), + create: (string) => utf8Encoder.encode(string), slice: (buf, start, end) => buf.utf8Slice(start, end), write: (buf, string, offset, len) => buf.utf8Write(string, offset, len), }, @@ -2210,6 +2269,7 @@ export const encodingOps = { encodingsMap.utf16le, dir, ), + create: (string) => utf16leToBytes(string), slice: (buf, start, end) => buf.ucs2Slice(start, end), write: (buf, string, offset, len) => buf.ucs2Write(string, offset, len), }, diff --git a/ext/node/polyfills/internal_binding/_utils.ts b/ext/node/polyfills/internal_binding/_utils.ts index 74dc3cbcd634e3..0b1e1709ddaf49 100644 --- a/ext/node/polyfills/internal_binding/_utils.ts +++ b/ext/node/polyfills/internal_binding/_utils.ts @@ -7,6 +7,7 @@ import { forgivingBase64Decode, forgivingBase64UrlDecode, } from "ext:deno_web/00_infra.js"; +import { op_base64_write } from "ext:core/ops"; export function asciiToBytes(str: string) { const length = str.length; @@ -27,6 +28,22 @@ export function base64ToBytes(str: string) { } } +export function base64Write( + str: string, + buffer: Uint8Array, + offset: number = 0, + length?: number, +): number { + length = length ?? buffer.byteLength - offset; + try { + return op_base64_write(str, buffer, offset, length); + } catch { + str = base64clean(str); + str = str.replaceAll("-", "+").replaceAll("_", "/"); + return op_base64_write(str, buffer, offset, length); + } +} + const INVALID_BASE64_RE = /[^+/0-9A-Za-z-_]/g; function base64clean(str: string) { // Node takes equal signs as end of the Base64 encoding diff --git a/ext/node/polyfills/string_decoder.ts b/ext/node/polyfills/string_decoder.ts index 4a49c2e3e671fe..19aff8124867e6 100644 --- a/ext/node/polyfills/string_decoder.ts +++ b/ext/node/polyfills/string_decoder.ts @@ -39,10 +39,15 @@ const { Symbol, MathMin, DataViewPrototypeGetBuffer, + DataViewPrototypeGetByteLength, + DataViewPrototypeGetByteOffset, ObjectPrototypeIsPrototypeOf, String, TypedArrayPrototypeGetBuffer, + TypedArrayPrototypeGetByteLength, + TypedArrayPrototypeGetByteOffset, StringPrototypeToLowerCase, + Uint8Array, } = primordials; const { isTypedArray } = core; @@ -83,11 +88,21 @@ function normalizeBuffer(buf: Buffer) { } if (isBufferType(buf)) { return buf; + } else if (isTypedArray(buf)) { + return Buffer.from( + new Uint8Array( + TypedArrayPrototypeGetBuffer(buf), + TypedArrayPrototypeGetByteOffset(buf), + TypedArrayPrototypeGetByteLength(buf), + ), + ); } else { return Buffer.from( - isTypedArray(buf) - ? TypedArrayPrototypeGetBuffer(buf) - : DataViewPrototypeGetBuffer(buf), + new Uint8Array( + DataViewPrototypeGetBuffer(buf), + DataViewPrototypeGetByteOffset(buf), + DataViewPrototypeGetByteLength(buf), + ), ); } } diff --git a/ext/web/lib.rs b/ext/web/lib.rs index 89f1197e71c9ce..c8badbf8a89e79 100644 --- a/ext/web/lib.rs +++ b/ext/web/lib.rs @@ -16,7 +16,6 @@ use deno_core::ByteString; use deno_core::OpState; use deno_core::Resource; use deno_core::ResourceId; -use deno_core::ToJsBuffer; use deno_core::U16String; use encoding_rs::CoderResult; @@ -62,6 +61,7 @@ deno_core::extension!(deno_web, parameters = [P: TimersPermission], ops = [ op_base64_decode, + op_base64_write, op_base64_encode, op_base64_atob, op_base64_btoa, @@ -130,12 +130,43 @@ deno_core::extension!(deno_web, ); #[op2] -#[serde] -fn op_base64_decode(#[string] input: String) -> Result { +#[buffer] +fn op_base64_decode(#[string] input: String) -> Result, AnyError> { let mut s = input.into_bytes(); let decoded_len = forgiving_base64_decode_inplace(&mut s)?; s.truncate(decoded_len); - Ok(s.into()) + Ok(s) +} + +#[op2(fast)] +#[smi] +fn op_base64_write( + #[string] input: String, + #[buffer] buffer: &mut [u8], + #[smi] start: u32, + #[smi] max_len: u32, +) -> Result { + let tsb_len = buffer.len() as u32; + + if start > tsb_len { + return Err(type_error("Offset is out of bounds")); + } + + let max_len = std::cmp::min(max_len, tsb_len - start) as usize; + let start = start as usize; + + if max_len == 0 { + return Ok(0); + } + + let mut s = input.into_bytes(); + let decoded_len = forgiving_base64_decode_inplace(&mut s)?; + + let max_len = std::cmp::min(max_len, decoded_len); + + buffer[start..start + max_len].copy_from_slice(&s[..max_len]); + + Ok(max_len as u32) } #[op2] diff --git a/tests/unit_node/zlib_test.ts b/tests/unit_node/zlib_test.ts index 215717dded04e2..657433f11a6bad 100644 --- a/tests/unit_node/zlib_test.ts +++ b/tests/unit_node/zlib_test.ts @@ -123,7 +123,9 @@ Deno.test( Deno.test("should work with dataview", () => { const buf = Buffer.from("hello world"); - const compressed = brotliCompressSync(new DataView(buf.buffer)); + const compressed = brotliCompressSync( + new DataView(buf.buffer, buf.byteOffset, buf.byteLength), + ); const decompressed = brotliDecompressSync(compressed); assertEquals(decompressed.toString(), "hello world"); }); @@ -141,7 +143,9 @@ Deno.test( "zlib compression with dataview", () => { const buf = Buffer.from("hello world"); - const compressed = gzipSync(new DataView(buf.buffer)); + const compressed = gzipSync( + new DataView(buf.buffer, buf.byteOffset, buf.byteLength), + ); const decompressed = unzipSync(compressed); assertEquals(decompressed.toString(), "hello world"); },