Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/jsc/bindings/JSBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1632,7 +1632,7 @@ static int64_t indexOf(JSC::JSGlobalObject* lexicalGlobalObject, ThrowScope& sco
if (byteOffsetValue.isString()) {
encodingValue = byteOffsetValue;
byteOffsetValue = jsUndefined();
byteOffsetD = 0;
byteOffsetD = std::numeric_limits<double>::quiet_NaN();
} else {
// toNumber() can trigger JavaScript execution (valueOf/Symbol.toPrimitive),
// which could detach the underlying ArrayBuffer. We must re-fetch the
Expand Down
104 changes: 104 additions & 0 deletions test/js/node/buffer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2256,6 +2256,110 @@ for (let withOverridenBufferWrite of [false, true]) {
expect(b.lastIndexOf("b", [])).toBe(-1);
});

it("lastIndexOf(value, encoding) defaults to searching from the end", () => {
// When the second argument is an encoding string (no byteOffset), the
// search must start from the end of the buffer, matching Node.js.
const b = Buffer.from("ello hello hello");
expect(b.lastIndexOf("hello", "utf8")).toBe(11);
expect(b.lastIndexOf("hello", "latin1")).toBe(11);
expect(b.lastIndexOf("hello", "binary")).toBe(11);

const b16 = Buffer.from("ello hello hello", "utf16le");
expect(b16.lastIndexOf("hello", "utf16le")).toBe(22);
expect(b16.lastIndexOf("hello", "ucs2")).toBe(22);

const bhex = Buffer.from("aabbccaabbcc", "hex");
expect(bhex.lastIndexOf("aabb", "hex")).toBe(3);

const bb64 = Buffer.from("Zm9vYmFyZm9v", "base64");
expect(bb64.lastIndexOf("Zm9v", "base64")).toBe(6);

// Forward indexOf with the same overload must remain 0-based.
expect(b.indexOf("hello", "utf8")).toBe(5);
expect(b.includes("hello", "utf8")).toBe(true);

// Explicit byteOffset still works.
expect(b.lastIndexOf("hello", 8, "utf8")).toBe(5);
});

it("indexOf(value, encoding) is unchanged by the lastIndexOf fix", () => {
// The lastIndexOf fix routes the encoding-as-2nd-arg case through the
// direction-aware default. For forward indexOf/includes that default is
// 0, so these must keep returning the FIRST match from offset 0. The
// buffer repeats "hello" so a regression that searched from the end
// would return 12 instead of 0.
const b = Buffer.from("hello world hello");
expect(b.indexOf("hello", "utf8")).toBe(0);
expect(b.indexOf("hello", "latin1")).toBe(0);
expect(b.indexOf("hello", "binary")).toBe(0);
expect(b.indexOf("hello", "ascii")).toBe(0);
expect(b.includes("hello", "utf8")).toBe(true);
expect(b.indexOf("zzz", "utf8")).toBe(-1);
expect(b.indexOf("o", "utf8")).toBe(4);

const b16 = Buffer.from("hello world hello", "utf16le");
expect(b16.indexOf("hello", "utf16le")).toBe(0);
expect(b16.indexOf("hello", "ucs2")).toBe(0);
expect(b16.includes("hello", "utf16le")).toBe(true);

const bhex = Buffer.from("aabbccaabbcc", "hex");
expect(bhex.indexOf("aabb", "hex")).toBe(0);
expect(bhex.includes("aabb", "hex")).toBe(true);

const bb64 = Buffer.from("Zm9vYmFyZm9v", "base64");
expect(bb64.indexOf("Zm9v", "base64")).toBe(0);
expect(bb64.includes("Zm9v", "base64")).toBe(true);

// A 3-arg indexOf(value, byteOffset, encoding) still skips forward.
expect(b.indexOf("hello", 1, "utf8")).toBe(12);

// A non-string 2nd arg is a byteOffset, not an encoding: these go
// through the other branch and must also be unchanged.
const abc = Buffer.from("abcdef");
expect(abc.indexOf("b", undefined)).toBe(1);
expect(abc.indexOf("b", null)).toBe(1);
expect(abc.indexOf("b", {})).toBe(1);
expect(abc.indexOf("b", [])).toBe(1);
});

it("indexOf/lastIndexOf with an explicit byteOffset are unchanged by the fix", () => {
// When a numeric byteOffset is supplied (with or without a trailing
// encoding), both methods take the non-string branch that the fix does
// NOT touch. The needle repeats ("hello" at 0 and 12, "o" at 4/7/16) so
// the offset genuinely selects which occurrence is returned.
const b = Buffer.from("hello world hello");

// indexOf(value, byteOffset): searches forward from the offset.
expect(b.indexOf("hello", 0)).toBe(0);
expect(b.indexOf("hello", 1)).toBe(12);
expect(b.indexOf("hello", 12)).toBe(12);
expect(b.indexOf("hello", 13)).toBe(-1);
expect(b.indexOf("hello", -5)).toBe(12); // negative counts from the end
expect(b.indexOf("o", 5)).toBe(7);
expect(b.indexOf("o", 8)).toBe(16);

// indexOf(value, byteOffset, encoding): same, with an explicit encoding.
expect(b.indexOf("hello", 1, "utf8")).toBe(12);
expect(b.indexOf("hello", 0, "latin1")).toBe(0);

// lastIndexOf(value, byteOffset): searches backward from the offset.
expect(b.lastIndexOf("hello", -1)).toBe(12);
expect(b.lastIndexOf("hello", 11)).toBe(0);
expect(b.lastIndexOf("hello", 12)).toBe(12);
expect(b.lastIndexOf("hello", 0)).toBe(0);
expect(b.lastIndexOf("o", 6)).toBe(4);
expect(b.lastIndexOf("o", 100)).toBe(16); // past the end is clamped

// lastIndexOf(value, byteOffset, encoding): same, with an encoding.
expect(b.lastIndexOf("hello", 11, "utf8")).toBe(0);
expect(b.lastIndexOf("hello", 16, "utf8")).toBe(12);

// utf16le: the byteOffset is in bytes ("hello" starts at byte 0 and 24).
const b16 = Buffer.from("hello world hello", "utf16le");
expect(b16.indexOf("hello", 2, "ucs2")).toBe(24);
expect(b16.lastIndexOf("hello", 22, "ucs2")).toBe(0);
});

for (let fn of [Buffer.prototype.slice, Buffer.prototype.subarray]) {
it(`Buffer.${fn.name}`, () => {
const buf = new Buffer("buffer");
Expand Down
Loading