If a Buffer view and a direct file read produce different hashes, compare their byte lengths before changing the hash algorithm. subarray(start, end) takes an exclusive end index. readSync(fd, buffer, offset, length, position) takes a requested length, a destination offset, and a file position. Those numbers describe different things.
For a range starting at byte 10 with a length of 20, use whole.subarray(10, 30). Using whole.subarray(10, 20) selects only 10 bytes. A hash comparison cannot repair that range mismatch.
Map the arguments to one range
Use names that preserve the distinction:
const start = 10;
const length = 20;
const end = start + length;
const expected = whole.subarray(start, end);The corresponding read places up to 20 bytes into the destination beginning at destination offset zero, reading from file position 10:
const destination = Buffer.alloc(length);
const bytesRead = readSync(fd, destination, 0, length, start);
const actual = destination.subarray(0, bytesRead);The fourth argument requests bytes; it does not promise that all requested bytes were read. Hash actual, whose length comes from the result, rather than the entire allocation. Buffer.alloc() initializes unused bytes to zero, but those zeros are still not bytes read from the file.
Run a complete comparison on a disposable file
Save this as range-check.cjs and run node range-check.cjs. It creates and removes its own temporary directory and never opens an existing application file.
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const assert = require('node:assert/strict');
const { createHash } = require('node:crypto');
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'buffer-range-'));
const file = path.join(dir, 'sample.bin');
const hash = b => createHash('sha256').update(b).digest('hex');
let fd;
try {
fs.writeFileSync(file, Buffer.from(Array.from({ length: 64 }, (_, i) => i)));
const whole = fs.readFileSync(file);
fd = fs.openSync(file, 'r');
for (const [start, length] of [[10, 20], [60, 20], [64, 20]]) {
const destination = Buffer.alloc(length);
const bytesRead = fs.readSync(fd, destination, 0, length, start);
const actual = destination.subarray(0, bytesRead);
const expected = whole.subarray(start, start + length);
assert.ok(actual.equals(expected));
assert.equal(hash(actual), hash(expected));
console.log(JSON.stringify({ start, requested: length, bytesRead }));
}
} finally {
if (fd !== undefined) fs.closeSync(fd);
fs.rmSync(dir, { recursive: true, force: true });
}For this small, unchanged local fixture, the observed counts are 20, 4, and 0. The second request reaches EOF after four bytes; the third starts at EOF. Neither case justifies hashing a 20-byte destination in full. This example compares available bytes, rather than requiring the file to contain the entire requested range.
Choose an available-bytes or exact-length contract
If your operation requires exactly 20 bytes, treat a short result as incomplete. A general exact-length reader should loop, advancing both destination offset and explicit file position by the number actually read, until it fills the destination or receives zero at EOF. If EOF arrives before the required length, reject the incomplete range according to your application contract. Do not pad the missing input and claim it came from the file.
Validate application inputs before calling these APIs. Negative indexes have meaning in subarray; they are not a universal file-range notation. For an application accepting byte ranges, require nonnegative safe integers for start and length and ensure their sum remains a safe integer. This example does not implement an untrusted-input interface or a large-file streaming reader.
Keep the compared bytes stable
subarray creates a view sharing the original memory. It does not create an independent snapshot:
const original = Buffer.from([1, 2, 3]);
const view = original.subarray(1, 3);
const snapshot = Buffer.from(view);
original[1] = 9;
console.log(view[0]); // 9
console.log(snapshot[0]); // 2Copy with Buffer.from(view) when later mutations must not change the selected bytes. An older example may use Buffer.slice; the Buffer method also shares memory, so changing the method name alone does not create a copy. This differs from the copying behavior people may expect from an ordinary array's slice method.
A copy stabilizes memory after the copy. It does not make two separate filesystem reads an atomic snapshot. If another process changes the file between readFileSync and readSync, equal coordinates can refer to different contents. Reproduce with an unchanged fixture first. For a real mutable input, define how the application obtains a consistent source, rather than interpreting every mismatch as a Buffer indexing bug.
Check lengths, bytes, then hashes
Log the start, requested length, exclusive end, and returned count. Compare Buffer lengths and equals() on a known fixture before comparing digests. Ensure both hashes consume raw Buffers with the same algorithm; decoding binary data to text creates another transformation to investigate. Keep explicit file positions when comparing coordinates so that a previous read does not silently move the comparison.