Compare commits

..

1 Commits

Author SHA1 Message Date
Claude Bot
c7273a1893 fix(ffi): CString.byteLength and CString.byteOffset are undefined when not explicitly passed
When constructing a CString with only a pointer argument, byteOffset and
byteLength were left as undefined because they were only set when
explicitly passed. Now byteOffset defaults to 0 and byteLength is
computed from the string's UTF-8 byte length when not provided.

Closes #22920

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-20 04:40:50 +00:00
5 changed files with 75 additions and 114 deletions

View File

@@ -117,19 +117,20 @@ class JSCallback {
class CString extends String {
constructor(ptr, byteOffset?, byteLength?) {
super(
ptr
? typeof byteLength === "number" && Number.isSafeInteger(byteLength)
? BunCString(ptr, byteOffset || 0, byteLength)
: BunCString(ptr, byteOffset || 0)
: "",
);
const str = ptr
? typeof byteLength === "number" && Number.isSafeInteger(byteLength)
? BunCString(ptr, byteOffset || 0, byteLength)
: BunCString(ptr, byteOffset || 0)
: "";
super(str);
this.ptr = typeof ptr === "number" ? ptr : 0;
if (typeof byteOffset !== "undefined") {
this.byteOffset = byteOffset;
}
if (typeof byteLength !== "undefined") {
this.byteOffset = typeof byteOffset === "number" ? byteOffset : 0;
if (typeof byteLength === "number") {
this.byteLength = byteLength;
} else if (this.ptr) {
this.byteLength = Buffer.byteLength(str, "utf8");
} else {
this.byteLength = 0;
}
}

View File

@@ -539,6 +539,8 @@ pub fn downloadStream(
) void {
const range = brk: {
if (size) |size_| {
if (offset == 0) break :brk null;
var end = (offset + size_);
if (size_ > 0) {
end -= 1;

View File

@@ -880,44 +880,6 @@ for (let credentials of allCredentials) {
expect(SHA1).toBe(SHA1_2);
}
}, 30_000);
it("should work with sliced files (offset 0)", async () => {
await using tmpfile = await tmp();
const s3file = s3(tmpfile.name + "-readable-stream-slice", options);
await s3file.write("Hello Bun!");
const sliced = s3file.slice(0, 5);
const stream = sliced.stream();
const reader = stream.getReader();
let bytes = 0;
let chunks: Array<Buffer> = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
bytes += value?.length ?? 0;
if (value) chunks.push(value as Buffer);
}
expect(bytes).toBe(5);
expect(Buffer.concat(chunks)).toEqual(Buffer.from("Hello"));
});
it("should work with sliced files (non-zero offset)", async () => {
await using tmpfile = await tmp();
const s3file = s3(tmpfile.name + "-readable-stream-slice-offset", options);
await s3file.write("Hello Bun!");
const sliced = s3file.slice(6, 10);
const stream = sliced.stream();
const reader = stream.getReader();
let bytes = 0;
let chunks: Array<Buffer> = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
bytes += value?.length ?? 0;
if (value) chunks.push(value as Buffer);
}
expect(bytes).toBe(4);
expect(Buffer.concat(chunks)).toEqual(Buffer.from("Bun!"));
});
});
});
});

View File

@@ -0,0 +1,61 @@
import { CString, ptr } from "bun:ffi";
import { expect, test } from "bun:test";
test("CString byteLength and byteOffset are defined when constructed with only a pointer", () => {
const buffer = Buffer.from("Hello world!\0");
const bufferPtr = ptr(buffer);
const cString = new CString(bufferPtr);
expect(cString.byteOffset).toBe(0);
expect(cString.byteLength).toBe(12);
expect(cString.toString()).toBe("Hello world!");
});
test("CString byteOffset defaults to 0 when only ptr and byteLength are provided", () => {
const buffer = Buffer.from("Hello world!\0");
const bufferPtr = ptr(buffer);
const cString = new CString(bufferPtr, 0, 12);
expect(cString.byteOffset).toBe(0);
expect(cString.byteLength).toBe(12);
expect(cString.toString()).toBe("Hello world!");
});
test("CString with byteOffset", () => {
const buffer = Buffer.from("Hello world!\0");
const bufferPtr = ptr(buffer);
const cString = new CString(bufferPtr, 6);
expect(cString.byteOffset).toBe(6);
expect(cString.byteLength).toBe(6);
expect(cString.toString()).toBe("world!");
});
test("CString with byteOffset and byteLength", () => {
const buffer = Buffer.from("Hello world!\0");
const bufferPtr = ptr(buffer);
const cString = new CString(bufferPtr, 6, 5);
expect(cString.byteOffset).toBe(6);
expect(cString.byteLength).toBe(5);
expect(cString.toString()).toBe("world");
});
test("CString with null pointer has byteLength 0 and byteOffset 0", () => {
const cString = new CString(0);
expect(cString.byteOffset).toBe(0);
expect(cString.byteLength).toBe(0);
expect(cString.toString()).toBe("");
});
test("CString byteLength is correct for multi-byte UTF-8 strings", () => {
// "café" in UTF-8 is 5 bytes (c=1, a=1, f=1, é=2)
const buffer = Buffer.from("café\0");
const bufferPtr = ptr(buffer);
const cString = new CString(bufferPtr);
expect(cString.byteOffset).toBe(0);
expect(cString.byteLength).toBe(5);
expect(cString.toString()).toBe("café");
});

View File

@@ -1,65 +0,0 @@
import { S3Client } from "bun";
import { describe, expect, it } from "bun:test";
import { getSecret } from "harness";
const s3Options = {
accessKeyId: getSecret("S3_R2_ACCESS_KEY"),
secretAccessKey: getSecret("S3_R2_SECRET_KEY"),
endpoint: getSecret("S3_R2_ENDPOINT"),
bucket: getSecret("S3_R2_BUCKET"),
};
describe.skipIf(!s3Options.accessKeyId)("issue#27272 - S3 .slice().stream() ignores slice range", () => {
const client = new S3Client(s3Options);
it("slice(0, N).stream() should only return N bytes", async () => {
const filename = `test-issue-27272-${crypto.randomUUID()}`;
const s3file = client.file(filename);
try {
await s3file.write("Hello Bun! This is a longer string for testing.");
const sliced = s3file.slice(0, 5);
const stream = sliced.stream();
const reader = stream.getReader();
let bytes = 0;
const chunks: Array<Buffer> = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
bytes += value?.length ?? 0;
if (value) chunks.push(value as Buffer);
}
expect(bytes).toBe(5);
expect(Buffer.concat(chunks).toString()).toBe("Hello");
} finally {
await s3file.unlink();
}
});
it("slice(0, N).text() and slice(0, N).stream() should return the same data", async () => {
const filename = `test-issue-27272-consistency-${crypto.randomUUID()}`;
const s3file = client.file(filename);
try {
await s3file.write("Hello Bun! This is a longer string for testing.");
const textResult = await s3file.slice(0, 10).text();
const stream = s3file.slice(0, 10).stream();
const reader = stream.getReader();
const chunks: Array<Buffer> = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (value) chunks.push(value as Buffer);
}
const streamResult = Buffer.concat(chunks).toString();
expect(streamResult).toBe(textResult);
expect(streamResult).toBe("Hello Bun!");
} finally {
await s3file.unlink();
}
});
});