1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-21 15:04:11 -05:00

fix(node/fs): Use correct offset and length in node:fs.read and write (#25049)

My fix in #25030 was buggy, I forgot to pass the `byteOffset` and
`byteLength`. Whoops.

I also discovered that fs.read was not respecting the `offset` argument,
and we were constructing a new `Buffer` for the callback instead of just
passing the original one (which is what node does, and the @types/node
definitions also indicate the callback should get the same type).
Fixes #25028.
This commit is contained in:
Nathan Whitaker 2024-08-16 09:48:57 -07:00 committed by GitHub
parent 4d1b263e91
commit ff4226a3cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 160 additions and 68 deletions

View file

@ -28,7 +28,7 @@ type readSyncOptions = {
type BinaryCallback = ( type BinaryCallback = (
err: Error | null, err: Error | null,
bytesRead: number | null, bytesRead: number | null,
data?: Buffer, data?: ArrayBufferView,
) => void; ) => void;
type Callback = BinaryCallback; type Callback = BinaryCallback;
@ -56,7 +56,7 @@ export function read(
) { ) {
let cb: Callback | undefined; let cb: Callback | undefined;
let offset = 0, let offset = 0,
buffer: Buffer | Uint8Array; buffer: ArrayBufferView;
if (typeof fd !== "number") { if (typeof fd !== "number") {
throw new ERR_INVALID_ARG_TYPE("fd", "number", fd); throw new ERR_INVALID_ARG_TYPE("fd", "number", fd);
@ -79,7 +79,7 @@ export function read(
if ( if (
isArrayBufferView(optOrBufferOrCb) isArrayBufferView(optOrBufferOrCb)
) { ) {
buffer = arrayBufferViewToUint8Array(optOrBufferOrCb); buffer = optOrBufferOrCb;
} else if (typeof optOrBufferOrCb === "function") { } else if (typeof optOrBufferOrCb === "function") {
offset = 0; offset = 0;
buffer = Buffer.alloc(16384); buffer = Buffer.alloc(16384);
@ -99,10 +99,10 @@ export function read(
if (opt.buffer === undefined) { if (opt.buffer === undefined) {
buffer = Buffer.alloc(16384); buffer = Buffer.alloc(16384);
} else { } else {
buffer = arrayBufferViewToUint8Array(opt.buffer); buffer = opt.buffer;
} }
offset = opt.offset ?? 0; offset = opt.offset ?? 0;
length = opt.length ?? buffer.byteLength; length = opt.length ?? buffer.byteLength - offset;
position = opt.position ?? null; position = opt.position ?? null;
} }
@ -123,12 +123,18 @@ export function read(
// We use sync calls below to avoid being affected by others during // We use sync calls below to avoid being affected by others during
// these calls. // these calls.
fs.seekSync(fd, position, io.SeekMode.Start); fs.seekSync(fd, position, io.SeekMode.Start);
nread = io.readSync(fd, buffer); nread = io.readSync(
fd,
arrayBufferViewToUint8Array(buffer).subarray(offset, offset + length),
);
fs.seekSync(fd, currentPosition, io.SeekMode.Start); fs.seekSync(fd, currentPosition, io.SeekMode.Start);
} else { } else {
nread = await io.read(fd, buffer); nread = await io.read(
fd,
arrayBufferViewToUint8Array(buffer).subarray(offset, offset + length),
);
} }
cb(null, nread ?? 0, Buffer.from(buffer.buffer, offset, length)); cb(null, nread ?? 0, buffer);
} catch (error) { } catch (error) {
cb(error as Error, null); cb(error as Error, null);
} }
@ -162,8 +168,6 @@ export function readSync(
validateBuffer(buffer); validateBuffer(buffer);
buffer = arrayBufferViewToUint8Array(buffer);
if (length == null) { if (length == null) {
length = 0; length = 0;
} }
@ -174,7 +178,7 @@ export function readSync(
} else if (offsetOrOpt !== undefined) { } else if (offsetOrOpt !== undefined) {
const opt = offsetOrOpt as readSyncOptions; const opt = offsetOrOpt as readSyncOptions;
offset = opt.offset ?? 0; offset = opt.offset ?? 0;
length = opt.length ?? buffer.byteLength; length = opt.length ?? buffer.byteLength - offset;
position = opt.position ?? null; position = opt.position ?? null;
} }
@ -191,7 +195,10 @@ export function readSync(
fs.seekSync(fd, position, io.SeekMode.Start); fs.seekSync(fd, position, io.SeekMode.Start);
} }
const numberOfBytesRead = io.readSync(fd, buffer); const numberOfBytesRead = io.readSync(
fd,
arrayBufferViewToUint8Array(buffer).subarray(offset, offset + length),
);
if (typeof position === "number" && position >= 0) { if (typeof position === "number" && position >= 0) {
fs.seekSync(fd, currentPosition, io.SeekMode.Start); fs.seekSync(fd, currentPosition, io.SeekMode.Start);

View file

@ -990,7 +990,11 @@ export const validatePosition = hideStackFrames((position) => {
export const arrayBufferViewToUint8Array = hideStackFrames( export const arrayBufferViewToUint8Array = hideStackFrames(
(buffer) => { (buffer) => {
if (!(buffer instanceof Uint8Array)) { if (!(buffer instanceof Uint8Array)) {
return new Uint8Array(buffer.buffer); return new Uint8Array(
buffer.buffer,
buffer.byteOffset,
buffer.byteLength,
);
} }
return buffer; return buffer;
}, },

View file

@ -38,11 +38,15 @@ Deno.test("read specify opt", async function () {
buffer: new Buffer(byteLength), buffer: new Buffer(byteLength),
offset: 6, offset: 6,
length: 5, length: 5,
position: 6,
}; };
let res = await fileHandle.read(opt); let res = await fileHandle.read(opt);
assertEquals(res.bytesRead, byteLength); assertEquals(res.bytesRead, 5);
assertEquals(new TextDecoder().decode(res.buffer as Uint8Array), "world"); assertEquals(
new TextDecoder().decode(res.buffer.subarray(6) as Uint8Array),
"world",
);
const opt2 = { const opt2 = {
buffer: new Buffer(byteLength), buffer: new Buffer(byteLength),
@ -51,8 +55,11 @@ Deno.test("read specify opt", async function () {
}; };
res = await fileHandle.read(opt2); res = await fileHandle.read(opt2);
assertEquals(res.bytesRead, byteLength); assertEquals(res.bytesRead, 5);
assertEquals(decoder.decode(res.buffer as Uint8Array), "hello"); assertEquals(
decoder.decode(res.buffer.subarray(0, 5) as Uint8Array),
"hello",
);
await fileHandle.close(); await fileHandle.close();
}); });

View file

@ -1,6 +1,7 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
/// <reference types="npm:@types/node" /> /// <reference types="npm:@types/node" />
import { import {
assert,
assertEquals, assertEquals,
assertFalse, assertFalse,
assertMatch, assertMatch,
@ -12,23 +13,23 @@ import { Buffer } from "node:buffer";
import * as path from "@std/path"; import * as path from "@std/path";
import { closeSync } from "node:fs"; import { closeSync } from "node:fs";
async function readTest( async function readTest<T extends NodeJS.ArrayBufferView>(
testData: string, testData: string,
buffer: NodeJS.ArrayBufferView, buffer: T,
offset: number, offset: number,
length: number, length: number,
position: number | null = null, position: number | null = null,
expected: ( expected: (
fd: number, fd: number,
bytesRead: number | null, bytesRead: number | null,
data: ArrayBufferView | undefined, data: T | undefined,
) => void, ) => void,
) { ) {
let fd1 = 0; let fd1 = 0;
await new Promise<{ await new Promise<{
fd: number; fd: number;
bytesRead: number | null; bytesRead: number | null;
data: ArrayBufferView | undefined; data: T | undefined;
}>((resolve, reject) => { }>((resolve, reject) => {
open(testData, "r", (err, fd) => { open(testData, "r", (err, fd) => {
if (err) reject(err); if (err) reject(err);
@ -323,33 +324,82 @@ Deno.test({
}); });
Deno.test({ Deno.test({
name: "accepts non Uint8Array buffer", name: "read with offset TypedArray buffers",
async fn() { async fn() {
const moduleDir = path.dirname(path.fromFileUrl(import.meta.url)); const moduleDir = path.dirname(path.fromFileUrl(import.meta.url));
const testData = path.resolve(moduleDir, "testdata", "hello.txt"); const testData = path.resolve(moduleDir, "testdata", "hello.txt");
const buffer = new ArrayBuffer(1024); const buffer = new ArrayBuffer(1024);
const buf = new Int8Array(buffer);
const bufConstructors = [
Int8Array,
Uint8Array,
];
const offsets = [0, 24, 48];
const resetBuffer = () => {
new Uint8Array(buffer).fill(0);
};
const decoder = new TextDecoder();
for (const constr of bufConstructors) {
// test combinations of buffers internally offset from their backing array buffer,
// and also offset in the read call
for (const innerOffset of offsets) {
for (const offset of offsets) {
// test read
resetBuffer();
const buf = new constr(
buffer,
innerOffset,
);
await readTest( await readTest(
testData, testData,
buf, buf,
buf.byteOffset, offset,
buf.byteLength, buf.byteLength - offset,
null, null,
(_fd, bytesRead, data) => { (_fd, bytesRead, data) => {
assert(data);
assert(bytesRead);
assertStrictEquals(bytesRead, 11); assertStrictEquals(bytesRead, 11);
assertEquals(data instanceof Buffer, true); assertEquals(data == buf, true);
assertMatch((data as Buffer).toString(), /hello world/); const got = decoder.decode(
data.subarray(
offset,
offset + bytesRead,
),
);
const want = "hello world";
assertEquals(got.length, want.length);
assertEquals(
got,
want,
);
}, },
); );
// test readSync
resetBuffer();
const fd = openSync(testData, "r"); const fd = openSync(testData, "r");
try { try {
const nRead = readSync(fd, buf); const bytesRead = readSync(
const expected = new TextEncoder().encode("hello world"); fd,
buf,
offset,
buf.byteLength - offset,
null,
);
assertEquals(buf.slice(0, nRead), new Int8Array(expected.buffer)); assertStrictEquals(bytesRead, 11);
assertEquals(
decoder.decode(buf.subarray(offset, offset + bytesRead)),
"hello world",
);
} finally { } finally {
closeSync(fd); closeSync(fd);
} }
}
}
}
}, },
}); });

View file

@ -75,7 +75,7 @@ Deno.test({
}); });
Deno.test({ Deno.test({
name: "Accepts non Uint8Array buffer", name: "write with offset TypedArray buffers",
async fn() { async fn() {
const tempFile: string = Deno.makeTempFileSync(); const tempFile: string = Deno.makeTempFileSync();
using file = Deno.openSync(tempFile, { using file = Deno.openSync(tempFile, {
@ -83,24 +83,44 @@ Deno.test({
write: true, write: true,
read: true, read: true,
}); });
const arrayBuffer = new ArrayBuffer(128);
const resetBuffer = () => {
new Uint8Array(arrayBuffer).fill(0);
};
const bufConstructors = [
Int8Array,
Uint8Array,
];
const offsets = [0, 24, 48];
const bytes = [0, 1, 2, 3, 4]; const bytes = [0, 1, 2, 3, 4];
const buffer = new Int8Array(bytes); for (const constr of bufConstructors) {
for (let i = 0; i < buffer.length; i++) { // test combinations of buffers internally offset from their backing array buffer,
buffer[i] = i; // and also offset in the write call
for (const innerOffset of offsets) {
for (const offset of offsets) {
resetBuffer();
const buffer = new constr(
arrayBuffer,
innerOffset,
offset + bytes.length,
);
for (let i = 0; i < bytes.length; i++) {
buffer[offset + i] = i;
} }
let nWritten = writeSync(file.rid, buffer); let nWritten = writeSync(file.rid, buffer, offset, bytes.length, 0);
const data = Deno.readFileSync(tempFile); let data = Deno.readFileSync(tempFile);
assertEquals(nWritten, 5); assertEquals(nWritten, bytes.length);
console.log(constr, innerOffset, offset);
assertEquals(data, new Uint8Array(bytes)); assertEquals(data, new Uint8Array(bytes));
nWritten = await new Promise((resolve, reject) => nWritten = await new Promise((resolve, reject) =>
write( write(
file.rid, file.rid,
buffer, buffer,
offset,
bytes.length,
0, 0,
5,
(err: unknown, nwritten: number) => { (err: unknown, nwritten: number) => {
if (err) return reject(err); if (err) return reject(err);
resolve(nwritten); resolve(nwritten);
@ -108,7 +128,11 @@ Deno.test({
) )
); );
data = Deno.readFileSync(tempFile);
assertEquals(nWritten, 5); assertEquals(nWritten, 5);
assertEquals(data, new Uint8Array(bytes)); assertEquals(data, new Uint8Array(bytes));
}
}
}
}, },
}); });