mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 06:12:08 +00:00
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>
410 lines
14 KiB
Zig
410 lines
14 KiB
Zig
const Mkdir = @This();
|
|
|
|
opts: Opts = .{},
|
|
state: union(enum) {
|
|
idle,
|
|
exec: struct {
|
|
started: bool = false,
|
|
tasks_count: usize = 0,
|
|
tasks_done: usize = 0,
|
|
output_waiting: u16 = 0,
|
|
output_done: u16 = 0,
|
|
args: []const [*:0]const u8,
|
|
err: ?jsc.SystemError = null,
|
|
},
|
|
waiting_write_err,
|
|
done,
|
|
} = .idle,
|
|
|
|
pub fn onIOWriterChunk(this: *Mkdir, _: usize, e: ?jsc.SystemError) Yield {
|
|
if (e) |err| err.deref();
|
|
|
|
switch (this.state) {
|
|
.waiting_write_err => return this.bltn().done(1),
|
|
.exec => {
|
|
this.state.exec.output_done += 1;
|
|
},
|
|
.idle, .done => @panic("Invalid state"),
|
|
}
|
|
|
|
return this.next();
|
|
}
|
|
|
|
pub fn writeFailingError(this: *Mkdir, 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);
|
|
// if (this.bltn().writeNoIO(.stderr, buf).asErr()) |e| {
|
|
// return .{ .err = e };
|
|
// }
|
|
|
|
return this.bltn().done(exit_code);
|
|
}
|
|
|
|
pub fn start(this: *Mkdir) 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(.mkdir, "illegal option -- {s}\n", .{opt_str}),
|
|
.show_usage => Builtin.Kind.mkdir.usageString(),
|
|
.unsupported => |unsupported| this.bltn().fmtErrorArena(.mkdir, "unsupported option, please open a GitHub issue -- {s}\n", .{unsupported}),
|
|
};
|
|
|
|
return this.writeFailingError(buf, 1);
|
|
},
|
|
} orelse {
|
|
return this.writeFailingError(Builtin.Kind.mkdir.usageString(), 1);
|
|
};
|
|
|
|
this.state = .{
|
|
.exec = .{
|
|
.args = filepath_args,
|
|
},
|
|
};
|
|
|
|
return this.next();
|
|
}
|
|
|
|
pub fn next(this: *Mkdir) 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;
|
|
if (this.state.exec.err) |e| e.deref();
|
|
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 = ShellMkdirTask.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 onShellMkdirTaskDone(this: *Mkdir, task: *ShellMkdirTask) void {
|
|
defer task.deinit();
|
|
this.state.exec.tasks_done += 1;
|
|
var output = task.takeOutput();
|
|
const err = task.err;
|
|
const output_task: *ShellMkdirOutputTask = bun.new(ShellMkdirOutputTask, .{
|
|
.parent = this,
|
|
.output = .{ .arrlist = output.moveToUnmanaged() },
|
|
.state = .waiting_write_err,
|
|
});
|
|
|
|
if (err) |e| {
|
|
const error_string = this.bltn().taskErrorToString(.mkdir, e);
|
|
this.state.exec.err = e;
|
|
output_task.start(error_string).run();
|
|
return;
|
|
}
|
|
output_task.start(null).run();
|
|
}
|
|
|
|
pub const ShellMkdirOutputTask = OutputTask(Mkdir, .{
|
|
.writeErr = ShellMkdirOutputTaskVTable.writeErr,
|
|
.onWriteErr = ShellMkdirOutputTaskVTable.onWriteErr,
|
|
.writeOut = ShellMkdirOutputTaskVTable.writeOut,
|
|
.onWriteOut = ShellMkdirOutputTaskVTable.onWriteOut,
|
|
.onDone = ShellMkdirOutputTaskVTable.onDone,
|
|
});
|
|
|
|
const ShellMkdirOutputTaskVTable = struct {
|
|
pub fn writeErr(this: *Mkdir, 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: *Mkdir) void {
|
|
this.state.exec.output_done += 1;
|
|
}
|
|
|
|
pub fn writeOut(this: *Mkdir, 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: *Mkdir) void {
|
|
this.state.exec.output_done += 1;
|
|
}
|
|
|
|
pub fn onDone(this: *Mkdir) Yield {
|
|
return this.next();
|
|
}
|
|
};
|
|
|
|
pub fn deinit(this: *Mkdir) void {
|
|
_ = this;
|
|
}
|
|
|
|
pub const ShellMkdirTask = struct {
|
|
mkdir: *Mkdir,
|
|
|
|
opts: Opts,
|
|
filepath: [:0]const u8,
|
|
cwd_path: [:0]const u8,
|
|
created_directories: ArrayList(u8),
|
|
|
|
err: ?jsc.SystemError = null,
|
|
task: jsc.WorkPoolTask = .{ .callback = &runFromThreadPool },
|
|
event_loop: jsc.EventLoopHandle,
|
|
concurrent_task: jsc.EventLoopTask,
|
|
|
|
pub fn deinit(this: *ShellMkdirTask) void {
|
|
this.created_directories.deinit();
|
|
bun.default_allocator.destroy(this);
|
|
}
|
|
|
|
fn takeOutput(this: *ShellMkdirTask) ArrayList(u8) {
|
|
const out = this.created_directories;
|
|
this.created_directories = ArrayList(u8).init(bun.default_allocator);
|
|
return out;
|
|
}
|
|
|
|
pub fn format(this: *const ShellMkdirTask, writer: *std.Io.Writer) !void {
|
|
try writer.print("ShellMkdirTask(0x{x}, filepath={s})", .{ @intFromPtr(this), this.filepath });
|
|
}
|
|
|
|
pub fn create(
|
|
mkdir: *Mkdir,
|
|
opts: Opts,
|
|
filepath: [:0]const u8,
|
|
cwd_path: [:0]const u8,
|
|
) *ShellMkdirTask {
|
|
const task = bun.handleOom(bun.default_allocator.create(ShellMkdirTask));
|
|
const evtloop = mkdir.bltn().parentCmd().base.eventLoop();
|
|
task.* = ShellMkdirTask{
|
|
.mkdir = mkdir,
|
|
.opts = opts,
|
|
.cwd_path = cwd_path,
|
|
.filepath = filepath,
|
|
.created_directories = ArrayList(u8).init(bun.default_allocator),
|
|
.event_loop = evtloop,
|
|
.concurrent_task = jsc.EventLoopTask.fromEventLoop(evtloop),
|
|
};
|
|
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.mkdir.onShellMkdirTaskDone(this);
|
|
}
|
|
|
|
pub fn runFromMainThreadMini(this: *@This(), _: *void) void {
|
|
this.runFromMainThread();
|
|
}
|
|
|
|
fn runFromThreadPool(task: *jsc.WorkPoolTask) void {
|
|
var this: *ShellMkdirTask = @fieldParentPtr("task", task);
|
|
debug("{f} runFromThreadPool", .{this});
|
|
|
|
// We have to give an absolute path to our mkdir
|
|
// implementation for it to work with cwd
|
|
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, .mkdir).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{};
|
|
// Recursive
|
|
if (this.opts.parents) {
|
|
const args = jsc.Node.fs.Arguments.Mkdir{
|
|
.path = jsc.Node.PathLike{ .string = bun.PathString.init(filepath) },
|
|
.recursive = true,
|
|
.always_return_none = true,
|
|
};
|
|
|
|
var vtable = MkdirVerboseVTable{ .inner = this, .active = this.opts.verbose };
|
|
|
|
switch (node_fs.mkdirRecursiveImpl(args, *MkdirVerboseVTable, &vtable)) {
|
|
.result => {},
|
|
.err => |e| {
|
|
this.err = e.withPath(bun.handleOom(bun.default_allocator.dupe(u8, filepath))).toShellSystemError();
|
|
std.mem.doNotOptimizeAway(&node_fs);
|
|
},
|
|
}
|
|
} else {
|
|
const args = jsc.Node.fs.Arguments.Mkdir{
|
|
.path = jsc.Node.PathLike{ .string = bun.PathString.init(filepath) },
|
|
.recursive = false,
|
|
.always_return_none = true,
|
|
};
|
|
switch (node_fs.mkdirNonRecursive(args)) {
|
|
.result => {
|
|
if (this.opts.verbose) {
|
|
bun.handleOom(this.created_directories.appendSlice(filepath[0..filepath.len]));
|
|
bun.handleOom(this.created_directories.append('\n'));
|
|
}
|
|
},
|
|
.err => |e| {
|
|
this.err = e.withPath(bun.handleOom(bun.default_allocator.dupe(u8, filepath))).toShellSystemError();
|
|
std.mem.doNotOptimizeAway(&node_fs);
|
|
},
|
|
}
|
|
}
|
|
|
|
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 MkdirVerboseVTable = struct {
|
|
inner: *ShellMkdirTask,
|
|
active: bool,
|
|
|
|
pub fn onCreateDir(vtable: *@This(), dirpath: bun.OSPathSliceZ) void {
|
|
if (!vtable.active) return;
|
|
if (bun.Environment.isWindows) {
|
|
var buf: bun.PathBuffer = undefined;
|
|
const str = bun.strings.fromWPath(&buf, dirpath[0..dirpath.len]);
|
|
bun.handleOom(vtable.inner.created_directories.appendSlice(str));
|
|
bun.handleOom(vtable.inner.created_directories.append('\n'));
|
|
} else {
|
|
bun.handleOom(vtable.inner.created_directories.appendSlice(dirpath));
|
|
bun.handleOom(vtable.inner.created_directories.append('\n'));
|
|
}
|
|
return;
|
|
}
|
|
};
|
|
};
|
|
|
|
const Opts = struct {
|
|
/// -m, --mode
|
|
///
|
|
/// set file mode (as in chmod), not a=rwx - umask
|
|
mode: ?u32 = null,
|
|
|
|
/// -p, --parents
|
|
///
|
|
/// no error if existing, make parent directories as needed,
|
|
/// with their file modes unaffected by any -m option.
|
|
parents: bool = false,
|
|
|
|
/// -v, --verbose
|
|
///
|
|
/// print a message for each created directory
|
|
verbose: bool = false,
|
|
|
|
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 {
|
|
if (bun.strings.eqlComptime(flag, "--mode")) {
|
|
return .{ .unsupported = "--mode" };
|
|
} else if (bun.strings.eqlComptime(flag, "--parents")) {
|
|
this.parents = true;
|
|
return .continue_parsing;
|
|
} else if (bun.strings.eqlComptime(flag, "--vebose")) {
|
|
this.verbose = true;
|
|
return .continue_parsing;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
pub fn parseShort(this: *Opts, char: u8, smallflags: []const u8, i: usize) ?ParseFlagResult {
|
|
switch (char) {
|
|
'm' => {
|
|
return .{ .unsupported = "-m " };
|
|
},
|
|
'p' => {
|
|
this.parents = true;
|
|
},
|
|
'v' => {
|
|
this.verbose = true;
|
|
},
|
|
else => {
|
|
return .{ .illegal_option = smallflags[1 + i ..] };
|
|
},
|
|
}
|
|
|
|
return null;
|
|
}
|
|
};
|
|
|
|
pub inline fn bltn(this: *Mkdir) *Builtin {
|
|
const impl: *Builtin.Impl = @alignCast(@fieldParentPtr("mkdir", this));
|
|
return @fieldParentPtr("impl", impl);
|
|
}
|
|
|
|
// --
|
|
const debug = bun.Output.scoped(.ShellMkdir, .hidden);
|
|
|
|
const log = debug;
|
|
|
|
const std = @import("std");
|
|
const ArrayList = std.array_list.Managed;
|
|
|
|
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 Builtin = Interpreter.Builtin;
|
|
const Result = Interpreter.Builtin.Result;
|
|
|
|
const bun = @import("bun");
|
|
const ResolvePath = bun.path;
|
|
|
|
const jsc = bun.jsc;
|
|
const WorkPool = bun.jsc.WorkPool;
|
|
|
|
const shell = bun.shell;
|
|
const ExitCode = shell.ExitCode;
|
|
const Yield = bun.shell.Yield;
|