Files
bun.sh/src/shell/builtin/touch.zig
Dylan Conway 68a542b4b3 fix(shell): move PATH_MAX check before joinZ to prevent panic
The previous fix checked the path length after joinZ(), but joinZ()
itself panics with "index out of bounds" when the combined path exceeds
its fixed-size buffer. Move the check before the joinZ() call.

Add regression tests for touch and mkdir with paths exceeding PATH_MAX.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 02:24:14 +00:00

423 lines
14 KiB
Zig

const Touch = @This();
opts: Opts = .{},
state: union(enum) {
idle,
exec: struct {
started: bool = false,
tasks_count: usize = 0,
tasks_done: usize = 0,
output_done: usize = 0,
output_waiting: usize = 0,
started_output_queue: bool = false,
args: []const [*:0]const u8,
err: ?jsc.SystemError = null,
},
waiting_write_err,
done,
} = .idle,
pub fn format(this: *const Touch, writer: *std.Io.Writer) !void {
try writer.print("Touch(0x{x}, state={s})", .{ @intFromPtr(this), @tagName(this.state) });
}
pub fn deinit(this: *Touch) void {
log("{f} deinit", .{this});
}
pub fn start(this: *Touch) Yield {
const filepath_args = switch (this.opts.parse(this.bltn().argsSlice())) {
.ok => |filepath_args| filepath_args,
.err => |e| {
const buf = switch (e) {
.illegal_option => |opt_str| this.bltn().fmtErrorArena(.touch, "illegal option -- {s}\n", .{opt_str}),
.show_usage => Builtin.Kind.touch.usageString(),
.unsupported => |unsupported| this.bltn().fmtErrorArena(.touch, "unsupported option, please open a GitHub issue -- {s}\n", .{unsupported}),
};
return this.writeFailingError(buf, 1);
},
} orelse {
return this.writeFailingError(Builtin.Kind.touch.usageString(), 1);
};
this.state = .{
.exec = .{
.args = filepath_args,
},
};
return this.next();
}
pub fn next(this: *Touch) Yield {
switch (this.state) {
.idle => @panic("Invalid state"),
.exec => {
var exec = &this.state.exec;
if (exec.started) {
if (this.state.exec.tasks_done >= this.state.exec.tasks_count and this.state.exec.output_done >= this.state.exec.output_waiting) {
const exit_code: ExitCode = if (this.state.exec.err != null) 1 else 0;
this.state = .done;
return this.bltn().done(exit_code);
}
return .suspended;
}
exec.started = true;
exec.tasks_count = exec.args.len;
for (exec.args) |dir_to_mk_| {
const dir_to_mk = dir_to_mk_[0..std.mem.len(dir_to_mk_) :0];
var task = ShellTouchTask.create(this, this.opts, dir_to_mk, this.bltn().parentCmd().base.shell.cwdZ());
task.schedule();
}
return .suspended;
},
.waiting_write_err => return .failed,
.done => return this.bltn().done(0),
}
}
pub fn onIOWriterChunk(this: *Touch, _: usize, e: ?jsc.SystemError) Yield {
if (this.state == .waiting_write_err) {
return this.bltn().done(1);
}
if (e) |err| err.deref();
return this.next();
}
pub fn writeFailingError(this: *Touch, buf: []const u8, exit_code: ExitCode) Yield {
if (this.bltn().stderr.needsIO()) |safeguard| {
this.state = .waiting_write_err;
return this.bltn().stderr.enqueue(this, buf, safeguard);
}
_ = this.bltn().writeNoIO(.stderr, buf);
return this.bltn().done(exit_code);
}
pub fn onShellTouchTaskDone(this: *Touch, task: *ShellTouchTask) void {
log("{f} onShellTouchTaskDone {f} tasks_done={d} tasks_count={d}", .{ this, task, this.state.exec.tasks_done, this.state.exec.tasks_count });
defer bun.default_allocator.destroy(task);
this.state.exec.tasks_done += 1;
const err = task.err;
if (err) |e| {
const output_task: *ShellTouchOutputTask = bun.new(ShellTouchOutputTask, .{
.parent = this,
.output = .{ .arrlist = .{} },
.state = .waiting_write_err,
});
const error_string = this.bltn().taskErrorToString(.touch, e);
this.state.exec.err = e;
output_task.start(error_string).run();
return;
}
this.next().run();
}
pub const ShellTouchOutputTask = OutputTask(Touch, .{
.writeErr = ShellTouchOutputTaskVTable.writeErr,
.onWriteErr = ShellTouchOutputTaskVTable.onWriteErr,
.writeOut = ShellTouchOutputTaskVTable.writeOut,
.onWriteOut = ShellTouchOutputTaskVTable.onWriteOut,
.onDone = ShellTouchOutputTaskVTable.onDone,
});
const ShellTouchOutputTaskVTable = struct {
pub fn writeErr(this: *Touch, childptr: anytype, errbuf: []const u8) ?Yield {
if (this.bltn().stderr.needsIO()) |safeguard| {
this.state.exec.output_waiting += 1;
return this.bltn().stderr.enqueue(childptr, errbuf, safeguard);
}
_ = this.bltn().writeNoIO(.stderr, errbuf);
return null;
}
pub fn onWriteErr(this: *Touch) void {
this.state.exec.output_done += 1;
}
pub fn writeOut(this: *Touch, childptr: anytype, output: *OutputSrc) ?Yield {
if (this.bltn().stdout.needsIO()) |safeguard| {
this.state.exec.output_waiting += 1;
const slice = output.slice();
log("THE SLICE: {d} {s}", .{ slice.len, slice });
return this.bltn().stdout.enqueue(childptr, slice, safeguard);
}
_ = this.bltn().writeNoIO(.stdout, output.slice());
return null;
}
pub fn onWriteOut(this: *Touch) void {
this.state.exec.output_done += 1;
}
pub fn onDone(this: *Touch) Yield {
return this.next();
}
};
pub const ShellTouchTask = struct {
touch: *Touch,
opts: Opts,
filepath: [:0]const u8,
cwd_path: [:0]const u8,
err: ?jsc.SystemError = null,
task: jsc.WorkPoolTask = .{ .callback = &runFromThreadPool },
event_loop: jsc.EventLoopHandle,
concurrent_task: jsc.EventLoopTask,
pub fn format(this: *const ShellTouchTask, writer: *std.Io.Writer) !void {
try writer.print("ShellTouchTask(0x{x}, filepath={s})", .{ @intFromPtr(this), this.filepath });
}
pub fn deinit(this: *ShellTouchTask) void {
if (this.err) |*e| {
e.deref();
}
bun.default_allocator.destroy(this);
}
pub fn create(touch: *Touch, opts: Opts, filepath: [:0]const u8, cwd_path: [:0]const u8) *ShellTouchTask {
const task = bun.handleOom(bun.default_allocator.create(ShellTouchTask));
task.* = ShellTouchTask{
.touch = touch,
.opts = opts,
.cwd_path = cwd_path,
.filepath = filepath,
.event_loop = touch.bltn().eventLoop(),
.concurrent_task = jsc.EventLoopTask.fromEventLoop(touch.bltn().eventLoop()),
};
return task;
}
pub fn schedule(this: *@This()) void {
debug("{f} schedule", .{this});
WorkPool.schedule(&this.task);
}
pub fn runFromMainThread(this: *@This()) void {
debug("{f} runFromJS", .{this});
this.touch.onShellTouchTaskDone(this);
}
pub fn runFromMainThreadMini(this: *@This(), _: *void) void {
this.runFromMainThread();
}
fn runFromThreadPool(task: *jsc.WorkPoolTask) void {
var this: *ShellTouchTask = @fieldParentPtr("task", task);
debug("{f} runFromThreadPool", .{this});
// We have to give an absolute path
const filepath: [:0]const u8 = brk: {
if (ResolvePath.Platform.auto.isAbsolute(this.filepath)) break :brk this.filepath;
// Check combined length before joining to avoid panic in joinZ's fixed-size buffer
if (this.cwd_path.len + this.filepath.len + 1 >= bun.MAX_PATH_BYTES) {
this.err = bun.sys.Error.fromCode(.NAMETOOLONG, .open).withPath(bun.handleOom(bun.default_allocator.dupe(u8, this.filepath))).toShellSystemError();
if (this.event_loop == .js) {
this.event_loop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(this, .manual_deinit));
} else {
this.event_loop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(this, "runFromMainThreadMini"));
}
return;
}
const parts: []const []const u8 = &.{
this.cwd_path[0..],
this.filepath[0..],
};
break :brk ResolvePath.joinZ(parts, .auto);
};
var node_fs = jsc.Node.fs.NodeFS{};
const milliseconds: f64 = @floatFromInt(std.time.milliTimestamp());
const atime: jsc.Node.TimeLike = if (bun.Environment.isWindows) milliseconds / 1000.0 else jsc.Node.TimeLike{
.sec = @intFromFloat(@divFloor(milliseconds, std.time.ms_per_s)),
.nsec = @intFromFloat(@mod(milliseconds, std.time.ms_per_s) * std.time.ns_per_ms),
};
const mtime = atime;
const args = jsc.Node.fs.Arguments.Utimes{
.atime = atime,
.mtime = mtime,
.path = .{ .string = bun.PathString.init(filepath) },
};
if (node_fs.utimes(args, .sync).asErr()) |err| out: {
if (err.getErrno() == .NOENT) {
const perm = 0o664;
switch (Syscall.open(filepath, bun.O.CREAT | bun.O.WRONLY, perm)) {
.result => |fd| {
fd.close();
break :out;
},
.err => |e| {
this.err = e.withPath(bun.handleOom(bun.default_allocator.dupe(u8, filepath))).toShellSystemError();
break :out;
},
}
}
this.err = err.withPath(bun.handleOom(bun.default_allocator.dupe(u8, filepath))).toShellSystemError();
}
if (this.event_loop == .js) {
this.event_loop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(this, .manual_deinit));
} else {
this.event_loop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(this, "runFromMainThreadMini"));
}
}
};
const Opts = struct {
/// -a
///
/// change only the access time
access_time_only: bool = false,
/// -c, --no-create
///
/// do not create any files
no_create: bool = false,
/// -d, --date=STRING
///
/// parse STRING and use it instead of current time
date: ?[]const u8 = null,
/// -h, --no-dereference
///
/// affect each symbolic link instead of any referenced file
/// (useful only on systems that can change the timestamps of a symlink)
no_dereference: bool = false,
/// -m
///
/// change only the modification time
modification_time_only: bool = false,
/// -r, --reference=FILE
///
/// use this file's times instead of current time
reference: ?[]const u8 = null,
/// -t STAMP
///
/// use [[CC]YY]MMDDhhmm[.ss] instead of current time
timestamp: ?[]const u8 = null,
/// --time=WORD
///
/// change the specified time:
/// WORD is access, atime, or use: equivalent to -a
/// WORD is modify or mtime: equivalent to -m
time: ?[]const u8 = null,
const Parse = FlagParser(*@This());
pub fn parse(opts: *Opts, args: []const [*:0]const u8) Result(?[]const [*:0]const u8, ParseError) {
return Parse.parseFlags(opts, args);
}
pub fn parseLong(this: *Opts, flag: []const u8) ?ParseFlagResult {
_ = this;
if (bun.strings.eqlComptime(flag, "--no-create")) {
return .{
.unsupported = unsupportedFlag("--no-create"),
};
}
if (bun.strings.eqlComptime(flag, "--date")) {
return .{
.unsupported = unsupportedFlag("--date"),
};
}
if (bun.strings.eqlComptime(flag, "--reference")) {
return .{
.unsupported = unsupportedFlag("--reference=FILE"),
};
}
if (bun.strings.eqlComptime(flag, "--time")) {
return .{
.unsupported = unsupportedFlag("--reference=FILE"),
};
}
return null;
}
pub fn parseShort(this: *Opts, char: u8, smallflags: []const u8, i: usize) ?ParseFlagResult {
_ = this;
switch (char) {
'a' => {
return .{ .unsupported = unsupportedFlag("-a") };
},
'c' => {
return .{ .unsupported = unsupportedFlag("-c") };
},
'd' => {
return .{ .unsupported = unsupportedFlag("-d") };
},
'h' => {
return .{ .unsupported = unsupportedFlag("-h") };
},
'm' => {
return .{ .unsupported = unsupportedFlag("-m") };
},
'r' => {
return .{ .unsupported = unsupportedFlag("-r") };
},
't' => {
return .{ .unsupported = unsupportedFlag("-t") };
},
else => {
return .{ .illegal_option = smallflags[1 + i ..] };
},
}
return null;
}
};
pub inline fn bltn(this: *Touch) *Builtin {
const impl: *Builtin.Impl = @alignCast(@fieldParentPtr("touch", this));
return @fieldParentPtr("impl", impl);
}
// --
const debug = bun.Output.scoped(.ShellTouch, .hidden);
const log = debug;
const std = @import("std");
const interpreter = @import("../interpreter.zig");
const FlagParser = interpreter.FlagParser;
const Interpreter = interpreter.Interpreter;
const OutputSrc = interpreter.OutputSrc;
const OutputTask = interpreter.OutputTask;
const ParseError = interpreter.ParseError;
const ParseFlagResult = interpreter.ParseFlagResult;
const unsupportedFlag = interpreter.unsupportedFlag;
const Builtin = Interpreter.Builtin;
const Result = Interpreter.Builtin.Result;
const bun = @import("bun");
const ResolvePath = bun.path;
const Syscall = bun.sys;
const jsc = bun.jsc;
const WorkPool = bun.jsc.WorkPool;
const shell = bun.shell;
const ExitCode = shell.ExitCode;
const Yield = bun.shell.Yield;