Compare commits

..

3 Commits

Author SHA1 Message Date
Claude Bot
78c1023b97 test: refactor mv illegal option tests to use test.each
Address code review feedback to reduce duplication by using parameterized tests.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 07:26:57 +00:00
Claude Bot
8bda817ed9 test: add exit code assertions to mv illegal option tests
Address code review feedback to verify exit codes in catch blocks.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 07:17:05 +00:00
Claude Bot
60b2339c19 fix(shell): report correct illegal option character in mv command
The mv command was reporting "-" for all unrecognized flags instead of
the actual flag character. For example, `mv -T` would show
"mv: illegal option -- -" instead of "mv: illegal option -- T".

The bug was in parseFlag() where the else branch returned a hardcoded
"-" instead of the actual unrecognized character from the flag string.

Fixes #26749

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 07:06:54 +00:00
4 changed files with 32 additions and 104 deletions

View File

@@ -447,7 +447,7 @@ pub fn parseFlag(this: *Mv, flag: []const u8) union(enum) { continue_parsing, do
if (flag[0] != '-') return .done;
const small_flags = flag[1..];
for (small_flags) |char| {
for (small_flags, 0..) |char, i| {
switch (char) {
'f' => {
this.opts.force_overwrite = true;
@@ -471,7 +471,7 @@ pub fn parseFlag(this: *Mv, flag: []const u8) union(enum) { continue_parsing, do
this.opts.verbose_output = true;
},
else => {
return .{ .illegal_option = "-" };
return .{ .illegal_option = small_flags[i..][0..1] };
},
}
}

View File

@@ -976,21 +976,12 @@ pub const FormData = struct {
}
pub const Field = struct {
/// Offset into the input buffer for the value (binary-safe, no null-termination).
value_off: u32 = 0,
/// Length of the value (binary-safe, no null-termination).
value_len: u32 = 0,
value: bun.Semver.String = .{},
filename: bun.Semver.String = .{},
content_type: bun.Semver.String = .{},
is_file: bool = false,
zero_count: u8 = 0,
/// Get the value slice from the buffer.
pub fn valueSlice(self: Field, buf: []const u8) []const u8 {
if (self.value_len == 0) return "";
return buf[self.value_off..][0..self.value_len];
}
pub const Entry = union(enum) {
field: Field,
list: bun.BabyList(Field),
@@ -1097,7 +1088,7 @@ pub const FormData = struct {
form: *jsc.DOMFormData,
pub fn onEntry(wrap: *@This(), name: bun.Semver.String, field: Field, buf: []const u8) void {
const value_str = field.valueSlice(buf);
const value_str = field.value.slice(buf);
var key = jsc.ZigString.initUTF8(name.slice(buf));
if (field.is_file) {
@@ -1287,9 +1278,7 @@ pub const FormData = struct {
if (strings.endsWithComptime(body, "\r\n")) {
body = body[0 .. body.len - 2];
}
// Store offset and length directly to preserve binary data with null bytes
field.value_off = @truncate(@intFromPtr(body.ptr) - @intFromPtr(input.ptr));
field.value_len = @truncate(body.len);
field.value = subslicer.sub(body).value();
field.filename = filename orelse .{};
field.is_file = is_file;

View File

@@ -1,88 +0,0 @@
import { expect, test } from "bun:test";
// Regression test for https://github.com/oven-sh/bun/issues/26740
// FormData multipart parsing was truncating binary file content at null bytes
// for files 8 bytes or smaller due to Semver.String inline storage optimization.
test("FormData preserves binary data with null bytes in small files", async () => {
const testCases = [
{ name: "8 bytes with null at position 3", data: [0x01, 0x02, 0x03, 0x00, 0x05, 0x06, 0x07, 0x08] },
{ name: "4 bytes with null at end", data: [0x1f, 0x8b, 0x08, 0x00] },
{ name: "1 byte null", data: [0x00] },
{ name: "all nulls (8 bytes)", data: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] },
{ name: "7 bytes ending with null", data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x00] },
{ name: "6 bytes starting with null", data: [0x00, 0x02, 0x03, 0x04, 0x05, 0x06] },
];
await using server = Bun.serve({
port: 0,
async fetch(req) {
const formData = await req.formData();
const file = formData.get("file") as File;
const bytes = new Uint8Array(await file.arrayBuffer());
return Response.json({
expectedSize: parseInt(req.headers.get("x-expected-size") || "0"),
actualSize: bytes.byteLength,
content: Array.from(bytes),
});
},
});
for (const tc of testCases) {
const content = new Uint8Array(tc.data);
const file = new File([content], "test.bin", { type: "application/octet-stream" });
const formData = new FormData();
formData.append("file", file);
const res = await fetch(`http://localhost:${server.port}`, {
method: "POST",
body: formData,
headers: { "x-expected-size": String(tc.data.length) },
});
const result = (await res.json()) as { expectedSize: number; actualSize: number; content: number[] };
expect(result.actualSize).toBe(result.expectedSize);
expect(result.content).toEqual(tc.data);
}
});
test("FormData preserves binary data in larger files (> 8 bytes)", async () => {
// This should have worked before the fix, but let's verify it still works
const testCases = [
{ name: "16 bytes with nulls", data: Array.from({ length: 16 }, (_, i) => (i % 3 === 0 ? 0x00 : i)) },
{ name: "9 bytes with null at start", data: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] },
];
await using server = Bun.serve({
port: 0,
async fetch(req) {
const formData = await req.formData();
const file = formData.get("file") as File;
const bytes = new Uint8Array(await file.arrayBuffer());
return Response.json({
expectedSize: parseInt(req.headers.get("x-expected-size") || "0"),
actualSize: bytes.byteLength,
content: Array.from(bytes),
});
},
});
for (const tc of testCases) {
const content = new Uint8Array(tc.data);
const file = new File([content], "test.bin", { type: "application/octet-stream" });
const formData = new FormData();
formData.append("file", file);
const res = await fetch(`http://localhost:${server.port}`, {
method: "POST",
body: formData,
headers: { "x-expected-size": String(tc.data.length) },
});
const result = (await res.json()) as { expectedSize: number; actualSize: number; content: number[] };
expect(result.actualSize).toBe(result.expectedSize);
expect(result.content).toEqual(tc.data);
}
});

View File

@@ -0,0 +1,27 @@
import { $ } from "bun";
import { expect, test } from "bun:test";
// https://github.com/oven-sh/bun/issues/26749
// mv command should report the correct illegal option character in error messages
const cases: [string, string][] = [
["-T", "T"],
["-X", "X"],
["-fX", "X"],
["-vZ", "Z"],
];
test.each(cases)("mv reports correct illegal option for %s", async (flag, expectedChar) => {
$.throws(true);
try {
await Bun.$`mv ${flag} ./a ./b`;
expect.unreachable("should have thrown");
} catch (e: unknown) {
const err = e as Bun.$.ShellError;
const stderr = new TextDecoder().decode(err.stderr);
expect(stderr).toContain(`mv: illegal option -- ${expectedChar}`);
expect(err.exitCode).not.toBe(0);
} finally {
$.nothrow();
}
});