mirror of
https://github.com/oven-sh/bun
synced 2026-02-13 04:18:58 +00:00
### What does this PR do? fixes #7157, fixes #14662 migrates pnpm-workspace.yaml data to package.json & converts pnpm-lock.yml to bun.lock --- ### How did you verify your code works? manually, tests and real world examples --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
453 lines
14 KiB
Zig
453 lines
14 KiB
Zig
//! This is a similar API to std.fs.File, except it:
|
|
//! - Preserves errors from the operating system
|
|
//! - Supports normalizing BOM to UTF-8
|
|
//! - Has several optimizations somewhat specific to Bun
|
|
//! - Potentially goes through libuv on Windows
|
|
//! - Does not use unreachable in system calls.
|
|
|
|
const File = @This();
|
|
|
|
// "handle" matches std.fs.File
|
|
handle: bun.FileDescriptor,
|
|
|
|
pub fn openat(dir: bun.FileDescriptor, path: [:0]const u8, flags: i32, mode: bun.Mode) Maybe(File) {
|
|
return switch (sys.openat(dir, path, flags, mode)) {
|
|
.result => |fd| .{ .result = .{ .handle = fd } },
|
|
.err => |err| .{ .err = err },
|
|
};
|
|
}
|
|
|
|
pub fn open(path: [:0]const u8, flags: i32, mode: bun.Mode) Maybe(File) {
|
|
return File.openat(bun.FD.cwd(), path, flags, mode);
|
|
}
|
|
|
|
pub fn makeOpen(path: [:0]const u8, flags: i32, mode: bun.Mode) Maybe(File) {
|
|
return File.makeOpenat(bun.FD.cwd(), path, flags, mode);
|
|
}
|
|
|
|
pub fn makeOpenat(other: bun.FD, path: [:0]const u8, flags: i32, mode: bun.Mode) Maybe(File) {
|
|
const fd = switch (sys.openat(other, path, flags, mode)) {
|
|
.result => |fd| fd,
|
|
.err => |err| fd: {
|
|
if (std.fs.path.dirname(path)) |dir_path| {
|
|
bun.makePath(other.stdDir(), dir_path) catch {};
|
|
break :fd switch (sys.openat(other, path, flags, mode)) {
|
|
.result => |fd| fd,
|
|
.err => |err2| return .{ .err = err2 },
|
|
};
|
|
}
|
|
|
|
return .{ .err = err };
|
|
},
|
|
};
|
|
|
|
return .{ .result = .{ .handle = fd } };
|
|
}
|
|
|
|
pub fn openatOSPath(other: bun.FD, path: bun.OSPathSliceZ, flags: i32, mode: bun.Mode) Maybe(File) {
|
|
return switch (sys.openatOSPath(other, path, flags, mode)) {
|
|
.result => |fd| .{ .result = .{ .handle = fd } },
|
|
.err => |err| .{ .err = err },
|
|
};
|
|
}
|
|
|
|
pub fn from(other: anytype) File {
|
|
const T = @TypeOf(other);
|
|
|
|
if (T == File) {
|
|
return other;
|
|
}
|
|
|
|
if (T == std.posix.fd_t) {
|
|
return .{ .handle = .fromNative(other) };
|
|
}
|
|
|
|
if (T == bun.FileDescriptor) {
|
|
return .{ .handle = other };
|
|
}
|
|
|
|
if (T == std.fs.File) {
|
|
return .{ .handle = .fromStdFile(other) };
|
|
}
|
|
|
|
if (T == std.fs.Dir) {
|
|
return File{ .handle = .fromStdDir(other) };
|
|
}
|
|
|
|
if (comptime Environment.isLinux) {
|
|
if (T == u64) {
|
|
return File{ .handle = .fromNative(@intCast(other)) };
|
|
}
|
|
}
|
|
|
|
@compileError("Unsupported type " ++ bun.meta.typeName(T));
|
|
}
|
|
|
|
pub fn write(self: File, buf: []const u8) Maybe(usize) {
|
|
return sys.write(self.handle, buf);
|
|
}
|
|
|
|
pub fn read(self: File, buf: []u8) Maybe(usize) {
|
|
return sys.read(self.handle, buf);
|
|
}
|
|
|
|
pub fn readAll(self: File, buf: []u8) Maybe(usize) {
|
|
return sys.readAll(self.handle, buf);
|
|
}
|
|
|
|
pub fn writeAll(self: File, buf: []const u8) Maybe(void) {
|
|
var remain = buf;
|
|
while (remain.len > 0) {
|
|
const rc = sys.write(self.handle, remain);
|
|
switch (rc) {
|
|
.err => |err| return .{ .err = err },
|
|
.result => |amt| {
|
|
if (amt == 0) {
|
|
return .success;
|
|
}
|
|
remain = remain[amt..];
|
|
},
|
|
}
|
|
}
|
|
|
|
return .success;
|
|
}
|
|
|
|
pub fn writeFile(
|
|
relative_dir_or_cwd: anytype,
|
|
path: bun.OSPathSliceZ,
|
|
data: []const u8,
|
|
) Maybe(void) {
|
|
const file = switch (File.openatOSPath(relative_dir_or_cwd, path, bun.O.WRONLY | bun.O.CREAT | bun.O.TRUNC, 0o664)) {
|
|
.err => |err| return .{ .err = err },
|
|
.result => |fd| fd,
|
|
};
|
|
defer file.close();
|
|
switch (file.writeAll(data)) {
|
|
.err => |err| return .{ .err = err },
|
|
.result => {},
|
|
}
|
|
return .success;
|
|
}
|
|
|
|
pub const ReadError = anyerror;
|
|
|
|
pub fn closeAndMoveTo(this: File, src: [:0]const u8, dest: [:0]const u8) !void {
|
|
// On POSIX, close the file after moving it.
|
|
defer if (Environment.isPosix) this.close();
|
|
// On Windows, close the file before moving it.
|
|
if (Environment.isWindows) this.close();
|
|
const cwd = bun.FD.cwd();
|
|
try bun.sys.moveFileZWithHandle(this.handle, cwd, src, cwd, dest);
|
|
}
|
|
|
|
fn stdIoRead(this: File, buf: []u8) ReadError!usize {
|
|
return try this.read(buf).unwrap();
|
|
}
|
|
|
|
pub const Reader = std.io.Reader(File, anyerror, stdIoRead);
|
|
|
|
pub fn reader(self: File) Reader {
|
|
return Reader{ .context = self };
|
|
}
|
|
|
|
pub const WriteError = anyerror;
|
|
fn stdIoWrite(this: File, bytes: []const u8) WriteError!usize {
|
|
try this.writeAll(bytes).unwrap();
|
|
|
|
return bytes.len;
|
|
}
|
|
|
|
fn stdIoWriteQuietDebug(this: File, bytes: []const u8) WriteError!usize {
|
|
bun.Output.disableScopedDebugWriter();
|
|
defer bun.Output.enableScopedDebugWriter();
|
|
try this.writeAll(bytes).unwrap();
|
|
|
|
return bytes.len;
|
|
}
|
|
|
|
pub const Writer = std.io.Writer(File, anyerror, stdIoWrite);
|
|
pub const QuietWriter = if (Environment.isDebug) std.io.Writer(File, anyerror, stdIoWriteQuietDebug) else Writer;
|
|
|
|
pub fn writer(self: File) Writer {
|
|
return Writer{ .context = self };
|
|
}
|
|
|
|
pub fn quietWriter(self: File) QuietWriter {
|
|
return QuietWriter{ .context = self };
|
|
}
|
|
|
|
pub fn isTty(self: File) bool {
|
|
return std.posix.isatty(self.handle.cast());
|
|
}
|
|
|
|
/// Asserts in debug that this File object is valid
|
|
pub fn close(self: File) void {
|
|
self.handle.close();
|
|
}
|
|
|
|
pub fn getEndPos(self: File) Maybe(usize) {
|
|
return getFileSize(self.handle);
|
|
}
|
|
|
|
pub fn stat(self: File) Maybe(bun.Stat) {
|
|
return fstat(self.handle);
|
|
}
|
|
|
|
/// Be careful about using this on Linux or macOS.
|
|
///
|
|
/// File calls stat() internally.
|
|
pub fn kind(self: File) Maybe(std.fs.File.Kind) {
|
|
if (Environment.isWindows) {
|
|
const rt = windows.GetFileType(self.handle.cast());
|
|
if (rt == windows.FILE_TYPE_UNKNOWN) {
|
|
switch (windows.GetLastError()) {
|
|
.SUCCESS => {},
|
|
else => |err| {
|
|
return .{ .err = Error.fromCode((SystemErrno.init(err) orelse SystemErrno.EUNKNOWN).toE(), .fstat) };
|
|
},
|
|
}
|
|
}
|
|
|
|
return .{
|
|
.result = switch (rt) {
|
|
windows.FILE_TYPE_CHAR => .character_device,
|
|
windows.FILE_TYPE_REMOTE, windows.FILE_TYPE_DISK => .file,
|
|
windows.FILE_TYPE_PIPE => .named_pipe,
|
|
windows.FILE_TYPE_UNKNOWN => .unknown,
|
|
else => .file,
|
|
},
|
|
};
|
|
}
|
|
|
|
const st = switch (self.stat()) {
|
|
.err => |err| return .{ .err = err },
|
|
.result => |s| s,
|
|
};
|
|
|
|
const m = st.mode & posix.S.IFMT;
|
|
switch (m) {
|
|
posix.S.IFBLK => return .{ .result = .block_device },
|
|
posix.S.IFCHR => return .{ .result = .character_device },
|
|
posix.S.IFDIR => return .{ .result = .directory },
|
|
posix.S.IFIFO => return .{ .result = .named_pipe },
|
|
posix.S.IFLNK => return .{ .result = .sym_link },
|
|
posix.S.IFREG => return .{ .result = .file },
|
|
posix.S.IFSOCK => return .{ .result = .unix_domain_socket },
|
|
else => {
|
|
return .{ .result = .file };
|
|
},
|
|
}
|
|
}
|
|
|
|
pub const ReadToEndResult = struct {
|
|
bytes: std.ArrayList(u8) = std.ArrayList(u8).init(default_allocator),
|
|
err: ?Error = null,
|
|
|
|
pub fn unwrap(self: *const ReadToEndResult) ![]u8 {
|
|
if (self.err) |err| {
|
|
try (bun.sys.Maybe(void){ .err = err }).unwrap();
|
|
}
|
|
return self.bytes.items;
|
|
}
|
|
};
|
|
|
|
pub fn readFillBuf(this: File, buf: []u8) Maybe([]u8) {
|
|
var read_amount: usize = 0;
|
|
while (read_amount < buf.len) {
|
|
switch (if (comptime Environment.isPosix)
|
|
pread(this.handle, buf[read_amount..], @intCast(read_amount))
|
|
else
|
|
sys.read(this.handle, buf[read_amount..])) {
|
|
.err => |err| {
|
|
return .{ .err = err };
|
|
},
|
|
.result => |bytes_read| {
|
|
if (bytes_read == 0) {
|
|
break;
|
|
}
|
|
|
|
read_amount += bytes_read;
|
|
},
|
|
}
|
|
}
|
|
|
|
return .{ .result = buf[0..read_amount] };
|
|
}
|
|
|
|
pub fn readToEndWithArrayList(this: File, list: *std.ArrayList(u8), size_guess: enum { probably_small, unknown_size }) Maybe(usize) {
|
|
if (size_guess == .probably_small) {
|
|
bun.handleOom(list.ensureUnusedCapacity(64));
|
|
} else {
|
|
list.ensureTotalCapacityPrecise(
|
|
switch (this.getEndPos()) {
|
|
.err => |err| {
|
|
return .{ .err = err };
|
|
},
|
|
.result => |s| s,
|
|
} + 16,
|
|
) catch |err| bun.handleOom(err);
|
|
}
|
|
|
|
var total: i64 = 0;
|
|
while (true) {
|
|
if (list.unusedCapacitySlice().len == 0) {
|
|
bun.handleOom(list.ensureUnusedCapacity(16));
|
|
}
|
|
|
|
switch (if (comptime Environment.isPosix)
|
|
pread(this.handle, list.unusedCapacitySlice(), total)
|
|
else
|
|
sys.read(this.handle, list.unusedCapacitySlice())) {
|
|
.err => |err| {
|
|
return .{ .err = err };
|
|
},
|
|
.result => |bytes_read| {
|
|
if (bytes_read == 0) {
|
|
break;
|
|
}
|
|
|
|
list.items.len += bytes_read;
|
|
total += @intCast(bytes_read);
|
|
},
|
|
}
|
|
}
|
|
|
|
return .{ .result = @intCast(total) };
|
|
}
|
|
|
|
/// Use this function on potentially large files.
|
|
/// Calls fstat() on the file to get the size of the file and avoids reallocations + extra read() calls.
|
|
pub fn readToEnd(this: File, allocator: std.mem.Allocator) ReadToEndResult {
|
|
var list = std.ArrayList(u8).init(allocator);
|
|
return switch (readToEndWithArrayList(this, &list, .unknown_size)) {
|
|
.err => |err| .{ .err = err, .bytes = list },
|
|
.result => .{ .err = null, .bytes = list },
|
|
};
|
|
}
|
|
|
|
/// Use this function on small files <= 1024 bytes.
|
|
/// File will skip the fstat() call, preallocating 64 bytes instead of the file's size.
|
|
pub fn readToEndSmall(this: File, allocator: std.mem.Allocator) ReadToEndResult {
|
|
var list = std.ArrayList(u8).init(allocator);
|
|
return switch (readToEndWithArrayList(this, &list, .probably_small)) {
|
|
.err => |err| .{ .err = err, .bytes = list },
|
|
.result => .{ .err = null, .bytes = list },
|
|
};
|
|
}
|
|
|
|
pub fn getPath(this: File, out_buffer: *bun.PathBuffer) Maybe([]u8) {
|
|
return getFdPath(this.handle, out_buffer);
|
|
}
|
|
|
|
/// 1. Normalize the file path
|
|
/// 2. Open a file for reading
|
|
/// 2. Read the file to a buffer
|
|
/// 3. Return the File handle and the buffer
|
|
pub fn readFromUserInput(dir_fd: anytype, input_path: anytype, allocator: std.mem.Allocator) Maybe([]u8) {
|
|
var buf: bun.PathBuffer = undefined;
|
|
const normalized = bun.path.joinAbsStringBufZ(
|
|
bun.fs.FileSystem.instance.top_level_dir,
|
|
&buf,
|
|
&.{input_path},
|
|
.loose,
|
|
);
|
|
return readFrom(dir_fd, normalized, allocator);
|
|
}
|
|
|
|
/// 1. Open a file for reading
|
|
/// 2. Read the file to a buffer
|
|
/// 3. Return the File handle and the buffer
|
|
pub fn readFileFrom(dir_fd: anytype, path: anytype, allocator: std.mem.Allocator) Maybe(struct { File, []u8 }) {
|
|
const ElementType = std.meta.Elem(@TypeOf(path));
|
|
|
|
const rc = brk: {
|
|
if (comptime Environment.isWindows and ElementType == u16) {
|
|
break :brk openatWindowsTMaybeNormalize(u16, from(dir_fd).handle, path, O.RDONLY, false);
|
|
}
|
|
|
|
if (comptime ElementType == u8 and std.meta.sentinel(@TypeOf(path)) == null) {
|
|
break :brk sys.openatA(from(dir_fd).handle, path, O.RDONLY, 0);
|
|
}
|
|
|
|
break :brk sys.openat(from(dir_fd).handle, path, O.RDONLY, 0);
|
|
};
|
|
|
|
const this = switch (rc) {
|
|
.err => |err| return .{ .err = err },
|
|
.result => |fd| from(fd),
|
|
};
|
|
|
|
var result = this.readToEnd(allocator);
|
|
|
|
if (result.err) |err| {
|
|
this.close();
|
|
result.bytes.deinit();
|
|
return .{ .err = err };
|
|
}
|
|
|
|
if (result.bytes.items.len == 0) {
|
|
// Don't allocate an empty string.
|
|
// We won't be modifying an empty slice, anyway.
|
|
return .{ .result = .{ this, @ptrCast(@constCast("")) } };
|
|
}
|
|
|
|
return .{ .result = .{ this, result.bytes.items } };
|
|
}
|
|
|
|
/// 1. Open a file for reading relative to a directory
|
|
/// 2. Read the file to a buffer
|
|
/// 3. Close the file
|
|
/// 4. Return the buffer
|
|
pub fn readFrom(dir_fd: anytype, path: anytype, allocator: std.mem.Allocator) Maybe([]u8) {
|
|
const file, const bytes = switch (readFileFrom(dir_fd, path, allocator)) {
|
|
.err => |err| return .{ .err = err },
|
|
.result => |result| result,
|
|
};
|
|
|
|
file.close();
|
|
return .{ .result = bytes };
|
|
}
|
|
|
|
const ToSourceOptions = struct {
|
|
convert_bom: bool = false,
|
|
};
|
|
|
|
pub fn toSourceAt(dir_fd: anytype, path: anytype, allocator: std.mem.Allocator, opts: ToSourceOptions) Maybe(bun.logger.Source) {
|
|
var bytes = switch (readFrom(dir_fd, path, allocator)) {
|
|
.err => |err| return .{ .err = err },
|
|
.result => |bytes| bytes,
|
|
};
|
|
|
|
if (opts.convert_bom) {
|
|
if (bun.strings.BOM.detect(bytes)) |bom| {
|
|
bytes = bun.handleOom(bom.removeAndConvertToUTF8AndFree(allocator, bytes));
|
|
}
|
|
}
|
|
|
|
return .{ .result = bun.logger.Source.initPathString(path, bytes) };
|
|
}
|
|
|
|
pub fn toSource(path: anytype, allocator: std.mem.Allocator, opts: ToSourceOptions) Maybe(bun.logger.Source) {
|
|
return toSourceAt(std.fs.cwd(), path, allocator, opts);
|
|
}
|
|
|
|
const bun = @import("bun");
|
|
const Environment = bun.Environment;
|
|
const default_allocator = bun.default_allocator;
|
|
const windows = bun.windows;
|
|
|
|
const sys = bun.sys;
|
|
const Error = bun.sys.Error;
|
|
const Maybe = bun.sys.Maybe;
|
|
const O = sys.O;
|
|
const SystemErrno = bun.sys.SystemErrno;
|
|
const fstat = sys.fstat;
|
|
const getFdPath = sys.getFdPath;
|
|
const getFileSize = sys.getFileSize;
|
|
const openatWindowsTMaybeNormalize = sys.openatWindowsTMaybeNormalize;
|
|
const pread = sys.pread;
|
|
|
|
const std = @import("std");
|
|
const posix = std.posix;
|