diff --git a/CMakeLists.txt b/CMakeLists.txt index 9b71e3c78f..9a758008f6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -413,7 +413,6 @@ set(ZIG_STAGE2_SOURCES lib/std/Thread/Futex.zig lib/std/Thread/Mutex.zig lib/std/Thread/Pool.zig - lib/std/Thread/ResetEvent.zig lib/std/Thread/WaitGroup.zig lib/std/array_hash_map.zig lib/std/array_list.zig diff --git a/README.md b/README.md index 5a346c24d7..04fa4c618e 100644 --- a/README.md +++ b/README.md @@ -76,23 +76,15 @@ This produces a `zig2` executable in the current working directory. This is a [without LLVM extensions](https://github.com/ziglang/zig/issues/16270), and is therefore lacking these features: - Release mode optimizations -- [aarch64 machine code backend](https://github.com/ziglang/zig/issues/21172) -- [@cImport](https://github.com/ziglang/zig/issues/20630) -- [zig translate-c](https://github.com/ziglang/zig/issues/20875) -- [Ability to compile assembly files](https://github.com/ziglang/zig/issues/21169) - [Some ELF linking features](https://github.com/ziglang/zig/issues/17749) -- [Most COFF/PE linking features](https://github.com/ziglang/zig/issues/17751) +- [Some COFF/PE linking features](https://github.com/ziglang/zig/issues/17751) - [Some WebAssembly linking features](https://github.com/ziglang/zig/issues/17750) -- [Ability to create import libs from def files](https://github.com/ziglang/zig/issues/17807) - [Ability to create static archives from object files](https://github.com/ziglang/zig/issues/9828) +- [Ability to compile assembly files](https://github.com/ziglang/zig/issues/21169) - Ability to compile C, C++, Objective-C, and Objective-C++ files -However, a compiler built this way does provide a C backend, which may be -useful for creating system packages of Zig projects using the system C -toolchain. **In this case, LLVM is not needed!** - -Furthermore, a compiler built this way provides an LLVM backend that produces -bitcode files, which may be compiled into object files via a system Clang +Even when built this way, Zig provides an LLVM backend that produces bitcode +files, which may be optimized and compiled into object files via a system Clang package. This can be used to produce system packages of Zig applications without the Zig package dependency on LLVM. diff --git a/ci/x86_64-windows-debug.ps1 b/ci/x86_64-windows-debug.ps1 index 6cd28db466..9349a74547 100644 --- a/ci/x86_64-windows-debug.ps1 +++ b/ci/x86_64-windows-debug.ps1 @@ -95,7 +95,7 @@ Enter-VsDevShell -VsInstallPath "C:\Program Files (x86)\Microsoft Visual Studio\ CheckLastExitCode Write-Output "Build and run behavior tests with msvc..." -& cl.exe -I..\lib test-x86_64-windows-msvc.c compiler_rt-x86_64-windows-msvc.c /W3 /Z7 -link -nologo -debug -subsystem:console kernel32.lib ntdll.lib libcmt.lib +& cl.exe -I..\lib test-x86_64-windows-msvc.c compiler_rt-x86_64-windows-msvc.c /W3 /Z7 -link -nologo -debug -subsystem:console kernel32.lib ntdll.lib libcmt.lib ws2_32.lib CheckLastExitCode & .\test-x86_64-windows-msvc.exe diff --git a/ci/x86_64-windows-release.ps1 b/ci/x86_64-windows-release.ps1 index f3cdd66f48..a348c72fe9 100644 --- a/ci/x86_64-windows-release.ps1 +++ b/ci/x86_64-windows-release.ps1 @@ -113,7 +113,7 @@ Enter-VsDevShell -VsInstallPath "C:\Program Files (x86)\Microsoft Visual Studio\ CheckLastExitCode Write-Output "Build and run behavior tests with msvc..." -& cl.exe -I..\lib test-x86_64-windows-msvc.c compiler_rt-x86_64-windows-msvc.c /W3 /Z7 -link -nologo -debug -subsystem:console kernel32.lib ntdll.lib libcmt.lib +& cl.exe -I..\lib test-x86_64-windows-msvc.c compiler_rt-x86_64-windows-msvc.c /W3 /Z7 -link -nologo -debug -subsystem:console kernel32.lib ntdll.lib libcmt.lib ws2_32.lib CheckLastExitCode & .\test-x86_64-windows-msvc.exe diff --git a/lib/compiler/aro/aro/Compilation.zig b/lib/compiler/aro/aro/Compilation.zig index 2de501e37b..6b70aea94a 100644 --- a/lib/compiler/aro/aro/Compilation.zig +++ b/lib/compiler/aro/aro/Compilation.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const Io = std.Io; const assert = std.debug.assert; const EpochSeconds = std.time.epoch.EpochSeconds; const mem = std.mem; @@ -113,7 +114,7 @@ pub const Environment = struct { if (parsed > max_timestamp) return error.InvalidEpoch; return .{ .provided = parsed }; } else { - const timestamp = std.math.cast(u64, std.time.timestamp()) orelse return error.InvalidEpoch; + const timestamp = std.math.cast(u64, 0) orelse return error.InvalidEpoch; return .{ .system = std.math.clamp(timestamp, 0, max_timestamp) }; } } @@ -124,6 +125,7 @@ const Compilation = @This(); gpa: Allocator, /// Allocations in this arena live all the way until `Compilation.deinit`. arena: Allocator, +io: Io, diagnostics: *Diagnostics, code_gen_options: CodeGenOptions = .default, @@ -157,10 +159,11 @@ type_store: TypeStore = .{}, ms_cwd_source_id: ?Source.Id = null, cwd: std.fs.Dir, -pub fn init(gpa: Allocator, arena: Allocator, diagnostics: *Diagnostics, cwd: std.fs.Dir) Compilation { +pub fn init(gpa: Allocator, arena: Allocator, io: Io, diagnostics: *Diagnostics, cwd: std.fs.Dir) Compilation { return .{ .gpa = gpa, .arena = arena, + .io = io, .diagnostics = diagnostics, .cwd = cwd, }; @@ -168,10 +171,11 @@ pub fn init(gpa: Allocator, arena: Allocator, diagnostics: *Diagnostics, cwd: st /// Initialize Compilation with default environment, /// pragma handlers and emulation mode set to target. -pub fn initDefault(gpa: Allocator, arena: Allocator, diagnostics: *Diagnostics, cwd: std.fs.Dir) !Compilation { +pub fn initDefault(gpa: Allocator, arena: Allocator, io: Io, diagnostics: *Diagnostics, cwd: std.fs.Dir) !Compilation { var comp: Compilation = .{ .gpa = gpa, .arena = arena, + .io = io, .diagnostics = diagnostics, .environment = try Environment.loadAll(gpa), .cwd = cwd, @@ -222,14 +226,14 @@ pub const SystemDefinesMode = enum { include_system_defines, }; -fn generateSystemDefines(comp: *Compilation, w: *std.Io.Writer) !void { +fn generateSystemDefines(comp: *Compilation, w: *Io.Writer) !void { const define = struct { - fn define(_w: *std.Io.Writer, name: []const u8) !void { + fn define(_w: *Io.Writer, name: []const u8) !void { try _w.print("#define {s} 1\n", .{name}); } }.define; const defineStd = struct { - fn defineStd(_w: *std.Io.Writer, name: []const u8, is_gnu: bool) !void { + fn defineStd(_w: *Io.Writer, name: []const u8, is_gnu: bool) !void { if (is_gnu) { try _w.print("#define {s} 1\n", .{name}); } @@ -956,7 +960,7 @@ fn generateSystemDefines(comp: *Compilation, w: *std.Io.Writer) !void { pub fn generateBuiltinMacros(comp: *Compilation, system_defines_mode: SystemDefinesMode) AddSourceError!Source { try comp.type_store.initNamedTypes(comp); - var allocating: std.Io.Writer.Allocating = try .initCapacity(comp.gpa, 2 << 13); + var allocating: Io.Writer.Allocating = try .initCapacity(comp.gpa, 2 << 13); defer allocating.deinit(); comp.writeBuiltinMacros(system_defines_mode, &allocating.writer) catch |err| switch (err) { @@ -970,7 +974,7 @@ pub fn generateBuiltinMacros(comp: *Compilation, system_defines_mode: SystemDefi return comp.addSourceFromOwnedBuffer("", contents, .user); } -fn writeBuiltinMacros(comp: *Compilation, system_defines_mode: SystemDefinesMode, w: *std.Io.Writer) !void { +fn writeBuiltinMacros(comp: *Compilation, system_defines_mode: SystemDefinesMode, w: *Io.Writer) !void { if (system_defines_mode == .include_system_defines) { try w.writeAll( \\#define __VERSION__ "Aro @@ -1018,7 +1022,7 @@ fn writeBuiltinMacros(comp: *Compilation, system_defines_mode: SystemDefinesMode } } -fn generateFloatMacros(w: *std.Io.Writer, prefix: []const u8, semantics: target_util.FPSemantics, ext: []const u8) !void { +fn generateFloatMacros(w: *Io.Writer, prefix: []const u8, semantics: target_util.FPSemantics, ext: []const u8) !void { const denormMin = semantics.chooseValue( []const u8, .{ @@ -1093,7 +1097,7 @@ fn generateFloatMacros(w: *std.Io.Writer, prefix: []const u8, semantics: target_ try w.print("#define __{s}_MIN__ {s}{s}\n", .{ prefix, min, ext }); } -fn generateTypeMacro(comp: *const Compilation, w: *std.Io.Writer, name: []const u8, qt: QualType) !void { +fn generateTypeMacro(comp: *const Compilation, w: *Io.Writer, name: []const u8, qt: QualType) !void { try w.print("#define {s} ", .{name}); try qt.print(comp, w); try w.writeByte('\n'); @@ -1128,7 +1132,7 @@ fn generateFastOrLeastType( bits: usize, kind: enum { least, fast }, signedness: std.builtin.Signedness, - w: *std.Io.Writer, + w: *Io.Writer, ) !void { const ty = comp.intLeastN(bits, signedness); // defining the fast types as the least types is permitted @@ -1158,7 +1162,7 @@ fn generateFastOrLeastType( try comp.generateFmt(prefix, w, ty); } -fn generateFastAndLeastWidthTypes(comp: *Compilation, w: *std.Io.Writer) !void { +fn generateFastAndLeastWidthTypes(comp: *Compilation, w: *Io.Writer) !void { const sizes = [_]usize{ 8, 16, 32, 64 }; for (sizes) |size| { try comp.generateFastOrLeastType(size, .least, .signed, w); @@ -1168,7 +1172,7 @@ fn generateFastAndLeastWidthTypes(comp: *Compilation, w: *std.Io.Writer) !void { } } -fn generateExactWidthTypes(comp: *Compilation, w: *std.Io.Writer) !void { +fn generateExactWidthTypes(comp: *Compilation, w: *Io.Writer) !void { try comp.generateExactWidthType(w, .schar); if (QualType.short.sizeof(comp) > QualType.char.sizeof(comp)) { @@ -1216,7 +1220,7 @@ fn generateExactWidthTypes(comp: *Compilation, w: *std.Io.Writer) !void { } } -fn generateFmt(comp: *const Compilation, prefix: []const u8, w: *std.Io.Writer, qt: QualType) !void { +fn generateFmt(comp: *const Compilation, prefix: []const u8, w: *Io.Writer, qt: QualType) !void { const unsigned = qt.signedness(comp) == .unsigned; const modifier = qt.formatModifier(comp); const formats = if (unsigned) "ouxX" else "di"; @@ -1225,7 +1229,7 @@ fn generateFmt(comp: *const Compilation, prefix: []const u8, w: *std.Io.Writer, } } -fn generateSuffixMacro(comp: *const Compilation, prefix: []const u8, w: *std.Io.Writer, qt: QualType) !void { +fn generateSuffixMacro(comp: *const Compilation, prefix: []const u8, w: *Io.Writer, qt: QualType) !void { return w.print("#define {s}_C_SUFFIX__ {s}\n", .{ prefix, qt.intValueSuffix(comp) }); } @@ -1233,7 +1237,7 @@ fn generateSuffixMacro(comp: *const Compilation, prefix: []const u8, w: *std.Io. /// Name macro (e.g. #define __UINT32_TYPE__ unsigned int) /// Format strings (e.g. #define __UINT32_FMTu__ "u") /// Suffix macro (e.g. #define __UINT32_C_SUFFIX__ U) -fn generateExactWidthType(comp: *Compilation, w: *std.Io.Writer, original_qt: QualType) !void { +fn generateExactWidthType(comp: *Compilation, w: *Io.Writer, original_qt: QualType) !void { var qt = original_qt; const width = qt.sizeof(comp) * 8; const unsigned = qt.signedness(comp) == .unsigned; @@ -1266,7 +1270,7 @@ pub fn hasHalfPrecisionFloatABI(comp: *const Compilation) bool { return comp.langopts.allow_half_args_and_returns or target_util.hasHalfPrecisionFloatABI(comp.target); } -fn generateIntMax(comp: *const Compilation, w: *std.Io.Writer, name: []const u8, qt: QualType) !void { +fn generateIntMax(comp: *const Compilation, w: *Io.Writer, name: []const u8, qt: QualType) !void { const unsigned = qt.signedness(comp) == .unsigned; const max: u128 = switch (qt.bitSizeof(comp)) { 8 => if (unsigned) std.math.maxInt(u8) else std.math.maxInt(i8), @@ -1290,7 +1294,7 @@ pub fn wcharMax(comp: *const Compilation) u32 { }; } -fn generateExactWidthIntMax(comp: *Compilation, w: *std.Io.Writer, original_qt: QualType) !void { +fn generateExactWidthIntMax(comp: *Compilation, w: *Io.Writer, original_qt: QualType) !void { var qt = original_qt; const bit_count: u8 = @intCast(qt.sizeof(comp) * 8); const unsigned = qt.signedness(comp) == .unsigned; @@ -1307,16 +1311,16 @@ fn generateExactWidthIntMax(comp: *Compilation, w: *std.Io.Writer, original_qt: return comp.generateIntMax(w, name, qt); } -fn generateIntWidth(comp: *Compilation, w: *std.Io.Writer, name: []const u8, qt: QualType) !void { +fn generateIntWidth(comp: *Compilation, w: *Io.Writer, name: []const u8, qt: QualType) !void { try w.print("#define __{s}_WIDTH__ {d}\n", .{ name, qt.sizeof(comp) * 8 }); } -fn generateIntMaxAndWidth(comp: *Compilation, w: *std.Io.Writer, name: []const u8, qt: QualType) !void { +fn generateIntMaxAndWidth(comp: *Compilation, w: *Io.Writer, name: []const u8, qt: QualType) !void { try comp.generateIntMax(w, name, qt); try comp.generateIntWidth(w, name, qt); } -fn generateSizeofType(comp: *Compilation, w: *std.Io.Writer, name: []const u8, qt: QualType) !void { +fn generateSizeofType(comp: *Compilation, w: *Io.Writer, name: []const u8, qt: QualType) !void { try w.print("#define {s} {d}\n", .{ name, qt.sizeof(comp) }); } @@ -1797,7 +1801,7 @@ pub const IncludeType = enum { angle_brackets, }; -fn getPathContents(comp: *Compilation, path: []const u8, limit: std.Io.Limit) ![]u8 { +fn getPathContents(comp: *Compilation, path: []const u8, limit: Io.Limit) ![]u8 { if (mem.indexOfScalar(u8, path, 0) != null) { return error.FileNotFound; } @@ -1807,11 +1811,12 @@ fn getPathContents(comp: *Compilation, path: []const u8, limit: std.Io.Limit) ![ return comp.getFileContents(file, limit); } -fn getFileContents(comp: *Compilation, file: std.fs.File, limit: std.Io.Limit) ![]u8 { +fn getFileContents(comp: *Compilation, file: std.fs.File, limit: Io.Limit) ![]u8 { + const io = comp.io; var file_buf: [4096]u8 = undefined; - var file_reader = file.reader(&file_buf); + var file_reader = file.reader(io, &file_buf); - var allocating: std.Io.Writer.Allocating = .init(comp.gpa); + var allocating: Io.Writer.Allocating = .init(comp.gpa); defer allocating.deinit(); if (file_reader.getSize()) |size| { const limited_size = limit.minInt64(size); @@ -1838,7 +1843,7 @@ pub fn findEmbed( includer_token_source: Source.Id, /// angle bracket vs quotes include_type: IncludeType, - limit: std.Io.Limit, + limit: Io.Limit, opt_dep_file: ?*DepFile, ) !?[]u8 { if (std.fs.path.isAbsolute(filename)) { @@ -2002,8 +2007,7 @@ pub fn locSlice(comp: *const Compilation, loc: Source.Location) []const u8 { pub fn getSourceMTimeUncached(comp: *const Compilation, source_id: Source.Id) ?u64 { const source = comp.getSource(source_id); if (comp.cwd.statFile(source.path)) |stat| { - const mtime = @divTrunc(stat.mtime, std.time.ns_per_s); - return std.math.cast(u64, mtime); + return std.math.cast(u64, stat.mtime.toSeconds()); } else |_| { return null; } diff --git a/lib/compiler/aro/aro/Driver.zig b/lib/compiler/aro/aro/Driver.zig index ec186a28d2..ef871d7d26 100644 --- a/lib/compiler/aro/aro/Driver.zig +++ b/lib/compiler/aro/aro/Driver.zig @@ -273,6 +273,7 @@ pub fn parseArgs( macro_buf: *std.ArrayList(u8), args: []const []const u8, ) (Compilation.Error || std.Io.Writer.Error)!bool { + const io = d.comp.io; var i: usize = 1; var comment_arg: []const u8 = ""; var hosted: ?bool = null; @@ -772,7 +773,7 @@ pub fn parseArgs( opts.arch_os_abi, @errorName(e), }), }; - d.comp.target = std.zig.system.resolveTargetQuery(query) catch |e| { + d.comp.target = std.zig.system.resolveTargetQuery(io, query) catch |e| { return d.fatal("unable to resolve target: {s}", .{errorDescription(e)}); }; } @@ -916,8 +917,7 @@ pub fn errorDescription(e: anyerror) []const u8 { error.NotDir => "is not a directory", error.NotOpenForReading => "file is not open for reading", error.NotOpenForWriting => "file is not open for writing", - error.InvalidUtf8 => "path is not valid UTF-8", - error.InvalidWtf8 => "path is not valid WTF-8", + error.BadPathName => "bad path name", error.FileBusy => "file is busy", error.NameTooLong => "file name is too long", error.AccessDenied => "access denied", diff --git a/lib/compiler/build_runner.zig b/lib/compiler/build_runner.zig index 374bfa6ed3..545cab6083 100644 --- a/lib/compiler/build_runner.zig +++ b/lib/compiler/build_runner.zig @@ -1,5 +1,8 @@ -const std = @import("std"); +const runner = @This(); const builtin = @import("builtin"); + +const std = @import("std"); +const Io = std.Io; const assert = std.debug.assert; const fmt = std.fmt; const mem = std.mem; @@ -11,7 +14,6 @@ const WebServer = std.Build.WebServer; const Allocator = std.mem.Allocator; const fatal = std.process.fatal; const Writer = std.Io.Writer; -const runner = @This(); const tty = std.Io.tty; pub const root = @import("@build"); @@ -38,6 +40,10 @@ pub fn main() !void { const args = try process.argsAlloc(arena); + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + // skip my own exe name var arg_idx: usize = 1; @@ -68,8 +74,10 @@ pub fn main() !void { }; var graph: std.Build.Graph = .{ + .io = io, .arena = arena, .cache = .{ + .io = io, .gpa = arena, .manifest_dir = try local_cache_directory.handle.makeOpenPath("h", .{}), }, @@ -79,7 +87,7 @@ pub fn main() !void { .zig_lib_directory = zig_lib_directory, .host = .{ .query = .{}, - .result = try std.zig.system.resolveTargetQuery(.{}), + .result = try std.zig.system.resolveTargetQuery(io, .{}), }, .time_report = false, }; @@ -116,7 +124,7 @@ pub fn main() !void { var watch = false; var fuzz: ?std.Build.Fuzz.Mode = null; var debounce_interval_ms: u16 = 50; - var webui_listen: ?std.net.Address = null; + var webui_listen: ?Io.net.IpAddress = null; if (try std.zig.EnvVar.ZIG_BUILD_ERROR_STYLE.get(arena)) |str| { if (std.meta.stringToEnum(ErrorStyle, str)) |style| { @@ -283,11 +291,11 @@ pub fn main() !void { }); }; } else if (mem.eql(u8, arg, "--webui")) { - webui_listen = std.net.Address.parseIp("::1", 0) catch unreachable; + if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) }; } else if (mem.startsWith(u8, arg, "--webui=")) { const addr_str = arg["--webui=".len..]; if (std.mem.eql(u8, addr_str, "-")) fatal("web interface cannot listen on stdio", .{}); - webui_listen = std.net.Address.parseIpAndPort(addr_str) catch |err| { + webui_listen = Io.net.IpAddress.parseLiteral(addr_str) catch |err| { fatal("invalid web UI address '{s}': {s}", .{ addr_str, @errorName(err) }); }; } else if (mem.eql(u8, arg, "--debug-log")) { @@ -329,14 +337,10 @@ pub fn main() !void { watch = true; } else if (mem.eql(u8, arg, "--time-report")) { graph.time_report = true; - if (webui_listen == null) { - webui_listen = std.net.Address.parseIp("::1", 0) catch unreachable; - } + if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) }; } else if (mem.eql(u8, arg, "--fuzz")) { fuzz = .{ .forever = undefined }; - if (webui_listen == null) { - webui_listen = std.net.Address.parseIp("::1", 0) catch unreachable; - } + if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) }; } else if (mem.startsWith(u8, arg, "--fuzz=")) { const value = arg["--fuzz=".len..]; if (value.len == 0) fatal("missing argument to --fuzz", .{}); @@ -545,13 +549,15 @@ pub fn main() !void { var w: Watch = w: { if (!watch) break :w undefined; - if (!Watch.have_impl) fatal("--watch not yet implemented for {s}", .{@tagName(builtin.os.tag)}); + if (!Watch.have_impl) fatal("--watch not yet implemented for {t}", .{builtin.os.tag}); break :w try .init(); }; try run.thread_pool.init(thread_pool_options); defer run.thread_pool.deinit(); + const now = Io.Clock.Timestamp.now(io, .awake) catch |err| fatal("failed to collect timestamp: {t}", .{err}); + run.web_server = if (webui_listen) |listen_address| ws: { if (builtin.single_threaded) unreachable; // `fatal` above break :ws .init(.{ @@ -563,11 +569,12 @@ pub fn main() !void { .root_prog_node = main_progress_node, .watch = watch, .listen_address = listen_address, + .base_timestamp = now, }); } else null; if (run.web_server) |*ws| { - ws.start() catch |err| fatal("failed to start web server: {s}", .{@errorName(err)}); + ws.start() catch |err| fatal("failed to start web server: {t}", .{err}); } rebuild: while (true) : (if (run.error_style.clearOnUpdate()) { @@ -750,6 +757,7 @@ fn runStepNames( fuzz: ?std.Build.Fuzz.Mode, ) !void { const gpa = run.gpa; + const io = b.graph.io; const step_stack = &run.step_stack; const thread_pool = &run.thread_pool; @@ -853,6 +861,7 @@ fn runStepNames( assert(mode == .limit); var f = std.Build.Fuzz.init( gpa, + io, thread_pool, step_stack.keys(), parent_prog_node, diff --git a/lib/compiler/libc.zig b/lib/compiler/libc.zig index ef3aabb6dc..a18a7a0e06 100644 --- a/lib/compiler/libc.zig +++ b/lib/compiler/libc.zig @@ -29,6 +29,10 @@ pub fn main() !void { const arena = arena_instance.allocator(); const gpa = arena; + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + const args = try std.process.argsAlloc(arena); const zig_lib_directory = args[1]; @@ -66,7 +70,7 @@ pub fn main() !void { const target_query = std.zig.parseTargetQueryOrReportFatalError(gpa, .{ .arch_os_abi = target_arch_os_abi, }); - const target = std.zig.resolveTargetQueryOrFatal(target_query); + const target = std.zig.resolveTargetQueryOrFatal(io, target_query); if (print_includes) { const libc_installation: ?*LibCInstallation = libc: { diff --git a/lib/compiler/objcopy.zig b/lib/compiler/objcopy.zig index 5908f8b73d..ee7456a87c 100644 --- a/lib/compiler/objcopy.zig +++ b/lib/compiler/objcopy.zig @@ -29,7 +29,6 @@ pub fn main() !void { } fn cmdObjCopy(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { - _ = gpa; var i: usize = 0; var opt_out_fmt: ?std.Target.ObjectFormat = null; var opt_input: ?[]const u8 = null; @@ -148,12 +147,16 @@ fn cmdObjCopy(gpa: Allocator, arena: Allocator, args: []const []const u8) !void const input = opt_input orelse fatal("expected input parameter", .{}); const output = opt_output orelse fatal("expected output parameter", .{}); + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + const input_file = fs.cwd().openFile(input, .{}) catch |err| fatal("failed to open {s}: {t}", .{ input, err }); defer input_file.close(); const stat = input_file.stat() catch |err| fatal("failed to stat {s}: {t}", .{ input, err }); - var in: File.Reader = .initSize(input_file, &input_buffer, stat.size); + var in: File.Reader = .initSize(input_file.adaptToNewApi(), io, &input_buffer, stat.size); const elf_hdr = std.elf.Header.read(&in.interface) catch |err| switch (err) { error.ReadFailed => fatal("unable to read {s}: {t}", .{ input, in.err.? }), @@ -218,7 +221,7 @@ fn cmdObjCopy(gpa: Allocator, arena: Allocator, args: []const []const u8) !void try out.end(); if (listen) { - var stdin_reader = fs.File.stdin().reader(&stdin_buffer); + var stdin_reader = fs.File.stdin().reader(io, &stdin_buffer); var stdout_writer = fs.File.stdout().writer(&stdout_buffer); var server = try Server.init(.{ .in = &stdin_reader.interface, diff --git a/lib/compiler/resinator/compile.zig b/lib/compiler/resinator/compile.zig index fbb3797fde..df1bbbcddf 100644 --- a/lib/compiler/resinator/compile.zig +++ b/lib/compiler/resinator/compile.zig @@ -1,6 +1,12 @@ -const std = @import("std"); const builtin = @import("builtin"); +const native_endian = builtin.cpu.arch.endian(); + +const std = @import("std"); +const Io = std.Io; const Allocator = std.mem.Allocator; +const WORD = std.os.windows.WORD; +const DWORD = std.os.windows.DWORD; + const Node = @import("ast.zig").Node; const lex = @import("lex.zig"); const Parser = @import("parse.zig").Parser; @@ -17,8 +23,6 @@ const res = @import("res.zig"); const ico = @import("ico.zig"); const ani = @import("ani.zig"); const bmp = @import("bmp.zig"); -const WORD = std.os.windows.WORD; -const DWORD = std.os.windows.DWORD; const utils = @import("utils.zig"); const NameOrOrdinal = res.NameOrOrdinal; const SupportedCodePage = @import("code_pages.zig").SupportedCodePage; @@ -28,7 +32,6 @@ const windows1252 = @import("windows1252.zig"); const lang = @import("lang.zig"); const code_pages = @import("code_pages.zig"); const errors = @import("errors.zig"); -const native_endian = builtin.cpu.arch.endian(); pub const CompileOptions = struct { cwd: std.fs.Dir, @@ -77,7 +80,7 @@ pub const Dependencies = struct { } }; -pub fn compile(allocator: Allocator, source: []const u8, writer: *std.Io.Writer, options: CompileOptions) !void { +pub fn compile(allocator: Allocator, io: Io, source: []const u8, writer: *std.Io.Writer, options: CompileOptions) !void { var lexer = lex.Lexer.init(source, .{ .default_code_page = options.default_code_page, .source_mappings = options.source_mappings, @@ -166,10 +169,11 @@ pub fn compile(allocator: Allocator, source: []const u8, writer: *std.Io.Writer, defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); - var compiler = Compiler{ + var compiler: Compiler = .{ .source = source, .arena = arena, .allocator = allocator, + .io = io, .cwd = options.cwd, .diagnostics = options.diagnostics, .dependencies = options.dependencies, @@ -191,6 +195,7 @@ pub const Compiler = struct { source: []const u8, arena: Allocator, allocator: Allocator, + io: Io, cwd: std.fs.Dir, state: State = .{}, diagnostics: *Diagnostics, @@ -409,7 +414,7 @@ pub const Compiler = struct { } } - var first_error: ?std.fs.File.OpenError = null; + var first_error: ?(std.fs.File.OpenError || std.fs.File.StatError) = null; for (self.search_dirs) |search_dir| { if (utils.openFileNotDir(search_dir.dir, path, .{})) |file| { errdefer file.close(); @@ -496,6 +501,8 @@ pub const Compiler = struct { } pub fn writeResourceExternal(self: *Compiler, node: *Node.ResourceExternal, writer: *std.Io.Writer) !void { + const io = self.io; + // Init header with data size zero for now, will need to fill it in later var header = try self.resourceHeader(node.id, node.type, .{}); defer header.deinit(self.allocator); @@ -582,7 +589,7 @@ pub const Compiler = struct { }; defer file_handle.close(); var file_buffer: [2048]u8 = undefined; - var file_reader = file_handle.reader(&file_buffer); + var file_reader = file_handle.reader(io, &file_buffer); if (maybe_predefined_type) |predefined_type| { switch (predefined_type) { diff --git a/lib/compiler/resinator/cvtres.zig b/lib/compiler/resinator/cvtres.zig index 50d2c6e96a..26b6620af2 100644 --- a/lib/compiler/resinator/cvtres.zig +++ b/lib/compiler/resinator/cvtres.zig @@ -1,5 +1,7 @@ const std = @import("std"); +const Io = std.Io; const Allocator = std.mem.Allocator; + const res = @import("res.zig"); const NameOrOrdinal = res.NameOrOrdinal; const MemoryFlags = res.MemoryFlags; @@ -169,8 +171,7 @@ pub fn parseNameOrOrdinal(allocator: Allocator, reader: *std.Io.Reader) !NameOrO pub const CoffOptions = struct { target: std.coff.IMAGE.FILE.MACHINE = .AMD64, - /// If true, zeroes will be written to all timestamp fields - reproducible: bool = true, + timestamp: i64 = 0, /// If true, the MEM_WRITE flag will not be set in the .rsrc section header read_only: bool = false, /// If non-null, a symbol with this name and storage class EXTERNAL will be added to the symbol table. @@ -188,7 +189,13 @@ pub const Diagnostics = union { overflow_resource: usize, }; -pub fn writeCoff(allocator: Allocator, writer: *std.Io.Writer, resources: []const Resource, options: CoffOptions, diagnostics: ?*Diagnostics) !void { +pub fn writeCoff( + allocator: Allocator, + writer: *std.Io.Writer, + resources: []const Resource, + options: CoffOptions, + diagnostics: ?*Diagnostics, +) !void { var resource_tree = ResourceTree.init(allocator, options); defer resource_tree.deinit(); @@ -215,7 +222,7 @@ pub fn writeCoff(allocator: Allocator, writer: *std.Io.Writer, resources: []cons const pointer_to_rsrc02_data = pointer_to_relocations + relocations_len; const pointer_to_symbol_table = pointer_to_rsrc02_data + lengths.rsrc02; - const timestamp: i64 = if (options.reproducible) 0 else std.time.timestamp(); + const timestamp: i64 = options.timestamp; const size_of_optional_header = 0; const machine_type: std.coff.IMAGE.FILE.MACHINE = options.target; const flags = std.coff.Header.Flags{ diff --git a/lib/compiler/resinator/errors.zig b/lib/compiler/resinator/errors.zig index 1d9cbf4c5b..aad74a3ca3 100644 --- a/lib/compiler/resinator/errors.zig +++ b/lib/compiler/resinator/errors.zig @@ -1,5 +1,11 @@ +const builtin = @import("builtin"); +const native_endian = builtin.cpu.arch.endian(); + const std = @import("std"); +const Io = std.Io; const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + const Token = @import("lex.zig").Token; const SourceMappings = @import("source_mapping.zig").SourceMappings; const utils = @import("utils.zig"); @@ -11,19 +17,19 @@ const parse = @import("parse.zig"); const lang = @import("lang.zig"); const code_pages = @import("code_pages.zig"); const SupportedCodePage = code_pages.SupportedCodePage; -const builtin = @import("builtin"); -const native_endian = builtin.cpu.arch.endian(); pub const Diagnostics = struct { errors: std.ArrayList(ErrorDetails) = .empty, /// Append-only, cannot handle removing strings. /// Expects to own all strings within the list. strings: std.ArrayList([]const u8) = .empty, - allocator: std.mem.Allocator, + allocator: Allocator, + io: Io, - pub fn init(allocator: std.mem.Allocator) Diagnostics { + pub fn init(allocator: Allocator, io: Io) Diagnostics { return .{ .allocator = allocator, + .io = io, }; } @@ -62,10 +68,11 @@ pub const Diagnostics = struct { } pub fn renderToStdErr(self: *Diagnostics, cwd: std.fs.Dir, source: []const u8, tty_config: std.Io.tty.Config, source_mappings: ?SourceMappings) void { + const io = self.io; const stderr = std.debug.lockStderrWriter(&.{}); defer std.debug.unlockStderrWriter(); for (self.errors.items) |err_details| { - renderErrorMessage(stderr, tty_config, cwd, err_details, source, self.strings.items, source_mappings) catch return; + renderErrorMessage(io, stderr, tty_config, cwd, err_details, source, self.strings.items, source_mappings) catch return; } } @@ -167,9 +174,9 @@ pub const ErrorDetails = struct { filename_string_index: FilenameStringIndex, pub const FilenameStringIndex = std.meta.Int(.unsigned, 32 - @bitSizeOf(FileOpenErrorEnum)); - pub const FileOpenErrorEnum = std.meta.FieldEnum(std.fs.File.OpenError); + pub const FileOpenErrorEnum = std.meta.FieldEnum(std.fs.File.OpenError || std.fs.File.StatError); - pub fn enumFromError(err: std.fs.File.OpenError) FileOpenErrorEnum { + pub fn enumFromError(err: (std.fs.File.OpenError || std.fs.File.StatError)) FileOpenErrorEnum { return switch (err) { inline else => |e| @field(ErrorDetails.FileOpenError.FileOpenErrorEnum, @errorName(e)), }; @@ -894,7 +901,16 @@ fn cellCount(code_page: SupportedCodePage, source: []const u8, start_index: usiz const truncated_str = "<...truncated...>"; -pub fn renderErrorMessage(writer: *std.Io.Writer, tty_config: std.Io.tty.Config, cwd: std.fs.Dir, err_details: ErrorDetails, source: []const u8, strings: []const []const u8, source_mappings: ?SourceMappings) !void { +pub fn renderErrorMessage( + io: Io, + writer: *std.Io.Writer, + tty_config: std.Io.tty.Config, + cwd: std.fs.Dir, + err_details: ErrorDetails, + source: []const u8, + strings: []const []const u8, + source_mappings: ?SourceMappings, +) !void { if (err_details.type == .hint) return; const source_line_start = err_details.token.getLineStartForErrorDisplay(source); @@ -989,6 +1005,7 @@ pub fn renderErrorMessage(writer: *std.Io.Writer, tty_config: std.Io.tty.Config, var initial_lines_err: ?anyerror = null; var file_reader_buf: [max_source_line_bytes * 2]u8 = undefined; var corresponding_lines: ?CorrespondingLines = CorrespondingLines.init( + io, cwd, err_details, source_line_for_display.line, @@ -1084,6 +1101,7 @@ const CorrespondingLines = struct { code_page: SupportedCodePage, pub fn init( + io: Io, cwd: std.fs.Dir, err_details: ErrorDetails, line_for_comparison: []const u8, @@ -1108,7 +1126,7 @@ const CorrespondingLines = struct { .code_page = err_details.code_page, .file_reader = undefined, }; - corresponding_lines.file_reader = corresponding_lines.file.reader(file_reader_buf); + corresponding_lines.file_reader = corresponding_lines.file.reader(io, file_reader_buf); errdefer corresponding_lines.deinit(); try corresponding_lines.writeLineFromStreamVerbatim( diff --git a/lib/compiler/resinator/main.zig b/lib/compiler/resinator/main.zig index ce8532ae9f..b32237b06b 100644 --- a/lib/compiler/resinator/main.zig +++ b/lib/compiler/resinator/main.zig @@ -1,5 +1,9 @@ -const std = @import("std"); const builtin = @import("builtin"); + +const std = @import("std"); +const Io = std.Io; +const Allocator = std.mem.Allocator; + const removeComments = @import("comments.zig").removeComments; const parseAndRemoveLineCommands = @import("source_mapping.zig").parseAndRemoveLineCommands; const compile = @import("compile.zig").compile; @@ -16,19 +20,18 @@ const aro = @import("aro"); const compiler_util = @import("../util.zig"); pub fn main() !void { - var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; - defer std.debug.assert(gpa.deinit() == .ok); - const allocator = gpa.allocator(); + var debug_allocator: std.heap.DebugAllocator(.{}) = .init; + defer std.debug.assert(debug_allocator.deinit() == .ok); + const gpa = debug_allocator.allocator(); - var arena_state = std.heap.ArenaAllocator.init(allocator); + var arena_state = std.heap.ArenaAllocator.init(gpa); defer arena_state.deinit(); const arena = arena_state.allocator(); const stderr = std.fs.File.stderr(); const stderr_config = std.Io.tty.detectConfig(stderr); - const args = try std.process.argsAlloc(allocator); - defer std.process.argsFree(allocator, args); + const args = try std.process.argsAlloc(arena); if (args.len < 2) { try renderErrorMessage(std.debug.lockStderrWriter(&.{}), stderr_config, .err, "expected zig lib dir as first argument", .{}); @@ -59,11 +62,11 @@ pub fn main() !void { }; var options = options: { - var cli_diagnostics = cli.Diagnostics.init(allocator); + var cli_diagnostics = cli.Diagnostics.init(gpa); defer cli_diagnostics.deinit(); - var options = cli.parse(allocator, cli_args, &cli_diagnostics) catch |err| switch (err) { + var options = cli.parse(gpa, cli_args, &cli_diagnostics) catch |err| switch (err) { error.ParseError => { - try error_handler.emitCliDiagnostics(allocator, cli_args, &cli_diagnostics); + try error_handler.emitCliDiagnostics(gpa, cli_args, &cli_diagnostics); std.process.exit(1); }, else => |e| return e, @@ -84,6 +87,10 @@ pub fn main() !void { }; defer options.deinit(); + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + if (options.print_help_and_exit) { try cli.writeUsage(stdout, "zig rc"); try stdout.flush(); @@ -99,12 +106,13 @@ pub fn main() !void { try stdout.flush(); } - var dependencies = Dependencies.init(allocator); + var dependencies = Dependencies.init(gpa); defer dependencies.deinit(); const maybe_dependencies: ?*Dependencies = if (options.depfile_path != null) &dependencies else null; var include_paths = LazyIncludePaths{ .arena = arena, + .io = io, .auto_includes_option = options.auto_includes, .zig_lib_dir = zig_lib_dir, .target_machine_type = options.coff_options.target, @@ -112,12 +120,12 @@ pub fn main() !void { const full_input = full_input: { if (options.input_format == .rc and options.preprocess != .no) { - var preprocessed_buf: std.Io.Writer.Allocating = .init(allocator); + var preprocessed_buf: std.Io.Writer.Allocating = .init(gpa); errdefer preprocessed_buf.deinit(); // We're going to throw away everything except the final preprocessed output anyway, // so we can use a scoped arena for everything else. - var aro_arena_state = std.heap.ArenaAllocator.init(allocator); + var aro_arena_state = std.heap.ArenaAllocator.init(gpa); defer aro_arena_state.deinit(); const aro_arena = aro_arena_state.allocator(); @@ -129,12 +137,12 @@ pub fn main() !void { .color = stderr_config, } } }, true => .{ .output = .{ .to_list = .{ - .arena = .init(allocator), + .arena = .init(gpa), } } }, }; defer diagnostics.deinit(); - var comp = aro.Compilation.init(aro_arena, aro_arena, &diagnostics, std.fs.cwd()); + var comp = aro.Compilation.init(aro_arena, aro_arena, io, &diagnostics, std.fs.cwd()); defer comp.deinit(); var argv: std.ArrayList([]const u8) = .empty; @@ -159,20 +167,20 @@ pub fn main() !void { preprocess.preprocess(&comp, &preprocessed_buf.writer, argv.items, maybe_dependencies) catch |err| switch (err) { error.GeneratedSourceError => { - try error_handler.emitAroDiagnostics(allocator, "failed during preprocessor setup (this is always a bug)", &comp); + try error_handler.emitAroDiagnostics(gpa, "failed during preprocessor setup (this is always a bug)", &comp); std.process.exit(1); }, // ArgError can occur if e.g. the .rc file is not found error.ArgError, error.PreprocessError => { - try error_handler.emitAroDiagnostics(allocator, "failed during preprocessing", &comp); + try error_handler.emitAroDiagnostics(gpa, "failed during preprocessing", &comp); std.process.exit(1); }, error.FileTooBig => { - try error_handler.emitMessage(allocator, .err, "failed during preprocessing: maximum file size exceeded", .{}); + try error_handler.emitMessage(gpa, .err, "failed during preprocessing: maximum file size exceeded", .{}); std.process.exit(1); }, error.WriteFailed => { - try error_handler.emitMessage(allocator, .err, "failed during preprocessing: error writing the preprocessed output", .{}); + try error_handler.emitMessage(gpa, .err, "failed during preprocessing: error writing the preprocessed output", .{}); std.process.exit(1); }, error.OutOfMemory => |e| return e, @@ -182,22 +190,22 @@ pub fn main() !void { } else { switch (options.input_source) { .stdio => |file| { - var file_reader = file.reader(&.{}); - break :full_input file_reader.interface.allocRemaining(allocator, .unlimited) catch |err| { - try error_handler.emitMessage(allocator, .err, "unable to read input from stdin: {s}", .{@errorName(err)}); + var file_reader = file.reader(io, &.{}); + break :full_input file_reader.interface.allocRemaining(gpa, .unlimited) catch |err| { + try error_handler.emitMessage(gpa, .err, "unable to read input from stdin: {s}", .{@errorName(err)}); std.process.exit(1); }; }, .filename => |input_filename| { - break :full_input std.fs.cwd().readFileAlloc(input_filename, allocator, .unlimited) catch |err| { - try error_handler.emitMessage(allocator, .err, "unable to read input file path '{s}': {s}", .{ input_filename, @errorName(err) }); + break :full_input std.fs.cwd().readFileAlloc(input_filename, gpa, .unlimited) catch |err| { + try error_handler.emitMessage(gpa, .err, "unable to read input file path '{s}': {s}", .{ input_filename, @errorName(err) }); std.process.exit(1); }; }, } } }; - defer allocator.free(full_input); + defer gpa.free(full_input); if (options.preprocess == .only) { switch (options.output_source) { @@ -221,55 +229,55 @@ pub fn main() !void { } else if (options.input_format == .res) IoStream.fromIoSource(options.input_source, .input) catch |err| { - try error_handler.emitMessage(allocator, .err, "unable to read res file path '{s}': {s}", .{ options.input_source.filename, @errorName(err) }); + try error_handler.emitMessage(gpa, .err, "unable to read res file path '{s}': {s}", .{ options.input_source.filename, @errorName(err) }); std.process.exit(1); } else IoStream.fromIoSource(options.output_source, .output) catch |err| { - try error_handler.emitMessage(allocator, .err, "unable to create output file '{s}': {s}", .{ options.output_source.filename, @errorName(err) }); + try error_handler.emitMessage(gpa, .err, "unable to create output file '{s}': {s}", .{ options.output_source.filename, @errorName(err) }); std.process.exit(1); }; - defer res_stream.deinit(allocator); + defer res_stream.deinit(gpa); const res_data = res_data: { if (options.input_format != .res) { // Note: We still want to run this when no-preprocess is set because: // 1. We want to print accurate line numbers after removing multiline comments // 2. We want to be able to handle an already-preprocessed input with #line commands in it - var mapping_results = parseAndRemoveLineCommands(allocator, full_input, full_input, .{ .initial_filename = options.input_source.filename }) catch |err| switch (err) { + var mapping_results = parseAndRemoveLineCommands(gpa, full_input, full_input, .{ .initial_filename = options.input_source.filename }) catch |err| switch (err) { error.InvalidLineCommand => { // TODO: Maybe output the invalid line command - try error_handler.emitMessage(allocator, .err, "invalid line command in the preprocessed source", .{}); + try error_handler.emitMessage(gpa, .err, "invalid line command in the preprocessed source", .{}); if (options.preprocess == .no) { - try error_handler.emitMessage(allocator, .note, "line commands must be of the format: #line \"\"", .{}); + try error_handler.emitMessage(gpa, .note, "line commands must be of the format: #line \"\"", .{}); } else { - try error_handler.emitMessage(allocator, .note, "this is likely to be a bug, please report it", .{}); + try error_handler.emitMessage(gpa, .note, "this is likely to be a bug, please report it", .{}); } std.process.exit(1); }, error.LineNumberOverflow => { // TODO: Better error message - try error_handler.emitMessage(allocator, .err, "line number count exceeded maximum of {}", .{std.math.maxInt(usize)}); + try error_handler.emitMessage(gpa, .err, "line number count exceeded maximum of {}", .{std.math.maxInt(usize)}); std.process.exit(1); }, error.OutOfMemory => |e| return e, }; - defer mapping_results.mappings.deinit(allocator); + defer mapping_results.mappings.deinit(gpa); const default_code_page = options.default_code_page orelse .windows1252; const has_disjoint_code_page = hasDisjointCodePage(mapping_results.result, &mapping_results.mappings, default_code_page); const final_input = try removeComments(mapping_results.result, mapping_results.result, &mapping_results.mappings); - var diagnostics = Diagnostics.init(allocator); + var diagnostics = Diagnostics.init(gpa, io); defer diagnostics.deinit(); var output_buffer: [4096]u8 = undefined; - var res_stream_writer = res_stream.source.writer(allocator, &output_buffer); + var res_stream_writer = res_stream.source.writer(gpa, &output_buffer); defer res_stream_writer.deinit(&res_stream.source); const output_buffered_stream = res_stream_writer.interface(); - compile(allocator, final_input, output_buffered_stream, .{ + compile(gpa, io, final_input, output_buffered_stream, .{ .cwd = std.fs.cwd(), .diagnostics = &diagnostics, .source_mappings = &mapping_results.mappings, @@ -287,7 +295,7 @@ pub fn main() !void { .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page, }) catch |err| switch (err) { error.ParseError, error.CompileError => { - try error_handler.emitDiagnostics(allocator, std.fs.cwd(), final_input, &diagnostics, mapping_results.mappings); + try error_handler.emitDiagnostics(gpa, std.fs.cwd(), final_input, &diagnostics, mapping_results.mappings); // Delete the output file on error res_stream.cleanupAfterError(); std.process.exit(1); @@ -305,7 +313,7 @@ pub fn main() !void { // write the depfile if (options.depfile_path) |depfile_path| { var depfile = std.fs.cwd().createFile(depfile_path, .{}) catch |err| { - try error_handler.emitMessage(allocator, .err, "unable to create depfile '{s}': {s}", .{ depfile_path, @errorName(err) }); + try error_handler.emitMessage(gpa, .err, "unable to create depfile '{s}': {s}", .{ depfile_path, @errorName(err) }); std.process.exit(1); }; defer depfile.close(); @@ -332,41 +340,41 @@ pub fn main() !void { if (options.output_format != .coff) return; - break :res_data res_stream.source.readAll(allocator) catch |err| { - try error_handler.emitMessage(allocator, .err, "unable to read res from '{s}': {s}", .{ res_stream.name, @errorName(err) }); + break :res_data res_stream.source.readAll(gpa, io) catch |err| { + try error_handler.emitMessage(gpa, .err, "unable to read res from '{s}': {s}", .{ res_stream.name, @errorName(err) }); std.process.exit(1); }; }; // No need to keep the res_data around after parsing the resources from it - defer res_data.deinit(allocator); + defer res_data.deinit(gpa); std.debug.assert(options.output_format == .coff); // TODO: Maybe use a buffered file reader instead of reading file into memory -> fbs var res_reader: std.Io.Reader = .fixed(res_data.bytes); - break :resources cvtres.parseRes(allocator, &res_reader, .{ .max_size = res_data.bytes.len }) catch |err| { + break :resources cvtres.parseRes(gpa, &res_reader, .{ .max_size = res_data.bytes.len }) catch |err| { // TODO: Better errors - try error_handler.emitMessage(allocator, .err, "unable to parse res from '{s}': {s}", .{ res_stream.name, @errorName(err) }); + try error_handler.emitMessage(gpa, .err, "unable to parse res from '{s}': {s}", .{ res_stream.name, @errorName(err) }); std.process.exit(1); }; }; defer resources.deinit(); var coff_stream = IoStream.fromIoSource(options.output_source, .output) catch |err| { - try error_handler.emitMessage(allocator, .err, "unable to create output file '{s}': {s}", .{ options.output_source.filename, @errorName(err) }); + try error_handler.emitMessage(gpa, .err, "unable to create output file '{s}': {s}", .{ options.output_source.filename, @errorName(err) }); std.process.exit(1); }; - defer coff_stream.deinit(allocator); + defer coff_stream.deinit(gpa); var coff_output_buffer: [4096]u8 = undefined; - var coff_output_buffered_stream = coff_stream.source.writer(allocator, &coff_output_buffer); + var coff_output_buffered_stream = coff_stream.source.writer(gpa, &coff_output_buffer); var cvtres_diagnostics: cvtres.Diagnostics = .{ .none = {} }; - cvtres.writeCoff(allocator, coff_output_buffered_stream.interface(), resources.list.items, options.coff_options, &cvtres_diagnostics) catch |err| { + cvtres.writeCoff(gpa, coff_output_buffered_stream.interface(), resources.list.items, options.coff_options, &cvtres_diagnostics) catch |err| { switch (err) { error.DuplicateResource => { const duplicate_resource = resources.list.items[cvtres_diagnostics.duplicate_resource]; - try error_handler.emitMessage(allocator, .err, "duplicate resource [id: {f}, type: {f}, language: {f}]", .{ + try error_handler.emitMessage(gpa, .err, "duplicate resource [id: {f}, type: {f}, language: {f}]", .{ duplicate_resource.name_value, fmtResourceType(duplicate_resource.type_value), duplicate_resource.language, @@ -374,8 +382,8 @@ pub fn main() !void { }, error.ResourceDataTooLong => { const overflow_resource = resources.list.items[cvtres_diagnostics.duplicate_resource]; - try error_handler.emitMessage(allocator, .err, "resource has a data length that is too large to be written into a coff section", .{}); - try error_handler.emitMessage(allocator, .note, "the resource with the invalid size is [id: {f}, type: {f}, language: {f}]", .{ + try error_handler.emitMessage(gpa, .err, "resource has a data length that is too large to be written into a coff section", .{}); + try error_handler.emitMessage(gpa, .note, "the resource with the invalid size is [id: {f}, type: {f}, language: {f}]", .{ overflow_resource.name_value, fmtResourceType(overflow_resource.type_value), overflow_resource.language, @@ -383,15 +391,15 @@ pub fn main() !void { }, error.TotalResourceDataTooLong => { const overflow_resource = resources.list.items[cvtres_diagnostics.duplicate_resource]; - try error_handler.emitMessage(allocator, .err, "total resource data exceeds the maximum of the coff 'size of raw data' field", .{}); - try error_handler.emitMessage(allocator, .note, "size overflow occurred when attempting to write this resource: [id: {f}, type: {f}, language: {f}]", .{ + try error_handler.emitMessage(gpa, .err, "total resource data exceeds the maximum of the coff 'size of raw data' field", .{}); + try error_handler.emitMessage(gpa, .note, "size overflow occurred when attempting to write this resource: [id: {f}, type: {f}, language: {f}]", .{ overflow_resource.name_value, fmtResourceType(overflow_resource.type_value), overflow_resource.language, }); }, else => { - try error_handler.emitMessage(allocator, .err, "unable to write coff output file '{s}': {s}", .{ coff_stream.name, @errorName(err) }); + try error_handler.emitMessage(gpa, .err, "unable to write coff output file '{s}': {s}", .{ coff_stream.name, @errorName(err) }); }, } // Delete the output file on error @@ -423,7 +431,7 @@ const IoStream = struct { }; } - pub fn deinit(self: *IoStream, allocator: std.mem.Allocator) void { + pub fn deinit(self: *IoStream, allocator: Allocator) void { self.source.deinit(allocator); } @@ -458,7 +466,7 @@ const IoStream = struct { } } - pub fn deinit(self: *Source, allocator: std.mem.Allocator) void { + pub fn deinit(self: *Source, allocator: Allocator) void { switch (self.*) { .file => |file| file.close(), .stdio => {}, @@ -471,18 +479,18 @@ const IoStream = struct { bytes: []const u8, needs_free: bool, - pub fn deinit(self: Data, allocator: std.mem.Allocator) void { + pub fn deinit(self: Data, allocator: Allocator) void { if (self.needs_free) { allocator.free(self.bytes); } } }; - pub fn readAll(self: Source, allocator: std.mem.Allocator) !Data { + pub fn readAll(self: Source, allocator: Allocator, io: Io) !Data { return switch (self) { inline .file, .stdio => |file| .{ .bytes = b: { - var file_reader = file.reader(&.{}); + var file_reader = file.reader(io, &.{}); break :b try file_reader.interface.allocRemaining(allocator, .unlimited); }, .needs_free = true, @@ -496,7 +504,7 @@ const IoStream = struct { file: std.fs.File.Writer, allocating: std.Io.Writer.Allocating, - pub const Error = std.mem.Allocator.Error || std.fs.File.WriteError; + pub const Error = Allocator.Error || std.fs.File.WriteError; pub fn interface(this: *@This()) *std.Io.Writer { return switch (this.*) { @@ -514,7 +522,7 @@ const IoStream = struct { } }; - pub fn writer(source: *Source, allocator: std.mem.Allocator, buffer: []u8) Writer { + pub fn writer(source: *Source, allocator: Allocator, buffer: []u8) Writer { return switch (source.*) { .file, .stdio => |file| .{ .file = file.writer(buffer) }, .memory => |*list| .{ .allocating = .fromArrayList(allocator, list) }, @@ -525,17 +533,20 @@ const IoStream = struct { }; const LazyIncludePaths = struct { - arena: std.mem.Allocator, + arena: Allocator, + io: Io, auto_includes_option: cli.Options.AutoIncludes, zig_lib_dir: []const u8, target_machine_type: std.coff.IMAGE.FILE.MACHINE, resolved_include_paths: ?[]const []const u8 = null, pub fn get(self: *LazyIncludePaths, error_handler: *ErrorHandler) ![]const []const u8 { + const io = self.io; + if (self.resolved_include_paths) |include_paths| return include_paths; - return getIncludePaths(self.arena, self.auto_includes_option, self.zig_lib_dir, self.target_machine_type) catch |err| switch (err) { + return getIncludePaths(self.arena, io, self.auto_includes_option, self.zig_lib_dir, self.target_machine_type) catch |err| switch (err) { error.OutOfMemory => |e| return e, else => |e| { switch (e) { @@ -556,7 +567,13 @@ const LazyIncludePaths = struct { } }; -fn getIncludePaths(arena: std.mem.Allocator, auto_includes_option: cli.Options.AutoIncludes, zig_lib_dir: []const u8, target_machine_type: std.coff.IMAGE.FILE.MACHINE) ![]const []const u8 { +fn getIncludePaths( + arena: Allocator, + io: Io, + auto_includes_option: cli.Options.AutoIncludes, + zig_lib_dir: []const u8, + target_machine_type: std.coff.IMAGE.FILE.MACHINE, +) ![]const []const u8 { if (auto_includes_option == .none) return &[_][]const u8{}; const includes_arch: std.Target.Cpu.Arch = switch (target_machine_type) { @@ -600,7 +617,7 @@ fn getIncludePaths(arena: std.mem.Allocator, auto_includes_option: cli.Options.A .cpu_arch = includes_arch, .abi = .msvc, }; - const target = std.zig.resolveTargetQueryOrFatal(target_query); + const target = std.zig.resolveTargetQueryOrFatal(io, target_query); const is_native_abi = target_query.isNativeAbi(); const detected_libc = std.zig.LibCDirs.detect(arena, zig_lib_dir, &target, is_native_abi, true, null) catch { if (includes == .any) { @@ -626,7 +643,7 @@ fn getIncludePaths(arena: std.mem.Allocator, auto_includes_option: cli.Options.A .cpu_arch = includes_arch, .abi = .gnu, }; - const target = std.zig.resolveTargetQueryOrFatal(target_query); + const target = std.zig.resolveTargetQueryOrFatal(io, target_query); const is_native_abi = target_query.isNativeAbi(); const detected_libc = std.zig.LibCDirs.detect(arena, zig_lib_dir, &target, is_native_abi, true, null) catch |err| switch (err) { error.OutOfMemory => |e| return e, @@ -647,7 +664,7 @@ const ErrorHandler = union(enum) { pub fn emitCliDiagnostics( self: *ErrorHandler, - allocator: std.mem.Allocator, + allocator: Allocator, args: []const []const u8, diagnostics: *cli.Diagnostics, ) !void { @@ -666,7 +683,7 @@ const ErrorHandler = union(enum) { pub fn emitAroDiagnostics( self: *ErrorHandler, - allocator: std.mem.Allocator, + allocator: Allocator, fail_msg: []const u8, comp: *aro.Compilation, ) !void { @@ -692,7 +709,7 @@ const ErrorHandler = union(enum) { pub fn emitDiagnostics( self: *ErrorHandler, - allocator: std.mem.Allocator, + allocator: Allocator, cwd: std.fs.Dir, source: []const u8, diagnostics: *Diagnostics, @@ -713,7 +730,7 @@ const ErrorHandler = union(enum) { pub fn emitMessage( self: *ErrorHandler, - allocator: std.mem.Allocator, + allocator: Allocator, msg_type: @import("utils.zig").ErrorMessageType, comptime format: []const u8, args: anytype, @@ -738,7 +755,7 @@ const ErrorHandler = union(enum) { }; fn cliDiagnosticsToErrorBundle( - gpa: std.mem.Allocator, + gpa: Allocator, diagnostics: *cli.Diagnostics, ) !ErrorBundle { @branchHint(.cold); @@ -783,7 +800,7 @@ fn cliDiagnosticsToErrorBundle( } fn diagnosticsToErrorBundle( - gpa: std.mem.Allocator, + gpa: Allocator, source: []const u8, diagnostics: *Diagnostics, mappings: SourceMappings, @@ -870,7 +887,7 @@ fn diagnosticsToErrorBundle( return try bundle.toOwnedBundle(""); } -fn errorStringToErrorBundle(allocator: std.mem.Allocator, comptime format: []const u8, args: anytype) !ErrorBundle { +fn errorStringToErrorBundle(allocator: Allocator, comptime format: []const u8, args: anytype) !ErrorBundle { @branchHint(.cold); var bundle: ErrorBundle.Wip = undefined; try bundle.init(allocator); diff --git a/lib/compiler/resinator/utils.zig b/lib/compiler/resinator/utils.zig index b535ab9c71..021b8cf4de 100644 --- a/lib/compiler/resinator/utils.zig +++ b/lib/compiler/resinator/utils.zig @@ -26,7 +26,11 @@ pub const UncheckedSliceWriter = struct { /// Cross-platform 'std.fs.Dir.openFile' wrapper that will always return IsDir if /// a directory is attempted to be opened. /// TODO: Remove once https://github.com/ziglang/zig/issues/5732 is addressed. -pub fn openFileNotDir(cwd: std.fs.Dir, path: []const u8, flags: std.fs.File.OpenFlags) std.fs.File.OpenError!std.fs.File { +pub fn openFileNotDir( + cwd: std.fs.Dir, + path: []const u8, + flags: std.fs.File.OpenFlags, +) (std.fs.File.OpenError || std.fs.File.StatError)!std.fs.File { const file = try cwd.openFile(path, flags); errdefer file.close(); // https://github.com/ziglang/zig/issues/5732 diff --git a/lib/compiler/test_runner.zig b/lib/compiler/test_runner.zig index 673ea2cb2d..0d6f451947 100644 --- a/lib/compiler/test_runner.zig +++ b/lib/compiler/test_runner.zig @@ -2,6 +2,7 @@ const builtin = @import("builtin"); const std = @import("std"); +const Io = std.Io; const fatal = std.process.fatal; const testing = std.testing; const assert = std.debug.assert; @@ -12,10 +13,11 @@ pub const std_options: std.Options = .{ }; var log_err_count: usize = 0; -var fba = std.heap.FixedBufferAllocator.init(&fba_buffer); +var fba: std.heap.FixedBufferAllocator = .init(&fba_buffer); var fba_buffer: [8192]u8 = undefined; var stdin_buffer: [4096]u8 = undefined; var stdout_buffer: [4096]u8 = undefined; +var runner_threaded_io: Io.Threaded = .init_single_threaded; /// Keep in sync with logic in `std.Build.addRunArtifact` which decides whether /// the test runner will communicate with the build runner via `std.zig.Server`. @@ -63,8 +65,6 @@ pub fn main() void { fuzz_abi.fuzzer_init(.fromSlice(cache_dir)); } - fba.reset(); - if (listen) { return mainServer() catch @panic("internal test runner failure"); } else { @@ -74,7 +74,7 @@ pub fn main() void { fn mainServer() !void { @disableInstrumentation(); - var stdin_reader = std.fs.File.stdin().readerStreaming(&stdin_buffer); + var stdin_reader = std.fs.File.stdin().readerStreaming(runner_threaded_io.io(), &stdin_buffer); var stdout_writer = std.fs.File.stdout().writerStreaming(&stdout_buffer); var server = try std.zig.Server.init(.{ .in = &stdin_reader.interface, @@ -131,6 +131,7 @@ fn mainServer() !void { .run_test => { testing.allocator_instance = .{}; + testing.io_instance = .init(testing.allocator); log_err_count = 0; const index = try server.receiveBody_u32(); const test_fn = builtin.test_functions[index]; @@ -152,6 +153,7 @@ fn mainServer() !void { break :s .fail; }, }; + testing.io_instance.deinit(); const leak_count = testing.allocator_instance.detectLeaks(); testing.allocator_instance.deinitWithoutLeakChecks(); try server.serveTestResults(.{ @@ -228,18 +230,13 @@ fn mainTerminal() void { }); const have_tty = std.fs.File.stderr().isTty(); - var async_frame_buffer: []align(builtin.target.stackAlignment()) u8 = undefined; - // TODO this is on the next line (using `undefined` above) because otherwise zig incorrectly - // ignores the alignment of the slice. - async_frame_buffer = &[_]u8{}; - var leaks: usize = 0; for (test_fn_list, 0..) |test_fn, i| { testing.allocator_instance = .{}; + testing.io_instance = .init(testing.allocator); defer { - if (testing.allocator_instance.deinit() == .leak) { - leaks += 1; - } + testing.io_instance.deinit(); + if (testing.allocator_instance.deinit() == .leak) leaks += 1; } testing.log_level = .warn; @@ -326,7 +323,7 @@ pub fn mainSimple() anyerror!void { .stage2_aarch64, .stage2_riscv64 => true, else => false, }; - // is the backend capable of calling `std.Io.Writer.print`? + // is the backend capable of calling `Io.Writer.print`? const enable_print = switch (builtin.zig_backend) { .stage2_aarch64, .stage2_riscv64 => true, else => false, diff --git a/lib/compiler/translate-c/main.zig b/lib/compiler/translate-c/main.zig index 3a3fd7577e..d0e8faf8c2 100644 --- a/lib/compiler/translate-c/main.zig +++ b/lib/compiler/translate-c/main.zig @@ -18,6 +18,10 @@ pub fn main() u8 { defer arena_instance.deinit(); const arena = arena_instance.allocator(); + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + var args = process.argsAlloc(arena) catch { std.debug.print("ran out of memory allocating arguments\n", .{}); if (fast_exit) process.exit(1); @@ -42,7 +46,7 @@ pub fn main() u8 { }; defer diagnostics.deinit(); - var comp = aro.Compilation.initDefault(gpa, arena, &diagnostics, std.fs.cwd()) catch |err| switch (err) { + var comp = aro.Compilation.initDefault(gpa, arena, io, &diagnostics, std.fs.cwd()) catch |err| switch (err) { error.OutOfMemory => { std.debug.print("ran out of memory initializing C compilation\n", .{}); if (fast_exit) process.exit(1); diff --git a/lib/std/Build.zig b/lib/std/Build.zig index 9fd906e333..e9d2e81fba 100644 --- a/lib/std/Build.zig +++ b/lib/std/Build.zig @@ -1,5 +1,7 @@ -const std = @import("std.zig"); const builtin = @import("builtin"); + +const std = @import("std.zig"); +const Io = std.Io; const fs = std.fs; const mem = std.mem; const debug = std.debug; @@ -110,6 +112,7 @@ pub const ReleaseMode = enum { /// Shared state among all Build instances. /// Settings that are here rather than in Build are not configurable per-package. pub const Graph = struct { + io: Io, arena: Allocator, system_library_options: std.StringArrayHashMapUnmanaged(SystemLibraryMode) = .empty, system_package_mode: bool = false, @@ -1834,6 +1837,8 @@ pub fn runAllowFail( if (!process.can_spawn) return error.ExecNotSupported; + const io = b.graph.io; + const max_output_size = 400 * 1024; var child = std.process.Child.init(argv, b.allocator); child.stdin_behavior = .Ignore; @@ -1844,7 +1849,7 @@ pub fn runAllowFail( try Step.handleVerbose2(b, null, child.env_map, argv); try child.spawn(); - var stdout_reader = child.stdout.?.readerStreaming(&.{}); + var stdout_reader = child.stdout.?.readerStreaming(io, &.{}); const stdout = stdout_reader.interface.allocRemaining(b.allocator, .limited(max_output_size)) catch { return error.ReadFailure; }; @@ -2666,9 +2671,10 @@ pub fn resolveTargetQuery(b: *Build, query: Target.Query) ResolvedTarget { // Hot path. This is faster than querying the native CPU and OS again. return b.graph.host; } + const io = b.graph.io; return .{ .query = query, - .result = std.zig.system.resolveTargetQuery(query) catch + .result = std.zig.system.resolveTargetQuery(io, query) catch @panic("unable to resolve target query"), }; } diff --git a/lib/std/Build/Cache.zig b/lib/std/Build/Cache.zig index 14063001a2..b966c25efc 100644 --- a/lib/std/Build/Cache.zig +++ b/lib/std/Build/Cache.zig @@ -3,8 +3,10 @@ //! not to withstand attacks using specially-crafted input. const Cache = @This(); -const std = @import("std"); const builtin = @import("builtin"); + +const std = @import("std"); +const Io = std.Io; const crypto = std.crypto; const fs = std.fs; const assert = std.debug.assert; @@ -15,10 +17,11 @@ const Allocator = std.mem.Allocator; const log = std.log.scoped(.cache); gpa: Allocator, +io: Io, manifest_dir: fs.Dir, hash: HashHelper = .{}, /// This value is accessed from multiple threads, protected by mutex. -recent_problematic_timestamp: i128 = 0, +recent_problematic_timestamp: Io.Timestamp = .zero, mutex: std.Thread.Mutex = .{}, /// A set of strings such as the zig library directory or project source root, which @@ -152,7 +155,7 @@ pub const File = struct { pub const Stat = struct { inode: fs.File.INode, size: u64, - mtime: i128, + mtime: Io.Timestamp, pub fn fromFs(fs_stat: fs.File.Stat) Stat { return .{ @@ -327,7 +330,7 @@ pub const Manifest = struct { diagnostic: Diagnostic = .none, /// Keeps track of the last time we performed a file system write to observe /// what time the file system thinks it is, according to its own granularity. - recent_problematic_timestamp: i128 = 0, + recent_problematic_timestamp: Io.Timestamp = .zero, pub const Diagnostic = union(enum) { none, @@ -661,9 +664,10 @@ pub const Manifest = struct { }, } { const gpa = self.cache.gpa; + const io = self.cache.io; const input_file_count = self.files.entries.len; var tiny_buffer: [1]u8 = undefined; // allows allocRemaining to detect limit exceeded - var manifest_reader = self.manifest_file.?.reader(&tiny_buffer); // Reads positionally from zero. + var manifest_reader = self.manifest_file.?.reader(io, &tiny_buffer); // Reads positionally from zero. const limit: std.Io.Limit = .limited(manifest_file_size_max); const file_contents = manifest_reader.interface.allocRemaining(gpa, limit) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, @@ -724,7 +728,7 @@ pub const Manifest = struct { file.stat = .{ .size = stat_size, .inode = stat_inode, - .mtime = stat_mtime, + .mtime = .{ .nanoseconds = stat_mtime }, }; file.bin_digest = file_bin_digest; break :f file; @@ -743,7 +747,7 @@ pub const Manifest = struct { .stat = .{ .size = stat_size, .inode = stat_inode, - .mtime = stat_mtime, + .mtime = .{ .nanoseconds = stat_mtime }, }, .bin_digest = file_bin_digest, }; @@ -776,7 +780,7 @@ pub const Manifest = struct { return error.CacheCheckFailed; }; const size_match = actual_stat.size == cache_hash_file.stat.size; - const mtime_match = actual_stat.mtime == cache_hash_file.stat.mtime; + const mtime_match = actual_stat.mtime.nanoseconds == cache_hash_file.stat.mtime.nanoseconds; const inode_match = actual_stat.inode == cache_hash_file.stat.inode; if (!size_match or !mtime_match or !inode_match) { @@ -788,7 +792,7 @@ pub const Manifest = struct { if (self.isProblematicTimestamp(cache_hash_file.stat.mtime)) { // The actual file has an unreliable timestamp, force it to be hashed - cache_hash_file.stat.mtime = 0; + cache_hash_file.stat.mtime = .zero; cache_hash_file.stat.inode = 0; } @@ -844,10 +848,10 @@ pub const Manifest = struct { } } - fn isProblematicTimestamp(man: *Manifest, file_time: i128) bool { + fn isProblematicTimestamp(man: *Manifest, timestamp: Io.Timestamp) bool { // If the file_time is prior to the most recent problematic timestamp // then we don't need to access the filesystem. - if (file_time < man.recent_problematic_timestamp) + if (timestamp.nanoseconds < man.recent_problematic_timestamp.nanoseconds) return false; // Next we will check the globally shared Cache timestamp, which is accessed @@ -857,7 +861,7 @@ pub const Manifest = struct { // Save the global one to our local one to avoid locking next time. man.recent_problematic_timestamp = man.cache.recent_problematic_timestamp; - if (file_time < man.recent_problematic_timestamp) + if (timestamp.nanoseconds < man.recent_problematic_timestamp.nanoseconds) return false; // This flag prevents multiple filesystem writes for the same hit() call. @@ -875,7 +879,7 @@ pub const Manifest = struct { man.cache.recent_problematic_timestamp = man.recent_problematic_timestamp; } - return file_time >= man.recent_problematic_timestamp; + return timestamp.nanoseconds >= man.recent_problematic_timestamp.nanoseconds; } fn populateFileHash(self: *Manifest, ch_file: *File) !void { @@ -900,7 +904,7 @@ pub const Manifest = struct { if (self.isProblematicTimestamp(ch_file.stat.mtime)) { // The actual file has an unreliable timestamp, force it to be hashed - ch_file.stat.mtime = 0; + ch_file.stat.mtime = .zero; ch_file.stat.inode = 0; } @@ -1036,7 +1040,7 @@ pub const Manifest = struct { if (self.isProblematicTimestamp(new_file.stat.mtime)) { // The actual file has an unreliable timestamp, force it to be hashed - new_file.stat.mtime = 0; + new_file.stat.mtime = .zero; new_file.stat.inode = 0; } @@ -1301,7 +1305,7 @@ fn hashFile(file: fs.File, bin_digest: *[Hasher.mac_length]u8) fs.File.PReadErro } // Create/Write a file, close it, then grab its stat.mtime timestamp. -fn testGetCurrentFileTimestamp(dir: fs.Dir) !i128 { +fn testGetCurrentFileTimestamp(dir: fs.Dir) !Io.Timestamp { const test_out_file = "test-filetimestamp.tmp"; var file = try dir.createFile(test_out_file, .{ @@ -1317,6 +1321,8 @@ fn testGetCurrentFileTimestamp(dir: fs.Dir) !i128 { } test "cache file and then recall it" { + const io = std.testing.io; + var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); @@ -1327,15 +1333,16 @@ test "cache file and then recall it" { // Wait for file timestamps to tick const initial_time = try testGetCurrentFileTimestamp(tmp.dir); - while ((try testGetCurrentFileTimestamp(tmp.dir)) == initial_time) { - std.Thread.sleep(1); + while ((try testGetCurrentFileTimestamp(tmp.dir)).nanoseconds == initial_time.nanoseconds) { + try std.Io.Clock.Duration.sleep(.{ .clock = .boot, .raw = .fromNanoseconds(1) }, io); } var digest1: HexDigest = undefined; var digest2: HexDigest = undefined; { - var cache = Cache{ + var cache: Cache = .{ + .io = io, .gpa = testing.allocator, .manifest_dir = try tmp.dir.makeOpenPath(temp_manifest_dir, .{}), }; @@ -1378,6 +1385,8 @@ test "cache file and then recall it" { } test "check that changing a file makes cache fail" { + const io = std.testing.io; + var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); @@ -1390,15 +1399,16 @@ test "check that changing a file makes cache fail" { // Wait for file timestamps to tick const initial_time = try testGetCurrentFileTimestamp(tmp.dir); - while ((try testGetCurrentFileTimestamp(tmp.dir)) == initial_time) { - std.Thread.sleep(1); + while ((try testGetCurrentFileTimestamp(tmp.dir)).nanoseconds == initial_time.nanoseconds) { + try std.Io.Clock.Duration.sleep(.{ .clock = .boot, .raw = .fromNanoseconds(1) }, io); } var digest1: HexDigest = undefined; var digest2: HexDigest = undefined; { - var cache = Cache{ + var cache: Cache = .{ + .io = io, .gpa = testing.allocator, .manifest_dir = try tmp.dir.makeOpenPath(temp_manifest_dir, .{}), }; @@ -1447,6 +1457,8 @@ test "check that changing a file makes cache fail" { } test "no file inputs" { + const io = testing.io; + var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); @@ -1455,7 +1467,8 @@ test "no file inputs" { var digest1: HexDigest = undefined; var digest2: HexDigest = undefined; - var cache = Cache{ + var cache: Cache = .{ + .io = io, .gpa = testing.allocator, .manifest_dir = try tmp.dir.makeOpenPath(temp_manifest_dir, .{}), }; @@ -1490,6 +1503,8 @@ test "no file inputs" { } test "Manifest with files added after initial hash work" { + const io = std.testing.io; + var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); @@ -1502,8 +1517,8 @@ test "Manifest with files added after initial hash work" { // Wait for file timestamps to tick const initial_time = try testGetCurrentFileTimestamp(tmp.dir); - while ((try testGetCurrentFileTimestamp(tmp.dir)) == initial_time) { - std.Thread.sleep(1); + while ((try testGetCurrentFileTimestamp(tmp.dir)).nanoseconds == initial_time.nanoseconds) { + try std.Io.Clock.Duration.sleep(.{ .clock = .boot, .raw = .fromNanoseconds(1) }, io); } var digest1: HexDigest = undefined; @@ -1511,7 +1526,8 @@ test "Manifest with files added after initial hash work" { var digest3: HexDigest = undefined; { - var cache = Cache{ + var cache: Cache = .{ + .io = io, .gpa = testing.allocator, .manifest_dir = try tmp.dir.makeOpenPath(temp_manifest_dir, .{}), }; @@ -1552,8 +1568,8 @@ test "Manifest with files added after initial hash work" { // Wait for file timestamps to tick const initial_time2 = try testGetCurrentFileTimestamp(tmp.dir); - while ((try testGetCurrentFileTimestamp(tmp.dir)) == initial_time2) { - std.Thread.sleep(1); + while ((try testGetCurrentFileTimestamp(tmp.dir)).nanoseconds == initial_time2.nanoseconds) { + try std.Io.Clock.Duration.sleep(.{ .clock = .boot, .raw = .fromNanoseconds(1) }, io); } { diff --git a/lib/std/Build/Cache/Path.zig b/lib/std/Build/Cache/Path.zig index bf16fc6814..92290cfdf4 100644 --- a/lib/std/Build/Cache/Path.zig +++ b/lib/std/Build/Cache/Path.zig @@ -1,5 +1,7 @@ const Path = @This(); + const std = @import("../../std.zig"); +const Io = std.Io; const assert = std.debug.assert; const fs = std.fs; const Allocator = std.mem.Allocator; @@ -119,7 +121,7 @@ pub fn atomicFile( return p.root_dir.handle.atomicFile(joined_path, options); } -pub fn access(p: Path, sub_path: []const u8, flags: fs.File.OpenFlags) !void { +pub fn access(p: Path, sub_path: []const u8, flags: Io.Dir.AccessOptions) !void { var buf: [fs.max_path_bytes]u8 = undefined; const joined_path = if (p.sub_path.len == 0) sub_path else p: { break :p std.fmt.bufPrint(&buf, "{s}" ++ fs.path.sep_str ++ "{s}", .{ @@ -151,7 +153,7 @@ pub fn fmtEscapeString(path: Path) std.fmt.Alt(Path, formatEscapeString) { return .{ .data = path }; } -pub fn formatEscapeString(path: Path, writer: *std.Io.Writer) std.Io.Writer.Error!void { +pub fn formatEscapeString(path: Path, writer: *Io.Writer) Io.Writer.Error!void { if (path.root_dir.path) |p| { try std.zig.stringEscape(p, writer); if (path.sub_path.len > 0) try std.zig.stringEscape(fs.path.sep_str, writer); @@ -167,7 +169,7 @@ pub fn fmtEscapeChar(path: Path) std.fmt.Alt(Path, formatEscapeChar) { } /// Deprecated, use double quoted escape to print paths. -pub fn formatEscapeChar(path: Path, writer: *std.Io.Writer) std.Io.Writer.Error!void { +pub fn formatEscapeChar(path: Path, writer: *Io.Writer) Io.Writer.Error!void { if (path.root_dir.path) |p| { for (p) |byte| try std.zig.charEscape(byte, writer); if (path.sub_path.len > 0) try writer.writeByte(fs.path.sep); @@ -177,7 +179,7 @@ pub fn formatEscapeChar(path: Path, writer: *std.Io.Writer) std.Io.Writer.Error! } } -pub fn format(self: Path, writer: *std.Io.Writer) std.Io.Writer.Error!void { +pub fn format(self: Path, writer: *Io.Writer) Io.Writer.Error!void { if (std.fs.path.isAbsolute(self.sub_path)) { try writer.writeAll(self.sub_path); return; diff --git a/lib/std/Build/Fuzz.zig b/lib/std/Build/Fuzz.zig index 8281fc4726..d342628871 100644 --- a/lib/std/Build/Fuzz.zig +++ b/lib/std/Build/Fuzz.zig @@ -1,4 +1,5 @@ const std = @import("../std.zig"); +const Io = std.Io; const Build = std.Build; const Cache = Build.Cache; const Step = std.Build.Step; @@ -14,6 +15,7 @@ const Fuzz = @This(); const build_runner = @import("root"); gpa: Allocator, +io: Io, mode: Mode, /// Allocated into `gpa`. @@ -75,6 +77,7 @@ const CoverageMap = struct { pub fn init( gpa: Allocator, + io: Io, thread_pool: *std.Thread.Pool, all_steps: []const *Build.Step, root_prog_node: std.Progress.Node, @@ -111,6 +114,7 @@ pub fn init( return .{ .gpa = gpa, + .io = io, .mode = mode, .run_steps = run_steps, .wait_group = .{}, @@ -484,6 +488,7 @@ fn addEntryPoint(fuzz: *Fuzz, coverage_id: u64, addr: u64) error{ AlreadyReporte pub fn waitAndPrintReport(fuzz: *Fuzz) void { assert(fuzz.mode == .limit); + const io = fuzz.io; fuzz.wait_group.wait(); fuzz.wait_group.reset(); @@ -506,7 +511,7 @@ pub fn waitAndPrintReport(fuzz: *Fuzz) void { const fuzz_abi = std.Build.abi.fuzz; var rbuf: [0x1000]u8 = undefined; - var r = coverage_file.reader(&rbuf); + var r = coverage_file.reader(io, &rbuf); var header: fuzz_abi.SeenPcsHeader = undefined; r.interface.readSliceAll(std.mem.asBytes(&header)) catch |err| { diff --git a/lib/std/Build/Step.zig b/lib/std/Build/Step.zig index 72eb66e530..aa922ff37b 100644 --- a/lib/std/Build/Step.zig +++ b/lib/std/Build/Step.zig @@ -1,9 +1,11 @@ const Step = @This(); +const builtin = @import("builtin"); + const std = @import("../std.zig"); +const Io = std.Io; const Build = std.Build; const Allocator = std.mem.Allocator; const assert = std.debug.assert; -const builtin = @import("builtin"); const Cache = Build.Cache; const Path = Cache.Path; const ArrayList = std.ArrayList; @@ -327,7 +329,7 @@ pub fn cast(step: *Step, comptime T: type) ?*T { } /// For debugging purposes, prints identifying information about this Step. -pub fn dump(step: *Step, w: *std.Io.Writer, tty_config: std.Io.tty.Config) void { +pub fn dump(step: *Step, w: *Io.Writer, tty_config: Io.tty.Config) void { if (step.debug_stack_trace.instruction_addresses.len > 0) { w.print("name: '{s}'. creation stack trace:\n", .{step.name}) catch {}; std.debug.writeStackTrace(&step.debug_stack_trace, w, tty_config) catch {}; @@ -382,7 +384,7 @@ pub fn addError(step: *Step, comptime fmt: []const u8, args: anytype) error{OutO pub const ZigProcess = struct { child: std.process.Child, - poller: std.Io.Poller(StreamEnum), + poller: Io.Poller(StreamEnum), progress_ipc_fd: if (std.Progress.have_ipc) ?std.posix.fd_t else void, pub const StreamEnum = enum { stdout, stderr }; @@ -458,7 +460,7 @@ pub fn evalZigProcess( const zp = try gpa.create(ZigProcess); zp.* = .{ .child = child, - .poller = std.Io.poll(gpa, ZigProcess.StreamEnum, .{ + .poller = Io.poll(gpa, ZigProcess.StreamEnum, .{ .stdout = child.stdout.?, .stderr = child.stderr.?, }), @@ -505,11 +507,12 @@ pub fn evalZigProcess( } /// Wrapper around `std.fs.Dir.updateFile` that handles verbose and error output. -pub fn installFile(s: *Step, src_lazy_path: Build.LazyPath, dest_path: []const u8) !std.fs.Dir.PrevStatus { +pub fn installFile(s: *Step, src_lazy_path: Build.LazyPath, dest_path: []const u8) !Io.Dir.PrevStatus { const b = s.owner; + const io = b.graph.io; const src_path = src_lazy_path.getPath3(b, s); try handleVerbose(b, null, &.{ "install", "-C", b.fmt("{f}", .{src_path}), dest_path }); - return src_path.root_dir.handle.updateFile(src_path.sub_path, std.fs.cwd(), dest_path, .{}) catch |err| { + return Io.Dir.updateFile(src_path.root_dir.handle.adaptToNewApi(), io, src_path.sub_path, .cwd(), dest_path, .{}) catch |err| { return s.fail("unable to update file from '{f}' to '{s}': {s}", .{ src_path, dest_path, @errorName(err), }); @@ -738,7 +741,7 @@ pub fn allocPrintCmd2( argv: []const []const u8, ) Allocator.Error![]u8 { const shell = struct { - fn escape(writer: *std.Io.Writer, string: []const u8, is_argv0: bool) !void { + fn escape(writer: *Io.Writer, string: []const u8, is_argv0: bool) !void { for (string) |c| { if (switch (c) { else => true, @@ -772,7 +775,7 @@ pub fn allocPrintCmd2( } }; - var aw: std.Io.Writer.Allocating = .init(gpa); + var aw: Io.Writer.Allocating = .init(gpa); defer aw.deinit(); const writer = &aw.writer; if (opt_cwd) |cwd| writer.print("cd {s} && ", .{cwd}) catch return error.OutOfMemory; diff --git a/lib/std/Build/Step/Compile.zig b/lib/std/Build/Step/Compile.zig index 0f47a0b647..2188d8bfc7 100644 --- a/lib/std/Build/Step/Compile.zig +++ b/lib/std/Build/Step/Compile.zig @@ -1701,7 +1701,7 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 { // This prevents a warning, that should probably be upgraded to an error in Zig's // CLI parsing code, when the linker sees an -L directory that does not exist. - if (prefix_dir.accessZ("lib", .{})) |_| { + if (prefix_dir.access("lib", .{})) |_| { try zig_args.appendSlice(&.{ "-L", b.pathJoin(&.{ search_prefix, "lib" }), }); @@ -1712,7 +1712,7 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 { }), } - if (prefix_dir.accessZ("include", .{})) |_| { + if (prefix_dir.access("include", .{})) |_| { try zig_args.appendSlice(&.{ "-I", b.pathJoin(&.{ search_prefix, "include" }), }); diff --git a/lib/std/Build/Step/Options.zig b/lib/std/Build/Step/Options.zig index fd6194f7ff..8590fa9608 100644 --- a/lib/std/Build/Step/Options.zig +++ b/lib/std/Build/Step/Options.zig @@ -532,12 +532,16 @@ const Arg = struct { test Options { if (builtin.os.tag == .wasi) return error.SkipZigTest; + const io = std.testing.io; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); var graph: std.Build.Graph = .{ + .io = io, .arena = arena.allocator(), .cache = .{ + .io = io, .gpa = arena.allocator(), .manifest_dir = std.fs.cwd(), }, @@ -546,7 +550,7 @@ test Options { .global_cache_root = .{ .path = "test", .handle = std.fs.cwd() }, .host = .{ .query = .{}, - .result = try std.zig.system.resolveTargetQuery(.{}), + .result = try std.zig.system.resolveTargetQuery(io, .{}), }, .zig_lib_directory = std.Build.Cache.Directory.cwd(), .time_report = false, diff --git a/lib/std/Build/Step/Run.zig b/lib/std/Build/Step/Run.zig index cd29262612..314862e201 100644 --- a/lib/std/Build/Step/Run.zig +++ b/lib/std/Build/Step/Run.zig @@ -761,6 +761,7 @@ const IndexedOutput = struct { }; fn make(step: *Step, options: Step.MakeOptions) !void { const b = step.owner; + const io = b.graph.io; const arena = b.allocator; const run: *Run = @fieldParentPtr("step", step); const has_side_effects = run.hasSideEffects(); @@ -834,7 +835,7 @@ fn make(step: *Step, options: Step.MakeOptions) !void { defer file.close(); var buf: [1024]u8 = undefined; - var file_reader = file.reader(&buf); + var file_reader = file.reader(io, &buf); _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) { error.ReadFailed => return step.fail( "failed to read from '{f}': {t}", @@ -1067,6 +1068,7 @@ pub fn rerunInFuzzMode( ) !void { const step = &run.step; const b = step.owner; + const io = b.graph.io; const arena = b.allocator; var argv_list: std.ArrayList([]const u8) = .empty; for (run.argv.items) |arg| { @@ -1093,7 +1095,7 @@ pub fn rerunInFuzzMode( defer file.close(); var buf: [1024]u8 = undefined; - var file_reader = file.reader(&buf); + var file_reader = file.reader(io, &buf); _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) { error.ReadFailed => return file_reader.err.?, error.WriteFailed => return error.OutOfMemory, @@ -2090,6 +2092,7 @@ fn sendRunFuzzTestMessage( fn evalGeneric(run: *Run, child: *std.process.Child) !EvalGenericResult { const b = run.step.owner; + const io = b.graph.io; const arena = b.allocator; try child.spawn(); @@ -2113,7 +2116,7 @@ fn evalGeneric(run: *Run, child: *std.process.Child) !EvalGenericResult { defer file.close(); // TODO https://github.com/ziglang/zig/issues/23955 var read_buffer: [1024]u8 = undefined; - var file_reader = file.reader(&read_buffer); + var file_reader = file.reader(io, &read_buffer); var write_buffer: [1024]u8 = undefined; var stdin_writer = child.stdin.?.writer(&write_buffer); _ = stdin_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) { @@ -2159,7 +2162,7 @@ fn evalGeneric(run: *Run, child: *std.process.Child) !EvalGenericResult { stdout_bytes = try poller.toOwnedSlice(.stdout); stderr_bytes = try poller.toOwnedSlice(.stderr); } else { - var stdout_reader = stdout.readerStreaming(&.{}); + var stdout_reader = stdout.readerStreaming(io, &.{}); stdout_bytes = stdout_reader.interface.allocRemaining(arena, run.stdio_limit) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.ReadFailed => return stdout_reader.err.?, @@ -2167,7 +2170,7 @@ fn evalGeneric(run: *Run, child: *std.process.Child) !EvalGenericResult { }; } } else if (child.stderr) |stderr| { - var stderr_reader = stderr.readerStreaming(&.{}); + var stderr_reader = stderr.readerStreaming(io, &.{}); stderr_bytes = stderr_reader.interface.allocRemaining(arena, run.stdio_limit) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.ReadFailed => return stderr_reader.err.?, diff --git a/lib/std/Build/Step/UpdateSourceFiles.zig b/lib/std/Build/Step/UpdateSourceFiles.zig index 674e2a01c6..6f1559bd68 100644 --- a/lib/std/Build/Step/UpdateSourceFiles.zig +++ b/lib/std/Build/Step/UpdateSourceFiles.zig @@ -3,11 +3,13 @@ //! not be used during the normal build process, but as a utility run by a //! developer with intention to update source files, which will then be //! committed to version control. +const UpdateSourceFiles = @This(); + const std = @import("std"); +const Io = std.Io; const Step = std.Build.Step; const fs = std.fs; const ArrayList = std.ArrayList; -const UpdateSourceFiles = @This(); step: Step, output_source_files: std.ArrayListUnmanaged(OutputSourceFile), @@ -70,22 +72,21 @@ pub fn addBytesToSource(usf: *UpdateSourceFiles, bytes: []const u8, sub_path: [] fn make(step: *Step, options: Step.MakeOptions) !void { _ = options; const b = step.owner; + const io = b.graph.io; const usf: *UpdateSourceFiles = @fieldParentPtr("step", step); var any_miss = false; for (usf.output_source_files.items) |output_source_file| { if (fs.path.dirname(output_source_file.sub_path)) |dirname| { b.build_root.handle.makePath(dirname) catch |err| { - return step.fail("unable to make path '{f}{s}': {s}", .{ - b.build_root, dirname, @errorName(err), - }); + return step.fail("unable to make path '{f}{s}': {t}", .{ b.build_root, dirname, err }); }; } switch (output_source_file.contents) { .bytes => |bytes| { b.build_root.handle.writeFile(.{ .sub_path = output_source_file.sub_path, .data = bytes }) catch |err| { - return step.fail("unable to write file '{f}{s}': {s}", .{ - b.build_root, output_source_file.sub_path, @errorName(err), + return step.fail("unable to write file '{f}{s}': {t}", .{ + b.build_root, output_source_file.sub_path, err, }); }; any_miss = true; @@ -94,15 +95,16 @@ fn make(step: *Step, options: Step.MakeOptions) !void { if (!step.inputs.populated()) try step.addWatchInput(file_source); const source_path = file_source.getPath2(b, step); - const prev_status = fs.Dir.updateFile( - fs.cwd(), + const prev_status = Io.Dir.updateFile( + .cwd(), + io, source_path, - b.build_root.handle, + b.build_root.handle.adaptToNewApi(), output_source_file.sub_path, .{}, ) catch |err| { - return step.fail("unable to update file from '{s}' to '{f}{s}': {s}", .{ - source_path, b.build_root, output_source_file.sub_path, @errorName(err), + return step.fail("unable to update file from '{s}' to '{f}{s}': {t}", .{ + source_path, b.build_root, output_source_file.sub_path, err, }); }; any_miss = any_miss or prev_status == .stale; diff --git a/lib/std/Build/Step/WriteFile.zig b/lib/std/Build/Step/WriteFile.zig index b1cfb3b42a..1a531604e3 100644 --- a/lib/std/Build/Step/WriteFile.zig +++ b/lib/std/Build/Step/WriteFile.zig @@ -2,6 +2,7 @@ //! the local cache which has a set of files that have either been generated //! during the build, or are copied from the source package. const std = @import("std"); +const Io = std.Io; const Step = std.Build.Step; const fs = std.fs; const ArrayList = std.ArrayList; @@ -174,6 +175,7 @@ fn maybeUpdateName(write_file: *WriteFile) void { fn make(step: *Step, options: Step.MakeOptions) !void { _ = options; const b = step.owner; + const io = b.graph.io; const arena = b.allocator; const gpa = arena; const write_file: *WriteFile = @fieldParentPtr("step", step); @@ -264,40 +266,27 @@ fn make(step: *Step, options: Step.MakeOptions) !void { }; defer cache_dir.close(); - const cwd = fs.cwd(); - for (write_file.files.items) |file| { if (fs.path.dirname(file.sub_path)) |dirname| { cache_dir.makePath(dirname) catch |err| { - return step.fail("unable to make path '{f}{s}{c}{s}': {s}", .{ - b.cache_root, cache_path, fs.path.sep, dirname, @errorName(err), + return step.fail("unable to make path '{f}{s}{c}{s}': {t}", .{ + b.cache_root, cache_path, fs.path.sep, dirname, err, }); }; } switch (file.contents) { .bytes => |bytes| { cache_dir.writeFile(.{ .sub_path = file.sub_path, .data = bytes }) catch |err| { - return step.fail("unable to write file '{f}{s}{c}{s}': {s}", .{ - b.cache_root, cache_path, fs.path.sep, file.sub_path, @errorName(err), + return step.fail("unable to write file '{f}{s}{c}{s}': {t}", .{ + b.cache_root, cache_path, fs.path.sep, file.sub_path, err, }); }; }, .copy => |file_source| { const source_path = file_source.getPath2(b, step); - const prev_status = fs.Dir.updateFile( - cwd, - source_path, - cache_dir, - file.sub_path, - .{}, - ) catch |err| { - return step.fail("unable to update file from '{s}' to '{f}{s}{c}{s}': {s}", .{ - source_path, - b.cache_root, - cache_path, - fs.path.sep, - file.sub_path, - @errorName(err), + const prev_status = Io.Dir.updateFile(.cwd(), io, source_path, cache_dir.adaptToNewApi(), file.sub_path, .{}) catch |err| { + return step.fail("unable to update file from '{s}' to '{f}{s}{c}{s}': {t}", .{ + source_path, b.cache_root, cache_path, fs.path.sep, file.sub_path, err, }); }; // At this point we already will mark the step as a cache miss. @@ -331,10 +320,11 @@ fn make(step: *Step, options: Step.MakeOptions) !void { switch (entry.kind) { .directory => try cache_dir.makePath(dest_path), .file => { - const prev_status = fs.Dir.updateFile( - src_entry_path.root_dir.handle, + const prev_status = Io.Dir.updateFile( + src_entry_path.root_dir.handle.adaptToNewApi(), + io, src_entry_path.sub_path, - cache_dir, + cache_dir.adaptToNewApi(), dest_path, .{}, ) catch |err| { diff --git a/lib/std/Build/WebServer.zig b/lib/std/Build/WebServer.zig index 3d6e700a5a..50e304c950 100644 --- a/lib/std/Build/WebServer.zig +++ b/lib/std/Build/WebServer.zig @@ -2,15 +2,16 @@ gpa: Allocator, thread_pool: *std.Thread.Pool, graph: *const Build.Graph, all_steps: []const *Build.Step, -listen_address: std.net.Address, -ttyconf: std.Io.tty.Config, +listen_address: net.IpAddress, +ttyconf: Io.tty.Config, root_prog_node: std.Progress.Node, watch: bool, -tcp_server: ?std.net.Server, +tcp_server: ?net.Server, serve_thread: ?std.Thread, -base_timestamp: i128, +/// Uses `Io.Clock.awake`. +base_timestamp: Io.Timestamp, /// The "step name" data which trails `abi.Hello`, for the steps in `all_steps`. step_names_trailing: []u8, @@ -42,6 +43,8 @@ runner_request: ?RunnerRequest, /// on a fixed interval of this many milliseconds. const default_update_interval_ms = 500; +pub const base_clock: Io.Clock = .awake; + /// Thread-safe. Triggers updates to be sent to connected WebSocket clients; see `update_id`. pub fn notifyUpdate(ws: *WebServer) void { _ = ws.update_id.rmw(.Add, 1, .release); @@ -53,15 +56,17 @@ pub const Options = struct { thread_pool: *std.Thread.Pool, graph: *const std.Build.Graph, all_steps: []const *Build.Step, - ttyconf: std.Io.tty.Config, + ttyconf: Io.tty.Config, root_prog_node: std.Progress.Node, watch: bool, - listen_address: std.net.Address, + listen_address: net.IpAddress, + base_timestamp: Io.Clock.Timestamp, }; pub fn init(opts: Options) WebServer { - // The upcoming `std.Io` interface should allow us to use `Io.async` and `Io.concurrent` + // The upcoming `Io` interface should allow us to use `Io.async` and `Io.concurrent` // instead of threads, so that the web server can function in single-threaded builds. comptime assert(!builtin.single_threaded); + assert(opts.base_timestamp.clock == base_clock); const all_steps = opts.all_steps; @@ -106,7 +111,7 @@ pub fn init(opts: Options) WebServer { .tcp_server = null, .serve_thread = null, - .base_timestamp = std.time.nanoTimestamp(), + .base_timestamp = opts.base_timestamp.raw, .step_names_trailing = step_names_trailing, .step_status_bits = step_status_bits, @@ -147,32 +152,34 @@ pub fn deinit(ws: *WebServer) void { pub fn start(ws: *WebServer) error{AlreadyReported}!void { assert(ws.tcp_server == null); assert(ws.serve_thread == null); + const io = ws.graph.io; - ws.tcp_server = ws.listen_address.listen(.{ .reuse_address = true }) catch |err| { + ws.tcp_server = ws.listen_address.listen(io, .{ .reuse_address = true }) catch |err| { log.err("failed to listen to port {d}: {s}", .{ ws.listen_address.getPort(), @errorName(err) }); return error.AlreadyReported; }; ws.serve_thread = std.Thread.spawn(.{}, serve, .{ws}) catch |err| { log.err("unable to spawn web server thread: {s}", .{@errorName(err)}); - ws.tcp_server.?.deinit(); + ws.tcp_server.?.deinit(io); ws.tcp_server = null; return error.AlreadyReported; }; - log.info("web interface listening at http://{f}/", .{ws.tcp_server.?.listen_address}); + log.info("web interface listening at http://{f}/", .{ws.tcp_server.?.socket.address}); if (ws.listen_address.getPort() == 0) { - log.info("hint: pass '--webui={f}' to use the same port next time", .{ws.tcp_server.?.listen_address}); + log.info("hint: pass '--webui={f}' to use the same port next time", .{ws.tcp_server.?.socket.address}); } } fn serve(ws: *WebServer) void { + const io = ws.graph.io; while (true) { - const connection = ws.tcp_server.?.accept() catch |err| { + var stream = ws.tcp_server.?.accept(io) catch |err| { log.err("failed to accept connection: {s}", .{@errorName(err)}); return; }; - _ = std.Thread.spawn(.{}, accept, .{ ws, connection }) catch |err| { + _ = std.Thread.spawn(.{}, accept, .{ ws, stream }) catch |err| { log.err("unable to spawn connection thread: {s}", .{@errorName(err)}); - connection.stream.close(); + stream.close(io); continue; }; } @@ -227,6 +234,7 @@ pub fn finishBuild(ws: *WebServer, opts: struct { ws.fuzz = Fuzz.init( ws.gpa, + ws.graph.io, ws.thread_pool, ws.all_steps, ws.root_prog_node, @@ -241,17 +249,24 @@ pub fn finishBuild(ws: *WebServer, opts: struct { } pub fn now(s: *const WebServer) i64 { - return @intCast(std.time.nanoTimestamp() - s.base_timestamp); + const io = s.graph.io; + const ts = base_clock.now(io) catch s.base_timestamp; + return @intCast(s.base_timestamp.durationTo(ts).toNanoseconds()); } -fn accept(ws: *WebServer, connection: std.net.Server.Connection) void { - defer connection.stream.close(); - +fn accept(ws: *WebServer, stream: net.Stream) void { + const io = ws.graph.io; + defer { + // `net.Stream.close` wants to helpfully overwrite `stream` with + // `undefined`, but it cannot do so since it is an immutable parameter. + var copy = stream; + copy.close(io); + } var send_buffer: [4096]u8 = undefined; var recv_buffer: [4096]u8 = undefined; - var connection_reader = connection.stream.reader(&recv_buffer); - var connection_writer = connection.stream.writer(&send_buffer); - var server: http.Server = .init(connection_reader.interface(), &connection_writer.interface); + var connection_reader = stream.reader(io, &recv_buffer); + var connection_writer = stream.writer(io, &send_buffer); + var server: http.Server = .init(&connection_reader.interface, &connection_writer.interface); while (true) { var request = server.receiveHead() catch |err| switch (err) { @@ -466,12 +481,9 @@ pub fn serveFile( }, }); } -pub fn serveTarFile( - ws: *WebServer, - request: *http.Server.Request, - paths: []const Cache.Path, -) !void { +pub fn serveTarFile(ws: *WebServer, request: *http.Server.Request, paths: []const Cache.Path) !void { const gpa = ws.gpa; + const io = ws.graph.io; var send_buffer: [0x4000]u8 = undefined; var response = try request.respondStreaming(&send_buffer, .{ @@ -496,7 +508,7 @@ pub fn serveTarFile( defer file.close(); const stat = try file.stat(); var read_buffer: [1024]u8 = undefined; - var file_reader: std.fs.File.Reader = .initSize(file, &read_buffer, stat.size); + var file_reader: Io.File.Reader = .initSize(file.adaptToNewApi(), io, &read_buffer, stat.size); // TODO: this logic is completely bogus -- obviously so, because `path.root_dir.path` can // be cwd-relative. This is also related to why linkification doesn't work in the fuzzer UI: @@ -508,7 +520,7 @@ pub fn serveTarFile( if (cached_cwd_path == null) cached_cwd_path = try std.process.getCwdAlloc(gpa); break :cwd cached_cwd_path.?; }; - try archiver.writeFile(path.sub_path, &file_reader, stat.mtime); + try archiver.writeFile(path.sub_path, &file_reader, @intCast(stat.mtime.toSeconds())); } // intentionally not calling `archiver.finishPedantically` @@ -516,6 +528,7 @@ pub fn serveTarFile( } fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.OptimizeMode) !Cache.Path { + const io = ws.graph.io; const root_name = "build-web"; const arch_os_abi = "wasm32-freestanding"; const cpu_features = "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext"; @@ -565,7 +578,7 @@ fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.Optim child.stderr_behavior = .Pipe; try child.spawn(); - var poller = std.Io.poll(gpa, enum { stdout, stderr }, .{ + var poller = Io.poll(gpa, enum { stdout, stderr }, .{ .stdout = child.stdout.?, .stderr = child.stderr.?, }); @@ -659,7 +672,7 @@ fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.Optim }; const bin_name = try std.zig.binNameAlloc(arena, .{ .root_name = root_name, - .target = &(std.zig.system.resolveTargetQuery(std.Build.parseTargetQuery(.{ + .target = &(std.zig.system.resolveTargetQuery(io, std.Build.parseTargetQuery(.{ .arch_os_abi = arch_os_abi, .cpu_features = cpu_features, }) catch unreachable) catch unreachable), @@ -841,7 +854,10 @@ const cache_control_header: http.Header = .{ }; const builtin = @import("builtin"); + const std = @import("std"); +const Io = std.Io; +const net = std.Io.net; const assert = std.debug.assert; const mem = std.mem; const log = std.log.scoped(.web_server); diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 18600c2140..d6b6fb7979 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -548,8 +548,1104 @@ pub fn PollFiles(comptime StreamEnum: type) type { } test { + _ = net; _ = Reader; _ = Writer; _ = tty; + _ = Evented; + _ = Threaded; _ = @import("Io/test.zig"); } + +const Io = @This(); + +pub const Evented = switch (builtin.os.tag) { + .linux => switch (builtin.cpu.arch) { + .x86_64, .aarch64 => @import("Io/IoUring.zig"), + else => void, // context-switching code not implemented yet + }, + .dragonfly, .freebsd, .netbsd, .openbsd, .macos, .ios, .tvos, .visionos, .watchos => switch (builtin.cpu.arch) { + .x86_64, .aarch64 => @import("Io/Kqueue.zig"), + else => void, // context-switching code not implemented yet + }, + else => void, +}; +pub const Threaded = @import("Io/Threaded.zig"); +pub const net = @import("Io/net.zig"); + +userdata: ?*anyopaque, +vtable: *const VTable, + +pub const VTable = struct { + /// If it returns `null` it means `result` has been already populated and + /// `await` will be a no-op. + /// + /// Thread-safe. + async: *const fn ( + /// Corresponds to `Io.userdata`. + userdata: ?*anyopaque, + /// The pointer of this slice is an "eager" result value. + /// The length is the size in bytes of the result type. + /// This pointer's lifetime expires directly after the call to this function. + result: []u8, + result_alignment: std.mem.Alignment, + /// Copied and then passed to `start`. + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, + ) ?*AnyFuture, + /// Thread-safe. + concurrent: *const fn ( + /// Corresponds to `Io.userdata`. + userdata: ?*anyopaque, + result_len: usize, + result_alignment: std.mem.Alignment, + /// Copied and then passed to `start`. + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, + ) ConcurrentError!*AnyFuture, + /// This function is only called when `async` returns a non-null value. + /// + /// Thread-safe. + await: *const fn ( + /// Corresponds to `Io.userdata`. + userdata: ?*anyopaque, + /// The same value that was returned from `async`. + any_future: *AnyFuture, + /// Points to a buffer where the result is written. + /// The length is equal to size in bytes of result type. + result: []u8, + result_alignment: std.mem.Alignment, + ) void, + /// Equivalent to `await` but initiates cancel request. + /// + /// This function is only called when `async` returns a non-null value. + /// + /// Thread-safe. + cancel: *const fn ( + /// Corresponds to `Io.userdata`. + userdata: ?*anyopaque, + /// The same value that was returned from `async`. + any_future: *AnyFuture, + /// Points to a buffer where the result is written. + /// The length is equal to size in bytes of result type. + result: []u8, + result_alignment: std.mem.Alignment, + ) void, + /// Returns whether the current thread of execution is known to have + /// been requested to cancel. + /// + /// Thread-safe. + cancelRequested: *const fn (?*anyopaque) bool, + + /// Executes `start` asynchronously in a manner such that it cleans itself + /// up. This mode does not support results, await, or cancel. + /// + /// Thread-safe. + groupAsync: *const fn ( + /// Corresponds to `Io.userdata`. + userdata: ?*anyopaque, + /// Owner of the spawned async task. + group: *Group, + /// Copied and then passed to `start`. + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (*Group, context: *const anyopaque) void, + ) void, + groupWait: *const fn (?*anyopaque, *Group, token: *anyopaque) void, + groupCancel: *const fn (?*anyopaque, *Group, token: *anyopaque) void, + + /// Blocks until one of the futures from the list has a result ready, such + /// that awaiting it will not block. Returns that index. + select: *const fn (?*anyopaque, futures: []const *AnyFuture) Cancelable!usize, + + mutexLock: *const fn (?*anyopaque, prev_state: Mutex.State, mutex: *Mutex) Cancelable!void, + mutexLockUncancelable: *const fn (?*anyopaque, prev_state: Mutex.State, mutex: *Mutex) void, + mutexUnlock: *const fn (?*anyopaque, prev_state: Mutex.State, mutex: *Mutex) void, + + conditionWait: *const fn (?*anyopaque, cond: *Condition, mutex: *Mutex) Cancelable!void, + conditionWaitUncancelable: *const fn (?*anyopaque, cond: *Condition, mutex: *Mutex) void, + conditionWake: *const fn (?*anyopaque, cond: *Condition, wake: Condition.Wake) void, + + dirMake: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.Mode) Dir.MakeError!void, + dirMakePath: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.Mode) Dir.MakeError!void, + dirMakeOpenPath: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.OpenOptions) Dir.MakeOpenPathError!Dir, + dirStat: *const fn (?*anyopaque, Dir) Dir.StatError!Dir.Stat, + dirStatPath: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.StatPathOptions) Dir.StatPathError!File.Stat, + dirAccess: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.AccessOptions) Dir.AccessError!void, + dirCreateFile: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.CreateFlags) File.OpenError!File, + dirOpenFile: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.OpenFlags) File.OpenError!File, + dirOpenDir: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.OpenOptions) Dir.OpenError!Dir, + dirClose: *const fn (?*anyopaque, Dir) void, + fileStat: *const fn (?*anyopaque, File) File.StatError!File.Stat, + fileClose: *const fn (?*anyopaque, File) void, + fileWriteStreaming: *const fn (?*anyopaque, File, buffer: [][]const u8) File.WriteStreamingError!usize, + fileWritePositional: *const fn (?*anyopaque, File, buffer: [][]const u8, offset: u64) File.WritePositionalError!usize, + /// Returns 0 on end of stream. + fileReadStreaming: *const fn (?*anyopaque, File, data: [][]u8) File.Reader.Error!usize, + /// Returns 0 on end of stream. + fileReadPositional: *const fn (?*anyopaque, File, data: [][]u8, offset: u64) File.ReadPositionalError!usize, + fileSeekBy: *const fn (?*anyopaque, File, relative_offset: i64) File.SeekError!void, + fileSeekTo: *const fn (?*anyopaque, File, absolute_offset: u64) File.SeekError!void, + openSelfExe: *const fn (?*anyopaque, File.OpenFlags) File.OpenSelfExeError!File, + + now: *const fn (?*anyopaque, Clock) Clock.Error!Timestamp, + sleep: *const fn (?*anyopaque, Timeout) SleepError!void, + + netListenIp: *const fn (?*anyopaque, address: net.IpAddress, net.IpAddress.ListenOptions) net.IpAddress.ListenError!net.Server, + netAccept: *const fn (?*anyopaque, server: net.Socket.Handle) net.Server.AcceptError!net.Stream, + netBindIp: *const fn (?*anyopaque, address: *const net.IpAddress, options: net.IpAddress.BindOptions) net.IpAddress.BindError!net.Socket, + netConnectIp: *const fn (?*anyopaque, address: *const net.IpAddress, options: net.IpAddress.ConnectOptions) net.IpAddress.ConnectError!net.Stream, + netListenUnix: *const fn (?*anyopaque, *const net.UnixAddress, net.UnixAddress.ListenOptions) net.UnixAddress.ListenError!net.Socket.Handle, + netConnectUnix: *const fn (?*anyopaque, *const net.UnixAddress) net.UnixAddress.ConnectError!net.Socket.Handle, + netSend: *const fn (?*anyopaque, net.Socket.Handle, []net.OutgoingMessage, net.SendFlags) struct { ?net.Socket.SendError, usize }, + netReceive: *const fn (?*anyopaque, net.Socket.Handle, message_buffer: []net.IncomingMessage, data_buffer: []u8, net.ReceiveFlags, Timeout) struct { ?net.Socket.ReceiveTimeoutError, usize }, + /// Returns 0 on end of stream. + netRead: *const fn (?*anyopaque, src: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize, + netWrite: *const fn (?*anyopaque, dest: net.Socket.Handle, header: []const u8, data: []const []const u8, splat: usize) net.Stream.Writer.Error!usize, + netClose: *const fn (?*anyopaque, handle: net.Socket.Handle) void, + netInterfaceNameResolve: *const fn (?*anyopaque, *const net.Interface.Name) net.Interface.Name.ResolveError!net.Interface, + netInterfaceName: *const fn (?*anyopaque, net.Interface) net.Interface.NameError!net.Interface.Name, + netLookup: *const fn (?*anyopaque, net.HostName, *Queue(net.HostName.LookupResult), net.HostName.LookupOptions) void, +}; + +pub const Cancelable = error{ + /// Caller has requested the async operation to stop. + Canceled, +}; + +pub const UnexpectedError = error{ + /// The Operating System returned an undocumented error code. + /// + /// This error is in theory not possible, but it would be better + /// to handle this error than to invoke undefined behavior. + /// + /// When this error code is observed, it usually means the Zig Standard + /// Library needs a small patch to add the error code to the error set for + /// the respective function. + Unexpected, +}; + +pub const Dir = @import("Io/Dir.zig"); +pub const File = @import("Io/File.zig"); + +pub const Clock = enum { + /// A settable system-wide clock that measures real (i.e. wall-clock) + /// time. This clock is affected by discontinuous jumps in the system + /// time (e.g., if the system administrator manually changes the + /// clock), and by frequency adjust‐ ments performed by NTP and similar + /// applications. + /// + /// This clock normally counts the number of seconds since 1970-01-01 + /// 00:00:00 Coordinated Universal Time (UTC) except that it ignores + /// leap seconds; near a leap second it is typically adjusted by NTP to + /// stay roughly in sync with UTC. + /// + /// The epoch is implementation-defined. For example NTFS/Windows uses + /// 1601-01-01. + real, + /// A nonsettable system-wide clock that represents time since some + /// unspecified point in the past. + /// + /// Monotonic: Guarantees that the time returned by consecutive calls + /// will not go backwards, but successive calls may return identical + /// (not-increased) time values. + /// + /// Not affected by discontinuous jumps in the system time (e.g., if + /// the system administrator manually changes the clock), but may be + /// affected by frequency adjustments. + /// + /// This clock expresses intent to **exclude time that the system is + /// suspended**. However, implementations may be unable to satisify + /// this, and may include that time. + /// + /// * On Linux, corresponds `CLOCK_MONOTONIC`. + /// * On macOS, corresponds to `CLOCK_UPTIME_RAW`. + awake, + /// Identical to `awake` except it expresses intent to **include time + /// that the system is suspended**, however, due to limitations it may + /// behave identically to `awake`. + /// + /// * On Linux, corresponds `CLOCK_BOOTTIME`. + /// * On macOS, corresponds to `CLOCK_MONOTONIC_RAW`. + boot, + /// Tracks the amount of CPU in user or kernel mode used by the calling + /// process. + cpu_process, + /// Tracks the amount of CPU in user or kernel mode used by the calling + /// thread. + cpu_thread, + + pub const Error = error{UnsupportedClock} || UnexpectedError; + + /// This function is not cancelable because first of all it does not block, + /// but more importantly, the cancelation logic itself may want to check + /// the time. + pub fn now(clock: Clock, io: Io) Error!Io.Timestamp { + return io.vtable.now(io.userdata, clock); + } + + pub const Timestamp = struct { + raw: Io.Timestamp, + clock: Clock, + + /// This function is not cancelable because first of all it does not block, + /// but more importantly, the cancelation logic itself may want to check + /// the time. + pub fn now(io: Io, clock: Clock) Error!Clock.Timestamp { + return .{ + .raw = try io.vtable.now(io.userdata, clock), + .clock = clock, + }; + } + + pub fn wait(t: Clock.Timestamp, io: Io) SleepError!void { + return io.vtable.sleep(io.userdata, .{ .deadline = t }); + } + + pub fn durationTo(from: Clock.Timestamp, to: Clock.Timestamp) Clock.Duration { + assert(from.clock == to.clock); + return .{ + .raw = from.raw.durationTo(to.raw), + .clock = from.clock, + }; + } + + pub fn addDuration(from: Clock.Timestamp, duration: Clock.Duration) Clock.Timestamp { + assert(from.clock == duration.clock); + return .{ + .raw = from.raw.addDuration(duration.raw), + .clock = from.clock, + }; + } + + pub fn subDuration(from: Clock.Timestamp, duration: Clock.Duration) Clock.Timestamp { + assert(from.clock == duration.clock); + return .{ + .raw = from.raw.subDuration(duration.raw), + .clock = from.clock, + }; + } + + pub fn fromNow(io: Io, duration: Clock.Duration) Error!Clock.Timestamp { + return .{ + .clock = duration.clock, + .raw = (try duration.clock.now(io)).addDuration(duration.raw), + }; + } + + pub fn untilNow(timestamp: Clock.Timestamp, io: Io) Error!Clock.Duration { + const now_ts = try Clock.Timestamp.now(io, timestamp.clock); + return timestamp.durationTo(now_ts); + } + + pub fn durationFromNow(timestamp: Clock.Timestamp, io: Io) Error!Clock.Duration { + const now_ts = try timestamp.clock.now(io); + return .{ + .clock = timestamp.clock, + .raw = now_ts.durationTo(timestamp.raw), + }; + } + + pub fn toClock(t: Clock.Timestamp, io: Io, clock: Clock) Error!Clock.Timestamp { + if (t.clock == clock) return t; + const now_old = try t.clock.now(io); + const now_new = try clock.now(io); + const duration = now_old.durationTo(t); + return .{ + .clock = clock, + .raw = now_new.addDuration(duration), + }; + } + + pub fn compare(lhs: Clock.Timestamp, op: std.math.CompareOperator, rhs: Clock.Timestamp) bool { + assert(lhs.clock == rhs.clock); + return std.math.compare(lhs.raw.nanoseconds, op, rhs.raw.nanoseconds); + } + }; + + pub const Duration = struct { + raw: Io.Duration, + clock: Clock, + + pub fn sleep(duration: Clock.Duration, io: Io) SleepError!void { + return io.vtable.sleep(io.userdata, .{ .duration = duration }); + } + }; +}; + +pub const Timestamp = struct { + nanoseconds: i96, + + pub const zero: Timestamp = .{ .nanoseconds = 0 }; + + pub fn durationTo(from: Timestamp, to: Timestamp) Duration { + return .{ .nanoseconds = to.nanoseconds - from.nanoseconds }; + } + + pub fn addDuration(from: Timestamp, duration: Duration) Timestamp { + return .{ .nanoseconds = from.nanoseconds + duration.nanoseconds }; + } + + pub fn subDuration(from: Timestamp, duration: Duration) Timestamp { + return .{ .nanoseconds = from.nanoseconds - duration.nanoseconds }; + } + + pub fn withClock(t: Timestamp, clock: Clock) Clock.Timestamp { + return .{ .nanoseconds = t.nanoseconds, .clock = clock }; + } + + pub fn fromNanoseconds(x: i96) Timestamp { + return .{ .nanoseconds = x }; + } + + pub fn toSeconds(t: Timestamp) i64 { + return @intCast(@divTrunc(t.nanoseconds, std.time.ns_per_s)); + } + + pub fn toNanoseconds(t: Timestamp) i96 { + return t.nanoseconds; + } + + pub fn formatNumber(t: Timestamp, w: *std.Io.Writer, n: std.fmt.Number) std.Io.Writer.Error!void { + return w.printInt(t.nanoseconds, n.mode.base() orelse 10, n.case, .{ + .precision = n.precision, + .width = n.width, + .alignment = n.alignment, + .fill = n.fill, + }); + } +}; + +pub const Duration = struct { + nanoseconds: i96, + + pub const zero: Duration = .{ .nanoseconds = 0 }; + pub const max: Duration = .{ .nanoseconds = std.math.maxInt(i96) }; + + pub fn fromNanoseconds(x: i96) Duration { + return .{ .nanoseconds = x }; + } + + pub fn fromMilliseconds(x: i64) Duration { + return .{ .nanoseconds = @as(i96, x) * std.time.ns_per_ms }; + } + + pub fn fromSeconds(x: i64) Duration { + return .{ .nanoseconds = @as(i96, x) * std.time.ns_per_s }; + } + + pub fn toMilliseconds(d: Duration) i64 { + return @intCast(@divTrunc(d.nanoseconds, std.time.ns_per_ms)); + } + + pub fn toSeconds(d: Duration) i64 { + return @intCast(@divTrunc(d.nanoseconds, std.time.ns_per_s)); + } + + pub fn toNanoseconds(d: Duration) i96 { + return d.nanoseconds; + } +}; + +/// Declares under what conditions an operation should return `error.Timeout`. +pub const Timeout = union(enum) { + none, + duration: Clock.Duration, + deadline: Clock.Timestamp, + + pub const Error = error{ Timeout, UnsupportedClock }; + + pub fn toDeadline(t: Timeout, io: Io) Clock.Error!?Clock.Timestamp { + return switch (t) { + .none => null, + .duration => |d| try .fromNow(io, d), + .deadline => |d| d, + }; + } + + pub fn toDurationFromNow(t: Timeout, io: Io) Clock.Error!?Clock.Duration { + return switch (t) { + .none => null, + .duration => |d| d, + .deadline => |d| try d.durationFromNow(io), + }; + } + + pub fn sleep(timeout: Timeout, io: Io) SleepError!void { + return io.vtable.sleep(io.userdata, timeout); + } +}; + +pub const AnyFuture = opaque {}; + +pub fn Future(Result: type) type { + return struct { + any_future: ?*AnyFuture, + result: Result, + + /// Equivalent to `await` but places a cancellation request. + /// + /// Idempotent. Not threadsafe. + pub fn cancel(f: *@This(), io: Io) Result { + const any_future = f.any_future orelse return f.result; + io.vtable.cancel(io.userdata, any_future, @ptrCast((&f.result)[0..1]), .of(Result)); + f.any_future = null; + return f.result; + } + + /// Idempotent. Not threadsafe. + pub fn await(f: *@This(), io: Io) Result { + const any_future = f.any_future orelse return f.result; + io.vtable.await(io.userdata, any_future, @ptrCast((&f.result)[0..1]), .of(Result)); + f.any_future = null; + return f.result; + } + }; +} + +pub const Group = struct { + state: usize, + context: ?*anyopaque, + token: ?*anyopaque, + + pub const init: Group = .{ .state = 0, .context = null, .token = null }; + + /// Calls `function` with `args` asynchronously. The resource spawned is + /// owned by the group. + /// + /// `function` *may* be called immediately, before `async` returns. + /// + /// After this is called, `wait` or `cancel` must be called before the + /// group is deinitialized. + /// + /// Threadsafe. + /// + /// See also: + /// * `Io.async` + /// * `concurrent` + pub fn async(g: *Group, io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) void { + const Args = @TypeOf(args); + const TypeErased = struct { + fn start(group: *Group, context: *const anyopaque) void { + _ = group; + const args_casted: *const Args = @ptrCast(@alignCast(context)); + @call(.auto, function, args_casted.*); + } + }; + io.vtable.groupAsync(io.userdata, g, @ptrCast((&args)[0..1]), .of(Args), TypeErased.start); + } + + /// Blocks until all tasks of the group finish. During this time, + /// cancellation requests propagate to all members of the group. + /// + /// Idempotent. Not threadsafe. + pub fn wait(g: *Group, io: Io) void { + const token = g.token orelse return; + g.token = null; + io.vtable.groupWait(io.userdata, g, token); + } + + /// Equivalent to `wait` but immediately requests cancellation on all + /// members of the group. + /// + /// Idempotent. Not threadsafe. + pub fn cancel(g: *Group, io: Io) void { + const token = g.token orelse return; + g.token = null; + io.vtable.groupCancel(io.userdata, g, token); + } +}; + +pub fn Select(comptime U: type) type { + return struct { + io: Io, + group: Group, + queue: Queue(U), + outstanding: usize, + + const S = @This(); + + pub const Union = U; + + pub const Field = std.meta.FieldEnum(U); + + pub fn init(io: Io, buffer: []U) S { + return .{ + .io = io, + .queue = .init(buffer), + .group = .init, + .outstanding = 0, + }; + } + + /// Calls `function` with `args` asynchronously. The resource spawned is + /// owned by the select. + /// + /// `function` must have return type matching the `field` field of `Union`. + /// + /// `function` *may* be called immediately, before `async` returns. + /// + /// After this is called, `wait` or `cancel` must be called before the + /// select is deinitialized. + /// + /// Threadsafe. + /// + /// Related: + /// * `Io.async` + /// * `Group.async` + pub fn async( + s: *S, + comptime field: Field, + function: anytype, + args: std.meta.ArgsTuple(@TypeOf(function)), + ) void { + const Args = @TypeOf(args); + const TypeErased = struct { + fn start(group: *Group, context: *const anyopaque) void { + const args_casted: *const Args = @ptrCast(@alignCast(context)); + const unerased_select: *S = @fieldParentPtr("group", group); + const elem = @unionInit(U, @tagName(field), @call(.auto, function, args_casted.*)); + unerased_select.queue.putOneUncancelable(unerased_select.io, elem); + } + }; + _ = @atomicRmw(usize, &s.outstanding, .Add, 1, .monotonic); + s.io.vtable.groupAsync(s.io.userdata, &s.group, @ptrCast((&args)[0..1]), .of(Args), TypeErased.start); + } + + /// Blocks until another task of the select finishes. + /// + /// Asserts there is at least one more `outstanding` task. + /// + /// Not threadsafe. + pub fn wait(s: *S) Cancelable!U { + s.outstanding -= 1; + return s.queue.getOne(s.io); + } + + /// Equivalent to `wait` but requests cancellation on all remaining + /// tasks owned by the select. + /// + /// It is illegal to call `wait` after this. + /// + /// Idempotent. Not threadsafe. + pub fn cancel(s: *S) void { + s.outstanding = 0; + s.group.cancel(s.io); + } + }; +} + +pub const Mutex = struct { + state: State, + + pub const State = enum(usize) { + locked_once = 0b00, + unlocked = 0b01, + contended = 0b10, + /// contended + _, + + pub fn isUnlocked(state: State) bool { + return @intFromEnum(state) & @intFromEnum(State.unlocked) == @intFromEnum(State.unlocked); + } + }; + + pub const init: Mutex = .{ .state = .unlocked }; + + pub fn tryLock(mutex: *Mutex) bool { + const prev_state: State = @enumFromInt(@atomicRmw( + usize, + @as(*usize, @ptrCast(&mutex.state)), + .And, + ~@intFromEnum(State.unlocked), + .acquire, + )); + return prev_state.isUnlocked(); + } + + pub fn lock(mutex: *Mutex, io: std.Io) Cancelable!void { + const prev_state: State = @enumFromInt(@atomicRmw( + usize, + @as(*usize, @ptrCast(&mutex.state)), + .And, + ~@intFromEnum(State.unlocked), + .acquire, + )); + if (prev_state.isUnlocked()) { + @branchHint(.likely); + return; + } + return io.vtable.mutexLock(io.userdata, prev_state, mutex); + } + + /// Same as `lock` but cannot be canceled. + pub fn lockUncancelable(mutex: *Mutex, io: std.Io) void { + const prev_state: State = @enumFromInt(@atomicRmw( + usize, + @as(*usize, @ptrCast(&mutex.state)), + .And, + ~@intFromEnum(State.unlocked), + .acquire, + )); + if (prev_state.isUnlocked()) { + @branchHint(.likely); + return; + } + return io.vtable.mutexLockUncancelable(io.userdata, prev_state, mutex); + } + + pub fn unlock(mutex: *Mutex, io: std.Io) void { + const prev_state = @cmpxchgWeak(State, &mutex.state, .locked_once, .unlocked, .release, .acquire) orelse { + @branchHint(.likely); + return; + }; + assert(prev_state != .unlocked); // mutex not locked + return io.vtable.mutexUnlock(io.userdata, prev_state, mutex); + } +}; + +pub const Condition = struct { + state: u64 = 0, + + pub fn wait(cond: *Condition, io: Io, mutex: *Mutex) Cancelable!void { + return io.vtable.conditionWait(io.userdata, cond, mutex); + } + + pub fn waitUncancelable(cond: *Condition, io: Io, mutex: *Mutex) void { + return io.vtable.conditionWaitUncancelable(io.userdata, cond, mutex); + } + + pub fn signal(cond: *Condition, io: Io) void { + io.vtable.conditionWake(io.userdata, cond, .one); + } + + pub fn broadcast(cond: *Condition, io: Io) void { + io.vtable.conditionWake(io.userdata, cond, .all); + } + + pub const Wake = enum { + /// Wake up only one thread. + one, + /// Wake up all threads. + all, + }; +}; + +pub const TypeErasedQueue = struct { + mutex: Mutex, + + /// Ring buffer. This data is logically *after* queued getters. + buffer: []u8, + put_index: usize, + get_index: usize, + + putters: std.DoublyLinkedList, + getters: std.DoublyLinkedList, + + const Put = struct { + remaining: []const u8, + condition: Condition, + node: std.DoublyLinkedList.Node, + }; + + const Get = struct { + remaining: []u8, + condition: Condition, + node: std.DoublyLinkedList.Node, + }; + + pub fn init(buffer: []u8) TypeErasedQueue { + return .{ + .mutex = .init, + .buffer = buffer, + .put_index = 0, + .get_index = 0, + .putters = .{}, + .getters = .{}, + }; + } + + pub fn put(q: *TypeErasedQueue, io: Io, elements: []const u8, min: usize) Cancelable!usize { + assert(elements.len >= min); + if (elements.len == 0) return 0; + try q.mutex.lock(io); + defer q.mutex.unlock(io); + return putLocked(q, io, elements, min, false); + } + + /// Same as `put` but cannot be canceled. + pub fn putUncancelable(q: *TypeErasedQueue, io: Io, elements: []const u8, min: usize) usize { + assert(elements.len >= min); + if (elements.len == 0) return 0; + q.mutex.lockUncancelable(io); + defer q.mutex.unlock(io); + return putLocked(q, io, elements, min, true) catch |err| switch (err) { + error.Canceled => unreachable, + }; + } + + fn putLocked(q: *TypeErasedQueue, io: Io, elements: []const u8, min: usize, uncancelable: bool) Cancelable!usize { + // Getters have first priority on the data, and only when the getters + // queue is empty do we start populating the buffer. + + var remaining = elements; + while (true) { + const getter: *Get = @alignCast(@fieldParentPtr("node", q.getters.popFirst() orelse break)); + const copy_len = @min(getter.remaining.len, remaining.len); + @memcpy(getter.remaining[0..copy_len], remaining[0..copy_len]); + remaining = remaining[copy_len..]; + getter.remaining = getter.remaining[copy_len..]; + if (getter.remaining.len == 0) { + getter.condition.signal(io); + continue; + } + q.getters.prepend(&getter.node); + assert(remaining.len == 0); + return elements.len; + } + + while (true) { + { + const available = q.buffer[q.put_index..]; + const copy_len = @min(available.len, remaining.len); + @memcpy(available[0..copy_len], remaining[0..copy_len]); + remaining = remaining[copy_len..]; + q.put_index += copy_len; + if (remaining.len == 0) return elements.len; + } + { + const available = q.buffer[0..q.get_index]; + const copy_len = @min(available.len, remaining.len); + @memcpy(available[0..copy_len], remaining[0..copy_len]); + remaining = remaining[copy_len..]; + q.put_index = copy_len; + if (remaining.len == 0) return elements.len; + } + + const total_filled = elements.len - remaining.len; + if (total_filled >= min) return total_filled; + + var pending: Put = .{ .remaining = remaining, .condition = .{}, .node = .{} }; + q.putters.append(&pending.node); + if (uncancelable) + pending.condition.waitUncancelable(io, &q.mutex) + else + try pending.condition.wait(io, &q.mutex); + remaining = pending.remaining; + } + } + + pub fn get(q: *@This(), io: Io, buffer: []u8, min: usize) Cancelable!usize { + assert(buffer.len >= min); + if (buffer.len == 0) return 0; + try q.mutex.lock(io); + defer q.mutex.unlock(io); + return getLocked(q, io, buffer, min, false); + } + + pub fn getUncancelable(q: *@This(), io: Io, buffer: []u8, min: usize) usize { + assert(buffer.len >= min); + if (buffer.len == 0) return 0; + q.mutex.lockUncancelable(io); + defer q.mutex.unlock(io); + return getLocked(q, io, buffer, min, true) catch |err| switch (err) { + error.Canceled => unreachable, + }; + } + + pub fn getLocked(q: *@This(), io: Io, buffer: []u8, min: usize, uncancelable: bool) Cancelable!usize { + // The ring buffer gets first priority, then data should come from any + // queued putters, then finally the ring buffer should be filled with + // data from putters so they can be resumed. + + var remaining = buffer; + while (true) { + if (q.get_index <= q.put_index) { + const available = q.buffer[q.get_index..q.put_index]; + const copy_len = @min(available.len, remaining.len); + @memcpy(remaining[0..copy_len], available[0..copy_len]); + q.get_index += copy_len; + remaining = remaining[copy_len..]; + if (remaining.len == 0) return fillRingBufferFromPutters(q, io, buffer.len); + } else { + { + const available = q.buffer[q.get_index..]; + const copy_len = @min(available.len, remaining.len); + @memcpy(remaining[0..copy_len], available[0..copy_len]); + q.get_index += copy_len; + remaining = remaining[copy_len..]; + if (remaining.len == 0) return fillRingBufferFromPutters(q, io, buffer.len); + } + { + const available = q.buffer[0..q.put_index]; + const copy_len = @min(available.len, remaining.len); + @memcpy(remaining[0..copy_len], available[0..copy_len]); + q.get_index = copy_len; + remaining = remaining[copy_len..]; + if (remaining.len == 0) return fillRingBufferFromPutters(q, io, buffer.len); + } + } + // Copy directly from putters into buffer. + while (remaining.len > 0) { + const putter: *Put = @alignCast(@fieldParentPtr("node", q.putters.popFirst() orelse break)); + const copy_len = @min(putter.remaining.len, remaining.len); + @memcpy(remaining[0..copy_len], putter.remaining[0..copy_len]); + putter.remaining = putter.remaining[copy_len..]; + remaining = remaining[copy_len..]; + if (putter.remaining.len == 0) { + putter.condition.signal(io); + } else { + assert(remaining.len == 0); + q.putters.prepend(&putter.node); + return fillRingBufferFromPutters(q, io, buffer.len); + } + } + // Both ring buffer and putters queue is empty. + const total_filled = buffer.len - remaining.len; + if (total_filled >= min) return total_filled; + + var pending: Get = .{ .remaining = remaining, .condition = .{}, .node = .{} }; + q.getters.append(&pending.node); + if (uncancelable) + pending.condition.waitUncancelable(io, &q.mutex) + else + try pending.condition.wait(io, &q.mutex); + remaining = pending.remaining; + } + } + + /// Called when there is nonzero space available in the ring buffer and + /// potentially putters waiting. The mutex is already held and the task is + /// to copy putter data to the ring buffer and signal any putters whose + /// buffers been fully copied. + fn fillRingBufferFromPutters(q: *TypeErasedQueue, io: Io, len: usize) usize { + while (true) { + const putter: *Put = @alignCast(@fieldParentPtr("node", q.putters.popFirst() orelse return len)); + const available = q.buffer[q.put_index..]; + const copy_len = @min(available.len, putter.remaining.len); + @memcpy(available[0..copy_len], putter.remaining[0..copy_len]); + putter.remaining = putter.remaining[copy_len..]; + q.put_index += copy_len; + if (putter.remaining.len == 0) { + putter.condition.signal(io); + continue; + } + const second_available = q.buffer[0..q.get_index]; + const second_copy_len = @min(second_available.len, putter.remaining.len); + @memcpy(second_available[0..second_copy_len], putter.remaining[0..second_copy_len]); + putter.remaining = putter.remaining[copy_len..]; + q.put_index = copy_len; + if (putter.remaining.len == 0) { + putter.condition.signal(io); + continue; + } + q.putters.prepend(&putter.node); + return len; + } + } +}; + +/// Many producer, many consumer, thread-safe, runtime configurable buffer size. +/// When buffer is empty, consumers suspend and are resumed by producers. +/// When buffer is full, producers suspend and are resumed by consumers. +pub fn Queue(Elem: type) type { + return struct { + type_erased: TypeErasedQueue, + + pub fn init(buffer: []Elem) @This() { + return .{ .type_erased = .init(@ptrCast(buffer)) }; + } + + /// Appends elements to the end of the queue. The function returns when + /// at least `min` elements have been added to the buffer or sent + /// directly to a consumer. + /// + /// Returns how many elements have been added to the queue. + /// + /// Asserts that `elements.len >= min`. + pub fn put(q: *@This(), io: Io, elements: []const Elem, min: usize) Cancelable!usize { + return @divExact(try q.type_erased.put(io, @ptrCast(elements), min * @sizeOf(Elem)), @sizeOf(Elem)); + } + + /// Same as `put` but blocks until all elements have been added to the queue. + pub fn putAll(q: *@This(), io: Io, elements: []const Elem) Cancelable!void { + assert(try q.put(io, elements, elements.len) == elements.len); + } + + /// Same as `put` but cannot be interrupted. + pub fn putUncancelable(q: *@This(), io: Io, elements: []const Elem, min: usize) usize { + return @divExact(q.type_erased.putUncancelable(io, @ptrCast(elements), min * @sizeOf(Elem)), @sizeOf(Elem)); + } + + pub fn putOne(q: *@This(), io: Io, item: Elem) Cancelable!void { + assert(try q.put(io, &.{item}, 1) == 1); + } + + pub fn putOneUncancelable(q: *@This(), io: Io, item: Elem) void { + assert(q.putUncancelable(io, &.{item}, 1) == 1); + } + + /// Receives elements from the beginning of the queue. The function + /// returns when at least `min` elements have been populated inside + /// `buffer`. + /// + /// Returns how many elements of `buffer` have been populated. + /// + /// Asserts that `buffer.len >= min`. + pub fn get(q: *@This(), io: Io, buffer: []Elem, min: usize) Cancelable!usize { + return @divExact(try q.type_erased.get(io, @ptrCast(buffer), min * @sizeOf(Elem)), @sizeOf(Elem)); + } + + pub fn getUncancelable(q: *@This(), io: Io, buffer: []Elem, min: usize) usize { + return @divExact(q.type_erased.getUncancelable(io, @ptrCast(buffer), min * @sizeOf(Elem)), @sizeOf(Elem)); + } + + pub fn getOne(q: *@This(), io: Io) Cancelable!Elem { + var buf: [1]Elem = undefined; + assert(try q.get(io, &buf, 1) == 1); + return buf[0]; + } + + pub fn getOneUncancelable(q: *@This(), io: Io) Elem { + var buf: [1]Elem = undefined; + assert(q.getUncancelable(io, &buf, 1) == 1); + return buf[0]; + } + + /// Returns buffer length in `Elem` units. + pub fn capacity(q: *const @This()) usize { + return @divExact(q.type_erased.buffer.len, @sizeOf(Elem)); + } + }; +} + +/// Calls `function` with `args`, such that the return value of the function is +/// not guaranteed to be available until `await` is called. +/// +/// `function` *may* be called immediately, before `async` returns. This has +/// weaker guarantees than `concurrent`, making more portable and +/// reusable. +/// +/// See also: +/// * `Group` +pub fn async( + io: Io, + function: anytype, + args: std.meta.ArgsTuple(@TypeOf(function)), +) Future(@typeInfo(@TypeOf(function)).@"fn".return_type.?) { + const Result = @typeInfo(@TypeOf(function)).@"fn".return_type.?; + const Args = @TypeOf(args); + const TypeErased = struct { + fn start(context: *const anyopaque, result: *anyopaque) void { + const args_casted: *const Args = @ptrCast(@alignCast(context)); + const result_casted: *Result = @ptrCast(@alignCast(result)); + result_casted.* = @call(.auto, function, args_casted.*); + } + }; + var future: Future(Result) = undefined; + future.any_future = io.vtable.async( + io.userdata, + @ptrCast((&future.result)[0..1]), + .of(Result), + @ptrCast((&args)[0..1]), + .of(Args), + TypeErased.start, + ); + return future; +} + +pub const ConcurrentError = error{ + /// May occur due to a temporary condition such as resource exhaustion, or + /// to the Io implementation not supporting concurrency. + ConcurrencyUnavailable, +}; + +/// Calls `function` with `args`, such that the return value of the function is +/// not guaranteed to be available until `await` is called, allowing the caller +/// to progress while waiting for any `Io` operations. +/// +/// This has stronger guarantee than `async`, placing restrictions on what kind +/// of `Io` implementations are supported. By calling `async` instead, one +/// allows, for example, stackful single-threaded blocking I/O. +pub fn concurrent( + io: Io, + function: anytype, + args: std.meta.ArgsTuple(@TypeOf(function)), +) ConcurrentError!Future(@typeInfo(@TypeOf(function)).@"fn".return_type.?) { + const Result = @typeInfo(@TypeOf(function)).@"fn".return_type.?; + const Args = @TypeOf(args); + const TypeErased = struct { + fn start(context: *const anyopaque, result: *anyopaque) void { + const args_casted: *const Args = @ptrCast(@alignCast(context)); + const result_casted: *Result = @ptrCast(@alignCast(result)); + result_casted.* = @call(.auto, function, args_casted.*); + } + }; + var future: Future(Result) = undefined; + future.any_future = try io.vtable.concurrent( + io.userdata, + @sizeOf(Result), + .of(Result), + @ptrCast((&args)[0..1]), + .of(Args), + TypeErased.start, + ); + return future; +} + +pub fn cancelRequested(io: Io) bool { + return io.vtable.cancelRequested(io.userdata); +} + +pub const SleepError = error{UnsupportedClock} || UnexpectedError || Cancelable; + +pub fn sleep(io: Io, duration: Duration, clock: Clock) SleepError!void { + return io.vtable.sleep(io.userdata, .{ .duration = .{ + .raw = duration, + .clock = clock, + } }); +} + +/// Given a struct with each field a `*Future`, returns a union with the same +/// fields, each field type the future's result. +pub fn SelectUnion(S: type) type { + const struct_fields = @typeInfo(S).@"struct".fields; + var fields: [struct_fields.len]std.builtin.Type.UnionField = undefined; + for (&fields, struct_fields) |*union_field, struct_field| { + const F = @typeInfo(struct_field.type).pointer.child; + const Result = @TypeOf(@as(F, undefined).result); + union_field.* = .{ + .name = struct_field.name, + .type = Result, + .alignment = struct_field.alignment, + }; + } + return @Type(.{ .@"union" = .{ + .layout = .auto, + .tag_type = std.meta.FieldEnum(S), + .fields = &fields, + .decls = &.{}, + } }); +} + +/// `s` is a struct with every field a `*Future(T)`, where `T` can be any type, +/// and can be different for each field. +pub fn select(io: Io, s: anytype) Cancelable!SelectUnion(@TypeOf(s)) { + const U = SelectUnion(@TypeOf(s)); + const S = @TypeOf(s); + const fields = @typeInfo(S).@"struct".fields; + var futures: [fields.len]*AnyFuture = undefined; + inline for (fields, &futures) |field, *any_future| { + const future = @field(s, field.name); + any_future.* = future.any_future orelse return @unionInit(U, field.name, future.result); + } + switch (try io.vtable.select(io.userdata, &futures)) { + inline 0...(fields.len - 1) => |selected_index| { + const field_name = fields[selected_index].name; + return @unionInit(U, field_name, @field(s, field_name).await(io)); + }, + else => unreachable, + } +} diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig new file mode 100644 index 0000000000..ab130dca8e --- /dev/null +++ b/lib/std/Io/Dir.zig @@ -0,0 +1,392 @@ +const Dir = @This(); + +const builtin = @import("builtin"); +const native_os = builtin.os.tag; + +const std = @import("../std.zig"); +const Io = std.Io; +const File = Io.File; + +handle: Handle, + +pub const Mode = Io.File.Mode; +pub const default_mode: Mode = 0o755; + +/// Returns a handle to the current working directory. +/// +/// It is not opened with iteration capability. Iterating over the result is +/// illegal behavior. +/// +/// Closing the returned `Dir` is checked illegal behavior. +/// +/// On POSIX targets, this function is comptime-callable. +pub fn cwd() Dir { + return switch (native_os) { + .windows => .{ .handle = std.os.windows.peb().ProcessParameters.CurrentDirectory.Handle }, + .wasi => .{ .handle = std.options.wasiCwd() }, + else => .{ .handle = std.posix.AT.FDCWD }, + }; +} + +pub const Handle = std.posix.fd_t; + +pub const PathNameError = error{ + NameTooLong, + /// File system cannot encode the requested file name bytes. + /// Could be due to invalid WTF-8 on Windows, invalid UTF-8 on WASI, + /// invalid characters on Windows, etc. Filesystem and operating specific. + BadPathName, +}; + +pub const AccessError = error{ + AccessDenied, + PermissionDenied, + FileNotFound, + InputOutput, + SystemResources, + FileBusy, + SymLinkLoop, + ReadOnlyFileSystem, +} || PathNameError || Io.Cancelable || Io.UnexpectedError; + +pub const AccessOptions = packed struct { + follow_symlinks: bool = true, + read: bool = false, + write: bool = false, + execute: bool = false, +}; + +/// Test accessing `sub_path`. +/// +/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). +/// On WASI, `sub_path` should be encoded as valid UTF-8. +/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. +/// +/// Be careful of Time-Of-Check-Time-Of-Use race conditions when using this +/// function. For example, instead of testing if a file exists and then opening +/// it, just open it and handle the error for file not found. +pub fn access(dir: Dir, io: Io, sub_path: []const u8, options: AccessOptions) AccessError!void { + return io.vtable.dirAccess(io.userdata, dir, sub_path, options); +} + +pub const OpenError = error{ + FileNotFound, + NotDir, + AccessDenied, + PermissionDenied, + SymLinkLoop, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, + NoDevice, + SystemResources, + DeviceBusy, + /// On Windows, `\\server` or `\\server\share` was not found. + NetworkNotFound, +} || PathNameError || Io.Cancelable || Io.UnexpectedError; + +pub const OpenOptions = struct { + /// `true` means the opened directory can be used as the `Dir` parameter + /// for functions which operate based on an open directory handle. When `false`, + /// such operations are Illegal Behavior. + access_sub_paths: bool = true, + /// `true` means the opened directory can be scanned for the files and sub-directories + /// of the result. It means the `iterate` function can be called. + iterate: bool = false, + /// `false` means it won't dereference the symlinks. + follow_symlinks: bool = true, +}; + +/// Opens a directory at the given path. The directory is a system resource that remains +/// open until `close` is called on the result. +/// +/// The directory cannot be iterated unless the `iterate` option is set to `true`. +/// +/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). +/// On WASI, `sub_path` should be encoded as valid UTF-8. +/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. +pub fn openDir(dir: Dir, io: Io, sub_path: []const u8, options: OpenOptions) OpenError!Dir { + return io.vtable.dirOpenDir(io.userdata, dir, sub_path, options); +} + +pub fn close(dir: Dir, io: Io) void { + return io.vtable.dirClose(io.userdata, dir); +} + +/// Opens a file for reading or writing, without attempting to create a new file. +/// +/// To create a new file, see `createFile`. +/// +/// Allocates a resource to be released with `File.close`. +/// +/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). +/// On WASI, `sub_path` should be encoded as valid UTF-8. +/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. +pub fn openFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { + return io.vtable.dirOpenFile(io.userdata, dir, sub_path, flags); +} + +/// Creates, opens, or overwrites a file with write access. +/// +/// Allocates a resource to be dellocated with `File.close`. +/// +/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). +/// On WASI, `sub_path` should be encoded as valid UTF-8. +/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. +pub fn createFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File { + return io.vtable.dirCreateFile(io.userdata, dir, sub_path, flags); +} + +pub const WriteFileOptions = struct { + /// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). + /// On WASI, `sub_path` should be encoded as valid UTF-8. + /// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. + sub_path: []const u8, + data: []const u8, + flags: File.CreateFlags = .{}, +}; + +pub const WriteFileError = File.WriteError || File.OpenError || Io.Cancelable; + +/// Writes content to the file system, using the file creation flags provided. +pub fn writeFile(dir: Dir, io: Io, options: WriteFileOptions) WriteFileError!void { + var file = try dir.createFile(io, options.sub_path, options.flags); + defer file.close(io); + try file.writeAll(io, options.data); +} + +pub const PrevStatus = enum { + stale, + fresh, +}; + +pub const UpdateFileError = File.OpenError; + +/// Check the file size, mtime, and mode of `source_path` and `dest_path`. If +/// they are equal, does nothing. Otherwise, atomically copies `source_path` to +/// `dest_path`, creating the parent directory hierarchy as needed. The +/// destination file gains the mtime, atime, and mode of the source file so +/// that the next call to `updateFile` will not need a copy. +/// +/// Returns the previous status of the file before updating. +/// +/// * On Windows, both paths should be encoded as [WTF-8](https://wtf-8.codeberg.page/). +/// * On WASI, both paths should be encoded as valid UTF-8. +/// * On other platforms, both paths are an opaque sequence of bytes with no particular encoding. +pub fn updateFile( + source_dir: Dir, + io: Io, + source_path: []const u8, + dest_dir: Dir, + /// If directories in this path do not exist, they are created. + dest_path: []const u8, + options: std.fs.Dir.CopyFileOptions, +) !PrevStatus { + var src_file = try source_dir.openFile(io, source_path, .{}); + defer src_file.close(io); + + const src_stat = try src_file.stat(io); + const actual_mode = options.override_mode orelse src_stat.mode; + check_dest_stat: { + const dest_stat = blk: { + var dest_file = dest_dir.openFile(io, dest_path, .{}) catch |err| switch (err) { + error.FileNotFound => break :check_dest_stat, + else => |e| return e, + }; + defer dest_file.close(io); + + break :blk try dest_file.stat(io); + }; + + if (src_stat.size == dest_stat.size and + src_stat.mtime.nanoseconds == dest_stat.mtime.nanoseconds and + actual_mode == dest_stat.mode) + { + return .fresh; + } + } + + if (std.fs.path.dirname(dest_path)) |dirname| { + try dest_dir.makePath(io, dirname); + } + + var buffer: [1000]u8 = undefined; // Used only when direct fd-to-fd is not available. + var atomic_file = try std.fs.Dir.atomicFile(.adaptFromNewApi(dest_dir), dest_path, .{ + .mode = actual_mode, + .write_buffer = &buffer, + }); + defer atomic_file.deinit(); + + var src_reader: File.Reader = .initSize(src_file, io, &.{}, src_stat.size); + const dest_writer = &atomic_file.file_writer.interface; + + _ = dest_writer.sendFileAll(&src_reader, .unlimited) catch |err| switch (err) { + error.ReadFailed => return src_reader.err.?, + error.WriteFailed => return atomic_file.file_writer.err.?, + }; + try atomic_file.flush(); + try atomic_file.file_writer.file.updateTimes(src_stat.atime, src_stat.mtime); + try atomic_file.renameIntoPlace(); + return .stale; +} + +pub const ReadFileError = File.OpenError || File.Reader.Error; + +/// Read all of file contents using a preallocated buffer. +/// +/// The returned slice has the same pointer as `buffer`. If the length matches `buffer.len` +/// the situation is ambiguous. It could either mean that the entire file was read, and +/// it exactly fits the buffer, or it could mean the buffer was not big enough for the +/// entire file. +/// +/// * On Windows, `file_path` should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/). +/// * On WASI, `file_path` should be encoded as valid UTF-8. +/// * On other platforms, `file_path` is an opaque sequence of bytes with no particular encoding. +pub fn readFile(dir: Dir, io: Io, file_path: []const u8, buffer: []u8) ReadFileError![]u8 { + var file = try dir.openFile(io, file_path, .{}); + defer file.close(io); + + var reader = file.reader(io, &.{}); + const n = reader.interface.readSliceShort(buffer) catch |err| switch (err) { + error.ReadFailed => return reader.err.?, + }; + + return buffer[0..n]; +} + +pub const MakeError = error{ + /// In WASI, this error may occur when the file descriptor does + /// not hold the required rights to create a new directory relative to it. + AccessDenied, + PermissionDenied, + DiskQuota, + PathAlreadyExists, + SymLinkLoop, + LinkQuotaExceeded, + FileNotFound, + SystemResources, + NoSpaceLeft, + NotDir, + ReadOnlyFileSystem, + NoDevice, + /// On Windows, `\\server` or `\\server\share` was not found. + NetworkNotFound, +} || PathNameError || Io.Cancelable || Io.UnexpectedError; + +/// Creates a single directory with a relative or absolute path. +/// +/// * On Windows, `sub_path` should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/). +/// * On WASI, `sub_path` should be encoded as valid UTF-8. +/// * On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. +/// +/// Related: +/// * `makePath` +/// * `makeDirAbsolute` +pub fn makeDir(dir: Dir, io: Io, sub_path: []const u8) MakeError!void { + return io.vtable.dirMake(io.userdata, dir, sub_path, default_mode); +} + +pub const MakePathError = MakeError || StatPathError; + +/// Calls makeDir iteratively to make an entire path, creating any parent +/// directories that do not exist. +/// +/// Returns success if the path already exists and is a directory. +/// +/// This function is not atomic, and if it returns an error, the file system +/// may have been modified regardless. +/// +/// Fails on an empty path with `error.BadPathName` as that is not a path that +/// can be created. +/// +/// On Windows, `sub_path` should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/). +/// On WASI, `sub_path` should be encoded as valid UTF-8. +/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. +/// +/// Paths containing `..` components are handled differently depending on the platform: +/// - On Windows, `..` are resolved before the path is passed to NtCreateFile, meaning +/// a `sub_path` like "first/../second" will resolve to "second" and only a +/// `./second` directory will be created. +/// - On other platforms, `..` are not resolved before the path is passed to `mkdirat`, +/// meaning a `sub_path` like "first/../second" will create both a `./first` +/// and a `./second` directory. +pub fn makePath(dir: Dir, io: Io, sub_path: []const u8) MakePathError!void { + _ = try makePathStatus(dir, io, sub_path); +} + +pub const MakePathStatus = enum { existed, created }; + +/// Same as `makePath` except returns whether the path already existed or was +/// successfully created. +pub fn makePathStatus(dir: Dir, io: Io, sub_path: []const u8) MakePathError!MakePathStatus { + var it = try std.fs.path.componentIterator(sub_path); + var status: MakePathStatus = .existed; + var component = it.last() orelse return error.BadPathName; + while (true) { + if (makeDir(dir, io, component.path)) |_| { + status = .created; + } else |err| switch (err) { + error.PathAlreadyExists => { + // stat the file and return an error if it's not a directory + // this is important because otherwise a dangling symlink + // could cause an infinite loop + check_dir: { + // workaround for windows, see https://github.com/ziglang/zig/issues/16738 + const fstat = statPath(dir, io, component.path, .{}) catch |stat_err| switch (stat_err) { + error.IsDir => break :check_dir, + else => |e| return e, + }; + if (fstat.kind != .directory) return error.NotDir; + } + }, + error.FileNotFound => |e| { + component = it.previous() orelse return e; + continue; + }, + else => |e| return e, + } + component = it.next() orelse return status; + } +} + +pub const MakeOpenPathError = MakeError || OpenError || StatPathError; + +/// Performs the equivalent of `makePath` followed by `openDir`, atomically if possible. +/// +/// When this operation is canceled, it may leave the file system in a +/// partially modified state. +/// +/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). +/// On WASI, `sub_path` should be encoded as valid UTF-8. +/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. +pub fn makeOpenPath(dir: Dir, io: Io, sub_path: []const u8, options: OpenOptions) MakeOpenPathError!Dir { + return io.vtable.dirMakeOpenPath(io.userdata, dir, sub_path, options); +} + +pub const Stat = File.Stat; +pub const StatError = File.StatError; + +pub fn stat(dir: Dir, io: Io) StatError!Stat { + return io.vtable.dirStat(io.userdata, dir); +} + +pub const StatPathError = File.OpenError || File.StatError; + +pub const StatPathOptions = struct { + follow_symlinks: bool = true, +}; + +/// Returns metadata for a file inside the directory. +/// +/// On Windows, this requires three syscalls. On other operating systems, it +/// only takes one. +/// +/// Symlinks are followed. +/// +/// `sub_path` may be absolute, in which case `self` is ignored. +/// +/// * On Windows, `sub_path` should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/). +/// * On WASI, `sub_path` should be encoded as valid UTF-8. +/// * On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. +pub fn statPath(dir: Dir, io: Io, sub_path: []const u8, options: StatPathOptions) StatPathError!Stat { + return io.vtable.dirStatPath(io.userdata, dir, sub_path, options); +} diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig new file mode 100644 index 0000000000..cc4ce64a22 --- /dev/null +++ b/lib/std/Io/File.zig @@ -0,0 +1,659 @@ +const File = @This(); + +const builtin = @import("builtin"); +const native_os = builtin.os.tag; +const is_windows = native_os == .windows; + +const std = @import("../std.zig"); +const Io = std.Io; +const assert = std.debug.assert; + +handle: Handle, + +pub const Handle = std.posix.fd_t; +pub const Mode = std.posix.mode_t; +pub const INode = std.posix.ino_t; + +pub const Kind = enum { + block_device, + character_device, + directory, + named_pipe, + sym_link, + file, + unix_domain_socket, + whiteout, + door, + event_port, + unknown, +}; + +pub const Stat = struct { + /// A number that the system uses to point to the file metadata. This + /// number is not guaranteed to be unique across time, as some file + /// systems may reuse an inode after its file has been deleted. Some + /// systems may change the inode of a file over time. + /// + /// On Linux, the inode is a structure that stores the metadata, and + /// the inode _number_ is what you see here: the index number of the + /// inode. + /// + /// The FileIndex on Windows is similar. It is a number for a file that + /// is unique to each filesystem. + inode: INode, + size: u64, + /// This is available on POSIX systems and is always 0 otherwise. + mode: Mode, + kind: Kind, + /// Last access time in nanoseconds, relative to UTC 1970-01-01. + atime: Io.Timestamp, + /// Last modification time in nanoseconds, relative to UTC 1970-01-01. + mtime: Io.Timestamp, + /// Last status/metadata change time in nanoseconds, relative to UTC 1970-01-01. + ctime: Io.Timestamp, +}; + +pub fn stdout() File { + return .{ .handle = if (is_windows) std.os.windows.peb().ProcessParameters.hStdOutput else std.posix.STDOUT_FILENO }; +} + +pub fn stderr() File { + return .{ .handle = if (is_windows) std.os.windows.peb().ProcessParameters.hStdError else std.posix.STDERR_FILENO }; +} + +pub fn stdin() File { + return .{ .handle = if (is_windows) std.os.windows.peb().ProcessParameters.hStdInput else std.posix.STDIN_FILENO }; +} + +pub const StatError = error{ + SystemResources, + /// In WASI, this error may occur when the file descriptor does + /// not hold the required rights to get its filestat information. + AccessDenied, + PermissionDenied, + /// Attempted to stat a non-file stream. + Streaming, +} || Io.Cancelable || Io.UnexpectedError; + +/// Returns `Stat` containing basic information about the `File`. +pub fn stat(file: File, io: Io) StatError!Stat { + return io.vtable.fileStat(io.userdata, file); +} + +pub const OpenMode = enum { + read_only, + write_only, + read_write, +}; + +pub const Lock = enum { + none, + shared, + exclusive, +}; + +pub const OpenFlags = struct { + mode: OpenMode = .read_only, + + /// Open the file with an advisory lock to coordinate with other processes + /// accessing it at the same time. An exclusive lock will prevent other + /// processes from acquiring a lock. A shared lock will prevent other + /// processes from acquiring a exclusive lock, but does not prevent + /// other process from getting their own shared locks. + /// + /// The lock is advisory, except on Linux in very specific circumstances[1]. + /// This means that a process that does not respect the locking API can still get access + /// to the file, despite the lock. + /// + /// On these operating systems, the lock is acquired atomically with + /// opening the file: + /// * Darwin + /// * DragonFlyBSD + /// * FreeBSD + /// * Haiku + /// * NetBSD + /// * OpenBSD + /// On these operating systems, the lock is acquired via a separate syscall + /// after opening the file: + /// * Linux + /// * Windows + /// + /// [1]: https://www.kernel.org/doc/Documentation/filesystems/mandatory-locking.txt + lock: Lock = .none, + + /// Sets whether or not to wait until the file is locked to return. If set to true, + /// `error.WouldBlock` will be returned. Otherwise, the file will wait until the file + /// is available to proceed. + lock_nonblocking: bool = false, + + /// Set this to allow the opened file to automatically become the + /// controlling TTY for the current process. + allow_ctty: bool = false, + + follow_symlinks: bool = true, + + pub fn isRead(self: OpenFlags) bool { + return self.mode != .write_only; + } + + pub fn isWrite(self: OpenFlags) bool { + return self.mode != .read_only; + } +}; + +pub const CreateFlags = std.fs.File.CreateFlags; + +pub const OpenError = error{ + SharingViolation, + PipeBusy, + NoDevice, + /// On Windows, `\\server` or `\\server\share` was not found. + NetworkNotFound, + ProcessNotFound, + /// On Windows, antivirus software is enabled by default. It can be + /// disabled, but Windows Update sometimes ignores the user's preference + /// and re-enables it. When enabled, antivirus software on Windows + /// intercepts file system operations and makes them significantly slower + /// in addition to possibly failing with this error code. + AntivirusInterference, + /// In WASI, this error may occur when the file descriptor does + /// not hold the required rights to open a new resource relative to it. + AccessDenied, + PermissionDenied, + SymLinkLoop, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, + /// Either: + /// * One of the path components does not exist. + /// * Cwd was used, but cwd has been deleted. + /// * The path associated with the open directory handle has been deleted. + /// * On macOS, multiple processes or threads raced to create the same file + /// with `O.EXCL` set to `false`. + FileNotFound, + /// The path exceeded `max_path_bytes` bytes. + /// Insufficient kernel memory was available, or + /// the named file is a FIFO and per-user hard limit on + /// memory allocation for pipes has been reached. + SystemResources, + /// The file is too large to be opened. This error is unreachable + /// for 64-bit targets, as well as when opening directories. + FileTooBig, + /// The path refers to directory but the `DIRECTORY` flag was not provided. + IsDir, + /// A new path cannot be created because the device has no room for the new file. + /// This error is only reachable when the `CREAT` flag is provided. + NoSpaceLeft, + /// A component used as a directory in the path was not, in fact, a directory, or + /// `DIRECTORY` was specified and the path was not a directory. + NotDir, + /// The path already exists and the `CREAT` and `EXCL` flags were provided. + PathAlreadyExists, + DeviceBusy, + FileLocksNotSupported, + /// One of these three things: + /// * pathname refers to an executable image which is currently being + /// executed and write access was requested. + /// * pathname refers to a file that is currently in use as a swap + /// file, and the O_TRUNC flag was specified. + /// * pathname refers to a file that is currently being read by the + /// kernel (e.g., for module/firmware loading), and write access was + /// requested. + FileBusy, + /// Non-blocking was requested and the operation cannot return immediately. + WouldBlock, +} || Io.Dir.PathNameError || Io.Cancelable || Io.UnexpectedError; + +pub fn close(file: File, io: Io) void { + return io.vtable.fileClose(io.userdata, file); +} + +pub const OpenSelfExeError = OpenError || std.fs.SelfExePathError || std.posix.FlockError; + +pub fn openSelfExe(io: Io, flags: OpenFlags) OpenSelfExeError!File { + return io.vtable.openSelfExe(io.userdata, flags); +} + +pub const ReadPositionalError = Reader.Error || error{Unseekable}; + +pub fn readPositional(file: File, io: Io, buffer: []u8, offset: u64) ReadPositionalError!usize { + return io.vtable.fileReadPositional(io.userdata, file, buffer, offset); +} + +pub const WriteStreamingError = error{} || Io.UnexpectedError || Io.Cancelable; + +pub fn writeStreaming(file: File, io: Io, buffer: [][]const u8) WriteStreamingError!usize { + return file.fileWriteStreaming(io, buffer); +} + +pub const WritePositionalError = WriteStreamingError || error{Unseekable}; + +pub fn writePositional(file: File, io: Io, buffer: [][]const u8, offset: u64) WritePositionalError!usize { + return io.vtable.fileWritePositional(io.userdata, file, buffer, offset); +} + +pub fn openAbsolute(io: Io, absolute_path: []const u8, flags: OpenFlags) OpenError!File { + assert(std.fs.path.isAbsolute(absolute_path)); + return Io.Dir.cwd().openFile(io, absolute_path, flags); +} + +/// Defaults to positional reading; falls back to streaming. +/// +/// Positional is more threadsafe, since the global seek position is not +/// affected. +pub fn reader(file: File, io: Io, buffer: []u8) Reader { + return .init(file, io, buffer); +} + +/// Positional is more threadsafe, since the global seek position is not +/// affected, but when such syscalls are not available, preemptively +/// initializing in streaming mode skips a failed syscall. +pub fn readerStreaming(file: File, io: Io, buffer: []u8) Reader { + return .initStreaming(file, io, buffer); +} + +pub const SeekError = error{ + Unseekable, + /// The file descriptor does not hold the required rights to seek on it. + AccessDenied, +} || Io.Cancelable || Io.UnexpectedError; + +/// Memoizes key information about a file handle such as: +/// * The size from calling stat, or the error that occurred therein. +/// * The current seek position. +/// * The error that occurred when trying to seek. +/// * Whether reading should be done positionally or streaming. +/// * Whether reading should be done via fd-to-fd syscalls (e.g. `sendfile`) +/// versus plain variants (e.g. `read`). +/// +/// Fulfills the `Io.Reader` interface. +pub const Reader = struct { + io: Io, + file: File, + err: ?Error = null, + mode: Reader.Mode = .positional, + /// Tracks the true seek position in the file. To obtain the logical + /// position, use `logicalPos`. + pos: u64 = 0, + size: ?u64 = null, + size_err: ?SizeError = null, + seek_err: ?Reader.SeekError = null, + interface: Io.Reader, + + pub const Error = error{ + InputOutput, + SystemResources, + IsDir, + BrokenPipe, + ConnectionResetByPeer, + Timeout, + /// In WASI, EBADF is mapped to this error because it is returned when + /// trying to read a directory file descriptor as if it were a file. + NotOpenForReading, + SocketUnconnected, + /// This error occurs when no global event loop is configured, + /// and reading from the file descriptor would block. + WouldBlock, + /// In WASI, this error occurs when the file descriptor does + /// not hold the required rights to read from it. + AccessDenied, + /// This error occurs in Linux if the process to be read from + /// no longer exists. + ProcessNotFound, + /// Unable to read file due to lock. + LockViolation, + } || Io.Cancelable || Io.UnexpectedError; + + pub const SizeError = std.os.windows.GetFileSizeError || StatError || error{ + /// Occurs if, for example, the file handle is a network socket and therefore does not have a size. + Streaming, + }; + + pub const SeekError = File.SeekError || error{ + /// Seeking fell back to reading, and reached the end before the requested seek position. + /// `pos` remains at the end of the file. + EndOfStream, + /// Seeking fell back to reading, which failed. + ReadFailed, + }; + + pub const Mode = enum { + streaming, + positional, + /// Avoid syscalls other than `read` and `readv`. + streaming_reading, + /// Avoid syscalls other than `pread` and `preadv`. + positional_reading, + /// Indicates reading cannot continue because of a seek failure. + failure, + + pub fn toStreaming(m: @This()) @This() { + return switch (m) { + .positional, .streaming => .streaming, + .positional_reading, .streaming_reading => .streaming_reading, + .failure => .failure, + }; + } + + pub fn toReading(m: @This()) @This() { + return switch (m) { + .positional, .positional_reading => .positional_reading, + .streaming, .streaming_reading => .streaming_reading, + .failure => .failure, + }; + } + }; + + pub fn initInterface(buffer: []u8) Io.Reader { + return .{ + .vtable = &.{ + .stream = Reader.stream, + .discard = Reader.discard, + .readVec = Reader.readVec, + }, + .buffer = buffer, + .seek = 0, + .end = 0, + }; + } + + pub fn init(file: File, io: Io, buffer: []u8) Reader { + return .{ + .io = io, + .file = file, + .interface = initInterface(buffer), + }; + } + + /// Takes a legacy `std.fs.File` to help with upgrading. + pub fn initAdapted(file: std.fs.File, io: Io, buffer: []u8) Reader { + return .init(.{ .handle = file.handle }, io, buffer); + } + + pub fn initSize(file: File, io: Io, buffer: []u8, size: ?u64) Reader { + return .{ + .io = io, + .file = file, + .interface = initInterface(buffer), + .size = size, + }; + } + + /// Positional is more threadsafe, since the global seek position is not + /// affected, but when such syscalls are not available, preemptively + /// initializing in streaming mode skips a failed syscall. + pub fn initStreaming(file: File, io: Io, buffer: []u8) Reader { + return .{ + .io = io, + .file = file, + .interface = Reader.initInterface(buffer), + .mode = .streaming, + .seek_err = error.Unseekable, + .size_err = error.Streaming, + }; + } + + pub fn getSize(r: *Reader) SizeError!u64 { + return r.size orelse { + if (r.size_err) |err| return err; + if (stat(r.file, r.io)) |st| { + if (st.kind == .file) { + r.size = st.size; + return st.size; + } else { + r.mode = r.mode.toStreaming(); + r.size_err = error.Streaming; + return error.Streaming; + } + } else |err| { + r.size_err = err; + return err; + } + }; + } + + pub fn seekBy(r: *Reader, offset: i64) Reader.SeekError!void { + const io = r.io; + switch (r.mode) { + .positional, .positional_reading => { + setLogicalPos(r, @intCast(@as(i64, @intCast(logicalPos(r))) + offset)); + }, + .streaming, .streaming_reading => { + const seek_err = r.seek_err orelse e: { + if (io.vtable.fileSeekBy(io.userdata, r.file, offset)) |_| { + setLogicalPos(r, @intCast(@as(i64, @intCast(logicalPos(r))) + offset)); + return; + } else |err| { + r.seek_err = err; + break :e err; + } + }; + var remaining = std.math.cast(u64, offset) orelse return seek_err; + while (remaining > 0) { + remaining -= discard(&r.interface, .limited64(remaining)) catch |err| { + r.seek_err = err; + return err; + }; + } + r.interface.seek = 0; + r.interface.end = 0; + }, + .failure => return r.seek_err.?, + } + } + + /// Repositions logical read offset relative to the beginning of the file. + pub fn seekTo(r: *Reader, offset: u64) Reader.SeekError!void { + const io = r.io; + switch (r.mode) { + .positional, .positional_reading => { + setLogicalPos(r, offset); + }, + .streaming, .streaming_reading => { + const logical_pos = logicalPos(r); + if (offset >= logical_pos) return Reader.seekBy(r, @intCast(offset - logical_pos)); + if (r.seek_err) |err| return err; + io.vtable.fileSeekTo(io.userdata, r.file, offset) catch |err| { + r.seek_err = err; + return err; + }; + setLogicalPos(r, offset); + }, + .failure => return r.seek_err.?, + } + } + + pub fn logicalPos(r: *const Reader) u64 { + return r.pos - r.interface.bufferedLen(); + } + + fn setLogicalPos(r: *Reader, offset: u64) void { + const logical_pos = logicalPos(r); + if (offset < logical_pos or offset >= r.pos) { + r.interface.seek = 0; + r.interface.end = 0; + r.pos = offset; + } else { + const logical_delta: usize = @intCast(offset - logical_pos); + r.interface.seek += logical_delta; + } + } + + /// Number of slices to store on the stack, when trying to send as many byte + /// vectors through the underlying read calls as possible. + const max_buffers_len = 16; + + fn stream(io_reader: *Io.Reader, w: *Io.Writer, limit: Io.Limit) Io.Reader.StreamError!usize { + const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader)); + return streamMode(r, w, limit, r.mode); + } + + pub fn streamMode(r: *Reader, w: *Io.Writer, limit: Io.Limit, mode: Reader.Mode) Io.Reader.StreamError!usize { + switch (mode) { + .positional, .streaming => return w.sendFile(r, limit) catch |write_err| switch (write_err) { + error.Unimplemented => { + r.mode = r.mode.toReading(); + return 0; + }, + else => |e| return e, + }, + .positional_reading => { + const dest = limit.slice(try w.writableSliceGreedy(1)); + var data: [1][]u8 = .{dest}; + const n = try readVecPositional(r, &data); + w.advance(n); + return n; + }, + .streaming_reading => { + const dest = limit.slice(try w.writableSliceGreedy(1)); + var data: [1][]u8 = .{dest}; + const n = try readVecStreaming(r, &data); + w.advance(n); + return n; + }, + .failure => return error.ReadFailed, + } + } + + fn readVec(io_reader: *Io.Reader, data: [][]u8) Io.Reader.Error!usize { + const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader)); + switch (r.mode) { + .positional, .positional_reading => return readVecPositional(r, data), + .streaming, .streaming_reading => return readVecStreaming(r, data), + .failure => return error.ReadFailed, + } + } + + fn readVecPositional(r: *Reader, data: [][]u8) Io.Reader.Error!usize { + const io = r.io; + var iovecs_buffer: [max_buffers_len][]u8 = undefined; + const dest_n, const data_size = try r.interface.writableVector(&iovecs_buffer, data); + const dest = iovecs_buffer[0..dest_n]; + assert(dest[0].len > 0); + const n = io.vtable.fileReadPositional(io.userdata, r.file, dest, r.pos) catch |err| switch (err) { + error.Unseekable => { + r.mode = r.mode.toStreaming(); + const pos = r.pos; + if (pos != 0) { + r.pos = 0; + r.seekBy(@intCast(pos)) catch { + r.mode = .failure; + return error.ReadFailed; + }; + } + return 0; + }, + else => |e| { + r.err = e; + return error.ReadFailed; + }, + }; + if (n == 0) { + r.size = r.pos; + return error.EndOfStream; + } + r.pos += n; + if (n > data_size) { + r.interface.end += n - data_size; + return data_size; + } + return n; + } + + fn readVecStreaming(r: *Reader, data: [][]u8) Io.Reader.Error!usize { + const io = r.io; + var iovecs_buffer: [max_buffers_len][]u8 = undefined; + const dest_n, const data_size = try r.interface.writableVector(&iovecs_buffer, data); + const dest = iovecs_buffer[0..dest_n]; + assert(dest[0].len > 0); + const n = io.vtable.fileReadStreaming(io.userdata, r.file, dest) catch |err| { + r.err = err; + return error.ReadFailed; + }; + if (n == 0) { + r.size = r.pos; + return error.EndOfStream; + } + r.pos += n; + if (n > data_size) { + r.interface.end += n - data_size; + return data_size; + } + return n; + } + + fn discard(io_reader: *Io.Reader, limit: Io.Limit) Io.Reader.Error!usize { + const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader)); + const io = r.io; + const file = r.file; + switch (r.mode) { + .positional, .positional_reading => { + const size = r.getSize() catch { + r.mode = r.mode.toStreaming(); + return 0; + }; + const logical_pos = logicalPos(r); + const delta = @min(@intFromEnum(limit), size - logical_pos); + setLogicalPos(r, logical_pos + delta); + return delta; + }, + .streaming, .streaming_reading => { + // Unfortunately we can't seek forward without knowing the + // size because the seek syscalls provided to us will not + // return the true end position if a seek would exceed the + // end. + fallback: { + if (r.size_err == null and r.seek_err == null) break :fallback; + + const buffered_len = r.interface.bufferedLen(); + var remaining = @intFromEnum(limit); + if (remaining <= buffered_len) { + r.interface.seek += remaining; + return remaining; + } + remaining -= buffered_len; + r.interface.seek = 0; + r.interface.end = 0; + + var trash_buffer: [128]u8 = undefined; + var data: [1][]u8 = .{trash_buffer[0..@min(trash_buffer.len, remaining)]}; + var iovecs_buffer: [max_buffers_len][]u8 = undefined; + const dest_n, const data_size = try r.interface.writableVector(&iovecs_buffer, &data); + const dest = iovecs_buffer[0..dest_n]; + assert(dest[0].len > 0); + const n = io.vtable.fileReadStreaming(io.userdata, file, dest) catch |err| { + r.err = err; + return error.ReadFailed; + }; + if (n == 0) { + r.size = r.pos; + return error.EndOfStream; + } + r.pos += n; + if (n > data_size) { + r.interface.end += n - data_size; + remaining -= data_size; + } else { + remaining -= n; + } + return @intFromEnum(limit) - remaining; + } + const size = r.getSize() catch return 0; + const n = @min(size - r.pos, std.math.maxInt(i64), @intFromEnum(limit)); + io.vtable.fileSeekBy(io.userdata, file, n) catch |err| { + r.seek_err = err; + return 0; + }; + r.pos += n; + return n; + }, + .failure => return error.ReadFailed, + } + } + + /// Returns whether the stream is at the logical end. + pub fn atEnd(r: *Reader) bool { + // Even if stat fails, size is set when end is encountered. + const size = r.size orelse return false; + return size - logicalPos(r) == 0; + } +}; diff --git a/lib/std/Io/IoUring.zig b/lib/std/Io/IoUring.zig new file mode 100644 index 0000000000..5561cdebd2 --- /dev/null +++ b/lib/std/Io/IoUring.zig @@ -0,0 +1,1497 @@ +const EventLoop = @This(); +const builtin = @import("builtin"); + +const std = @import("../std.zig"); +const Io = std.Io; +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const Alignment = std.mem.Alignment; +const IoUring = std.os.linux.IoUring; + +/// Must be a thread-safe allocator. +gpa: Allocator, +mutex: std.Thread.Mutex, +main_fiber_buffer: [@sizeOf(Fiber) + Fiber.max_result_size]u8 align(@alignOf(Fiber)), +threads: Thread.List, + +/// Empirically saw >128KB being used by the self-hosted backend to panic. +const idle_stack_size = 256 * 1024; + +const max_idle_search = 4; +const max_steal_ready_search = 4; + +const io_uring_entries = 64; + +const Thread = struct { + thread: std.Thread, + idle_context: Context, + current_context: *Context, + ready_queue: ?*Fiber, + io_uring: IoUring, + idle_search_index: u32, + steal_ready_search_index: u32, + + const canceling: ?*Thread = @ptrFromInt(@alignOf(Thread)); + + threadlocal var self: *Thread = undefined; + + fn current() *Thread { + return self; + } + + fn currentFiber(thread: *Thread) *Fiber { + return @fieldParentPtr("context", thread.current_context); + } + + const List = struct { + allocated: []Thread, + reserved: u32, + active: u32, + }; +}; + +const Fiber = struct { + required_align: void align(4), + context: Context, + awaiter: ?*Fiber, + queue_next: ?*Fiber, + cancel_thread: ?*Thread, + awaiting_completions: std.StaticBitSet(3), + + const finished: ?*Fiber = @ptrFromInt(@alignOf(Thread)); + + const max_result_align: Alignment = .@"16"; + const max_result_size = max_result_align.forward(64); + /// This includes any stack realignments that need to happen, and also the + /// initial frame return address slot and argument frame, depending on target. + const min_stack_size = 4 * 1024 * 1024; + const max_context_align: Alignment = .@"16"; + const max_context_size = max_context_align.forward(1024); + const max_closure_size: usize = @sizeOf(AsyncClosure); + const max_closure_align: Alignment = .of(AsyncClosure); + const allocation_size = std.mem.alignForward( + usize, + max_closure_align.max(max_context_align).forward( + max_result_align.forward(@sizeOf(Fiber)) + max_result_size + min_stack_size, + ) + max_closure_size + max_context_size, + std.heap.page_size_max, + ); + + fn allocate(el: *EventLoop) error{OutOfMemory}!*Fiber { + return @ptrCast(try el.gpa.alignedAlloc(u8, .of(Fiber), allocation_size)); + } + + fn allocatedSlice(f: *Fiber) []align(@alignOf(Fiber)) u8 { + return @as([*]align(@alignOf(Fiber)) u8, @ptrCast(f))[0..allocation_size]; + } + + fn allocatedEnd(f: *Fiber) [*]u8 { + const allocated_slice = f.allocatedSlice(); + return allocated_slice[allocated_slice.len..].ptr; + } + + fn resultPointer(f: *Fiber, comptime Result: type) *Result { + return @ptrCast(@alignCast(f.resultBytes(.of(Result)))); + } + + fn resultBytes(f: *Fiber, alignment: Alignment) [*]u8 { + return @ptrFromInt(alignment.forward(@intFromPtr(f) + @sizeOf(Fiber))); + } + + fn enterCancelRegion(fiber: *Fiber, thread: *Thread) error{Canceled}!void { + if (@cmpxchgStrong( + ?*Thread, + &fiber.cancel_thread, + null, + thread, + .acq_rel, + .acquire, + )) |cancel_thread| { + assert(cancel_thread == Thread.canceling); + return error.Canceled; + } + } + + fn exitCancelRegion(fiber: *Fiber, thread: *Thread) void { + if (@cmpxchgStrong( + ?*Thread, + &fiber.cancel_thread, + thread, + null, + .acq_rel, + .acquire, + )) |cancel_thread| assert(cancel_thread == Thread.canceling); + } + + const Queue = struct { head: *Fiber, tail: *Fiber }; +}; + +fn recycle(el: *EventLoop, fiber: *Fiber) void { + std.log.debug("recyling {*}", .{fiber}); + assert(fiber.queue_next == null); + el.gpa.free(fiber.allocatedSlice()); +} + +pub fn io(el: *EventLoop) Io { + return .{ + .userdata = el, + .vtable = &.{ + .async = async, + .concurrent = concurrent, + .await = await, + .select = select, + .cancel = cancel, + .cancelRequested = cancelRequested, + + .mutexLock = mutexLock, + .mutexUnlock = mutexUnlock, + + .conditionWait = conditionWait, + .conditionWake = conditionWake, + + .createFile = createFile, + .fileOpen = fileOpen, + .fileClose = fileClose, + .pread = pread, + .pwrite = pwrite, + + .now = now, + .sleep = sleep, + }, + }; +} + +pub fn init(el: *EventLoop, gpa: Allocator) !void { + const threads_size = @max(std.Thread.getCpuCount() catch 1, 1) * @sizeOf(Thread); + const idle_stack_end_offset = std.mem.alignForward(usize, threads_size + idle_stack_size, std.heap.page_size_max); + const allocated_slice = try gpa.alignedAlloc(u8, .of(Thread), idle_stack_end_offset); + errdefer gpa.free(allocated_slice); + el.* = .{ + .gpa = gpa, + .mutex = .{}, + .main_fiber_buffer = undefined, + .threads = .{ + .allocated = @ptrCast(allocated_slice[0..threads_size]), + .reserved = 1, + .active = 1, + }, + }; + const main_fiber: *Fiber = @ptrCast(&el.main_fiber_buffer); + main_fiber.* = .{ + .required_align = {}, + .context = undefined, + .awaiter = null, + .queue_next = null, + .cancel_thread = null, + .awaiting_completions = .initEmpty(), + }; + const main_thread = &el.threads.allocated[0]; + Thread.self = main_thread; + const idle_stack_end: [*]align(16) usize = @ptrCast(@alignCast(allocated_slice[idle_stack_end_offset..].ptr)); + (idle_stack_end - 1)[0..1].* = .{@intFromPtr(el)}; + main_thread.* = .{ + .thread = undefined, + .idle_context = switch (builtin.cpu.arch) { + .aarch64 => .{ + .sp = @intFromPtr(idle_stack_end), + .fp = 0, + .pc = @intFromPtr(&mainIdleEntry), + }, + .x86_64 => .{ + .rsp = @intFromPtr(idle_stack_end - 1), + .rbp = 0, + .rip = @intFromPtr(&mainIdleEntry), + }, + else => @compileError("unimplemented architecture"), + }, + .current_context = &main_fiber.context, + .ready_queue = null, + .io_uring = try IoUring.init(io_uring_entries, 0), + .idle_search_index = 1, + .steal_ready_search_index = 1, + }; + errdefer main_thread.io_uring.deinit(); + std.log.debug("created main idle {*}", .{&main_thread.idle_context}); + std.log.debug("created main {*}", .{main_fiber}); +} + +pub fn deinit(el: *EventLoop) void { + const active_threads = @atomicLoad(u32, &el.threads.active, .acquire); + for (el.threads.allocated[0..active_threads]) |*thread| { + const ready_fiber = @atomicLoad(?*Fiber, &thread.ready_queue, .monotonic); + assert(ready_fiber == null or ready_fiber == Fiber.finished); // pending async + } + el.yield(null, .exit); + const allocated_ptr: [*]align(@alignOf(Thread)) u8 = @ptrCast(@alignCast(el.threads.allocated.ptr)); + const idle_stack_end_offset = std.mem.alignForward(usize, el.threads.allocated.len * @sizeOf(Thread) + idle_stack_size, std.heap.page_size_max); + for (el.threads.allocated[1..active_threads]) |*thread| thread.thread.join(); + el.gpa.free(allocated_ptr[0..idle_stack_end_offset]); + el.* = undefined; +} + +fn findReadyFiber(el: *EventLoop, thread: *Thread) ?*Fiber { + if (@atomicRmw(?*Fiber, &thread.ready_queue, .Xchg, Fiber.finished, .acquire)) |ready_fiber| { + @atomicStore(?*Fiber, &thread.ready_queue, ready_fiber.queue_next, .release); + ready_fiber.queue_next = null; + return ready_fiber; + } + const active_threads = @atomicLoad(u32, &el.threads.active, .acquire); + for (0..@min(max_steal_ready_search, active_threads)) |_| { + defer thread.steal_ready_search_index += 1; + if (thread.steal_ready_search_index == active_threads) thread.steal_ready_search_index = 0; + const steal_ready_search_thread = &el.threads.allocated[0..active_threads][thread.steal_ready_search_index]; + if (steal_ready_search_thread == thread) continue; + const ready_fiber = @atomicLoad(?*Fiber, &steal_ready_search_thread.ready_queue, .acquire) orelse continue; + if (ready_fiber == Fiber.finished) continue; + if (@cmpxchgWeak( + ?*Fiber, + &steal_ready_search_thread.ready_queue, + ready_fiber, + null, + .acquire, + .monotonic, + )) |_| continue; + @atomicStore(?*Fiber, &thread.ready_queue, ready_fiber.queue_next, .release); + ready_fiber.queue_next = null; + return ready_fiber; + } + // couldn't find anything to do, so we are now open for business + @atomicStore(?*Fiber, &thread.ready_queue, null, .monotonic); + return null; +} + +fn yield(el: *EventLoop, maybe_ready_fiber: ?*Fiber, pending_task: SwitchMessage.PendingTask) void { + const thread: *Thread = .current(); + const ready_context = if (maybe_ready_fiber orelse el.findReadyFiber(thread)) |ready_fiber| + &ready_fiber.context + else + &thread.idle_context; + const message: SwitchMessage = .{ + .contexts = .{ + .prev = thread.current_context, + .ready = ready_context, + }, + .pending_task = pending_task, + }; + std.log.debug("switching from {*} to {*}", .{ message.contexts.prev, message.contexts.ready }); + contextSwitch(&message).handle(el); +} + +fn schedule(el: *EventLoop, thread: *Thread, ready_queue: Fiber.Queue) void { + { + var fiber = ready_queue.head; + while (true) { + std.log.debug("scheduling {*}", .{fiber}); + fiber = fiber.queue_next orelse break; + } + assert(fiber == ready_queue.tail); + } + // shared fields of previous `Thread` must be initialized before later ones are marked as active + const new_thread_index = @atomicLoad(u32, &el.threads.active, .acquire); + for (0..@min(max_idle_search, new_thread_index)) |_| { + defer thread.idle_search_index += 1; + if (thread.idle_search_index == new_thread_index) thread.idle_search_index = 0; + const idle_search_thread = &el.threads.allocated[0..new_thread_index][thread.idle_search_index]; + if (idle_search_thread == thread) continue; + if (@cmpxchgWeak( + ?*Fiber, + &idle_search_thread.ready_queue, + null, + ready_queue.head, + .release, + .monotonic, + )) |_| continue; + getSqe(&thread.io_uring).* = .{ + .opcode = .MSG_RING, + .flags = std.os.linux.IOSQE_CQE_SKIP_SUCCESS, + .ioprio = 0, + .fd = idle_search_thread.io_uring.fd, + .off = @intFromEnum(Completion.UserData.wakeup), + .addr = 0, + .len = 0, + .rw_flags = 0, + .user_data = @intFromEnum(Completion.UserData.wakeup), + .buf_index = 0, + .personality = 0, + .splice_fd_in = 0, + .addr3 = 0, + .resv = 0, + }; + return; + } + spawn_thread: { + // previous failed reservations must have completed before retrying + if (new_thread_index == el.threads.allocated.len or @cmpxchgWeak( + u32, + &el.threads.reserved, + new_thread_index, + new_thread_index + 1, + .acquire, + .monotonic, + ) != null) break :spawn_thread; + const new_thread = &el.threads.allocated[new_thread_index]; + const next_thread_index = new_thread_index + 1; + new_thread.* = .{ + .thread = undefined, + .idle_context = undefined, + .current_context = &new_thread.idle_context, + .ready_queue = ready_queue.head, + .io_uring = IoUring.init(io_uring_entries, 0) catch |err| { + @atomicStore(u32, &el.threads.reserved, new_thread_index, .release); + // no more access to `thread` after giving up reservation + std.log.warn("unable to create worker thread due to io_uring init failure: {s}", .{@errorName(err)}); + break :spawn_thread; + }, + .idle_search_index = 0, + .steal_ready_search_index = 0, + }; + new_thread.thread = std.Thread.spawn(.{ + .stack_size = idle_stack_size, + .allocator = el.gpa, + }, threadEntry, .{ el, new_thread_index }) catch |err| { + new_thread.io_uring.deinit(); + @atomicStore(u32, &el.threads.reserved, new_thread_index, .release); + // no more access to `thread` after giving up reservation + std.log.warn("unable to create worker thread due spawn failure: {s}", .{@errorName(err)}); + break :spawn_thread; + }; + // shared fields of `Thread` must be initialized before being marked active + @atomicStore(u32, &el.threads.active, next_thread_index, .release); + return; + } + // nobody wanted it, so just queue it on ourselves + while (@cmpxchgWeak( + ?*Fiber, + &thread.ready_queue, + ready_queue.tail.queue_next, + ready_queue.head, + .acq_rel, + .acquire, + )) |old_head| ready_queue.tail.queue_next = old_head; +} + +fn mainIdle(el: *EventLoop, message: *const SwitchMessage) callconv(.withStackAlign(.c, @max(@alignOf(Thread), @alignOf(Context)))) noreturn { + message.handle(el); + el.idle(&el.threads.allocated[0]); + el.yield(@ptrCast(&el.main_fiber_buffer), .nothing); + unreachable; // switched to dead fiber +} + +fn threadEntry(el: *EventLoop, index: u32) void { + const thread: *Thread = &el.threads.allocated[index]; + Thread.self = thread; + std.log.debug("created thread idle {*}", .{&thread.idle_context}); + el.idle(thread); +} + +const Completion = struct { + const UserData = enum(usize) { + unused, + wakeup, + cleanup, + exit, + /// *Fiber + _, + }; + result: i32, + flags: u32, +}; + +fn idle(el: *EventLoop, thread: *Thread) void { + var maybe_ready_fiber: ?*Fiber = null; + while (true) { + while (maybe_ready_fiber orelse el.findReadyFiber(thread)) |ready_fiber| { + el.yield(ready_fiber, .nothing); + maybe_ready_fiber = null; + } + _ = thread.io_uring.submit_and_wait(1) catch |err| switch (err) { + error.SignalInterrupt => std.log.warn("submit_and_wait failed with SignalInterrupt", .{}), + else => |e| @panic(@errorName(e)), + }; + var cqes_buffer: [io_uring_entries]std.os.linux.io_uring_cqe = undefined; + var maybe_ready_queue: ?Fiber.Queue = null; + for (cqes_buffer[0 .. thread.io_uring.copy_cqes(&cqes_buffer, 0) catch |err| switch (err) { + error.SignalInterrupt => cqes_len: { + std.log.warn("copy_cqes failed with SignalInterrupt", .{}); + break :cqes_len 0; + }, + else => |e| @panic(@errorName(e)), + }]) |cqe| switch (@as(Completion.UserData, @enumFromInt(cqe.user_data))) { + .unused => unreachable, // bad submission queued? + .wakeup => {}, + .cleanup => @panic("failed to notify other threads that we are exiting"), + .exit => { + assert(maybe_ready_fiber == null and maybe_ready_queue == null); // pending async + return; + }, + _ => switch (errno(cqe.res)) { + .INTR => getSqe(&thread.io_uring).* = .{ + .opcode = .ASYNC_CANCEL, + .flags = std.os.linux.IOSQE_CQE_SKIP_SUCCESS, + .ioprio = 0, + .fd = 0, + .off = 0, + .addr = cqe.user_data, + .len = 0, + .rw_flags = 0, + .user_data = @intFromEnum(Completion.UserData.wakeup), + .buf_index = 0, + .personality = 0, + .splice_fd_in = 0, + .addr3 = 0, + .resv = 0, + }, + else => { + const fiber: *Fiber = @ptrFromInt(cqe.user_data); + assert(fiber.queue_next == null); + fiber.resultPointer(Completion).* = .{ + .result = cqe.res, + .flags = cqe.flags, + }; + if (maybe_ready_fiber == null) maybe_ready_fiber = fiber else if (maybe_ready_queue) |*ready_queue| { + ready_queue.tail.queue_next = fiber; + ready_queue.tail = fiber; + } else maybe_ready_queue = .{ .head = fiber, .tail = fiber }; + }, + }, + }; + if (maybe_ready_queue) |ready_queue| el.schedule(thread, ready_queue); + } +} + +const SwitchMessage = struct { + contexts: extern struct { + prev: *Context, + ready: *Context, + }, + pending_task: PendingTask, + + const PendingTask = union(enum) { + nothing, + reschedule, + recycle: *Fiber, + register_awaiter: *?*Fiber, + register_select: []const *Io.AnyFuture, + mutex_lock: struct { + prev_state: Io.Mutex.State, + mutex: *Io.Mutex, + }, + condition_wait: struct { + cond: *Io.Condition, + mutex: *Io.Mutex, + }, + exit, + }; + + fn handle(message: *const SwitchMessage, el: *EventLoop) void { + const thread: *Thread = .current(); + thread.current_context = message.contexts.ready; + switch (message.pending_task) { + .nothing => {}, + .reschedule => if (message.contexts.prev != &thread.idle_context) { + const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); + assert(prev_fiber.queue_next == null); + el.schedule(thread, .{ .head = prev_fiber, .tail = prev_fiber }); + }, + .recycle => |fiber| { + el.recycle(fiber); + }, + .register_awaiter => |awaiter| { + const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); + assert(prev_fiber.queue_next == null); + if (@atomicRmw(?*Fiber, awaiter, .Xchg, prev_fiber, .acq_rel) == Fiber.finished) + el.schedule(thread, .{ .head = prev_fiber, .tail = prev_fiber }); + }, + .register_select => |futures| { + const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); + assert(prev_fiber.queue_next == null); + for (futures) |any_future| { + const future_fiber: *Fiber = @ptrCast(@alignCast(any_future)); + if (@atomicRmw(?*Fiber, &future_fiber.awaiter, .Xchg, prev_fiber, .acq_rel) == Fiber.finished) { + const closure: *AsyncClosure = .fromFiber(future_fiber); + if (!@atomicRmw(bool, &closure.already_awaited, .Xchg, true, .seq_cst)) { + el.schedule(thread, .{ .head = prev_fiber, .tail = prev_fiber }); + } + } + } + }, + .mutex_lock => |mutex_lock| { + const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); + assert(prev_fiber.queue_next == null); + var prev_state = mutex_lock.prev_state; + while (switch (prev_state) { + else => next_state: { + prev_fiber.queue_next = @ptrFromInt(@intFromEnum(prev_state)); + break :next_state @cmpxchgWeak( + Io.Mutex.State, + &mutex_lock.mutex.state, + prev_state, + @enumFromInt(@intFromPtr(prev_fiber)), + .release, + .acquire, + ); + }, + .unlocked => @cmpxchgWeak( + Io.Mutex.State, + &mutex_lock.mutex.state, + .unlocked, + .locked_once, + .acquire, + .acquire, + ) orelse { + prev_fiber.queue_next = null; + el.schedule(thread, .{ .head = prev_fiber, .tail = prev_fiber }); + return; + }, + }) |next_state| prev_state = next_state; + }, + .condition_wait => |condition_wait| { + const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); + assert(prev_fiber.queue_next == null); + const cond_impl = prev_fiber.resultPointer(ConditionImpl); + cond_impl.* = .{ + .tail = prev_fiber, + .event = .queued, + }; + if (@cmpxchgStrong( + ?*Fiber, + @as(*?*Fiber, @ptrCast(&condition_wait.cond.state)), + null, + prev_fiber, + .release, + .acquire, + )) |waiting_fiber| { + const waiting_cond_impl = waiting_fiber.?.resultPointer(ConditionImpl); + assert(waiting_cond_impl.tail.queue_next == null); + waiting_cond_impl.tail.queue_next = prev_fiber; + waiting_cond_impl.tail = prev_fiber; + } + condition_wait.mutex.unlock(el.io()); + }, + .exit => for (el.threads.allocated[0..@atomicLoad(u32, &el.threads.active, .acquire)]) |*each_thread| { + getSqe(&thread.io_uring).* = .{ + .opcode = .MSG_RING, + .flags = std.os.linux.IOSQE_CQE_SKIP_SUCCESS, + .ioprio = 0, + .fd = each_thread.io_uring.fd, + .off = @intFromEnum(Completion.UserData.exit), + .addr = 0, + .len = 0, + .rw_flags = 0, + .user_data = @intFromEnum(Completion.UserData.cleanup), + .buf_index = 0, + .personality = 0, + .splice_fd_in = 0, + .addr3 = 0, + .resv = 0, + }; + }, + } + } +}; + +const Context = switch (builtin.cpu.arch) { + .aarch64 => extern struct { + sp: u64, + fp: u64, + pc: u64, + }, + .x86_64 => extern struct { + rsp: u64, + rbp: u64, + rip: u64, + }, + else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), +}; + +inline fn contextSwitch(message: *const SwitchMessage) *const SwitchMessage { + return @fieldParentPtr("contexts", switch (builtin.cpu.arch) { + .aarch64 => asm volatile ( + \\ ldp x0, x2, [x1] + \\ ldr x3, [x2, #16] + \\ mov x4, sp + \\ stp x4, fp, [x0] + \\ adr x5, 0f + \\ ldp x4, fp, [x2] + \\ str x5, [x0, #16] + \\ mov sp, x4 + \\ br x3 + \\0: + : [received_message] "={x1}" (-> *const @FieldType(SwitchMessage, "contexts")), + : [message_to_send] "{x1}" (&message.contexts), + : .{ + .x0 = true, + .x1 = true, + .x2 = true, + .x3 = true, + .x4 = true, + .x5 = true, + .x6 = true, + .x7 = true, + .x8 = true, + .x9 = true, + .x10 = true, + .x11 = true, + .x12 = true, + .x13 = true, + .x14 = true, + .x15 = true, + .x16 = true, + .x17 = true, + .x18 = true, + .x19 = true, + .x20 = true, + .x21 = true, + .x22 = true, + .x23 = true, + .x24 = true, + .x25 = true, + .x26 = true, + .x27 = true, + .x28 = true, + .x30 = true, + .z0 = true, + .z1 = true, + .z2 = true, + .z3 = true, + .z4 = true, + .z5 = true, + .z6 = true, + .z7 = true, + .z8 = true, + .z9 = true, + .z10 = true, + .z11 = true, + .z12 = true, + .z13 = true, + .z14 = true, + .z15 = true, + .z16 = true, + .z17 = true, + .z18 = true, + .z19 = true, + .z20 = true, + .z21 = true, + .z22 = true, + .z23 = true, + .z24 = true, + .z25 = true, + .z26 = true, + .z27 = true, + .z28 = true, + .z29 = true, + .z30 = true, + .z31 = true, + .p0 = true, + .p1 = true, + .p2 = true, + .p3 = true, + .p4 = true, + .p5 = true, + .p6 = true, + .p7 = true, + .p8 = true, + .p9 = true, + .p10 = true, + .p11 = true, + .p12 = true, + .p13 = true, + .p14 = true, + .p15 = true, + .fpcr = true, + .fpsr = true, + .ffr = true, + .memory = true, + }), + .x86_64 => asm volatile ( + \\ movq 0(%%rsi), %%rax + \\ movq 8(%%rsi), %%rcx + \\ leaq 0f(%%rip), %%rdx + \\ movq %%rsp, 0(%%rax) + \\ movq %%rbp, 8(%%rax) + \\ movq %%rdx, 16(%%rax) + \\ movq 0(%%rcx), %%rsp + \\ movq 8(%%rcx), %%rbp + \\ jmpq *16(%%rcx) + \\0: + : [received_message] "={rsi}" (-> *const @FieldType(SwitchMessage, "contexts")), + : [message_to_send] "{rsi}" (&message.contexts), + : .{ + .rax = true, + .rcx = true, + .rdx = true, + .rbx = true, + .rsi = true, + .rdi = true, + .r8 = true, + .r9 = true, + .r10 = true, + .r11 = true, + .r12 = true, + .r13 = true, + .r14 = true, + .r15 = true, + .mm0 = true, + .mm1 = true, + .mm2 = true, + .mm3 = true, + .mm4 = true, + .mm5 = true, + .mm6 = true, + .mm7 = true, + .zmm0 = true, + .zmm1 = true, + .zmm2 = true, + .zmm3 = true, + .zmm4 = true, + .zmm5 = true, + .zmm6 = true, + .zmm7 = true, + .zmm8 = true, + .zmm9 = true, + .zmm10 = true, + .zmm11 = true, + .zmm12 = true, + .zmm13 = true, + .zmm14 = true, + .zmm15 = true, + .zmm16 = true, + .zmm17 = true, + .zmm18 = true, + .zmm19 = true, + .zmm20 = true, + .zmm21 = true, + .zmm22 = true, + .zmm23 = true, + .zmm24 = true, + .zmm25 = true, + .zmm26 = true, + .zmm27 = true, + .zmm28 = true, + .zmm29 = true, + .zmm30 = true, + .zmm31 = true, + .fpsr = true, + .fpcr = true, + .mxcsr = true, + .rflags = true, + .dirflag = true, + .memory = true, + }), + else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), + }); +} + +fn mainIdleEntry() callconv(.naked) void { + switch (builtin.cpu.arch) { + .x86_64 => asm volatile ( + \\ movq (%%rsp), %%rdi + \\ jmp %[mainIdle:P] + : + : [mainIdle] "X" (&mainIdle), + ), + .aarch64 => asm volatile ( + \\ ldr x0, [sp, #-8] + \\ b %[mainIdle] + : + : [mainIdle] "X" (&mainIdle), + ), + else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), + } +} + +fn fiberEntry() callconv(.naked) void { + switch (builtin.cpu.arch) { + .x86_64 => asm volatile ( + \\ leaq 8(%%rsp), %%rdi + \\ jmp %[AsyncClosure_call:P] + : + : [AsyncClosure_call] "X" (&AsyncClosure.call), + ), + else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), + } +} + +const AsyncClosure = struct { + event_loop: *EventLoop, + fiber: *Fiber, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, + result_align: Alignment, + already_awaited: bool, + + fn contextPointer(closure: *AsyncClosure) [*]align(Fiber.max_context_align.toByteUnits()) u8 { + return @alignCast(@as([*]u8, @ptrCast(closure)) + @sizeOf(AsyncClosure)); + } + + fn call(closure: *AsyncClosure, message: *const SwitchMessage) callconv(.withStackAlign(.c, @alignOf(AsyncClosure))) noreturn { + message.handle(closure.event_loop); + const fiber = closure.fiber; + std.log.debug("{*} performing async", .{fiber}); + closure.start(closure.contextPointer(), fiber.resultBytes(closure.result_align)); + const awaiter = @atomicRmw(?*Fiber, &fiber.awaiter, .Xchg, Fiber.finished, .acq_rel); + const ready_awaiter = r: { + const a = awaiter orelse break :r null; + if (@atomicRmw(bool, &closure.already_awaited, .Xchg, true, .acq_rel)) break :r null; + break :r a; + }; + closure.event_loop.yield(ready_awaiter, .nothing); + unreachable; // switched to dead fiber + } + + fn fromFiber(fiber: *Fiber) *AsyncClosure { + return @ptrFromInt(Fiber.max_context_align.max(.of(AsyncClosure)).backward( + @intFromPtr(fiber.allocatedEnd()) - Fiber.max_context_size, + ) - @sizeOf(AsyncClosure)); + } +}; + +fn async( + userdata: ?*anyopaque, + result: []u8, + result_alignment: Alignment, + context: []const u8, + context_alignment: Alignment, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, +) ?*std.Io.AnyFuture { + return concurrent(userdata, result.len, result_alignment, context, context_alignment, start) catch { + start(context.ptr, result.ptr); + return null; + }; +} + +fn concurrent( + userdata: ?*anyopaque, + result_len: usize, + result_alignment: Alignment, + context: []const u8, + context_alignment: Alignment, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, +) Io.ConcurrentError!*std.Io.AnyFuture { + assert(result_alignment.compare(.lte, Fiber.max_result_align)); // TODO + assert(context_alignment.compare(.lte, Fiber.max_context_align)); // TODO + assert(result_len <= Fiber.max_result_size); // TODO + assert(context.len <= Fiber.max_context_size); // TODO + + const event_loop: *EventLoop = @ptrCast(@alignCast(userdata)); + const fiber = try Fiber.allocate(event_loop); + std.log.debug("allocated {*}", .{fiber}); + + const closure: *AsyncClosure = .fromFiber(fiber); + fiber.* = .{ + .required_align = {}, + .context = switch (builtin.cpu.arch) { + .x86_64 => .{ + .rsp = @intFromPtr(closure) - @sizeOf(usize), + .rbp = 0, + .rip = @intFromPtr(&fiberEntry), + }, + .aarch64 => .{ + .sp = @intFromPtr(closure), + .fp = 0, + .pc = @intFromPtr(&fiberEntry), + }, + else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), + }, + .awaiter = null, + .queue_next = null, + .cancel_thread = null, + .awaiting_completions = .initEmpty(), + }; + closure.* = .{ + .event_loop = event_loop, + .fiber = fiber, + .start = start, + .result_align = result_alignment, + .already_awaited = false, + }; + @memcpy(closure.contextPointer(), context); + + event_loop.schedule(.current(), .{ .head = fiber, .tail = fiber }); + return @ptrCast(fiber); +} + +fn await( + userdata: ?*anyopaque, + any_future: *std.Io.AnyFuture, + result: []u8, + result_alignment: Alignment, +) void { + const event_loop: *EventLoop = @ptrCast(@alignCast(userdata)); + const future_fiber: *Fiber = @ptrCast(@alignCast(any_future)); + if (@atomicLoad(?*Fiber, &future_fiber.awaiter, .acquire) != Fiber.finished) + event_loop.yield(null, .{ .register_awaiter = &future_fiber.awaiter }); + @memcpy(result, future_fiber.resultBytes(result_alignment)); + event_loop.recycle(future_fiber); +} + +fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) usize { + const el: *EventLoop = @ptrCast(@alignCast(userdata)); + + // Optimization to avoid the yield below. + for (futures, 0..) |any_future, i| { + const future_fiber: *Fiber = @ptrCast(@alignCast(any_future)); + if (@atomicLoad(?*Fiber, &future_fiber.awaiter, .acquire) == Fiber.finished) + return i; + } + + el.yield(null, .{ .register_select = futures }); + + std.log.debug("back from select yield", .{}); + + const my_thread: *Thread = .current(); + const my_fiber = my_thread.currentFiber(); + var result: ?usize = null; + + for (futures, 0..) |any_future, i| { + const future_fiber: *Fiber = @ptrCast(@alignCast(any_future)); + if (@cmpxchgStrong(?*Fiber, &future_fiber.awaiter, my_fiber, null, .seq_cst, .seq_cst)) |awaiter| { + if (awaiter == Fiber.finished) { + if (result == null) result = i; + } else if (awaiter) |a| { + const closure: *AsyncClosure = .fromFiber(a); + closure.already_awaited = false; + } + } else { + const closure: *AsyncClosure = .fromFiber(my_fiber); + closure.already_awaited = false; + } + } + + return result.?; +} + +fn cancel( + userdata: ?*anyopaque, + any_future: *std.Io.AnyFuture, + result: []u8, + result_alignment: Alignment, +) void { + const future_fiber: *Fiber = @ptrCast(@alignCast(any_future)); + if (@atomicRmw( + ?*Thread, + &future_fiber.cancel_thread, + .Xchg, + Thread.canceling, + .acq_rel, + )) |cancel_thread| if (cancel_thread != Thread.canceling) { + getSqe(&Thread.current().io_uring).* = .{ + .opcode = .MSG_RING, + .flags = std.os.linux.IOSQE_CQE_SKIP_SUCCESS, + .ioprio = 0, + .fd = cancel_thread.io_uring.fd, + .off = @intFromPtr(future_fiber), + .addr = 0, + .len = @bitCast(-@as(i32, @intFromEnum(std.os.linux.E.INTR))), + .rw_flags = 0, + .user_data = @intFromEnum(Completion.UserData.cleanup), + .buf_index = 0, + .personality = 0, + .splice_fd_in = 0, + .addr3 = 0, + .resv = 0, + }; + }; + await(userdata, any_future, result, result_alignment); +} + +fn cancelRequested(userdata: ?*anyopaque) bool { + _ = userdata; + return @atomicLoad(?*Thread, &Thread.current().currentFiber().cancel_thread, .acquire) == Thread.canceling; +} + +fn createFile( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + flags: Io.File.CreateFlags, +) Io.File.OpenError!Io.File { + const el: *EventLoop = @ptrCast(@alignCast(userdata)); + const thread: *Thread = .current(); + const iou = &thread.io_uring; + const fiber = thread.currentFiber(); + try fiber.enterCancelRegion(thread); + + const posix = std.posix; + const sub_path_c = try posix.toPosixPath(sub_path); + + var os_flags: posix.O = .{ + .ACCMODE = if (flags.read) .RDWR else .WRONLY, + .CREAT = true, + .TRUNC = flags.truncate, + .EXCL = flags.exclusive, + }; + if (@hasField(posix.O, "LARGEFILE")) os_flags.LARGEFILE = true; + if (@hasField(posix.O, "CLOEXEC")) os_flags.CLOEXEC = true; + + // Use the O locking flags if the os supports them to acquire the lock + // atomically. Note that the NONBLOCK flag is removed after the openat() + // call is successful. + const has_flock_open_flags = @hasField(posix.O, "EXLOCK"); + if (has_flock_open_flags) switch (flags.lock) { + .none => {}, + .shared => { + os_flags.SHLOCK = true; + os_flags.NONBLOCK = flags.lock_nonblocking; + }, + .exclusive => { + os_flags.EXLOCK = true; + os_flags.NONBLOCK = flags.lock_nonblocking; + }, + }; + const have_flock = @TypeOf(posix.system.flock) != void; + + if (have_flock and !has_flock_open_flags and flags.lock != .none) { + @panic("TODO"); + } + + if (has_flock_open_flags and flags.lock_nonblocking) { + @panic("TODO"); + } + + getSqe(iou).* = .{ + .opcode = .OPENAT, + .flags = 0, + .ioprio = 0, + .fd = dir.handle, + .off = 0, + .addr = @intFromPtr(&sub_path_c), + .len = @intCast(flags.mode), + .rw_flags = @bitCast(os_flags), + .user_data = @intFromPtr(fiber), + .buf_index = 0, + .personality = 0, + .splice_fd_in = 0, + .addr3 = 0, + .resv = 0, + }; + + el.yield(null, .nothing); + fiber.exitCancelRegion(thread); + + const completion = fiber.resultPointer(Completion); + switch (errno(completion.result)) { + .SUCCESS => return .{ .handle = completion.result }, + .INTR => unreachable, + .CANCELED => return error.Canceled, + + .FAULT => unreachable, + .INVAL => return error.BadPathName, + .BADF => unreachable, + .ACCES => return error.AccessDenied, + .FBIG => return error.FileTooBig, + .OVERFLOW => return error.FileTooBig, + .ISDIR => return error.IsDir, + .LOOP => return error.SymLinkLoop, + .MFILE => return error.ProcessFdQuotaExceeded, + .NAMETOOLONG => return error.NameTooLong, + .NFILE => return error.SystemFdQuotaExceeded, + .NODEV => return error.NoDevice, + .NOENT => return error.FileNotFound, + .NOMEM => return error.SystemResources, + .NOSPC => return error.NoSpaceLeft, + .NOTDIR => return error.NotDir, + .PERM => return error.PermissionDenied, + .EXIST => return error.PathAlreadyExists, + .BUSY => return error.DeviceBusy, + .OPNOTSUPP => return error.FileLocksNotSupported, + .AGAIN => return error.WouldBlock, + .TXTBSY => return error.FileBusy, + .NXIO => return error.NoDevice, + else => |err| return posix.unexpectedErrno(err), + } +} + +fn fileOpen( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + flags: Io.File.OpenFlags, +) Io.File.OpenError!Io.File { + const el: *EventLoop = @ptrCast(@alignCast(userdata)); + const thread: *Thread = .current(); + const iou = &thread.io_uring; + const fiber = thread.currentFiber(); + try fiber.enterCancelRegion(thread); + + const posix = std.posix; + const sub_path_c = try posix.toPosixPath(sub_path); + + var os_flags: posix.O = .{ + .ACCMODE = switch (flags.mode) { + .read_only => .RDONLY, + .write_only => .WRONLY, + .read_write => .RDWR, + }, + }; + + if (@hasField(posix.O, "CLOEXEC")) os_flags.CLOEXEC = true; + if (@hasField(posix.O, "LARGEFILE")) os_flags.LARGEFILE = true; + if (@hasField(posix.O, "NOCTTY")) os_flags.NOCTTY = !flags.allow_ctty; + + // Use the O locking flags if the os supports them to acquire the lock + // atomically. + const has_flock_open_flags = @hasField(posix.O, "EXLOCK"); + if (has_flock_open_flags) { + // Note that the NONBLOCK flag is removed after the openat() call + // is successful. + switch (flags.lock) { + .none => {}, + .shared => { + os_flags.SHLOCK = true; + os_flags.NONBLOCK = flags.lock_nonblocking; + }, + .exclusive => { + os_flags.EXLOCK = true; + os_flags.NONBLOCK = flags.lock_nonblocking; + }, + } + } + const have_flock = @TypeOf(posix.system.flock) != void; + + if (have_flock and !has_flock_open_flags and flags.lock != .none) { + @panic("TODO"); + } + + if (has_flock_open_flags and flags.lock_nonblocking) { + @panic("TODO"); + } + + getSqe(iou).* = .{ + .opcode = .OPENAT, + .flags = 0, + .ioprio = 0, + .fd = dir.handle, + .off = 0, + .addr = @intFromPtr(&sub_path_c), + .len = 0, + .rw_flags = @bitCast(os_flags), + .user_data = @intFromPtr(fiber), + .buf_index = 0, + .personality = 0, + .splice_fd_in = 0, + .addr3 = 0, + .resv = 0, + }; + + el.yield(null, .nothing); + fiber.exitCancelRegion(thread); + + const completion = fiber.resultPointer(Completion); + switch (errno(completion.result)) { + .SUCCESS => return .{ .handle = completion.result }, + .INTR => unreachable, + .CANCELED => return error.Canceled, + + .FAULT => unreachable, + .INVAL => return error.BadPathName, + .BADF => unreachable, + .ACCES => return error.AccessDenied, + .FBIG => return error.FileTooBig, + .OVERFLOW => return error.FileTooBig, + .ISDIR => return error.IsDir, + .LOOP => return error.SymLinkLoop, + .MFILE => return error.ProcessFdQuotaExceeded, + .NAMETOOLONG => return error.NameTooLong, + .NFILE => return error.SystemFdQuotaExceeded, + .NODEV => return error.NoDevice, + .NOENT => return error.FileNotFound, + .NOMEM => return error.SystemResources, + .NOSPC => return error.NoSpaceLeft, + .NOTDIR => return error.NotDir, + .PERM => return error.PermissionDenied, + .EXIST => return error.PathAlreadyExists, + .BUSY => return error.DeviceBusy, + .OPNOTSUPP => return error.FileLocksNotSupported, + .AGAIN => return error.WouldBlock, + .TXTBSY => return error.FileBusy, + .NXIO => return error.NoDevice, + else => |err| return posix.unexpectedErrno(err), + } +} + +fn fileClose(userdata: ?*anyopaque, file: Io.File) void { + const el: *EventLoop = @ptrCast(@alignCast(userdata)); + const thread: *Thread = .current(); + const iou = &thread.io_uring; + const fiber = thread.currentFiber(); + + getSqe(iou).* = .{ + .opcode = .CLOSE, + .flags = 0, + .ioprio = 0, + .fd = file.handle, + .off = 0, + .addr = 0, + .len = 0, + .rw_flags = 0, + .user_data = @intFromPtr(fiber), + .buf_index = 0, + .personality = 0, + .splice_fd_in = 0, + .addr3 = 0, + .resv = 0, + }; + + el.yield(null, .nothing); + + const completion = fiber.resultPointer(Completion); + switch (errno(completion.result)) { + .SUCCESS => return, + .INTR => unreachable, + .CANCELED => return, + + .BADF => unreachable, // Always a race condition. + else => return, + } +} + +fn pread(userdata: ?*anyopaque, file: Io.File, buffer: []u8, offset: std.posix.off_t) Io.File.PReadError!usize { + const el: *EventLoop = @ptrCast(@alignCast(userdata)); + const thread: *Thread = .current(); + const iou = &thread.io_uring; + const fiber = thread.currentFiber(); + try fiber.enterCancelRegion(thread); + + getSqe(iou).* = .{ + .opcode = .READ, + .flags = 0, + .ioprio = 0, + .fd = file.handle, + .off = @bitCast(offset), + .addr = @intFromPtr(buffer.ptr), + .len = @min(buffer.len, 0x7ffff000), + .rw_flags = 0, + .user_data = @intFromPtr(fiber), + .buf_index = 0, + .personality = 0, + .splice_fd_in = 0, + .addr3 = 0, + .resv = 0, + }; + + el.yield(null, .nothing); + fiber.exitCancelRegion(thread); + + const completion = fiber.resultPointer(Completion); + switch (errno(completion.result)) { + .SUCCESS => return @as(u32, @bitCast(completion.result)), + .INTR => unreachable, + .CANCELED => return error.Canceled, + + .INVAL => unreachable, + .FAULT => unreachable, + .NOENT => return error.ProcessNotFound, + .AGAIN => return error.WouldBlock, + .BADF => return error.NotOpenForReading, // Can be a race condition. + .IO => return error.InputOutput, + .ISDIR => return error.IsDir, + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + .NOTCONN => return error.SocketUnconnected, + .CONNRESET => return error.ConnectionResetByPeer, + .TIMEDOUT => return error.Timeout, + .NXIO => return error.Unseekable, + .SPIPE => return error.Unseekable, + .OVERFLOW => return error.Unseekable, + else => |err| return std.posix.unexpectedErrno(err), + } +} + +fn pwrite(userdata: ?*anyopaque, file: Io.File, buffer: []const u8, offset: std.posix.off_t) Io.File.PWriteError!usize { + const el: *EventLoop = @ptrCast(@alignCast(userdata)); + const thread: *Thread = .current(); + const iou = &thread.io_uring; + const fiber = thread.currentFiber(); + try fiber.enterCancelRegion(thread); + + getSqe(iou).* = .{ + .opcode = .WRITE, + .flags = 0, + .ioprio = 0, + .fd = file.handle, + .off = @bitCast(offset), + .addr = @intFromPtr(buffer.ptr), + .len = @min(buffer.len, 0x7ffff000), + .rw_flags = 0, + .user_data = @intFromPtr(fiber), + .buf_index = 0, + .personality = 0, + .splice_fd_in = 0, + .addr3 = 0, + .resv = 0, + }; + + el.yield(null, .nothing); + fiber.exitCancelRegion(thread); + + const completion = fiber.resultPointer(Completion); + switch (errno(completion.result)) { + .SUCCESS => return @as(u32, @bitCast(completion.result)), + .INTR => unreachable, + .CANCELED => return error.Canceled, + + .INVAL => return error.InvalidArgument, + .FAULT => unreachable, + .NOENT => return error.ProcessNotFound, + .AGAIN => return error.WouldBlock, + .BADF => return error.NotOpenForWriting, // can be a race condition. + .DESTADDRREQ => unreachable, // `connect` was never called. + .DQUOT => return error.DiskQuota, + .FBIG => return error.FileTooBig, + .IO => return error.InputOutput, + .NOSPC => return error.NoSpaceLeft, + .ACCES => return error.AccessDenied, + .PERM => return error.PermissionDenied, + .PIPE => return error.BrokenPipe, + .NXIO => return error.Unseekable, + .SPIPE => return error.Unseekable, + .OVERFLOW => return error.Unseekable, + .BUSY => return error.DeviceBusy, + .CONNRESET => return error.ConnectionResetByPeer, + .MSGSIZE => return error.MessageOversize, + else => |err| return std.posix.unexpectedErrno(err), + } +} + +fn now(userdata: ?*anyopaque, clockid: std.posix.clockid_t) Io.ClockGetTimeError!Io.Timestamp { + _ = userdata; + const timespec = try std.posix.clock_gettime(clockid); + return @enumFromInt(@as(i128, timespec.sec) * std.time.ns_per_s + timespec.nsec); +} + +fn sleep(userdata: ?*anyopaque, clockid: std.posix.clockid_t, deadline: Io.Deadline) Io.SleepError!void { + const el: *EventLoop = @ptrCast(@alignCast(userdata)); + const thread: *Thread = .current(); + const iou = &thread.io_uring; + const fiber = thread.currentFiber(); + try fiber.enterCancelRegion(thread); + + const deadline_nanoseconds: i96 = switch (deadline) { + .duration => |duration| duration.nanoseconds, + .timestamp => |timestamp| @intFromEnum(timestamp), + }; + const timespec: std.os.linux.kernel_timespec = .{ + .sec = @intCast(@divFloor(deadline_nanoseconds, std.time.ns_per_s)), + .nsec = @intCast(@mod(deadline_nanoseconds, std.time.ns_per_s)), + }; + getSqe(iou).* = .{ + .opcode = .TIMEOUT, + .flags = 0, + .ioprio = 0, + .fd = 0, + .off = 0, + .addr = @intFromPtr(×pec), + .len = 1, + .rw_flags = @as(u32, switch (deadline) { + .duration => 0, + .timestamp => std.os.linux.IORING_TIMEOUT_ABS, + }) | @as(u32, switch (clockid) { + .REALTIME => std.os.linux.IORING_TIMEOUT_REALTIME, + .MONOTONIC => 0, + .BOOTTIME => std.os.linux.IORING_TIMEOUT_BOOTTIME, + else => return error.UnsupportedClock, + }), + .user_data = @intFromPtr(fiber), + .buf_index = 0, + .personality = 0, + .splice_fd_in = 0, + .addr3 = 0, + .resv = 0, + }; + + el.yield(null, .nothing); + fiber.exitCancelRegion(thread); + + const completion = fiber.resultPointer(Completion); + switch (errno(completion.result)) { + .SUCCESS, .TIME => return, + .INTR => unreachable, + .CANCELED => return error.Canceled, + + else => |err| return std.posix.unexpectedErrno(err), + } +} + +fn mutexLock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) error{Canceled}!void { + const el: *EventLoop = @ptrCast(@alignCast(userdata)); + el.yield(null, .{ .mutex_lock = .{ .prev_state = prev_state, .mutex = mutex } }); +} +fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void { + var maybe_waiting_fiber: ?*Fiber = @ptrFromInt(@intFromEnum(prev_state)); + while (if (maybe_waiting_fiber) |waiting_fiber| @cmpxchgWeak( + Io.Mutex.State, + &mutex.state, + @enumFromInt(@intFromPtr(waiting_fiber)), + @enumFromInt(@intFromPtr(waiting_fiber.queue_next)), + .release, + .acquire, + ) else @cmpxchgWeak( + Io.Mutex.State, + &mutex.state, + .locked_once, + .unlocked, + .release, + .acquire, + ) orelse return) |next_state| maybe_waiting_fiber = @ptrFromInt(@intFromEnum(next_state)); + maybe_waiting_fiber.?.queue_next = null; + const el: *EventLoop = @ptrCast(@alignCast(userdata)); + el.yield(maybe_waiting_fiber.?, .reschedule); +} + +const ConditionImpl = struct { + tail: *Fiber, + event: union(enum) { + queued, + wake: Io.Condition.Wake, + }, +}; + +fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) Io.Cancelable!void { + const el: *EventLoop = @ptrCast(@alignCast(userdata)); + el.yield(null, .{ .condition_wait = .{ .cond = cond, .mutex = mutex } }); + const thread = Thread.current(); + const fiber = thread.currentFiber(); + const cond_impl = fiber.resultPointer(ConditionImpl); + try mutex.lock(el.io()); + switch (cond_impl.event) { + .queued => {}, + .wake => |wake| if (fiber.queue_next) |next_fiber| switch (wake) { + .one => if (@cmpxchgStrong( + ?*Fiber, + @as(*?*Fiber, @ptrCast(&cond.state)), + null, + next_fiber, + .release, + .acquire, + )) |old_fiber| { + const old_cond_impl = old_fiber.?.resultPointer(ConditionImpl); + assert(old_cond_impl.tail.queue_next == null); + old_cond_impl.tail.queue_next = next_fiber; + old_cond_impl.tail = cond_impl.tail; + }, + .all => el.schedule(thread, .{ .head = next_fiber, .tail = cond_impl.tail }), + }, + } + fiber.queue_next = null; +} + +fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition.Wake) void { + const el: *EventLoop = @ptrCast(@alignCast(userdata)); + const waiting_fiber = @atomicRmw(?*Fiber, @as(*?*Fiber, @ptrCast(&cond.state)), .Xchg, null, .acquire) orelse return; + waiting_fiber.resultPointer(ConditionImpl).event = .{ .wake = wake }; + el.yield(waiting_fiber, .reschedule); +} + +fn errno(signed: i32) std.os.linux.E { + return .init(@bitCast(@as(isize, signed))); +} + +fn getSqe(iou: *IoUring) *std.os.linux.io_uring_sqe { + while (true) return iou.get_sqe() catch { + _ = iou.submit_and_wait(0) catch |err| switch (err) { + error.SignalInterrupt => std.log.warn("submit_and_wait failed with SignalInterrupt", .{}), + else => |e| @panic(@errorName(e)), + }; + continue; + }; +} diff --git a/lib/std/Io/Kqueue.zig b/lib/std/Io/Kqueue.zig new file mode 100644 index 0000000000..5b4f71da08 --- /dev/null +++ b/lib/std/Io/Kqueue.zig @@ -0,0 +1,1743 @@ +const Kqueue = @This(); +const builtin = @import("builtin"); + +const std = @import("../std.zig"); +const Io = std.Io; +const Dir = std.Io.Dir; +const File = std.Io.File; +const net = std.Io.net; +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const Alignment = std.mem.Alignment; +const IpAddress = std.Io.net.IpAddress; +const errnoBug = std.Io.Threaded.errnoBug; +const posix = std.posix; + +/// Must be a thread-safe allocator. +gpa: Allocator, +mutex: std.Thread.Mutex, +main_fiber_buffer: [@sizeOf(Fiber) + Fiber.max_result_size]u8 align(@alignOf(Fiber)), +threads: Thread.List, + +/// Empirically saw >128KB being used by the self-hosted backend to panic. +const idle_stack_size = 256 * 1024; + +const max_idle_search = 4; +const max_steal_ready_search = 4; +const max_iovecs_len = 8; + +const changes_buffer_len = 64; + +const Thread = struct { + thread: std.Thread, + idle_context: Context, + current_context: *Context, + ready_queue: ?*Fiber, + kq_fd: posix.fd_t, + idle_search_index: u32, + steal_ready_search_index: u32, + /// For ensuring multiple fibers waiting on the same file descriptor and + /// filter use the same kevent. + wait_queues: std.AutoArrayHashMapUnmanaged(WaitQueueKey, *Fiber), + + const WaitQueueKey = struct { + ident: usize, + filter: i32, + }; + + const canceling: ?*Thread = @ptrFromInt(@alignOf(Thread)); + + threadlocal var self: *Thread = undefined; + + fn current() *Thread { + return self; + } + + fn currentFiber(thread: *Thread) *Fiber { + return @fieldParentPtr("context", thread.current_context); + } + + const List = struct { + allocated: []Thread, + reserved: u32, + active: u32, + }; + + fn deinit(thread: *Thread, gpa: Allocator) void { + posix.close(thread.kq_fd); + assert(thread.wait_queues.count() == 0); + thread.wait_queues.deinit(gpa); + thread.* = undefined; + } +}; + +const Fiber = struct { + required_align: void align(4), + context: Context, + awaiter: ?*Fiber, + queue_next: ?*Fiber, + cancel_thread: ?*Thread, + awaiting_completions: std.StaticBitSet(3), + + const finished: ?*Fiber = @ptrFromInt(@alignOf(Thread)); + + const max_result_align: Alignment = .@"16"; + const max_result_size = max_result_align.forward(64); + /// This includes any stack realignments that need to happen, and also the + /// initial frame return address slot and argument frame, depending on target. + const min_stack_size = 4 * 1024 * 1024; + const max_context_align: Alignment = .@"16"; + const max_context_size = max_context_align.forward(1024); + const max_closure_size: usize = @sizeOf(AsyncClosure); + const max_closure_align: Alignment = .of(AsyncClosure); + const allocation_size = std.mem.alignForward( + usize, + max_closure_align.max(max_context_align).forward( + max_result_align.forward(@sizeOf(Fiber)) + max_result_size + min_stack_size, + ) + max_closure_size + max_context_size, + std.heap.page_size_max, + ); + + fn allocate(k: *Kqueue) error{OutOfMemory}!*Fiber { + return @ptrCast(try k.gpa.alignedAlloc(u8, .of(Fiber), allocation_size)); + } + + fn allocatedSlice(f: *Fiber) []align(@alignOf(Fiber)) u8 { + return @as([*]align(@alignOf(Fiber)) u8, @ptrCast(f))[0..allocation_size]; + } + + fn allocatedEnd(f: *Fiber) [*]u8 { + const allocated_slice = f.allocatedSlice(); + return allocated_slice[allocated_slice.len..].ptr; + } + + fn resultPointer(f: *Fiber, comptime Result: type) *Result { + return @ptrCast(@alignCast(f.resultBytes(.of(Result)))); + } + + fn resultBytes(f: *Fiber, alignment: Alignment) [*]u8 { + return @ptrFromInt(alignment.forward(@intFromPtr(f) + @sizeOf(Fiber))); + } + + fn enterCancelRegion(fiber: *Fiber, thread: *Thread) error{Canceled}!void { + if (@cmpxchgStrong( + ?*Thread, + &fiber.cancel_thread, + null, + thread, + .acq_rel, + .acquire, + )) |cancel_thread| { + assert(cancel_thread == Thread.canceling); + return error.Canceled; + } + } + + fn exitCancelRegion(fiber: *Fiber, thread: *Thread) void { + if (@cmpxchgStrong( + ?*Thread, + &fiber.cancel_thread, + thread, + null, + .acq_rel, + .acquire, + )) |cancel_thread| assert(cancel_thread == Thread.canceling); + } + + const Queue = struct { head: *Fiber, tail: *Fiber }; +}; + +fn recycle(k: *Kqueue, fiber: *Fiber) void { + std.log.debug("recyling {*}", .{fiber}); + assert(fiber.queue_next == null); + k.gpa.free(fiber.allocatedSlice()); +} + +pub const InitOptions = struct { + n_threads: ?usize = null, +}; + +pub fn init(k: *Kqueue, gpa: Allocator, options: InitOptions) !void { + assert(options.n_threads != 0); + const n_threads = @max(1, options.n_threads orelse std.Thread.getCpuCount() catch 1); + const threads_size = n_threads * @sizeOf(Thread); + const idle_stack_end_offset = std.mem.alignForward(usize, threads_size + idle_stack_size, std.heap.page_size_max); + const allocated_slice = try gpa.alignedAlloc(u8, .of(Thread), idle_stack_end_offset); + errdefer gpa.free(allocated_slice); + k.* = .{ + .gpa = gpa, + .mutex = .{}, + .main_fiber_buffer = undefined, + .threads = .{ + .allocated = @ptrCast(allocated_slice[0..threads_size]), + .reserved = 1, + .active = 1, + }, + }; + const main_fiber: *Fiber = @ptrCast(&k.main_fiber_buffer); + main_fiber.* = .{ + .required_align = {}, + .context = undefined, + .awaiter = null, + .queue_next = null, + .cancel_thread = null, + .awaiting_completions = .initEmpty(), + }; + const main_thread = &k.threads.allocated[0]; + Thread.self = main_thread; + const idle_stack_end: [*]align(16) usize = @ptrCast(@alignCast(allocated_slice[idle_stack_end_offset..].ptr)); + (idle_stack_end - 1)[0..1].* = .{@intFromPtr(k)}; + main_thread.* = .{ + .thread = undefined, + .idle_context = switch (builtin.cpu.arch) { + .aarch64 => .{ + .sp = @intFromPtr(idle_stack_end), + .fp = 0, + .pc = @intFromPtr(&mainIdleEntry), + }, + .x86_64 => .{ + .rsp = @intFromPtr(idle_stack_end - 1), + .rbp = 0, + .rip = @intFromPtr(&mainIdleEntry), + }, + else => @compileError("unimplemented architecture"), + }, + .current_context = &main_fiber.context, + .ready_queue = null, + .kq_fd = try posix.kqueue(), + .idle_search_index = 1, + .steal_ready_search_index = 1, + .wait_queues = .empty, + }; + errdefer std.posix.close(main_thread.kq_fd); + std.log.debug("created main idle {*}", .{&main_thread.idle_context}); + std.log.debug("created main {*}", .{main_fiber}); +} + +pub fn deinit(k: *Kqueue) void { + const active_threads = @atomicLoad(u32, &k.threads.active, .acquire); + for (k.threads.allocated[0..active_threads]) |*thread| { + const ready_fiber = @atomicLoad(?*Fiber, &thread.ready_queue, .monotonic); + assert(ready_fiber == null or ready_fiber == Fiber.finished); // pending async + } + k.yield(null, .exit); + const main_thread = &k.threads.allocated[0]; + const gpa = k.gpa; + main_thread.deinit(gpa); + const allocated_ptr: [*]align(@alignOf(Thread)) u8 = @ptrCast(@alignCast(k.threads.allocated.ptr)); + const idle_stack_end_offset = std.mem.alignForward(usize, k.threads.allocated.len * @sizeOf(Thread) + idle_stack_size, std.heap.page_size_max); + for (k.threads.allocated[1..active_threads]) |*thread| thread.thread.join(); + gpa.free(allocated_ptr[0..idle_stack_end_offset]); + k.* = undefined; +} + +fn findReadyFiber(k: *Kqueue, thread: *Thread) ?*Fiber { + if (@atomicRmw(?*Fiber, &thread.ready_queue, .Xchg, Fiber.finished, .acquire)) |ready_fiber| { + @atomicStore(?*Fiber, &thread.ready_queue, ready_fiber.queue_next, .release); + ready_fiber.queue_next = null; + return ready_fiber; + } + const active_threads = @atomicLoad(u32, &k.threads.active, .acquire); + for (0..@min(max_steal_ready_search, active_threads)) |_| { + defer thread.steal_ready_search_index += 1; + if (thread.steal_ready_search_index == active_threads) thread.steal_ready_search_index = 0; + const steal_ready_search_thread = &k.threads.allocated[0..active_threads][thread.steal_ready_search_index]; + if (steal_ready_search_thread == thread) continue; + const ready_fiber = @atomicLoad(?*Fiber, &steal_ready_search_thread.ready_queue, .acquire) orelse continue; + if (ready_fiber == Fiber.finished) continue; + if (@cmpxchgWeak( + ?*Fiber, + &steal_ready_search_thread.ready_queue, + ready_fiber, + null, + .acquire, + .monotonic, + )) |_| continue; + @atomicStore(?*Fiber, &thread.ready_queue, ready_fiber.queue_next, .release); + ready_fiber.queue_next = null; + return ready_fiber; + } + // couldn't find anything to do, so we are now open for business + @atomicStore(?*Fiber, &thread.ready_queue, null, .monotonic); + return null; +} + +fn yield(k: *Kqueue, maybe_ready_fiber: ?*Fiber, pending_task: SwitchMessage.PendingTask) void { + const thread: *Thread = .current(); + const ready_context = if (maybe_ready_fiber orelse k.findReadyFiber(thread)) |ready_fiber| + &ready_fiber.context + else + &thread.idle_context; + const message: SwitchMessage = .{ + .contexts = .{ + .prev = thread.current_context, + .ready = ready_context, + }, + .pending_task = pending_task, + }; + std.log.debug("switching from {*} to {*}", .{ message.contexts.prev, message.contexts.ready }); + contextSwitch(&message).handle(k); +} + +fn schedule(k: *Kqueue, thread: *Thread, ready_queue: Fiber.Queue) void { + { + var fiber = ready_queue.head; + while (true) { + std.log.debug("scheduling {*}", .{fiber}); + fiber = fiber.queue_next orelse break; + } + assert(fiber == ready_queue.tail); + } + // shared fields of previous `Thread` must be initialized before later ones are marked as active + const new_thread_index = @atomicLoad(u32, &k.threads.active, .acquire); + for (0..@min(max_idle_search, new_thread_index)) |_| { + defer thread.idle_search_index += 1; + if (thread.idle_search_index == new_thread_index) thread.idle_search_index = 0; + const idle_search_thread = &k.threads.allocated[0..new_thread_index][thread.idle_search_index]; + if (idle_search_thread == thread) continue; + if (@cmpxchgWeak( + ?*Fiber, + &idle_search_thread.ready_queue, + null, + ready_queue.head, + .release, + .monotonic, + )) |_| continue; + const changes = [_]posix.Kevent{ + .{ + .ident = 0, + .filter = std.c.EVFILT.USER, + .flags = std.c.EV.ADD | std.c.EV.ONESHOT, + .fflags = std.c.NOTE.TRIGGER, + .data = 0, + .udata = @intFromEnum(Completion.UserData.wakeup), + }, + }; + // If an error occurs it only pessimises scheduling. + _ = posix.kevent(idle_search_thread.kq_fd, &changes, &.{}, null) catch {}; + return; + } + spawn_thread: { + // previous failed reservations must have completed before retrying + if (new_thread_index == k.threads.allocated.len or @cmpxchgWeak( + u32, + &k.threads.reserved, + new_thread_index, + new_thread_index + 1, + .acquire, + .monotonic, + ) != null) break :spawn_thread; + const new_thread = &k.threads.allocated[new_thread_index]; + const next_thread_index = new_thread_index + 1; + new_thread.* = .{ + .thread = undefined, + .idle_context = undefined, + .current_context = &new_thread.idle_context, + .ready_queue = ready_queue.head, + .kq_fd = posix.kqueue() catch |err| { + @atomicStore(u32, &k.threads.reserved, new_thread_index, .release); + // no more access to `thread` after giving up reservation + std.log.warn("unable to create worker thread due to kqueue init failure: {t}", .{err}); + break :spawn_thread; + }, + .idle_search_index = 0, + .steal_ready_search_index = 0, + .wait_queues = .empty, + }; + new_thread.thread = std.Thread.spawn(.{ + .stack_size = idle_stack_size, + .allocator = k.gpa, + }, threadEntry, .{ k, new_thread_index }) catch |err| { + posix.close(new_thread.kq_fd); + @atomicStore(u32, &k.threads.reserved, new_thread_index, .release); + // no more access to `thread` after giving up reservation + std.log.warn("unable to create worker thread due spawn failure: {s}", .{@errorName(err)}); + break :spawn_thread; + }; + // shared fields of `Thread` must be initialized before being marked active + @atomicStore(u32, &k.threads.active, next_thread_index, .release); + return; + } + // nobody wanted it, so just queue it on ourselves + while (@cmpxchgWeak( + ?*Fiber, + &thread.ready_queue, + ready_queue.tail.queue_next, + ready_queue.head, + .acq_rel, + .acquire, + )) |old_head| ready_queue.tail.queue_next = old_head; +} + +fn mainIdle(k: *Kqueue, message: *const SwitchMessage) callconv(.withStackAlign(.c, @max(@alignOf(Thread), @alignOf(Context)))) noreturn { + message.handle(k); + k.idle(&k.threads.allocated[0]); + k.yield(@ptrCast(&k.main_fiber_buffer), .nothing); + unreachable; // switched to dead fiber +} + +fn threadEntry(k: *Kqueue, index: u32) void { + const thread: *Thread = &k.threads.allocated[index]; + Thread.self = thread; + std.log.debug("created thread idle {*}", .{&thread.idle_context}); + k.idle(thread); + thread.deinit(k.gpa); +} + +const Completion = struct { + const UserData = enum(usize) { + unused, + wakeup, + cleanup, + exit, + /// *Fiber + _, + }; + /// Corresponds to Kevent field. + flags: u16, + /// Corresponds to Kevent field. + fflags: u32, + /// Corresponds to Kevent field. + data: isize, +}; + +fn idle(k: *Kqueue, thread: *Thread) void { + var events_buffer: [changes_buffer_len]posix.Kevent = undefined; + var maybe_ready_fiber: ?*Fiber = null; + while (true) { + while (maybe_ready_fiber orelse k.findReadyFiber(thread)) |ready_fiber| { + k.yield(ready_fiber, .nothing); + maybe_ready_fiber = null; + } + const n = posix.kevent(thread.kq_fd, &.{}, &events_buffer, null) catch |err| { + // TODO handle EINTR for cancellation purposes + @panic(@errorName(err)); + }; + var maybe_ready_queue: ?Fiber.Queue = null; + for (events_buffer[0..n]) |event| switch (@as(Completion.UserData, @enumFromInt(event.udata))) { + .unused => unreachable, // bad submission queued? + .wakeup => {}, + .cleanup => @panic("failed to notify other threads that we are exiting"), + .exit => { + assert(maybe_ready_fiber == null and maybe_ready_queue == null); // pending async + return; + }, + _ => { + const event_head_fiber: *Fiber = @ptrFromInt(event.udata); + const event_tail_fiber = thread.wait_queues.fetchSwapRemove(.{ + .ident = event.ident, + .filter = event.filter, + }).?.value; + assert(event_tail_fiber.queue_next == null); + + // TODO reevaluate this logic + event_head_fiber.resultPointer(Completion).* = .{ + .flags = event.flags, + .fflags = event.fflags, + .data = event.data, + }; + + queue_ready: { + const head: *Fiber = if (maybe_ready_fiber == null) f: { + maybe_ready_fiber = event_head_fiber; + const next = event_head_fiber.queue_next orelse break :queue_ready; + event_head_fiber.queue_next = null; + break :f next; + } else event_head_fiber; + + if (maybe_ready_queue) |*ready_queue| { + ready_queue.tail.queue_next = head; + ready_queue.tail = event_tail_fiber; + } else { + maybe_ready_queue = .{ .head = head, .tail = event_tail_fiber }; + } + } + }, + }; + if (maybe_ready_queue) |ready_queue| k.schedule(thread, ready_queue); + } +} + +const SwitchMessage = struct { + contexts: extern struct { + prev: *Context, + ready: *Context, + }, + pending_task: PendingTask, + + const PendingTask = union(enum) { + nothing, + reschedule, + recycle: *Fiber, + register_awaiter: *?*Fiber, + register_select: []const *Io.AnyFuture, + mutex_lock: struct { + prev_state: Io.Mutex.State, + mutex: *Io.Mutex, + }, + condition_wait: struct { + cond: *Io.Condition, + mutex: *Io.Mutex, + }, + exit, + }; + + fn handle(message: *const SwitchMessage, k: *Kqueue) void { + const thread: *Thread = .current(); + thread.current_context = message.contexts.ready; + switch (message.pending_task) { + .nothing => {}, + .reschedule => if (message.contexts.prev != &thread.idle_context) { + const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); + assert(prev_fiber.queue_next == null); + k.schedule(thread, .{ .head = prev_fiber, .tail = prev_fiber }); + }, + .recycle => |fiber| { + k.recycle(fiber); + }, + .register_awaiter => |awaiter| { + const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); + assert(prev_fiber.queue_next == null); + if (@atomicRmw(?*Fiber, awaiter, .Xchg, prev_fiber, .acq_rel) == Fiber.finished) + k.schedule(thread, .{ .head = prev_fiber, .tail = prev_fiber }); + }, + .register_select => |futures| { + const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); + assert(prev_fiber.queue_next == null); + for (futures) |any_future| { + const future_fiber: *Fiber = @ptrCast(@alignCast(any_future)); + if (@atomicRmw(?*Fiber, &future_fiber.awaiter, .Xchg, prev_fiber, .acq_rel) == Fiber.finished) { + const closure: *AsyncClosure = .fromFiber(future_fiber); + if (!@atomicRmw(bool, &closure.already_awaited, .Xchg, true, .seq_cst)) { + k.schedule(thread, .{ .head = prev_fiber, .tail = prev_fiber }); + } + } + } + }, + .mutex_lock => |mutex_lock| { + const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); + assert(prev_fiber.queue_next == null); + var prev_state = mutex_lock.prev_state; + while (switch (prev_state) { + else => next_state: { + prev_fiber.queue_next = @ptrFromInt(@intFromEnum(prev_state)); + break :next_state @cmpxchgWeak( + Io.Mutex.State, + &mutex_lock.mutex.state, + prev_state, + @enumFromInt(@intFromPtr(prev_fiber)), + .release, + .acquire, + ); + }, + .unlocked => @cmpxchgWeak( + Io.Mutex.State, + &mutex_lock.mutex.state, + .unlocked, + .locked_once, + .acquire, + .acquire, + ) orelse { + prev_fiber.queue_next = null; + k.schedule(thread, .{ .head = prev_fiber, .tail = prev_fiber }); + return; + }, + }) |next_state| prev_state = next_state; + }, + .condition_wait => |condition_wait| { + const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); + assert(prev_fiber.queue_next == null); + const cond_impl = prev_fiber.resultPointer(Condition); + cond_impl.* = .{ + .tail = prev_fiber, + .event = .queued, + }; + if (@cmpxchgStrong( + ?*Fiber, + @as(*?*Fiber, @ptrCast(&condition_wait.cond.state)), + null, + prev_fiber, + .release, + .acquire, + )) |waiting_fiber| { + const waiting_cond_impl = waiting_fiber.?.resultPointer(Condition); + assert(waiting_cond_impl.tail.queue_next == null); + waiting_cond_impl.tail.queue_next = prev_fiber; + waiting_cond_impl.tail = prev_fiber; + } + condition_wait.mutex.unlock(k.io()); + }, + .exit => for (k.threads.allocated[0..@atomicLoad(u32, &k.threads.active, .acquire)]) |*each_thread| { + const changes = [_]posix.Kevent{ + .{ + .ident = 0, + .filter = std.c.EVFILT.USER, + .flags = std.c.EV.ADD | std.c.EV.ONESHOT, + .fflags = std.c.NOTE.TRIGGER, + .data = 0, + .udata = @intFromEnum(Completion.UserData.exit), + }, + }; + _ = posix.kevent(each_thread.kq_fd, &changes, &.{}, null) catch |err| { + @panic(@errorName(err)); + }; + }, + } + } +}; + +const Context = switch (builtin.cpu.arch) { + .aarch64 => extern struct { + sp: u64, + fp: u64, + pc: u64, + }, + .x86_64 => extern struct { + rsp: u64, + rbp: u64, + rip: u64, + }, + else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), +}; + +inline fn contextSwitch(message: *const SwitchMessage) *const SwitchMessage { + return @fieldParentPtr("contexts", switch (builtin.cpu.arch) { + .aarch64 => asm volatile ( + \\ ldp x0, x2, [x1] + \\ ldr x3, [x2, #16] + \\ mov x4, sp + \\ stp x4, fp, [x0] + \\ adr x5, 0f + \\ ldp x4, fp, [x2] + \\ str x5, [x0, #16] + \\ mov sp, x4 + \\ br x3 + \\0: + : [received_message] "={x1}" (-> *const @FieldType(SwitchMessage, "contexts")), + : [message_to_send] "{x1}" (&message.contexts), + : .{ + .x0 = true, + .x1 = true, + .x2 = true, + .x3 = true, + .x4 = true, + .x5 = true, + .x6 = true, + .x7 = true, + .x8 = true, + .x9 = true, + .x10 = true, + .x11 = true, + .x12 = true, + .x13 = true, + .x14 = true, + .x15 = true, + .x16 = true, + .x17 = true, + .x19 = true, + .x20 = true, + .x21 = true, + .x22 = true, + .x23 = true, + .x24 = true, + .x25 = true, + .x26 = true, + .x27 = true, + .x28 = true, + .x30 = true, + .z0 = true, + .z1 = true, + .z2 = true, + .z3 = true, + .z4 = true, + .z5 = true, + .z6 = true, + .z7 = true, + .z8 = true, + .z9 = true, + .z10 = true, + .z11 = true, + .z12 = true, + .z13 = true, + .z14 = true, + .z15 = true, + .z16 = true, + .z17 = true, + .z18 = true, + .z19 = true, + .z20 = true, + .z21 = true, + .z22 = true, + .z23 = true, + .z24 = true, + .z25 = true, + .z26 = true, + .z27 = true, + .z28 = true, + .z29 = true, + .z30 = true, + .z31 = true, + .p0 = true, + .p1 = true, + .p2 = true, + .p3 = true, + .p4 = true, + .p5 = true, + .p6 = true, + .p7 = true, + .p8 = true, + .p9 = true, + .p10 = true, + .p11 = true, + .p12 = true, + .p13 = true, + .p14 = true, + .p15 = true, + .fpcr = true, + .fpsr = true, + .ffr = true, + .memory = true, + }), + .x86_64 => asm volatile ( + \\ movq 0(%%rsi), %%rax + \\ movq 8(%%rsi), %%rcx + \\ leaq 0f(%%rip), %%rdx + \\ movq %%rsp, 0(%%rax) + \\ movq %%rbp, 8(%%rax) + \\ movq %%rdx, 16(%%rax) + \\ movq 0(%%rcx), %%rsp + \\ movq 8(%%rcx), %%rbp + \\ jmpq *16(%%rcx) + \\0: + : [received_message] "={rsi}" (-> *const @FieldType(SwitchMessage, "contexts")), + : [message_to_send] "{rsi}" (&message.contexts), + : .{ + .rax = true, + .rcx = true, + .rdx = true, + .rbx = true, + .rsi = true, + .rdi = true, + .r8 = true, + .r9 = true, + .r10 = true, + .r11 = true, + .r12 = true, + .r13 = true, + .r14 = true, + .r15 = true, + .mm0 = true, + .mm1 = true, + .mm2 = true, + .mm3 = true, + .mm4 = true, + .mm5 = true, + .mm6 = true, + .mm7 = true, + .zmm0 = true, + .zmm1 = true, + .zmm2 = true, + .zmm3 = true, + .zmm4 = true, + .zmm5 = true, + .zmm6 = true, + .zmm7 = true, + .zmm8 = true, + .zmm9 = true, + .zmm10 = true, + .zmm11 = true, + .zmm12 = true, + .zmm13 = true, + .zmm14 = true, + .zmm15 = true, + .zmm16 = true, + .zmm17 = true, + .zmm18 = true, + .zmm19 = true, + .zmm20 = true, + .zmm21 = true, + .zmm22 = true, + .zmm23 = true, + .zmm24 = true, + .zmm25 = true, + .zmm26 = true, + .zmm27 = true, + .zmm28 = true, + .zmm29 = true, + .zmm30 = true, + .zmm31 = true, + .fpsr = true, + .fpcr = true, + .mxcsr = true, + .rflags = true, + .dirflag = true, + .memory = true, + }), + else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), + }); +} + +fn mainIdleEntry() callconv(.naked) void { + switch (builtin.cpu.arch) { + .x86_64 => asm volatile ( + \\ movq (%%rsp), %%rdi + \\ jmp %[mainIdle:P] + : + : [mainIdle] "X" (&mainIdle), + ), + .aarch64 => asm volatile ( + \\ ldr x0, [sp, #-8] + \\ b %[mainIdle] + : + : [mainIdle] "X" (&mainIdle), + ), + else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), + } +} + +fn fiberEntry() callconv(.naked) void { + switch (builtin.cpu.arch) { + .x86_64 => asm volatile ( + \\ leaq 8(%%rsp), %%rdi + \\ jmp %[AsyncClosure_call:P] + : + : [AsyncClosure_call] "X" (&AsyncClosure.call), + ), + .aarch64 => asm volatile ( + \\ mov x0, sp + \\ b %[AsyncClosure_call] + : + : [AsyncClosure_call] "X" (&AsyncClosure.call), + ), + else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), + } +} + +const AsyncClosure = struct { + kqueue: *Kqueue, + fiber: *Fiber, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, + result_align: Alignment, + already_awaited: bool, + + fn contextPointer(closure: *AsyncClosure) [*]align(Fiber.max_context_align.toByteUnits()) u8 { + return @alignCast(@as([*]u8, @ptrCast(closure)) + @sizeOf(AsyncClosure)); + } + + fn call(closure: *AsyncClosure, message: *const SwitchMessage) callconv(.withStackAlign(.c, @alignOf(AsyncClosure))) noreturn { + message.handle(closure.kqueue); + const fiber = closure.fiber; + std.log.debug("{*} performing async", .{fiber}); + closure.start(closure.contextPointer(), fiber.resultBytes(closure.result_align)); + const awaiter = @atomicRmw(?*Fiber, &fiber.awaiter, .Xchg, Fiber.finished, .acq_rel); + const ready_awaiter = r: { + const a = awaiter orelse break :r null; + if (@atomicRmw(bool, &closure.already_awaited, .Xchg, true, .acq_rel)) break :r null; + break :r a; + }; + closure.kqueue.yield(ready_awaiter, .nothing); + unreachable; // switched to dead fiber + } + + fn fromFiber(fiber: *Fiber) *AsyncClosure { + return @ptrFromInt(Fiber.max_context_align.max(.of(AsyncClosure)).backward( + @intFromPtr(fiber.allocatedEnd()) - Fiber.max_context_size, + ) - @sizeOf(AsyncClosure)); + } +}; + +pub fn io(k: *Kqueue) Io { + return .{ + .userdata = k, + .vtable = &.{ + .async = async, + .concurrent = concurrent, + .await = await, + .cancel = cancel, + .cancelRequested = cancelRequested, + .select = select, + + .groupAsync = groupAsync, + .groupWait = groupWait, + .groupCancel = groupCancel, + + .mutexLock = mutexLock, + .mutexLockUncancelable = mutexLockUncancelable, + .mutexUnlock = mutexUnlock, + + .conditionWait = conditionWait, + .conditionWaitUncancelable = conditionWaitUncancelable, + .conditionWake = conditionWake, + + .dirMake = dirMake, + .dirMakePath = dirMakePath, + .dirMakeOpenPath = dirMakeOpenPath, + .dirStat = dirStat, + .dirStatPath = dirStatPath, + + .fileStat = fileStat, + .dirAccess = dirAccess, + .dirCreateFile = dirCreateFile, + .dirOpenFile = dirOpenFile, + .dirOpenDir = dirOpenDir, + .dirClose = dirClose, + .fileClose = fileClose, + .fileWriteStreaming = fileWriteStreaming, + .fileWritePositional = fileWritePositional, + .fileReadStreaming = fileReadStreaming, + .fileReadPositional = fileReadPositional, + .fileSeekBy = fileSeekBy, + .fileSeekTo = fileSeekTo, + .openSelfExe = openSelfExe, + + .now = now, + .sleep = sleep, + + .netListenIp = netListenIp, + .netListenUnix = netListenUnix, + .netAccept = netAccept, + .netBindIp = netBindIp, + .netConnectIp = netConnectIp, + .netConnectUnix = netConnectUnix, + .netClose = netClose, + .netRead = netRead, + .netWrite = netWrite, + .netSend = netSend, + .netReceive = netReceive, + .netInterfaceNameResolve = netInterfaceNameResolve, + .netInterfaceName = netInterfaceName, + .netLookup = netLookup, + }, + }; +} + +fn async( + userdata: ?*anyopaque, + result: []u8, + result_alignment: std.mem.Alignment, + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, +) ?*Io.AnyFuture { + return concurrent(userdata, result.len, result_alignment, context, context_alignment, start) catch { + start(context.ptr, result.ptr); + return null; + }; +} + +fn concurrent( + userdata: ?*anyopaque, + result_len: usize, + result_alignment: Alignment, + context: []const u8, + context_alignment: Alignment, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, +) Io.ConcurrentError!*Io.AnyFuture { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + assert(result_alignment.compare(.lte, Fiber.max_result_align)); // TODO + assert(context_alignment.compare(.lte, Fiber.max_context_align)); // TODO + assert(result_len <= Fiber.max_result_size); // TODO + assert(context.len <= Fiber.max_context_size); // TODO + + const fiber = Fiber.allocate(k) catch return error.ConcurrencyUnavailable; + std.log.debug("allocated {*}", .{fiber}); + + const closure: *AsyncClosure = .fromFiber(fiber); + fiber.* = .{ + .required_align = {}, + .context = switch (builtin.cpu.arch) { + .x86_64 => .{ + .rsp = @intFromPtr(closure) - @sizeOf(usize), + .rbp = 0, + .rip = @intFromPtr(&fiberEntry), + }, + .aarch64 => .{ + .sp = @intFromPtr(closure), + .fp = 0, + .pc = @intFromPtr(&fiberEntry), + }, + else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), + }, + .awaiter = null, + .queue_next = null, + .cancel_thread = null, + .awaiting_completions = .initEmpty(), + }; + closure.* = .{ + .kqueue = k, + .fiber = fiber, + .start = start, + .result_align = result_alignment, + .already_awaited = false, + }; + @memcpy(closure.contextPointer(), context); + + k.schedule(.current(), .{ .head = fiber, .tail = fiber }); + return @ptrCast(fiber); +} + +fn await( + userdata: ?*anyopaque, + any_future: *Io.AnyFuture, + result: []u8, + result_alignment: std.mem.Alignment, +) void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + const future_fiber: *Fiber = @ptrCast(@alignCast(any_future)); + if (@atomicLoad(?*Fiber, &future_fiber.awaiter, .acquire) != Fiber.finished) + k.yield(null, .{ .register_awaiter = &future_fiber.awaiter }); + @memcpy(result, future_fiber.resultBytes(result_alignment)); + k.recycle(future_fiber); +} + +fn cancel( + userdata: ?*anyopaque, + any_future: *Io.AnyFuture, + result: []u8, + result_alignment: std.mem.Alignment, +) void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = any_future; + _ = result; + _ = result_alignment; + @panic("TODO"); +} + +fn cancelRequested(userdata: ?*anyopaque) bool { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + return false; // TODO +} + +fn groupAsync( + userdata: ?*anyopaque, + group: *Io.Group, + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (*Io.Group, context: *const anyopaque) void, +) void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = group; + _ = context; + _ = context_alignment; + _ = start; + @panic("TODO"); +} + +fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = group; + _ = token; + @panic("TODO"); +} + +fn groupCancel(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = group; + _ = token; + @panic("TODO"); +} + +fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) Io.Cancelable!usize { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = futures; + @panic("TODO"); +} + +fn mutexLock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) Io.Cancelable!void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = prev_state; + _ = mutex; + @panic("TODO"); +} +fn mutexLockUncancelable(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = prev_state; + _ = mutex; + @panic("TODO"); +} +fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = prev_state; + _ = mutex; + @panic("TODO"); +} + +fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) Io.Cancelable!void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + k.yield(null, .{ .condition_wait = .{ .cond = cond, .mutex = mutex } }); + const thread = Thread.current(); + const fiber = thread.currentFiber(); + const cond_impl = fiber.resultPointer(Condition); + try mutex.lock(k.io()); + switch (cond_impl.event) { + .queued => {}, + .wake => |wake| if (fiber.queue_next) |next_fiber| switch (wake) { + .one => if (@cmpxchgStrong( + ?*Fiber, + @as(*?*Fiber, @ptrCast(&cond.state)), + null, + next_fiber, + .release, + .acquire, + )) |old_fiber| { + const old_cond_impl = old_fiber.?.resultPointer(Condition); + assert(old_cond_impl.tail.queue_next == null); + old_cond_impl.tail.queue_next = next_fiber; + old_cond_impl.tail = cond_impl.tail; + }, + .all => k.schedule(thread, .{ .head = next_fiber, .tail = cond_impl.tail }), + }, + } + fiber.queue_next = null; +} + +fn conditionWaitUncancelable(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = cond; + _ = mutex; + @panic("TODO"); +} +fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition.Wake) void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + const waiting_fiber = @atomicRmw(?*Fiber, @as(*?*Fiber, @ptrCast(&cond.state)), .Xchg, null, .acquire) orelse return; + waiting_fiber.resultPointer(Condition).event = .{ .wake = wake }; + k.yield(waiting_fiber, .reschedule); +} + +fn dirMake(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, mode: Dir.Mode) Dir.MakeError!void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = dir; + _ = sub_path; + _ = mode; + @panic("TODO"); +} +fn dirMakePath(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, mode: Dir.Mode) Dir.MakeError!void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = dir; + _ = sub_path; + _ = mode; + @panic("TODO"); +} +fn dirMakeOpenPath(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, options: Dir.OpenOptions) Dir.MakeOpenPathError!Dir { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = dir; + _ = sub_path; + _ = options; + @panic("TODO"); +} +fn dirStat(userdata: ?*anyopaque, dir: Dir) Dir.StatError!Dir.Stat { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = dir; + @panic("TODO"); +} +fn dirStatPath(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, options: Dir.StatPathOptions) Dir.StatPathError!File.Stat { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = dir; + _ = sub_path; + _ = options; + @panic("TODO"); +} +fn dirAccess(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, options: Dir.AccessOptions) Dir.AccessError!void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = dir; + _ = sub_path; + _ = options; + @panic("TODO"); +} +fn dirCreateFile(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = dir; + _ = sub_path; + _ = flags; + @panic("TODO"); +} +fn dirOpenFile(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = dir; + _ = sub_path; + _ = flags; + @panic("TODO"); +} +fn dirOpenDir(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, options: Dir.OpenOptions) Dir.OpenError!Dir { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = dir; + _ = sub_path; + _ = options; + @panic("TODO"); +} +fn dirClose(userdata: ?*anyopaque, dir: Dir) void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = dir; + @panic("TODO"); +} +fn fileStat(userdata: ?*anyopaque, file: File) File.StatError!File.Stat { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = file; + @panic("TODO"); +} +fn fileClose(userdata: ?*anyopaque, file: File) void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = file; + @panic("TODO"); +} +fn fileWriteStreaming(userdata: ?*anyopaque, file: File, buffer: [][]const u8) File.WriteStreamingError!usize { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = file; + _ = buffer; + @panic("TODO"); +} +fn fileWritePositional(userdata: ?*anyopaque, file: File, buffer: [][]const u8, offset: u64) File.WritePositionalError!usize { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = file; + _ = buffer; + _ = offset; + @panic("TODO"); +} +fn fileReadStreaming(userdata: ?*anyopaque, file: File, data: [][]u8) File.Reader.Error!usize { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = file; + _ = data; + @panic("TODO"); +} +fn fileReadPositional(userdata: ?*anyopaque, file: File, data: [][]u8, offset: u64) File.ReadPositionalError!usize { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = file; + _ = data; + _ = offset; + @panic("TODO"); +} +fn fileSeekBy(userdata: ?*anyopaque, file: File, relative_offset: i64) File.SeekError!void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = file; + _ = relative_offset; + @panic("TODO"); +} +fn fileSeekTo(userdata: ?*anyopaque, file: File, absolute_offset: u64) File.SeekError!void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = file; + _ = absolute_offset; + @panic("TODO"); +} +fn openSelfExe(userdata: ?*anyopaque, file: File.OpenFlags) File.OpenSelfExeError!File { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = file; + @panic("TODO"); +} + +fn now(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = clock; + @panic("TODO"); +} +fn sleep(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = timeout; + @panic("TODO"); +} + +fn netListenIp( + userdata: ?*anyopaque, + address: net.IpAddress, + options: net.IpAddress.ListenOptions, +) net.IpAddress.ListenError!net.Server { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = address; + _ = options; + @panic("TODO"); +} +fn netAccept(userdata: ?*anyopaque, server: net.Socket.Handle) net.Server.AcceptError!net.Stream { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = server; + @panic("TODO"); +} +fn netBindIp( + userdata: ?*anyopaque, + address: *const net.IpAddress, + options: net.IpAddress.BindOptions, +) net.IpAddress.BindError!net.Socket { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + const family = Io.Threaded.posixAddressFamily(address); + const socket_fd = try openSocketPosix(k, family, options); + errdefer std.posix.close(socket_fd); + var storage: Io.Threaded.PosixAddress = undefined; + var addr_len = Io.Threaded.addressToPosix(address, &storage); + try posixBind(k, socket_fd, &storage.any, addr_len); + try posixGetSockName(k, socket_fd, &storage.any, &addr_len); + return .{ + .handle = socket_fd, + .address = Io.Threaded.addressFromPosix(&storage), + }; +} +fn netConnectIp(userdata: ?*anyopaque, address: *const net.IpAddress, options: net.IpAddress.ConnectOptions) net.IpAddress.ConnectError!net.Stream { + if (options.timeout != .none) @panic("TODO"); + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + const family = Io.Threaded.posixAddressFamily(address); + const socket_fd = try openSocketPosix(k, family, .{ + .mode = options.mode, + .protocol = options.protocol, + }); + errdefer posix.close(socket_fd); + var storage: Io.Threaded.PosixAddress = undefined; + var addr_len = Io.Threaded.addressToPosix(address, &storage); + try posixConnect(k, socket_fd, &storage.any, addr_len); + try posixGetSockName(k, socket_fd, &storage.any, &addr_len); + return .{ .socket = .{ + .handle = socket_fd, + .address = Io.Threaded.addressFromPosix(&storage), + } }; +} + +fn posixConnect(k: *Kqueue, socket_fd: posix.socket_t, addr: *const posix.sockaddr, addr_len: posix.socklen_t) !void { + while (true) { + try k.checkCancel(); + switch (posix.errno(posix.system.connect(socket_fd, addr, addr_len))) { + .SUCCESS => return, + .INTR => continue, + .CANCELED => return error.Canceled, + .AGAIN => @panic("TODO"), + .INPROGRESS => return, // Due to TCP fast open, we find out possible error later. + + .ADDRNOTAVAIL => return error.AddressUnavailable, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .ALREADY => return error.ConnectionPending, + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .CONNREFUSED => return error.ConnectionRefused, + .CONNRESET => return error.ConnectionResetByPeer, + .FAULT => |err| return errnoBug(err), + .ISCONN => |err| return errnoBug(err), + .HOSTUNREACH => return error.HostUnreachable, + .NETUNREACH => return error.NetworkUnreachable, + .NOTSOCK => |err| return errnoBug(err), + .PROTOTYPE => |err| return errnoBug(err), + .TIMEDOUT => return error.Timeout, + .CONNABORTED => |err| return errnoBug(err), + .ACCES => return error.AccessDenied, + .PERM => |err| return errnoBug(err), + .NOENT => |err| return errnoBug(err), + .NETDOWN => return error.NetworkDown, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn netListenUnix( + userdata: ?*anyopaque, + unix_address: *const net.UnixAddress, + options: net.UnixAddress.ListenOptions, +) net.UnixAddress.ListenError!net.Socket.Handle { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = unix_address; + _ = options; + @panic("TODO"); +} +fn netConnectUnix( + userdata: ?*anyopaque, + unix_address: *const net.UnixAddress, +) net.UnixAddress.ConnectError!net.Socket.Handle { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = unix_address; + @panic("TODO"); +} + +fn netSend( + userdata: ?*anyopaque, + handle: net.Socket.Handle, + outgoing_messages: []net.OutgoingMessage, + flags: net.SendFlags, +) struct { ?net.Socket.SendError, usize } { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + + const posix_flags: u32 = + @as(u32, if (@hasDecl(posix.MSG, "CONFIRM") and flags.confirm) posix.MSG.CONFIRM else 0) | + @as(u32, if (@hasDecl(posix.MSG, "DONTROUTE") and flags.dont_route) posix.MSG.DONTROUTE else 0) | + @as(u32, if (@hasDecl(posix.MSG, "EOR") and flags.eor) posix.MSG.EOR else 0) | + @as(u32, if (@hasDecl(posix.MSG, "OOB") and flags.oob) posix.MSG.OOB else 0) | + @as(u32, if (@hasDecl(posix.MSG, "FASTOPEN") and flags.fastopen) posix.MSG.FASTOPEN else 0) | + posix.MSG.NOSIGNAL; + + for (outgoing_messages, 0..) |*msg, i| { + netSendOne(k, handle, msg, posix_flags) catch |err| return .{ err, i }; + } + + return .{ null, outgoing_messages.len }; +} + +fn netSendOne( + k: *Kqueue, + handle: net.Socket.Handle, + message: *net.OutgoingMessage, + flags: u32, +) net.Socket.SendError!void { + var addr: Io.Threaded.PosixAddress = undefined; + var iovec: posix.iovec_const = .{ .base = @constCast(message.data_ptr), .len = message.data_len }; + const msg: posix.msghdr_const = .{ + .name = &addr.any, + .namelen = Io.Threaded.addressToPosix(message.address, &addr), + .iov = (&iovec)[0..1], + .iovlen = 1, + // OS returns EINVAL if this pointer is invalid even if controllen is zero. + .control = if (message.control.len == 0) null else @constCast(message.control.ptr), + .controllen = @intCast(message.control.len), + .flags = 0, + }; + while (true) { + try k.checkCancel(); + const rc = posix.system.sendmsg(handle, &msg, flags); + switch (posix.errno(rc)) { + .SUCCESS => { + message.data_len = @intCast(rc); + return; + }, + .INTR => continue, + .CANCELED => return error.Canceled, + .AGAIN => @panic("TODO register kevent"), + + .ACCES => return error.AccessDenied, + .ALREADY => return error.FastOpenAlreadyInProgress, + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .CONNRESET => return error.ConnectionResetByPeer, + .DESTADDRREQ => |err| return errnoBug(err), + .FAULT => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), + .ISCONN => |err| return errnoBug(err), + .MSGSIZE => return error.MessageOversize, + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + .NOTSOCK => |err| return errnoBug(err), + .OPNOTSUPP => |err| return errnoBug(err), + .PIPE => return error.SocketUnconnected, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .HOSTUNREACH => return error.HostUnreachable, + .NETUNREACH => return error.NetworkUnreachable, + .NOTCONN => return error.SocketUnconnected, + .NETDOWN => return error.NetworkDown, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn netReceive( + userdata: ?*anyopaque, + handle: net.Socket.Handle, + message_buffer: []net.IncomingMessage, + data_buffer: []u8, + flags: net.ReceiveFlags, + timeout: Io.Timeout, +) struct { ?net.Socket.ReceiveTimeoutError, usize } { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = handle; + _ = message_buffer; + _ = data_buffer; + _ = flags; + _ = timeout; + @panic("TODO"); +} + +fn netRead(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + + var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined; + var i: usize = 0; + for (data) |buf| { + if (iovecs_buffer.len - i == 0) break; + if (buf.len != 0) { + iovecs_buffer[i] = .{ .base = buf.ptr, .len = buf.len }; + i += 1; + } + } + const dest = iovecs_buffer[0..i]; + assert(dest[0].len > 0); + + while (true) { + try k.checkCancel(); + const rc = posix.system.readv(fd, dest.ptr, @intCast(dest.len)); + switch (posix.errno(rc)) { + .SUCCESS => return @intCast(rc), + .INTR => continue, + .CANCELED => return error.Canceled, + .AGAIN => { + const thread: *Thread = .current(); + const fiber = thread.currentFiber(); + const ident: u32 = @bitCast(fd); + const filter = std.c.EVFILT.READ; + const gop = thread.wait_queues.getOrPut(k.gpa, .{ + .ident = ident, + .filter = filter, + }) catch return error.SystemResources; + if (gop.found_existing) { + const tail_fiber = gop.value_ptr.*; + assert(tail_fiber.queue_next == null); + tail_fiber.queue_next = fiber; + gop.value_ptr.* = fiber; + } else { + gop.value_ptr.* = fiber; + const changes = [_]posix.Kevent{ + .{ + .ident = ident, + .filter = filter, + .flags = std.c.EV.ADD | std.c.EV.ONESHOT, + .fflags = 0, + .data = 0, + .udata = @intFromPtr(fiber), + }, + }; + assert(0 == (posix.kevent(thread.kq_fd, &changes, &.{}, null) catch |err| { + @panic(@errorName(err)); // TODO + })); + } + yield(k, null, .nothing); + continue; + }, + + .INVAL => |err| return errnoBug(err), + .FAULT => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + .NOTCONN => return error.SocketUnconnected, + .CONNRESET => return error.ConnectionResetByPeer, + .TIMEDOUT => return error.Timeout, + .PIPE => return error.SocketUnconnected, + .NETDOWN => return error.NetworkDown, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn netWrite(userdata: ?*anyopaque, dest: net.Socket.Handle, header: []const u8, data: []const []const u8, splat: usize) net.Stream.Writer.Error!usize { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = dest; + _ = header; + _ = data; + _ = splat; + @panic("TODO"); +} +fn netClose(userdata: ?*anyopaque, handle: net.Socket.Handle) void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = handle; + @panic("TODO"); +} +fn netInterfaceNameResolve( + userdata: ?*anyopaque, + name: *const net.Interface.Name, +) net.Interface.Name.ResolveError!net.Interface { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = name; + @panic("TODO"); +} +fn netInterfaceName(userdata: ?*anyopaque, interface: net.Interface) net.Interface.NameError!net.Interface.Name { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = interface; + @panic("TODO"); +} +fn netLookup( + userdata: ?*anyopaque, + host_name: net.HostName, + result: *Io.Queue(net.HostName.LookupResult), + options: net.HostName.LookupOptions, +) void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = host_name; + _ = result; + _ = options; + @panic("TODO"); +} + +fn openSocketPosix( + k: *Kqueue, + family: posix.sa_family_t, + options: IpAddress.BindOptions, +) error{ + AddressFamilyUnsupported, + ProtocolUnsupportedBySystem, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, + SystemResources, + ProtocolUnsupportedByAddressFamily, + SocketModeUnsupported, + OptionUnsupported, + Unexpected, + Canceled, +}!posix.socket_t { + const mode = Io.Threaded.posixSocketMode(options.mode); + const protocol = Io.Threaded.posixProtocol(options.protocol); + const socket_fd = while (true) { + try k.checkCancel(); + const flags: u32 = mode | if (Io.Threaded.socket_flags_unsupported) 0 else posix.SOCK.CLOEXEC; + const socket_rc = posix.system.socket(family, flags, protocol); + switch (posix.errno(socket_rc)) { + .SUCCESS => { + const fd: posix.fd_t = @intCast(socket_rc); + errdefer posix.close(fd); + if (Io.Threaded.socket_flags_unsupported) { + while (true) { + try k.checkCancel(); + switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFD, @as(usize, posix.FD_CLOEXEC)))) { + .SUCCESS => break, + .INTR => continue, + .CANCELED => return error.Canceled, + else => |err| return posix.unexpectedErrno(err), + } + } + + var fl_flags: usize = while (true) { + try k.checkCancel(); + const rc = posix.system.fcntl(fd, posix.F.GETFL, @as(usize, 0)); + switch (posix.errno(rc)) { + .SUCCESS => break @intCast(rc), + .INTR => continue, + .CANCELED => return error.Canceled, + else => |err| return posix.unexpectedErrno(err), + } + }; + fl_flags |= @as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK")); + while (true) { + try k.checkCancel(); + switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFL, fl_flags))) { + .SUCCESS => break, + .INTR => continue, + .CANCELED => return error.Canceled, + else => |err| return posix.unexpectedErrno(err), + } + } + } + break fd; + }, + .INTR => continue, + .CANCELED => return error.Canceled, + + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .INVAL => return error.ProtocolUnsupportedBySystem, + .MFILE => return error.ProcessFdQuotaExceeded, + .NFILE => return error.SystemFdQuotaExceeded, + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + .PROTONOSUPPORT => return error.ProtocolUnsupportedByAddressFamily, + .PROTOTYPE => return error.SocketModeUnsupported, + else => |err| return posix.unexpectedErrno(err), + } + }; + errdefer posix.close(socket_fd); + + if (options.ip6_only) { + if (posix.IPV6 == void) return error.OptionUnsupported; + try setSocketOption(k, socket_fd, posix.IPPROTO.IPV6, posix.IPV6.V6ONLY, 0); + } + + return socket_fd; +} + +fn posixBind( + k: *Kqueue, + socket_fd: posix.socket_t, + addr: *const posix.sockaddr, + addr_len: posix.socklen_t, +) !void { + while (true) { + try k.checkCancel(); + switch (posix.errno(posix.system.bind(socket_fd, addr, addr_len))) { + .SUCCESS => break, + .INTR => continue, + .CANCELED => return error.Canceled, + + .ADDRINUSE => return error.AddressInUse, + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .INVAL => |err| return errnoBug(err), // invalid parameters + .NOTSOCK => |err| return errnoBug(err), // invalid `sockfd` + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .ADDRNOTAVAIL => return error.AddressUnavailable, + .FAULT => |err| return errnoBug(err), // invalid `addr` pointer + .NOMEM => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn posixGetSockName(k: *Kqueue, socket_fd: posix.fd_t, addr: *posix.sockaddr, addr_len: *posix.socklen_t) !void { + while (true) { + try k.checkCancel(); + switch (posix.errno(posix.system.getsockname(socket_fd, addr, addr_len))) { + .SUCCESS => break, + .INTR => continue, + .CANCELED => return error.Canceled, + + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .FAULT => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), // invalid parameters + .NOTSOCK => |err| return errnoBug(err), // always a race condition + .NOBUFS => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn setSocketOption(k: *Kqueue, fd: posix.fd_t, level: i32, opt_name: u32, option: u32) !void { + const o: []const u8 = @ptrCast(&option); + while (true) { + try k.checkCancel(); + switch (posix.errno(posix.system.setsockopt(fd, level, opt_name, o.ptr, @intCast(o.len)))) { + .SUCCESS => return, + .INTR => continue, + .CANCELED => return error.Canceled, + + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .NOTSOCK => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), + .FAULT => |err| return errnoBug(err), + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn checkCancel(k: *Kqueue) error{Canceled}!void { + if (cancelRequested(k)) return error.Canceled; +} + +const Condition = struct { + tail: *Fiber, + event: union(enum) { + queued, + wake: Io.Condition.Wake, + }, +}; diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig new file mode 100644 index 0000000000..168395d335 --- /dev/null +++ b/lib/std/Io/Threaded.zig @@ -0,0 +1,6156 @@ +const Threaded = @This(); + +const builtin = @import("builtin"); +const native_os = builtin.os.tag; +const is_windows = native_os == .windows; +const windows = std.os.windows; +const ws2_32 = std.os.windows.ws2_32; +const is_debug = builtin.mode == .Debug; + +const std = @import("../std.zig"); +const Io = std.Io; +const net = std.Io.net; +const HostName = std.Io.net.HostName; +const IpAddress = std.Io.net.IpAddress; +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const posix = std.posix; + +/// Thread-safe. +allocator: Allocator, +mutex: std.Thread.Mutex = .{}, +cond: std.Thread.Condition = .{}, +run_queue: std.SinglyLinkedList = .{}, +join_requested: bool = false, +threads: std.ArrayListUnmanaged(std.Thread), +stack_size: usize, +cpu_count: std.Thread.CpuCountError!usize, +concurrent_count: usize, + +wsa: if (is_windows) Wsa else struct {} = .{}, + +have_signal_handler: bool, +old_sig_io: if (have_sig_io) posix.Sigaction else void, +old_sig_pipe: if (have_sig_pipe) posix.Sigaction else void, + +threadlocal var current_closure: ?*Closure = null; + +const max_iovecs_len = 8; +const splat_buffer_size = 64; + +comptime { + if (@TypeOf(posix.IOV_MAX) != void) assert(max_iovecs_len <= posix.IOV_MAX); +} + +const CancelId = enum(usize) { + none = 0, + canceling = std.math.maxInt(usize), + _, + + const ThreadId = if (std.Thread.use_pthreads) std.c.pthread_t else std.Thread.Id; + + fn currentThread() CancelId { + if (std.Thread.use_pthreads) { + return @enumFromInt(@intFromPtr(std.c.pthread_self())); + } else { + return @enumFromInt(std.Thread.getCurrentId()); + } + } + + fn toThreadId(cancel_id: CancelId) ThreadId { + if (std.Thread.use_pthreads) { + return @ptrFromInt(@intFromEnum(cancel_id)); + } else { + return @intCast(@intFromEnum(cancel_id)); + } + } +}; + +const Closure = struct { + start: Start, + node: std.SinglyLinkedList.Node = .{}, + cancel_tid: CancelId, + /// Whether this task bumps minimum number of threads in the pool. + is_concurrent: bool, + + const Start = *const fn (*Closure) void; + + fn requestCancel(closure: *Closure) void { + switch (@atomicRmw(CancelId, &closure.cancel_tid, .Xchg, .canceling, .acq_rel)) { + .none, .canceling => {}, + else => |tid| { + if (std.Thread.use_pthreads) { + const rc = std.c.pthread_kill(tid.toThreadId(), .IO); + if (is_debug) assert(rc == 0); + } else if (native_os == .linux) { + _ = std.os.linux.tgkill(std.os.linux.getpid(), @bitCast(tid.toThreadId()), .IO); + } + }, + } + } +}; + +pub const InitError = std.Thread.CpuCountError || Allocator.Error; + +/// Related: +/// * `init_single_threaded` +pub fn init( + /// Must be threadsafe. Only used for the following functions: + /// * `Io.VTable.async` + /// * `Io.VTable.concurrent` + /// * `Io.VTable.groupAsync` + /// If these functions are avoided, then `Allocator.failing` may be passed + /// here. + gpa: Allocator, +) Threaded { + var t: Threaded = .{ + .allocator = gpa, + .threads = .empty, + .stack_size = std.Thread.SpawnConfig.default_stack_size, + .cpu_count = std.Thread.getCpuCount(), + .concurrent_count = 0, + .old_sig_io = undefined, + .old_sig_pipe = undefined, + .have_signal_handler = false, + }; + + if (t.cpu_count) |n| { + t.threads.ensureTotalCapacityPrecise(gpa, n - 1) catch {}; + } else |_| {} + + if (posix.Sigaction != void) { + // This causes sending `posix.SIG.IO` to thread to interrupt blocking + // syscalls, returning `posix.E.INTR`. + const act: posix.Sigaction = .{ + .handler = .{ .handler = doNothingSignalHandler }, + .mask = posix.sigemptyset(), + .flags = 0, + }; + if (have_sig_io) posix.sigaction(.IO, &act, &t.old_sig_io); + if (have_sig_pipe) posix.sigaction(.PIPE, &act, &t.old_sig_pipe); + t.have_signal_handler = true; + } + + return t; +} + +/// Statically initialize such that calls to `Io.VTable.concurrent` will fail +/// with `error.ConcurrencyUnavailable`. +/// +/// When initialized this way: +/// * cancel requests have no effect. +/// * `deinit` is safe, but unnecessary to call. +pub const init_single_threaded: Threaded = .{ + .allocator = .failing, + .threads = .empty, + .stack_size = std.Thread.SpawnConfig.default_stack_size, + .cpu_count = 1, + .concurrent_count = 0, + .old_sig_io = undefined, + .old_sig_pipe = undefined, + .have_signal_handler = false, +}; + +pub fn deinit(t: *Threaded) void { + const gpa = t.allocator; + t.join(); + t.threads.deinit(gpa); + if (is_windows and t.wsa.status == .initialized) { + if (ws2_32.WSACleanup() != 0) recoverableOsBugDetected(); + } + if (posix.Sigaction != void and t.have_signal_handler) { + if (have_sig_io) posix.sigaction(.IO, &t.old_sig_io, null); + if (have_sig_pipe) posix.sigaction(.PIPE, &t.old_sig_pipe, null); + } + t.* = undefined; +} + +fn join(t: *Threaded) void { + if (builtin.single_threaded) return; + { + t.mutex.lock(); + defer t.mutex.unlock(); + t.join_requested = true; + } + t.cond.broadcast(); + for (t.threads.items) |thread| thread.join(); +} + +fn worker(t: *Threaded) void { + t.mutex.lock(); + defer t.mutex.unlock(); + + while (true) { + while (t.run_queue.popFirst()) |closure_node| { + t.mutex.unlock(); + const closure: *Closure = @fieldParentPtr("node", closure_node); + const is_concurrent = closure.is_concurrent; + closure.start(closure); + t.mutex.lock(); + if (is_concurrent) { + t.concurrent_count -= 1; + } + } + if (t.join_requested) break; + t.cond.wait(&t.mutex); + } +} + +pub fn io(t: *Threaded) Io { + return .{ + .userdata = t, + .vtable = &.{ + .async = async, + .concurrent = concurrent, + .await = await, + .cancel = cancel, + .cancelRequested = cancelRequested, + .select = select, + + .groupAsync = groupAsync, + .groupWait = groupWait, + .groupCancel = groupCancel, + + .mutexLock = mutexLock, + .mutexLockUncancelable = mutexLockUncancelable, + .mutexUnlock = mutexUnlock, + + .conditionWait = conditionWait, + .conditionWaitUncancelable = conditionWaitUncancelable, + .conditionWake = conditionWake, + + .dirMake = dirMake, + .dirMakePath = dirMakePath, + .dirMakeOpenPath = dirMakeOpenPath, + .dirStat = dirStat, + .dirStatPath = dirStatPath, + .fileStat = fileStat, + .dirAccess = dirAccess, + .dirCreateFile = dirCreateFile, + .dirOpenFile = dirOpenFile, + .dirOpenDir = dirOpenDir, + .dirClose = dirClose, + .fileClose = fileClose, + .fileWriteStreaming = fileWriteStreaming, + .fileWritePositional = fileWritePositional, + .fileReadStreaming = fileReadStreaming, + .fileReadPositional = fileReadPositional, + .fileSeekBy = fileSeekBy, + .fileSeekTo = fileSeekTo, + .openSelfExe = openSelfExe, + + .now = now, + .sleep = sleep, + + .netListenIp = switch (native_os) { + .windows => netListenIpWindows, + else => netListenIpPosix, + }, + .netListenUnix = switch (native_os) { + .windows => netListenUnixWindows, + else => netListenUnixPosix, + }, + .netAccept = switch (native_os) { + .windows => netAcceptWindows, + else => netAcceptPosix, + }, + .netBindIp = switch (native_os) { + .windows => netBindIpWindows, + else => netBindIpPosix, + }, + .netConnectIp = switch (native_os) { + .windows => netConnectIpWindows, + else => netConnectIpPosix, + }, + .netConnectUnix = switch (native_os) { + .windows => netConnectUnixWindows, + else => netConnectUnixPosix, + }, + .netClose = netClose, + .netRead = switch (native_os) { + .windows => netReadWindows, + else => netReadPosix, + }, + .netWrite = switch (native_os) { + .windows => netWriteWindows, + else => netWritePosix, + }, + .netSend = switch (native_os) { + .windows => netSendWindows, + else => netSendPosix, + }, + .netReceive = switch (native_os) { + .windows => netReceiveWindows, + else => netReceivePosix, + }, + .netInterfaceNameResolve = netInterfaceNameResolve, + .netInterfaceName = netInterfaceName, + .netLookup = netLookup, + }, + }; +} + +/// Same as `io` but disables all networking functionality, which has +/// an additional dependency on Windows (ws2_32). +pub fn ioBasic(t: *Threaded) Io { + return .{ + .userdata = t, + .vtable = &.{ + .async = async, + .concurrent = concurrent, + .await = await, + .cancel = cancel, + .cancelRequested = cancelRequested, + .select = select, + + .groupAsync = groupAsync, + .groupWait = groupWait, + .groupCancel = groupCancel, + + .mutexLock = mutexLock, + .mutexLockUncancelable = mutexLockUncancelable, + .mutexUnlock = mutexUnlock, + + .conditionWait = conditionWait, + .conditionWaitUncancelable = conditionWaitUncancelable, + .conditionWake = conditionWake, + + .dirMake = dirMake, + .dirMakePath = dirMakePath, + .dirMakeOpenPath = dirMakeOpenPath, + .dirStat = dirStat, + .dirStatPath = dirStatPath, + .fileStat = fileStat, + .dirAccess = dirAccess, + .dirCreateFile = dirCreateFile, + .dirOpenFile = dirOpenFile, + .dirOpenDir = dirOpenDir, + .dirClose = dirClose, + .fileClose = fileClose, + .fileWriteStreaming = fileWriteStreaming, + .fileWritePositional = fileWritePositional, + .fileReadStreaming = fileReadStreaming, + .fileReadPositional = fileReadPositional, + .fileSeekBy = fileSeekBy, + .fileSeekTo = fileSeekTo, + .openSelfExe = openSelfExe, + + .now = now, + .sleep = sleep, + + .netListenIp = netListenIpUnavailable, + .netListenUnix = netListenUnixUnavailable, + .netAccept = netAcceptUnavailable, + .netBindIp = netBindIpUnavailable, + .netConnectIp = netConnectIpUnavailable, + .netConnectUnix = netConnectUnixUnavailable, + .netClose = netCloseUnavailable, + .netRead = netReadUnavailable, + .netWrite = netWriteUnavailable, + .netSend = netSendUnavailable, + .netReceive = netReceiveUnavailable, + .netInterfaceNameResolve = netInterfaceNameResolveUnavailable, + .netInterfaceName = netInterfaceNameUnavailable, + .netLookup = netLookupUnavailable, + }, + }; +} + +pub const socket_flags_unsupported = native_os.isDarwin() or native_os == .haiku; // 💩💩 +const have_accept4 = !socket_flags_unsupported; +const have_flock_open_flags = @hasField(posix.O, "EXLOCK"); +const have_networking = native_os != .wasi; +const have_flock = @TypeOf(posix.system.flock) != void; +const have_sendmmsg = native_os == .linux; +const have_futex = switch (builtin.cpu.arch) { + .wasm32, .wasm64 => builtin.cpu.has(.wasm, .atomics), + else => true, +}; +const have_preadv = switch (native_os) { + .windows, .haiku, .serenity => false, // 💩💩💩 + else => true, +}; +const have_sig_io = posix.SIG != void and @hasField(posix.SIG, "IO"); +const have_sig_pipe = posix.SIG != void and @hasField(posix.SIG, "PIPE"); + +const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat; +const fstat_sym = if (posix.lfs64_abi) posix.system.fstat64 else posix.system.fstat; +const fstatat_sym = if (posix.lfs64_abi) posix.system.fstatat64 else posix.system.fstatat; +const lseek_sym = if (posix.lfs64_abi) posix.system.lseek64 else posix.system.lseek; +const preadv_sym = if (posix.lfs64_abi) posix.system.preadv64 else posix.system.preadv; + +/// Trailing data: +/// 1. context +/// 2. result +const AsyncClosure = struct { + closure: Closure, + func: *const fn (context: *anyopaque, result: *anyopaque) void, + reset_event: ResetEvent, + select_condition: ?*ResetEvent, + context_alignment: std.mem.Alignment, + result_offset: usize, + + const done_reset_event: *ResetEvent = @ptrFromInt(@alignOf(ResetEvent)); + + fn start(closure: *Closure) void { + const ac: *AsyncClosure = @alignCast(@fieldParentPtr("closure", closure)); + const tid: CancelId = .currentThread(); + if (@cmpxchgStrong(CancelId, &closure.cancel_tid, .none, tid, .acq_rel, .acquire)) |cancel_tid| { + assert(cancel_tid == .canceling); + // Even though we already know the task is canceled, we must still + // run the closure in order to make the return value valid and in + // case there are side effects. + } + current_closure = closure; + ac.func(ac.contextPointer(), ac.resultPointer()); + current_closure = null; + + // In case a cancel happens after successful task completion, prevents + // signal from being delivered to the thread in `requestCancel`. + if (@cmpxchgStrong(CancelId, &closure.cancel_tid, tid, .none, .acq_rel, .acquire)) |cancel_tid| { + assert(cancel_tid == .canceling); + } + + if (@atomicRmw(?*ResetEvent, &ac.select_condition, .Xchg, done_reset_event, .release)) |select_reset| { + assert(select_reset != done_reset_event); + select_reset.set(); + } + ac.reset_event.set(); + } + + fn resultPointer(ac: *AsyncClosure) [*]u8 { + const base: [*]u8 = @ptrCast(ac); + return base + ac.result_offset; + } + + fn contextPointer(ac: *AsyncClosure) [*]u8 { + const base: [*]u8 = @ptrCast(ac); + return base + ac.context_alignment.forward(@sizeOf(AsyncClosure)); + } + + fn waitAndFree(ac: *AsyncClosure, gpa: Allocator, result: []u8) void { + ac.reset_event.waitUncancelable(); + @memcpy(result, ac.resultPointer()[0..result.len]); + free(ac, gpa, result.len); + } + + fn free(ac: *AsyncClosure, gpa: Allocator, result_len: usize) void { + const base: [*]align(@alignOf(AsyncClosure)) u8 = @ptrCast(ac); + gpa.free(base[0 .. ac.result_offset + result_len]); + } +}; + +fn async( + userdata: ?*anyopaque, + result: []u8, + result_alignment: std.mem.Alignment, + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, +) ?*Io.AnyFuture { + if (builtin.single_threaded) { + start(context.ptr, result.ptr); + return null; + } + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const cpu_count = t.cpu_count catch { + return concurrent(userdata, result.len, result_alignment, context, context_alignment, start) catch { + start(context.ptr, result.ptr); + return null; + }; + }; + const gpa = t.allocator; + const context_offset = context_alignment.forward(@sizeOf(AsyncClosure)); + const result_offset = result_alignment.forward(context_offset + context.len); + const n = result_offset + result.len; + const ac: *AsyncClosure = @ptrCast(@alignCast(gpa.alignedAlloc(u8, .of(AsyncClosure), n) catch { + start(context.ptr, result.ptr); + return null; + })); + + ac.* = .{ + .closure = .{ + .cancel_tid = .none, + .start = AsyncClosure.start, + .is_concurrent = false, + }, + .func = start, + .context_alignment = context_alignment, + .result_offset = result_offset, + .reset_event = .unset, + .select_condition = null, + }; + + @memcpy(ac.contextPointer()[0..context.len], context); + + t.mutex.lock(); + + const thread_capacity = cpu_count - 1 + t.concurrent_count; + + t.threads.ensureTotalCapacityPrecise(gpa, thread_capacity) catch { + t.mutex.unlock(); + ac.free(gpa, result.len); + start(context.ptr, result.ptr); + return null; + }; + + t.run_queue.prepend(&ac.closure.node); + + if (t.threads.items.len < thread_capacity) { + const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch { + if (t.threads.items.len == 0) { + assert(t.run_queue.popFirst() == &ac.closure.node); + t.mutex.unlock(); + ac.free(gpa, result.len); + start(context.ptr, result.ptr); + return null; + } + // Rely on other workers to do it. + t.mutex.unlock(); + t.cond.signal(); + return @ptrCast(ac); + }; + t.threads.appendAssumeCapacity(thread); + } + + t.mutex.unlock(); + t.cond.signal(); + return @ptrCast(ac); +} + +fn concurrent( + userdata: ?*anyopaque, + result_len: usize, + result_alignment: std.mem.Alignment, + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, +) Io.ConcurrentError!*Io.AnyFuture { + if (builtin.single_threaded) return error.ConcurrencyUnavailable; + + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const cpu_count = t.cpu_count catch 1; + const gpa = t.allocator; + const context_offset = context_alignment.forward(@sizeOf(AsyncClosure)); + const result_offset = result_alignment.forward(context_offset + context.len); + const n = result_offset + result_len; + const ac_bytes = gpa.alignedAlloc(u8, .of(AsyncClosure), n) catch + return error.ConcurrencyUnavailable; + const ac: *AsyncClosure = @ptrCast(@alignCast(ac_bytes)); + + ac.* = .{ + .closure = .{ + .cancel_tid = .none, + .start = AsyncClosure.start, + .is_concurrent = true, + }, + .func = start, + .context_alignment = context_alignment, + .result_offset = result_offset, + .reset_event = .unset, + .select_condition = null, + }; + @memcpy(ac.contextPointer()[0..context.len], context); + + t.mutex.lock(); + + t.concurrent_count += 1; + const thread_capacity = cpu_count - 1 + t.concurrent_count; + + t.threads.ensureTotalCapacity(gpa, thread_capacity) catch { + t.mutex.unlock(); + ac.free(gpa, result_len); + return error.ConcurrencyUnavailable; + }; + + t.run_queue.prepend(&ac.closure.node); + + if (t.threads.items.len < thread_capacity) { + const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch { + assert(t.run_queue.popFirst() == &ac.closure.node); + t.mutex.unlock(); + ac.free(gpa, result_len); + return error.ConcurrencyUnavailable; + }; + t.threads.appendAssumeCapacity(thread); + } + + t.mutex.unlock(); + t.cond.signal(); + return @ptrCast(ac); +} + +const GroupClosure = struct { + closure: Closure, + t: *Threaded, + group: *Io.Group, + /// Points to sibling `GroupClosure`. Used for walking the group to cancel all. + node: std.SinglyLinkedList.Node, + func: *const fn (*Io.Group, context: *anyopaque) void, + context_alignment: std.mem.Alignment, + context_len: usize, + + fn start(closure: *Closure) void { + const gc: *GroupClosure = @alignCast(@fieldParentPtr("closure", closure)); + const tid: CancelId = .currentThread(); + const group = gc.group; + const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state); + const reset_event: *ResetEvent = @ptrCast(&group.context); + if (@cmpxchgStrong(CancelId, &closure.cancel_tid, .none, tid, .acq_rel, .acquire)) |cancel_tid| { + assert(cancel_tid == .canceling); + // Even though we already know the task is canceled, we must still + // run the closure in case there are side effects. + } + current_closure = closure; + gc.func(group, gc.contextPointer()); + current_closure = null; + + // In case a cancel happens after successful task completion, prevents + // signal from being delivered to the thread in `requestCancel`. + if (@cmpxchgStrong(CancelId, &closure.cancel_tid, tid, .none, .acq_rel, .acquire)) |cancel_tid| { + assert(cancel_tid == .canceling); + } + + const prev_state = group_state.fetchSub(sync_one_pending, .acq_rel); + assert((prev_state / sync_one_pending) > 0); + if (prev_state == (sync_one_pending | sync_is_waiting)) reset_event.set(); + } + + fn free(gc: *GroupClosure, gpa: Allocator) void { + const base: [*]align(@alignOf(GroupClosure)) u8 = @ptrCast(gc); + gpa.free(base[0..contextEnd(gc.context_alignment, gc.context_len)]); + } + + fn contextOffset(context_alignment: std.mem.Alignment) usize { + return context_alignment.forward(@sizeOf(GroupClosure)); + } + + fn contextEnd(context_alignment: std.mem.Alignment, context_len: usize) usize { + return contextOffset(context_alignment) + context_len; + } + + fn contextPointer(gc: *GroupClosure) [*]u8 { + const base: [*]u8 = @ptrCast(gc); + return base + contextOffset(gc.context_alignment); + } + + const sync_is_waiting: usize = 1 << 0; + const sync_one_pending: usize = 1 << 1; +}; + +fn groupAsync( + userdata: ?*anyopaque, + group: *Io.Group, + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (*Io.Group, context: *const anyopaque) void, +) void { + if (builtin.single_threaded) return start(group, context.ptr); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const cpu_count = t.cpu_count catch 1; + const gpa = t.allocator; + const n = GroupClosure.contextEnd(context_alignment, context.len); + const gc: *GroupClosure = @ptrCast(@alignCast(gpa.alignedAlloc(u8, .of(GroupClosure), n) catch { + return start(group, context.ptr); + })); + gc.* = .{ + .closure = .{ + .cancel_tid = .none, + .start = GroupClosure.start, + .is_concurrent = false, + }, + .t = t, + .group = group, + .node = undefined, + .func = start, + .context_alignment = context_alignment, + .context_len = context.len, + }; + @memcpy(gc.contextPointer()[0..context.len], context); + + t.mutex.lock(); + + // Append to the group linked list inside the mutex to make `Io.Group.async` thread-safe. + gc.node = .{ .next = @ptrCast(@alignCast(group.token)) }; + group.token = &gc.node; + + const thread_capacity = cpu_count - 1 + t.concurrent_count; + + t.threads.ensureTotalCapacityPrecise(gpa, thread_capacity) catch { + t.mutex.unlock(); + gc.free(gpa); + return start(group, context.ptr); + }; + + t.run_queue.prepend(&gc.closure.node); + + if (t.threads.items.len < thread_capacity) { + const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch { + assert(t.run_queue.popFirst() == &gc.closure.node); + t.mutex.unlock(); + gc.free(gpa); + return start(group, context.ptr); + }; + t.threads.appendAssumeCapacity(thread); + } + + // This needs to be done before unlocking the mutex to avoid a race with + // the associated task finishing. + const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state); + const prev_state = group_state.fetchAdd(GroupClosure.sync_one_pending, .monotonic); + assert((prev_state / GroupClosure.sync_one_pending) < (std.math.maxInt(usize) / GroupClosure.sync_one_pending)); + + t.mutex.unlock(); + t.cond.signal(); +} + +fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const gpa = t.allocator; + + if (builtin.single_threaded) return; + + const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state); + const reset_event: *ResetEvent = @ptrCast(&group.context); + const prev_state = group_state.fetchAdd(GroupClosure.sync_is_waiting, .acquire); + assert(prev_state & GroupClosure.sync_is_waiting == 0); + if ((prev_state / GroupClosure.sync_one_pending) > 0) reset_event.wait(t) catch |err| switch (err) { + error.Canceled => { + var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token)); + while (true) { + const gc: *GroupClosure = @fieldParentPtr("node", node); + gc.closure.requestCancel(); + node = node.next orelse break; + } + reset_event.waitUncancelable(); + }, + }; + + var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token)); + while (true) { + const gc: *GroupClosure = @fieldParentPtr("node", node); + const node_next = node.next; + gc.free(gpa); + node = node_next orelse break; + } +} + +fn groupCancel(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const gpa = t.allocator; + + if (builtin.single_threaded) return; + + { + var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token)); + while (true) { + const gc: *GroupClosure = @fieldParentPtr("node", node); + gc.closure.requestCancel(); + node = node.next orelse break; + } + } + + const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state); + const reset_event: *ResetEvent = @ptrCast(&group.context); + const prev_state = group_state.fetchAdd(GroupClosure.sync_is_waiting, .acquire); + assert(prev_state & GroupClosure.sync_is_waiting == 0); + if ((prev_state / GroupClosure.sync_one_pending) > 0) reset_event.waitUncancelable(); + + { + var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token)); + while (true) { + const gc: *GroupClosure = @fieldParentPtr("node", node); + const node_next = node.next; + gc.free(gpa); + node = node_next orelse break; + } + } +} + +fn await( + userdata: ?*anyopaque, + any_future: *Io.AnyFuture, + result: []u8, + result_alignment: std.mem.Alignment, +) void { + _ = result_alignment; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const closure: *AsyncClosure = @ptrCast(@alignCast(any_future)); + closure.waitAndFree(t.allocator, result); +} + +fn cancel( + userdata: ?*anyopaque, + any_future: *Io.AnyFuture, + result: []u8, + result_alignment: std.mem.Alignment, +) void { + _ = result_alignment; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const ac: *AsyncClosure = @ptrCast(@alignCast(any_future)); + ac.closure.requestCancel(); + ac.waitAndFree(t.allocator, result); +} + +fn cancelRequested(userdata: ?*anyopaque) bool { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + const closure = current_closure orelse return false; + return @atomicLoad(CancelId, &closure.cancel_tid, .acquire) == .canceling; +} + +fn checkCancel(t: *Threaded) error{Canceled}!void { + if (cancelRequested(t)) return error.Canceled; +} + +fn mutexLock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) Io.Cancelable!void { + if (builtin.single_threaded) unreachable; // Interface should have prevented this. + if (native_os == .netbsd) @panic("TODO"); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + if (prev_state == .contended) { + try futexWait(t, @ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); + } + while (@atomicRmw(Io.Mutex.State, &mutex.state, .Xchg, .contended, .acquire) != .unlocked) { + try futexWait(t, @ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); + } +} + +fn mutexLockUncancelable(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void { + if (builtin.single_threaded) unreachable; // Interface should have prevented this. + if (native_os == .netbsd) @panic("TODO"); + _ = userdata; + if (prev_state == .contended) { + futexWaitUncancelable(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); + } + while (@atomicRmw(Io.Mutex.State, &mutex.state, .Xchg, .contended, .acquire) != .unlocked) { + futexWaitUncancelable(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); + } +} + +fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void { + if (builtin.single_threaded) unreachable; // Interface should have prevented this. + if (native_os == .netbsd) @panic("TODO"); + _ = userdata; + _ = prev_state; + if (@atomicRmw(Io.Mutex.State, &mutex.state, .Xchg, .unlocked, .release) == .contended) { + futexWake(@ptrCast(&mutex.state), 1); + } +} + +fn conditionWaitUncancelable(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) void { + if (builtin.single_threaded) unreachable; // Deadlock. + if (native_os == .netbsd) @panic("TODO"); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const t_io = ioBasic(t); + comptime assert(@TypeOf(cond.state) == u64); + const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state); + const cond_state = &ints[0]; + const cond_epoch = &ints[1]; + const one_waiter = 1; + const waiter_mask = 0xffff; + const one_signal = 1 << 16; + const signal_mask = 0xffff << 16; + var epoch = cond_epoch.load(.acquire); + var state = cond_state.fetchAdd(one_waiter, .monotonic); + assert(state & waiter_mask != waiter_mask); + state += one_waiter; + + mutex.unlock(t_io); + defer mutex.lockUncancelable(t_io); + + while (true) { + futexWaitUncancelable(cond_epoch, epoch); + epoch = cond_epoch.load(.acquire); + state = cond_state.load(.monotonic); + while (state & signal_mask != 0) { + const new_state = state - one_waiter - one_signal; + state = cond_state.cmpxchgWeak(state, new_state, .acquire, .monotonic) orelse return; + } + } +} + +fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) Io.Cancelable!void { + if (builtin.single_threaded) unreachable; // Deadlock. + if (native_os == .netbsd) @panic("TODO"); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const t_io = ioBasic(t); + comptime assert(@TypeOf(cond.state) == u64); + const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state); + const cond_state = &ints[0]; + const cond_epoch = &ints[1]; + const one_waiter = 1; + const waiter_mask = 0xffff; + const one_signal = 1 << 16; + const signal_mask = 0xffff << 16; + // Observe the epoch, then check the state again to see if we should wake up. + // The epoch must be observed before we check the state or we could potentially miss a wake() and deadlock: + // + // - T1: s = LOAD(&state) + // - T2: UPDATE(&s, signal) + // - T2: UPDATE(&epoch, 1) + FUTEX_WAKE(&epoch) + // - T1: e = LOAD(&epoch) (was reordered after the state load) + // - T1: s & signals == 0 -> FUTEX_WAIT(&epoch, e) (missed the state update + the epoch change) + // + // Acquire barrier to ensure the epoch load happens before the state load. + var epoch = cond_epoch.load(.acquire); + var state = cond_state.fetchAdd(one_waiter, .monotonic); + assert(state & waiter_mask != waiter_mask); + state += one_waiter; + + mutex.unlock(t_io); + defer mutex.lockUncancelable(t_io); + + while (true) { + try futexWait(t, cond_epoch, epoch); + + epoch = cond_epoch.load(.acquire); + state = cond_state.load(.monotonic); + + // Try to wake up by consuming a signal and decremented the waiter we + // added previously. Acquire barrier ensures code before the wake() + // which added the signal happens before we decrement it and return. + while (state & signal_mask != 0) { + const new_state = state - one_waiter - one_signal; + state = cond_state.cmpxchgWeak(state, new_state, .acquire, .monotonic) orelse return; + } + } +} + +fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition.Wake) void { + if (builtin.single_threaded) unreachable; // Nothing to wake up. + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + comptime assert(@TypeOf(cond.state) == u64); + const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state); + const cond_state = &ints[0]; + const cond_epoch = &ints[1]; + const one_waiter = 1; + const waiter_mask = 0xffff; + const one_signal = 1 << 16; + const signal_mask = 0xffff << 16; + var state = cond_state.load(.monotonic); + while (true) { + const waiters = (state & waiter_mask) / one_waiter; + const signals = (state & signal_mask) / one_signal; + + // Reserves which waiters to wake up by incrementing the signals count. + // Therefore, the signals count is always less than or equal to the + // waiters count. We don't need to Futex.wake if there's nothing to + // wake up or if other wake() threads have reserved to wake up the + // current waiters. + const wakeable = waiters - signals; + if (wakeable == 0) { + return; + } + + const to_wake = switch (wake) { + .one => 1, + .all => wakeable, + }; + + // Reserve the amount of waiters to wake by incrementing the signals + // count. Release barrier ensures code before the wake() happens before + // the signal it posted and consumed by the wait() threads. + const new_state = state + (one_signal * to_wake); + state = cond_state.cmpxchgWeak(state, new_state, .release, .monotonic) orelse { + // Wake up the waiting threads we reserved above by changing the epoch value. + // + // A waiting thread could miss a wake up if *exactly* ((1<<32)-1) + // wake()s happen between it observing the epoch and sleeping on + // it. This is very unlikely due to how many precise amount of + // Futex.wake() calls that would be between the waiting thread's + // potential preemption. + // + // Release barrier ensures the signal being added to the state + // happens before the epoch is changed. If not, the waiting thread + // could potentially deadlock from missing both the state and epoch + // change: + // + // - T2: UPDATE(&epoch, 1) (reordered before the state change) + // - T1: e = LOAD(&epoch) + // - T1: s = LOAD(&state) + // - T2: UPDATE(&state, signal) + FUTEX_WAKE(&epoch) + // - T1: s & signals == 0 -> FUTEX_WAIT(&epoch, e) (missed both epoch change and state change) + _ = cond_epoch.fetchAdd(1, .release); + if (native_os == .netbsd) @panic("TODO"); + futexWake(cond_epoch, to_wake); + return; + }; + } +} + +const dirMake = switch (native_os) { + .windows => dirMakeWindows, + .wasi => dirMakeWasi, + else => dirMakePosix, +}; + +fn dirMakePosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + var path_buffer: [posix.PATH_MAX]u8 = undefined; + const sub_path_posix = try pathToPosix(sub_path, &path_buffer); + + while (true) { + try t.checkCancel(); + switch (posix.errno(posix.system.mkdirat(dir.handle, sub_path_posix, mode))) { + .SUCCESS => return, + .INTR => continue, + .CANCELED => return error.Canceled, + + .ACCES => return error.AccessDenied, + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .PERM => return error.PermissionDenied, + .DQUOT => return error.DiskQuota, + .EXIST => return error.PathAlreadyExists, + .FAULT => |err| return errnoBug(err), + .LOOP => return error.SymLinkLoop, + .MLINK => return error.LinkQuotaExceeded, + .NAMETOOLONG => return error.NameTooLong, + .NOENT => return error.FileNotFound, + .NOMEM => return error.SystemResources, + .NOSPC => return error.NoSpaceLeft, + .NOTDIR => return error.NotDir, + .ROFS => return error.ReadOnlyFileSystem, + // dragonfly: when dir_fd is unlinked from filesystem + .NOTCONN => return error.FileNotFound, + .ILSEQ => return error.BadPathName, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn dirMakeWasi(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void { + if (builtin.link_libc) return dirMakePosix(userdata, dir, sub_path, mode); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + while (true) { + try t.checkCancel(); + switch (std.os.wasi.path_create_directory(dir.handle, sub_path.ptr, sub_path.len)) { + .SUCCESS => return, + .INTR => continue, + .CANCELED => return error.Canceled, + + .ACCES => return error.AccessDenied, + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .PERM => return error.PermissionDenied, + .DQUOT => return error.DiskQuota, + .EXIST => return error.PathAlreadyExists, + .FAULT => |err| return errnoBug(err), + .LOOP => return error.SymLinkLoop, + .MLINK => return error.LinkQuotaExceeded, + .NAMETOOLONG => return error.NameTooLong, + .NOENT => return error.FileNotFound, + .NOMEM => return error.SystemResources, + .NOSPC => return error.NoSpaceLeft, + .NOTDIR => return error.NotDir, + .ROFS => return error.ReadOnlyFileSystem, + .NOTCAPABLE => return error.AccessDenied, + .ILSEQ => return error.BadPathName, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn dirMakeWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + try t.checkCancel(); + + const sub_path_w = try windows.sliceToPrefixedFileW(dir.handle, sub_path); + _ = mode; + const sub_dir_handle = windows.OpenFile(sub_path_w.span(), .{ + .dir = dir.handle, + .access_mask = windows.GENERIC_READ | windows.SYNCHRONIZE, + .creation = windows.FILE_CREATE, + .filter = .dir_only, + }) catch |err| switch (err) { + error.IsDir => return error.Unexpected, + error.PipeBusy => return error.Unexpected, + error.NoDevice => return error.Unexpected, + error.WouldBlock => return error.Unexpected, + error.AntivirusInterference => return error.Unexpected, + else => |e| return e, + }; + windows.CloseHandle(sub_dir_handle); +} + +const dirMakePath = switch (native_os) { + .windows => dirMakePathWindows, + else => dirMakePathPosix, +}; + +fn dirMakePathPosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + _ = dir; + _ = sub_path; + _ = mode; + @panic("TODO implement dirMakePathPosix"); +} + +fn dirMakePathWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + _ = dir; + _ = sub_path; + _ = mode; + @panic("TODO implement dirMakePathWindows"); +} + +const dirMakeOpenPath = switch (native_os) { + .windows => dirMakeOpenPathWindows, + .wasi => dirMakeOpenPathWasi, + else => dirMakeOpenPathPosix, +}; + +fn dirMakeOpenPathPosix( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.OpenOptions, +) Io.Dir.MakeOpenPathError!Io.Dir { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const t_io = ioBasic(t); + return dirOpenDirPosix(t, dir, sub_path, options) catch |err| switch (err) { + error.FileNotFound => { + try dir.makePath(t_io, sub_path); + return dirOpenDirPosix(t, dir, sub_path, options); + }, + else => |e| return e, + }; +} + +fn dirMakeOpenPathWindows( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.OpenOptions, +) Io.Dir.MakeOpenPathError!Io.Dir { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const w = windows; + const access_mask = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | + w.SYNCHRONIZE | w.FILE_TRAVERSE | + (if (options.iterate) w.FILE_LIST_DIRECTORY else @as(u32, 0)); + + var it = try std.fs.path.componentIterator(sub_path); + // If there are no components in the path, then create a dummy component with the full path. + var component: std.fs.path.NativeComponentIterator.Component = it.last() orelse .{ + .name = "", + .path = sub_path, + }; + + while (true) { + try t.checkCancel(); + + const sub_path_w_array = try w.sliceToPrefixedFileW(dir.handle, component.path); + const sub_path_w = sub_path_w_array.span(); + const is_last = it.peekNext() == null; + const create_disposition: u32 = if (is_last) w.FILE_OPEN_IF else w.FILE_CREATE; + + var result: Io.Dir = .{ .handle = undefined }; + + const path_len_bytes: u16 = @intCast(sub_path_w.len * 2); + var nt_name: w.UNICODE_STRING = .{ + .Length = path_len_bytes, + .MaximumLength = path_len_bytes, + .Buffer = @constCast(sub_path_w.ptr), + }; + var attr: w.OBJECT_ATTRIBUTES = .{ + .Length = @sizeOf(w.OBJECT_ATTRIBUTES), + .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, + .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. + .ObjectName = &nt_name, + .SecurityDescriptor = null, + .SecurityQualityOfService = null, + }; + const open_reparse_point: w.DWORD = if (!options.follow_symlinks) w.FILE_OPEN_REPARSE_POINT else 0x0; + var io_status_block: w.IO_STATUS_BLOCK = undefined; + const rc = w.ntdll.NtCreateFile( + &result.handle, + access_mask, + &attr, + &io_status_block, + null, + w.FILE_ATTRIBUTE_NORMAL, + w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE, + create_disposition, + w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point, + null, + 0, + ); + + switch (rc) { + .SUCCESS => { + component = it.next() orelse return result; + w.CloseHandle(result.handle); + continue; + }, + .OBJECT_NAME_INVALID => return error.BadPathName, + .OBJECT_NAME_COLLISION => { + assert(!is_last); + // stat the file and return an error if it's not a directory + // this is important because otherwise a dangling symlink + // could cause an infinite loop + check_dir: { + // workaround for windows, see https://github.com/ziglang/zig/issues/16738 + const fstat = dirStatPathWindows(t, dir, component.path, .{ + .follow_symlinks = options.follow_symlinks, + }) catch |stat_err| switch (stat_err) { + error.IsDir => break :check_dir, + else => |e| return e, + }; + if (fstat.kind != .directory) return error.NotDir; + } + + component = it.next().?; + continue; + }, + + .OBJECT_NAME_NOT_FOUND, + .OBJECT_PATH_NOT_FOUND, + => { + component = it.previous() orelse return error.FileNotFound; + continue; + }, + + .NOT_A_DIRECTORY => return error.NotDir, + // This can happen if the directory has 'List folder contents' permission set to 'Deny' + // and the directory is trying to be opened for iteration. + .ACCESS_DENIED => return error.AccessDenied, + .INVALID_PARAMETER => |err| return w.statusBug(err), + else => return w.unexpectedStatus(rc), + } + } +} + +fn dirMakeOpenPathWasi( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.OpenOptions, +) Io.Dir.MakeOpenPathError!Io.Dir { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const t_io = ioBasic(t); + return dirOpenDirWasi(t, dir, sub_path, options) catch |err| switch (err) { + error.FileNotFound => { + try dir.makePath(t_io, sub_path); + return dirOpenDirWasi(t, dir, sub_path, options); + }, + else => |e| return e, + }; +} + +fn dirStat(userdata: ?*anyopaque, dir: Io.Dir) Io.Dir.StatError!Io.Dir.Stat { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + try t.checkCancel(); + + _ = dir; + @panic("TODO implement dirStat"); +} + +const dirStatPath = switch (native_os) { + .linux => dirStatPathLinux, + .windows => dirStatPathWindows, + .wasi => dirStatPathWasi, + else => dirStatPathPosix, +}; + +fn dirStatPathLinux( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.StatPathOptions, +) Io.Dir.StatPathError!Io.File.Stat { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const linux = std.os.linux; + + var path_buffer: [posix.PATH_MAX]u8 = undefined; + const sub_path_posix = try pathToPosix(sub_path, &path_buffer); + + const flags: u32 = linux.AT.NO_AUTOMOUNT | + @as(u32, if (!options.follow_symlinks) linux.AT.SYMLINK_NOFOLLOW else 0); + + while (true) { + try t.checkCancel(); + var statx = std.mem.zeroes(linux.Statx); + const rc = linux.statx( + dir.handle, + sub_path_posix, + flags, + linux.STATX_TYPE | linux.STATX_MODE | linux.STATX_ATIME | linux.STATX_MTIME | linux.STATX_CTIME, + &statx, + ); + switch (linux.E.init(rc)) { + .SUCCESS => return statFromLinux(&statx), + .INTR => continue, + .CANCELED => return error.Canceled, + + .ACCES => return error.AccessDenied, + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .FAULT => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), + .LOOP => return error.SymLinkLoop, + .NAMETOOLONG => |err| return errnoBug(err), // Handled by pathToPosix() above. + .NOENT => return error.FileNotFound, + .NOTDIR => return error.NotDir, + .NOMEM => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn dirStatPathPosix( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.StatPathOptions, +) Io.Dir.StatPathError!Io.File.Stat { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + var path_buffer: [posix.PATH_MAX]u8 = undefined; + const sub_path_posix = try pathToPosix(sub_path, &path_buffer); + + const flags: u32 = if (!options.follow_symlinks) posix.AT.SYMLINK_NOFOLLOW else 0; + + while (true) { + try t.checkCancel(); + var stat = std.mem.zeroes(posix.Stat); + switch (posix.errno(fstatat_sym(dir.handle, sub_path_posix, &stat, flags))) { + .SUCCESS => return statFromPosix(&stat), + .INTR => continue, + .CANCELED => return error.Canceled, + + .INVAL => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .NOMEM => return error.SystemResources, + .ACCES => return error.AccessDenied, + .PERM => return error.PermissionDenied, + .FAULT => |err| return errnoBug(err), + .NAMETOOLONG => return error.NameTooLong, + .LOOP => return error.SymLinkLoop, + .NOENT => return error.FileNotFound, + .NOTDIR => return error.FileNotFound, + .ILSEQ => return error.BadPathName, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn dirStatPathWindows( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.StatPathOptions, +) Io.Dir.StatPathError!Io.File.Stat { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const file = try dirOpenFileWindows(t, dir, sub_path, .{ + .follow_symlinks = options.follow_symlinks, + }); + defer windows.CloseHandle(file.handle); + return fileStatWindows(t, file); +} + +fn dirStatPathWasi( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.StatPathOptions, +) Io.Dir.StatPathError!Io.File.Stat { + if (builtin.link_libc) return dirStatPathPosix(userdata, dir, sub_path, options); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const wasi = std.os.wasi; + const flags: wasi.lookupflags_t = .{ + .SYMLINK_FOLLOW = options.follow_symlinks, + }; + var stat: wasi.filestat_t = undefined; + while (true) { + try t.checkCancel(); + switch (wasi.path_filestat_get(dir.handle, flags, sub_path.ptr, sub_path.len, &stat)) { + .SUCCESS => return statFromWasi(&stat), + .INTR => continue, + .CANCELED => return error.Canceled, + + .INVAL => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .NOMEM => return error.SystemResources, + .ACCES => return error.AccessDenied, + .FAULT => |err| return errnoBug(err), + .NAMETOOLONG => return error.NameTooLong, + .NOENT => return error.FileNotFound, + .NOTDIR => return error.FileNotFound, + .NOTCAPABLE => return error.AccessDenied, + .ILSEQ => return error.BadPathName, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +const fileStat = switch (native_os) { + .linux => fileStatLinux, + .windows => fileStatWindows, + .wasi => fileStatWasi, + else => fileStatPosix, +}; + +fn fileStatPosix(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + if (posix.Stat == void) return error.Streaming; + + while (true) { + try t.checkCancel(); + var stat = std.mem.zeroes(posix.Stat); + switch (posix.errno(fstat_sym(file.handle, &stat))) { + .SUCCESS => return statFromPosix(&stat), + .INTR => continue, + .CANCELED => return error.Canceled, + + .INVAL => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .NOMEM => return error.SystemResources, + .ACCES => return error.AccessDenied, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn fileStatLinux(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const linux = std.os.linux; + while (true) { + try t.checkCancel(); + var statx = std.mem.zeroes(linux.Statx); + const rc = linux.statx( + file.handle, + "", + linux.AT.EMPTY_PATH, + linux.STATX_TYPE | linux.STATX_MODE | linux.STATX_ATIME | linux.STATX_MTIME | linux.STATX_CTIME, + &statx, + ); + switch (linux.E.init(rc)) { + .SUCCESS => return statFromLinux(&statx), + .INTR => continue, + .CANCELED => return error.Canceled, + + .ACCES => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .FAULT => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), + .LOOP => |err| return errnoBug(err), + .NAMETOOLONG => |err| return errnoBug(err), + .NOENT => |err| return errnoBug(err), + .NOMEM => return error.SystemResources, + .NOTDIR => |err| return errnoBug(err), + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn fileStatWindows(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + try t.checkCancel(); + + var io_status_block: windows.IO_STATUS_BLOCK = undefined; + var info: windows.FILE_ALL_INFORMATION = undefined; + const rc = windows.ntdll.NtQueryInformationFile(file.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation); + switch (rc) { + .SUCCESS => {}, + // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer + // size provided. This is treated as success because the type of variable-length information that this would be relevant for + // (name, volume name, etc) we don't care about. + .BUFFER_OVERFLOW => {}, + .INVALID_PARAMETER => unreachable, + .ACCESS_DENIED => return error.AccessDenied, + else => return windows.unexpectedStatus(rc), + } + return .{ + .inode = info.InternalInformation.IndexNumber, + .size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)), + .mode = 0, + .kind = if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) reparse_point: { + var tag_info: windows.FILE_ATTRIBUTE_TAG_INFO = undefined; + const tag_rc = windows.ntdll.NtQueryInformationFile(file.handle, &io_status_block, &tag_info, @sizeOf(windows.FILE_ATTRIBUTE_TAG_INFO), .FileAttributeTagInformation); + switch (tag_rc) { + .SUCCESS => {}, + // INFO_LENGTH_MISMATCH and ACCESS_DENIED are the only documented possible errors + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/d295752f-ce89-4b98-8553-266d37c84f0e + .INFO_LENGTH_MISMATCH => unreachable, + .ACCESS_DENIED => return error.AccessDenied, + else => return windows.unexpectedStatus(rc), + } + if (tag_info.ReparseTag & windows.reparse_tag_name_surrogate_bit != 0) { + break :reparse_point .sym_link; + } + // Unknown reparse point + break :reparse_point .unknown; + } else if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) + .directory + else + .file, + .atime = windows.fromSysTime(info.BasicInformation.LastAccessTime), + .mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime), + .ctime = windows.fromSysTime(info.BasicInformation.ChangeTime), + }; +} + +fn fileStatWasi(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat { + if (builtin.link_libc) return fileStatPosix(userdata, file); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + while (true) { + try t.checkCancel(); + var stat: std.os.wasi.filestat_t = undefined; + switch (std.os.wasi.fd_filestat_get(file.handle, &stat)) { + .SUCCESS => return statFromWasi(&stat), + .INTR => continue, + .CANCELED => return error.Canceled, + + .INVAL => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .NOMEM => return error.SystemResources, + .ACCES => return error.AccessDenied, + .NOTCAPABLE => return error.AccessDenied, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +const dirAccess = switch (native_os) { + .windows => dirAccessWindows, + .wasi => dirAccessWasi, + else => dirAccessPosix, +}; + +fn dirAccessPosix( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.AccessOptions, +) Io.Dir.AccessError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + var path_buffer: [posix.PATH_MAX]u8 = undefined; + const sub_path_posix = try pathToPosix(sub_path, &path_buffer); + + const flags: u32 = @as(u32, if (!options.follow_symlinks) posix.AT.SYMLINK_NOFOLLOW else 0); + + const mode: u32 = + @as(u32, if (options.read) posix.R_OK else 0) | + @as(u32, if (options.write) posix.W_OK else 0) | + @as(u32, if (options.execute) posix.X_OK else 0); + + while (true) { + try t.checkCancel(); + switch (posix.errno(posix.system.faccessat(dir.handle, sub_path_posix, mode, flags))) { + .SUCCESS => return, + .INTR => continue, + .CANCELED => return error.Canceled, + + .ACCES => return error.AccessDenied, + .PERM => return error.PermissionDenied, + .ROFS => return error.ReadOnlyFileSystem, + .LOOP => return error.SymLinkLoop, + .TXTBSY => return error.FileBusy, + .NOTDIR => return error.FileNotFound, + .NOENT => return error.FileNotFound, + .NAMETOOLONG => return error.NameTooLong, + .INVAL => |err| return errnoBug(err), + .FAULT => |err| return errnoBug(err), + .IO => return error.InputOutput, + .NOMEM => return error.SystemResources, + .ILSEQ => return error.BadPathName, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn dirAccessWasi( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.AccessOptions, +) Io.Dir.AccessError!void { + if (builtin.link_libc) return dirAccessPosix(userdata, dir, sub_path, options); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const wasi = std.os.wasi; + const flags: wasi.lookupflags_t = .{ + .SYMLINK_FOLLOW = options.follow_symlinks, + }; + var stat: wasi.filestat_t = undefined; + while (true) { + try t.checkCancel(); + switch (wasi.path_filestat_get(dir.handle, flags, sub_path.ptr, sub_path.len, &stat)) { + .SUCCESS => break, + .INTR => continue, + .CANCELED => return error.Canceled, + + .INVAL => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .NOMEM => return error.SystemResources, + .ACCES => return error.AccessDenied, + .FAULT => |err| return errnoBug(err), + .NAMETOOLONG => return error.NameTooLong, + .NOENT => return error.FileNotFound, + .NOTDIR => return error.FileNotFound, + .NOTCAPABLE => return error.AccessDenied, + .ILSEQ => return error.BadPathName, + else => |err| return posix.unexpectedErrno(err), + } + } + + if (!options.read and !options.write and !options.execute) + return; + + var directory: wasi.fdstat_t = undefined; + if (wasi.fd_fdstat_get(dir.handle, &directory) != .SUCCESS) + return error.AccessDenied; + + var rights: wasi.rights_t = .{}; + if (options.read) { + if (stat.filetype == .DIRECTORY) { + rights.FD_READDIR = true; + } else { + rights.FD_READ = true; + } + } + if (options.write) + rights.FD_WRITE = true; + + // No validation for execution. + + // https://github.com/ziglang/zig/issues/18882 + const rights_int: u64 = @bitCast(rights); + const inheriting_int: u64 = @bitCast(directory.fs_rights_inheriting); + if ((rights_int & inheriting_int) != rights_int) + return error.AccessDenied; +} + +fn dirAccessWindows( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.AccessOptions, +) Io.Dir.AccessError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + try t.checkCancel(); + + _ = options; // TODO + + const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path); + const sub_path_w = sub_path_w_array.span(); + + if (sub_path_w[0] == '.' and sub_path_w[1] == 0) return; + if (sub_path_w[0] == '.' and sub_path_w[1] == '.' and sub_path_w[2] == 0) return; + + const path_len_bytes = std.math.cast(u16, std.mem.sliceTo(sub_path_w, 0).len * 2) orelse + return error.NameTooLong; + var nt_name: windows.UNICODE_STRING = .{ + .Length = path_len_bytes, + .MaximumLength = path_len_bytes, + .Buffer = @constCast(sub_path_w.ptr), + }; + var attr = windows.OBJECT_ATTRIBUTES{ + .Length = @sizeOf(windows.OBJECT_ATTRIBUTES), + .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, + .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. + .ObjectName = &nt_name, + .SecurityDescriptor = null, + .SecurityQualityOfService = null, + }; + var basic_info: windows.FILE_BASIC_INFORMATION = undefined; + switch (windows.ntdll.NtQueryAttributesFile(&attr, &basic_info)) { + .SUCCESS => return, + .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, + .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, + .OBJECT_NAME_INVALID => |err| return windows.statusBug(err), + .INVALID_PARAMETER => |err| return windows.statusBug(err), + .ACCESS_DENIED => return error.AccessDenied, + .OBJECT_PATH_SYNTAX_BAD => |err| return windows.statusBug(err), + else => |rc| return windows.unexpectedStatus(rc), + } +} + +const dirCreateFile = switch (native_os) { + .windows => dirCreateFileWindows, + .wasi => dirCreateFileWasi, + else => dirCreateFilePosix, +}; + +fn dirCreateFilePosix( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + flags: Io.File.CreateFlags, +) Io.File.OpenError!Io.File { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + var path_buffer: [posix.PATH_MAX]u8 = undefined; + const sub_path_posix = try pathToPosix(sub_path, &path_buffer); + + var os_flags: posix.O = .{ + .ACCMODE = if (flags.read) .RDWR else .WRONLY, + .CREAT = true, + .TRUNC = flags.truncate, + .EXCL = flags.exclusive, + }; + if (@hasField(posix.O, "LARGEFILE")) os_flags.LARGEFILE = true; + if (@hasField(posix.O, "CLOEXEC")) os_flags.CLOEXEC = true; + + // Use the O locking flags if the os supports them to acquire the lock + // atomically. Note that the NONBLOCK flag is removed after the openat() + // call is successful. + if (have_flock_open_flags) switch (flags.lock) { + .none => {}, + .shared => { + os_flags.SHLOCK = true; + os_flags.NONBLOCK = flags.lock_nonblocking; + }, + .exclusive => { + os_flags.EXLOCK = true; + os_flags.NONBLOCK = flags.lock_nonblocking; + }, + }; + + const fd: posix.fd_t = while (true) { + try t.checkCancel(); + const rc = openat_sym(dir.handle, sub_path_posix, os_flags, flags.mode); + switch (posix.errno(rc)) { + .SUCCESS => break @intCast(rc), + .INTR => continue, + .CANCELED => return error.Canceled, + + .FAULT => |err| return errnoBug(err), + .INVAL => return error.BadPathName, + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .ACCES => return error.AccessDenied, + .FBIG => return error.FileTooBig, + .OVERFLOW => return error.FileTooBig, + .ISDIR => return error.IsDir, + .LOOP => return error.SymLinkLoop, + .MFILE => return error.ProcessFdQuotaExceeded, + .NAMETOOLONG => return error.NameTooLong, + .NFILE => return error.SystemFdQuotaExceeded, + .NODEV => return error.NoDevice, + .NOENT => return error.FileNotFound, + .SRCH => return error.ProcessNotFound, + .NOMEM => return error.SystemResources, + .NOSPC => return error.NoSpaceLeft, + .NOTDIR => return error.NotDir, + .PERM => return error.PermissionDenied, + .EXIST => return error.PathAlreadyExists, + .BUSY => return error.DeviceBusy, + .OPNOTSUPP => return error.FileLocksNotSupported, + .AGAIN => return error.WouldBlock, + .TXTBSY => return error.FileBusy, + .NXIO => return error.NoDevice, + .ILSEQ => return error.BadPathName, + else => |err| return posix.unexpectedErrno(err), + } + }; + errdefer posix.close(fd); + + if (have_flock and !have_flock_open_flags and flags.lock != .none) { + const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0; + const lock_flags = switch (flags.lock) { + .none => unreachable, + .shared => posix.LOCK.SH | lock_nonblocking, + .exclusive => posix.LOCK.EX | lock_nonblocking, + }; + while (true) { + try t.checkCancel(); + switch (posix.errno(posix.system.flock(fd, lock_flags))) { + .SUCCESS => break, + .INTR => continue, + .CANCELED => return error.Canceled, + + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .INVAL => |err| return errnoBug(err), // invalid parameters + .NOLCK => return error.SystemResources, + .AGAIN => return error.WouldBlock, + .OPNOTSUPP => return error.FileLocksNotSupported, + else => |err| return posix.unexpectedErrno(err), + } + } + } + + if (have_flock_open_flags and flags.lock_nonblocking) { + var fl_flags: usize = while (true) { + try t.checkCancel(); + const rc = posix.system.fcntl(fd, posix.F.GETFL, @as(usize, 0)); + switch (posix.errno(rc)) { + .SUCCESS => break @intCast(rc), + .INTR => continue, + .CANCELED => return error.Canceled, + else => |err| return posix.unexpectedErrno(err), + } + }; + fl_flags |= @as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK")); + while (true) { + try t.checkCancel(); + switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFL, fl_flags))) { + .SUCCESS => break, + .INTR => continue, + .CANCELED => return error.Canceled, + else => |err| return posix.unexpectedErrno(err), + } + } + } + + return .{ .handle = fd }; +} + +fn dirCreateFileWindows( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + flags: Io.File.CreateFlags, +) Io.File.OpenError!Io.File { + const w = windows; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + try t.checkCancel(); + + const sub_path_w_array = try w.sliceToPrefixedFileW(dir.handle, sub_path); + const sub_path_w = sub_path_w_array.span(); + + const read_flag = if (flags.read) @as(u32, w.GENERIC_READ) else 0; + const handle = try w.OpenFile(sub_path_w, .{ + .dir = dir.handle, + .access_mask = w.SYNCHRONIZE | w.GENERIC_WRITE | read_flag, + .creation = if (flags.exclusive) + @as(u32, w.FILE_CREATE) + else if (flags.truncate) + @as(u32, w.FILE_OVERWRITE_IF) + else + @as(u32, w.FILE_OPEN_IF), + }); + errdefer w.CloseHandle(handle); + var io_status_block: w.IO_STATUS_BLOCK = undefined; + const range_off: w.LARGE_INTEGER = 0; + const range_len: w.LARGE_INTEGER = 1; + const exclusive = switch (flags.lock) { + .none => return .{ .handle = handle }, + .shared => false, + .exclusive => true, + }; + try w.LockFile( + handle, + null, + null, + null, + &io_status_block, + &range_off, + &range_len, + null, + @intFromBool(flags.lock_nonblocking), + @intFromBool(exclusive), + ); + return .{ .handle = handle }; +} + +fn dirCreateFileWasi( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + flags: Io.File.CreateFlags, +) Io.File.OpenError!Io.File { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const wasi = std.os.wasi; + const lookup_flags: wasi.lookupflags_t = .{}; + const oflags: wasi.oflags_t = .{ + .CREAT = true, + .TRUNC = flags.truncate, + .EXCL = flags.exclusive, + }; + const fdflags: wasi.fdflags_t = .{}; + const base: wasi.rights_t = .{ + .FD_READ = flags.read, + .FD_WRITE = true, + .FD_DATASYNC = true, + .FD_SEEK = true, + .FD_TELL = true, + .FD_FDSTAT_SET_FLAGS = true, + .FD_SYNC = true, + .FD_ALLOCATE = true, + .FD_ADVISE = true, + .FD_FILESTAT_SET_TIMES = true, + .FD_FILESTAT_SET_SIZE = true, + .FD_FILESTAT_GET = true, + // POLL_FD_READWRITE only grants extra rights if the corresponding FD_READ and/or + // FD_WRITE is also set. + .POLL_FD_READWRITE = true, + }; + const inheriting: wasi.rights_t = .{}; + var fd: posix.fd_t = undefined; + while (true) { + try t.checkCancel(); + switch (wasi.path_open(dir.handle, lookup_flags, sub_path.ptr, sub_path.len, oflags, base, inheriting, fdflags, &fd)) { + .SUCCESS => return .{ .handle = fd }, + .INTR => continue, + .CANCELED => return error.Canceled, + + .FAULT => |err| return errnoBug(err), + .INVAL => return error.BadPathName, + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .ACCES => return error.AccessDenied, + .FBIG => return error.FileTooBig, + .OVERFLOW => return error.FileTooBig, + .ISDIR => return error.IsDir, + .LOOP => return error.SymLinkLoop, + .MFILE => return error.ProcessFdQuotaExceeded, + .NAMETOOLONG => return error.NameTooLong, + .NFILE => return error.SystemFdQuotaExceeded, + .NODEV => return error.NoDevice, + .NOENT => return error.FileNotFound, + .NOMEM => return error.SystemResources, + .NOSPC => return error.NoSpaceLeft, + .NOTDIR => return error.NotDir, + .PERM => return error.PermissionDenied, + .EXIST => return error.PathAlreadyExists, + .BUSY => return error.DeviceBusy, + .NOTCAPABLE => return error.AccessDenied, + .ILSEQ => return error.BadPathName, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +const dirOpenFile = switch (native_os) { + .windows => dirOpenFileWindows, + .wasi => dirOpenFileWasi, + else => dirOpenFilePosix, +}; + +fn dirOpenFilePosix( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + flags: Io.File.OpenFlags, +) Io.File.OpenError!Io.File { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + var path_buffer: [posix.PATH_MAX]u8 = undefined; + const sub_path_posix = try pathToPosix(sub_path, &path_buffer); + + var os_flags: posix.O = switch (native_os) { + .wasi => .{ + .read = flags.mode != .write_only, + .write = flags.mode != .read_only, + }, + else => .{ + .ACCMODE = switch (flags.mode) { + .read_only => .RDONLY, + .write_only => .WRONLY, + .read_write => .RDWR, + }, + }, + }; + if (@hasField(posix.O, "CLOEXEC")) os_flags.CLOEXEC = true; + if (@hasField(posix.O, "LARGEFILE")) os_flags.LARGEFILE = true; + if (@hasField(posix.O, "NOCTTY")) os_flags.NOCTTY = !flags.allow_ctty; + + // Use the O locking flags if the os supports them to acquire the lock + // atomically. Note that the NONBLOCK flag is removed after the openat() + // call is successful. + if (have_flock_open_flags) switch (flags.lock) { + .none => {}, + .shared => { + os_flags.SHLOCK = true; + os_flags.NONBLOCK = flags.lock_nonblocking; + }, + .exclusive => { + os_flags.EXLOCK = true; + os_flags.NONBLOCK = flags.lock_nonblocking; + }, + }; + + const fd: posix.fd_t = while (true) { + try t.checkCancel(); + const rc = openat_sym(dir.handle, sub_path_posix, os_flags, @as(posix.mode_t, 0)); + switch (posix.errno(rc)) { + .SUCCESS => break @intCast(rc), + .INTR => continue, + .CANCELED => return error.Canceled, + + .FAULT => |err| return errnoBug(err), + .INVAL => return error.BadPathName, + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .ACCES => return error.AccessDenied, + .FBIG => return error.FileTooBig, + .OVERFLOW => return error.FileTooBig, + .ISDIR => return error.IsDir, + .LOOP => return error.SymLinkLoop, + .MFILE => return error.ProcessFdQuotaExceeded, + .NAMETOOLONG => return error.NameTooLong, + .NFILE => return error.SystemFdQuotaExceeded, + .NODEV => return error.NoDevice, + .NOENT => return error.FileNotFound, + .SRCH => return error.ProcessNotFound, + .NOMEM => return error.SystemResources, + .NOSPC => return error.NoSpaceLeft, + .NOTDIR => return error.NotDir, + .PERM => return error.PermissionDenied, + .EXIST => return error.PathAlreadyExists, + .BUSY => return error.DeviceBusy, + .OPNOTSUPP => return error.FileLocksNotSupported, + .AGAIN => return error.WouldBlock, + .TXTBSY => return error.FileBusy, + .NXIO => return error.NoDevice, + .ILSEQ => return error.BadPathName, + else => |err| return posix.unexpectedErrno(err), + } + }; + errdefer posix.close(fd); + + if (have_flock and !have_flock_open_flags and flags.lock != .none) { + const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0; + const lock_flags = switch (flags.lock) { + .none => unreachable, + .shared => posix.LOCK.SH | lock_nonblocking, + .exclusive => posix.LOCK.EX | lock_nonblocking, + }; + while (true) { + try t.checkCancel(); + switch (posix.errno(posix.system.flock(fd, lock_flags))) { + .SUCCESS => break, + .INTR => continue, + .CANCELED => return error.Canceled, + + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .INVAL => |err| return errnoBug(err), // invalid parameters + .NOLCK => return error.SystemResources, + .AGAIN => return error.WouldBlock, + .OPNOTSUPP => return error.FileLocksNotSupported, + else => |err| return posix.unexpectedErrno(err), + } + } + } + + if (have_flock_open_flags and flags.lock_nonblocking) { + var fl_flags: usize = while (true) { + try t.checkCancel(); + const rc = posix.system.fcntl(fd, posix.F.GETFL, @as(usize, 0)); + switch (posix.errno(rc)) { + .SUCCESS => break @intCast(rc), + .INTR => continue, + .CANCELED => return error.Canceled, + else => |err| return posix.unexpectedErrno(err), + } + }; + fl_flags |= @as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK")); + while (true) { + try t.checkCancel(); + switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFL, fl_flags))) { + .SUCCESS => break, + .INTR => continue, + .CANCELED => return error.Canceled, + else => |err| return posix.unexpectedErrno(err), + } + } + } + + return .{ .handle = fd }; +} + +fn dirOpenFileWindows( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + flags: Io.File.OpenFlags, +) Io.File.OpenError!Io.File { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path); + const sub_path_w = sub_path_w_array.span(); + const dir_handle = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle; + return dirOpenFileWtf16(t, dir_handle, sub_path_w, flags); +} + +pub fn dirOpenFileWtf16( + t: *Threaded, + dir_handle: ?windows.HANDLE, + sub_path_w: [:0]const u16, + flags: Io.File.OpenFlags, +) Io.File.OpenError!Io.File { + if (std.mem.eql(u16, sub_path_w, &.{'.'})) return error.IsDir; + if (std.mem.eql(u16, sub_path_w, &.{ '.', '.' })) return error.IsDir; + const path_len_bytes = std.math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong; + + const w = windows; + + var nt_name: w.UNICODE_STRING = .{ + .Length = path_len_bytes, + .MaximumLength = path_len_bytes, + .Buffer = @constCast(sub_path_w.ptr), + }; + var attr: w.OBJECT_ATTRIBUTES = .{ + .Length = @sizeOf(w.OBJECT_ATTRIBUTES), + .RootDirectory = dir_handle, + .Attributes = 0, + .ObjectName = &nt_name, + .SecurityDescriptor = null, + .SecurityQualityOfService = null, + }; + var io_status_block: w.IO_STATUS_BLOCK = undefined; + const blocking_flag: w.ULONG = w.FILE_SYNCHRONOUS_IO_NONALERT; + const file_or_dir_flag: w.ULONG = w.FILE_NON_DIRECTORY_FILE; + // If we're not following symlinks, we need to ensure we don't pass in any + // synchronization flags such as FILE_SYNCHRONOUS_IO_NONALERT. + const create_file_flags: w.ULONG = file_or_dir_flag | + if (flags.follow_symlinks) blocking_flag else w.FILE_OPEN_REPARSE_POINT; + + // There are multiple kernel bugs being worked around with retries. + const max_attempts = 13; + var attempt: u5 = 0; + + const handle = while (true) { + try t.checkCancel(); + + var result: w.HANDLE = undefined; + const rc = w.ntdll.NtCreateFile( + &result, + w.SYNCHRONIZE | + (if (flags.isRead()) @as(u32, w.GENERIC_READ) else 0) | + (if (flags.isWrite()) @as(u32, w.GENERIC_WRITE) else 0), + &attr, + &io_status_block, + null, + w.FILE_ATTRIBUTE_NORMAL, + w.FILE_SHARE_WRITE | w.FILE_SHARE_READ | w.FILE_SHARE_DELETE, + w.FILE_OPEN, + create_file_flags, + null, + 0, + ); + switch (rc) { + .SUCCESS => break result, + .OBJECT_NAME_INVALID => return error.BadPathName, + .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, + .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, + .BAD_NETWORK_PATH => return error.NetworkNotFound, // \\server was not found + .BAD_NETWORK_NAME => return error.NetworkNotFound, // \\server was found but \\server\share wasn't + .NO_MEDIA_IN_DEVICE => return error.NoDevice, + .INVALID_PARAMETER => |err| return w.statusBug(err), + .SHARING_VIOLATION => { + // This occurs if the file attempting to be opened is a running + // executable. However, there's a kernel bug: the error may be + // incorrectly returned for an indeterminate amount of time + // after an executable file is closed. Here we work around the + // kernel bug with retry attempts. + if (attempt - max_attempts == 0) return error.SharingViolation; + _ = w.kernel32.SleepEx((@as(u32, 1) << attempt) >> 1, w.TRUE); + attempt += 1; + continue; + }, + .ACCESS_DENIED => return error.AccessDenied, + .PIPE_BUSY => return error.PipeBusy, + .PIPE_NOT_AVAILABLE => return error.NoDevice, + .OBJECT_PATH_SYNTAX_BAD => |err| return w.statusBug(err), + .OBJECT_NAME_COLLISION => return error.PathAlreadyExists, + .FILE_IS_A_DIRECTORY => return error.IsDir, + .NOT_A_DIRECTORY => return error.NotDir, + .USER_MAPPED_FILE => return error.AccessDenied, + .INVALID_HANDLE => |err| return w.statusBug(err), + .DELETE_PENDING => { + // This error means that there *was* a file in this location on + // the file system, but it was deleted. However, the OS is not + // finished with the deletion operation, and so this CreateFile + // call has failed. Here, we simulate the kernel bug being + // fixed by sleeping and retrying until the error goes away. + if (attempt - max_attempts == 0) return error.SharingViolation; + _ = w.kernel32.SleepEx((@as(u32, 1) << attempt) >> 1, w.TRUE); + attempt += 1; + continue; + }, + .VIRUS_INFECTED, .VIRUS_DELETED => return error.AntivirusInterference, + else => return w.unexpectedStatus(rc), + } + }; + errdefer w.CloseHandle(handle); + + const range_off: w.LARGE_INTEGER = 0; + const range_len: w.LARGE_INTEGER = 1; + const exclusive = switch (flags.lock) { + .none => return .{ .handle = handle }, + .shared => false, + .exclusive => true, + }; + try w.LockFile( + handle, + null, + null, + null, + &io_status_block, + &range_off, + &range_len, + null, + @intFromBool(flags.lock_nonblocking), + @intFromBool(exclusive), + ); + return .{ .handle = handle }; +} + +fn dirOpenFileWasi( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + flags: Io.File.OpenFlags, +) Io.File.OpenError!Io.File { + if (builtin.link_libc) return dirOpenFilePosix(userdata, dir, sub_path, flags); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const wasi = std.os.wasi; + var base: std.os.wasi.rights_t = .{}; + // POLL_FD_READWRITE only grants extra rights if the corresponding FD_READ and/or FD_WRITE + // is also set. + if (flags.isRead()) { + base.FD_READ = true; + base.FD_TELL = true; + base.FD_SEEK = true; + base.FD_FILESTAT_GET = true; + base.POLL_FD_READWRITE = true; + } + if (flags.isWrite()) { + base.FD_WRITE = true; + base.FD_TELL = true; + base.FD_SEEK = true; + base.FD_DATASYNC = true; + base.FD_FDSTAT_SET_FLAGS = true; + base.FD_SYNC = true; + base.FD_ALLOCATE = true; + base.FD_ADVISE = true; + base.FD_FILESTAT_SET_TIMES = true; + base.FD_FILESTAT_SET_SIZE = true; + base.POLL_FD_READWRITE = true; + } + const lookup_flags: wasi.lookupflags_t = .{}; + const oflags: wasi.oflags_t = .{}; + const inheriting: wasi.rights_t = .{}; + const fdflags: wasi.fdflags_t = .{}; + var fd: posix.fd_t = undefined; + while (true) { + try t.checkCancel(); + switch (wasi.path_open(dir.handle, lookup_flags, sub_path.ptr, sub_path.len, oflags, base, inheriting, fdflags, &fd)) { + .SUCCESS => return .{ .handle = fd }, + .INTR => continue, + .CANCELED => return error.Canceled, + + .FAULT => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .ACCES => return error.AccessDenied, + .FBIG => return error.FileTooBig, + .OVERFLOW => return error.FileTooBig, + .ISDIR => return error.IsDir, + .LOOP => return error.SymLinkLoop, + .MFILE => return error.ProcessFdQuotaExceeded, + .NFILE => return error.SystemFdQuotaExceeded, + .NODEV => return error.NoDevice, + .NOENT => return error.FileNotFound, + .NOMEM => return error.SystemResources, + .NOTDIR => return error.NotDir, + .PERM => return error.PermissionDenied, + .BUSY => return error.DeviceBusy, + .NOTCAPABLE => return error.AccessDenied, + .NAMETOOLONG => return error.NameTooLong, + .INVAL => return error.BadPathName, + .ILSEQ => return error.BadPathName, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +const dirOpenDir = switch (native_os) { + .wasi => dirOpenDirWasi, + .haiku => dirOpenDirHaiku, + else => dirOpenDirPosix, +}; + +/// This function is also used for WASI when libc is linked. +fn dirOpenDirPosix( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.OpenOptions, +) Io.Dir.OpenError!Io.Dir { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + if (is_windows) { + const sub_path_w = try windows.sliceToPrefixedFileW(dir.handle, sub_path); + return dirOpenDirWindows(t, dir, sub_path_w.span(), options); + } + + var path_buffer: [posix.PATH_MAX]u8 = undefined; + const sub_path_posix = try pathToPosix(sub_path, &path_buffer); + + var flags: posix.O = switch (native_os) { + .wasi => .{ + .read = true, + .NOFOLLOW = !options.follow_symlinks, + .DIRECTORY = true, + }, + else => .{ + .ACCMODE = .RDONLY, + .NOFOLLOW = !options.follow_symlinks, + .DIRECTORY = true, + .CLOEXEC = true, + }, + }; + + if (@hasField(posix.O, "PATH") and !options.iterate) + flags.PATH = true; + + while (true) { + try t.checkCancel(); + const rc = openat_sym(dir.handle, sub_path_posix, flags, @as(usize, 0)); + switch (posix.errno(rc)) { + .SUCCESS => return .{ .handle = @intCast(rc) }, + .INTR => continue, + .CANCELED => return error.Canceled, + + .FAULT => |err| return errnoBug(err), + .INVAL => return error.BadPathName, + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .ACCES => return error.AccessDenied, + .LOOP => return error.SymLinkLoop, + .MFILE => return error.ProcessFdQuotaExceeded, + .NAMETOOLONG => return error.NameTooLong, + .NFILE => return error.SystemFdQuotaExceeded, + .NODEV => return error.NoDevice, + .NOENT => return error.FileNotFound, + .NOMEM => return error.SystemResources, + .NOTDIR => return error.NotDir, + .PERM => return error.PermissionDenied, + .BUSY => return error.DeviceBusy, + .NXIO => return error.NoDevice, + .ILSEQ => return error.BadPathName, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn dirOpenDirHaiku( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.OpenOptions, +) Io.Dir.OpenError!Io.Dir { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + var path_buffer: [posix.PATH_MAX]u8 = undefined; + const sub_path_posix = try pathToPosix(sub_path, &path_buffer); + + _ = options; + + while (true) { + try t.checkCancel(); + const rc = posix.system._kern_open_dir(dir.handle, sub_path_posix); + if (rc >= 0) return .{ .handle = rc }; + switch (@as(posix.E, @enumFromInt(rc))) { + .INTR => continue, + .CANCELED => return error.Canceled, + .FAULT => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .ACCES => return error.AccessDenied, + .LOOP => return error.SymLinkLoop, + .MFILE => return error.ProcessFdQuotaExceeded, + .NAMETOOLONG => return error.NameTooLong, + .NFILE => return error.SystemFdQuotaExceeded, + .NODEV => return error.NoDevice, + .NOENT => return error.FileNotFound, + .NOMEM => return error.SystemResources, + .NOTDIR => return error.NotDir, + .PERM => return error.PermissionDenied, + .BUSY => return error.DeviceBusy, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +pub fn dirOpenDirWindows( + t: *Io.Threaded, + dir: Io.Dir, + sub_path_w: [:0]const u16, + options: Io.Dir.OpenOptions, +) Io.Dir.OpenError!Io.Dir { + const w = windows; + // TODO remove some of these flags if options.access_sub_paths is false + const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | + w.SYNCHRONIZE | w.FILE_TRAVERSE; + const access_mask: u32 = if (options.iterate) base_flags | w.FILE_LIST_DIRECTORY else base_flags; + + const path_len_bytes: u16 = @intCast(sub_path_w.len * 2); + var nt_name: w.UNICODE_STRING = .{ + .Length = path_len_bytes, + .MaximumLength = path_len_bytes, + .Buffer = @constCast(sub_path_w.ptr), + }; + var attr: w.OBJECT_ATTRIBUTES = .{ + .Length = @sizeOf(w.OBJECT_ATTRIBUTES), + .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, + .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. + .ObjectName = &nt_name, + .SecurityDescriptor = null, + .SecurityQualityOfService = null, + }; + const open_reparse_point: w.DWORD = if (!options.follow_symlinks) w.FILE_OPEN_REPARSE_POINT else 0x0; + var io_status_block: w.IO_STATUS_BLOCK = undefined; + var result: Io.Dir = .{ .handle = undefined }; + try t.checkCancel(); + const rc = w.ntdll.NtCreateFile( + &result.handle, + access_mask, + &attr, + &io_status_block, + null, + w.FILE_ATTRIBUTE_NORMAL, + w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE, + w.FILE_OPEN, + w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point, + null, + 0, + ); + + switch (rc) { + .SUCCESS => return result, + .OBJECT_NAME_INVALID => return error.BadPathName, + .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, + .OBJECT_NAME_COLLISION => |err| return w.statusBug(err), + .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, + .NOT_A_DIRECTORY => return error.NotDir, + // This can happen if the directory has 'List folder contents' permission set to 'Deny' + // and the directory is trying to be opened for iteration. + .ACCESS_DENIED => return error.AccessDenied, + .INVALID_PARAMETER => |err| return w.statusBug(err), + else => return w.unexpectedStatus(rc), + } +} + +const MakeOpenDirAccessMaskWOptions = struct { + no_follow: bool, + create_disposition: u32, +}; + +fn dirClose(userdata: ?*anyopaque, dir: Io.Dir) void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + posix.close(dir.handle); +} + +fn dirOpenDirWasi( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.OpenOptions, +) Io.Dir.OpenError!Io.Dir { + if (builtin.link_libc) return dirOpenDirPosix(userdata, dir, sub_path, options); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const wasi = std.os.wasi; + + var base: std.os.wasi.rights_t = .{ + .FD_FILESTAT_GET = true, + .FD_FDSTAT_SET_FLAGS = true, + .FD_FILESTAT_SET_TIMES = true, + }; + if (options.access_sub_paths) { + base.FD_READDIR = true; + base.PATH_CREATE_DIRECTORY = true; + base.PATH_CREATE_FILE = true; + base.PATH_LINK_SOURCE = true; + base.PATH_LINK_TARGET = true; + base.PATH_OPEN = true; + base.PATH_READLINK = true; + base.PATH_RENAME_SOURCE = true; + base.PATH_RENAME_TARGET = true; + base.PATH_FILESTAT_GET = true; + base.PATH_FILESTAT_SET_SIZE = true; + base.PATH_FILESTAT_SET_TIMES = true; + base.PATH_SYMLINK = true; + base.PATH_REMOVE_DIRECTORY = true; + base.PATH_UNLINK_FILE = true; + } + + const lookup_flags: wasi.lookupflags_t = .{ .SYMLINK_FOLLOW = options.follow_symlinks }; + const oflags: wasi.oflags_t = .{ .DIRECTORY = true }; + const fdflags: wasi.fdflags_t = .{}; + var fd: posix.fd_t = undefined; + + while (true) { + try t.checkCancel(); + switch (wasi.path_open(dir.handle, lookup_flags, sub_path.ptr, sub_path.len, oflags, base, base, fdflags, &fd)) { + .SUCCESS => return .{ .handle = fd }, + .INTR => continue, + .CANCELED => return error.Canceled, + + .FAULT => |err| return errnoBug(err), + .INVAL => return error.BadPathName, + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .ACCES => return error.AccessDenied, + .LOOP => return error.SymLinkLoop, + .MFILE => return error.ProcessFdQuotaExceeded, + .NAMETOOLONG => return error.NameTooLong, + .NFILE => return error.SystemFdQuotaExceeded, + .NODEV => return error.NoDevice, + .NOENT => return error.FileNotFound, + .NOMEM => return error.SystemResources, + .NOTDIR => return error.NotDir, + .PERM => return error.PermissionDenied, + .BUSY => return error.DeviceBusy, + .NOTCAPABLE => return error.AccessDenied, + .ILSEQ => return error.BadPathName, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn fileClose(userdata: ?*anyopaque, file: Io.File) void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + posix.close(file.handle); +} + +const fileReadStreaming = switch (native_os) { + .windows => fileReadStreamingWindows, + else => fileReadStreamingPosix, +}; + +fn fileReadStreamingPosix(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File.Reader.Error!usize { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined; + var i: usize = 0; + for (data) |buf| { + if (iovecs_buffer.len - i == 0) break; + if (buf.len != 0) { + iovecs_buffer[i] = .{ .base = buf.ptr, .len = buf.len }; + i += 1; + } + } + const dest = iovecs_buffer[0..i]; + assert(dest[0].len > 0); + + if (native_os == .wasi and !builtin.link_libc) while (true) { + try t.checkCancel(); + var nread: usize = undefined; + switch (std.os.wasi.fd_read(file.handle, dest.ptr, dest.len, &nread)) { + .SUCCESS => return nread, + .INTR => continue, + .CANCELED => return error.Canceled, + + .INVAL => |err| return errnoBug(err), + .FAULT => |err| return errnoBug(err), + .BADF => return error.NotOpenForReading, // File operation on directory. + .IO => return error.InputOutput, + .ISDIR => return error.IsDir, + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + .NOTCONN => return error.SocketUnconnected, + .CONNRESET => return error.ConnectionResetByPeer, + .TIMEDOUT => return error.Timeout, + .NOTCAPABLE => return error.AccessDenied, + else => |err| return posix.unexpectedErrno(err), + } + }; + + while (true) { + try t.checkCancel(); + const rc = posix.system.readv(file.handle, dest.ptr, @intCast(dest.len)); + switch (posix.errno(rc)) { + .SUCCESS => return @intCast(rc), + .INTR => continue, + .CANCELED => return error.Canceled, + + .INVAL => |err| return errnoBug(err), + .FAULT => |err| return errnoBug(err), + .SRCH => return error.ProcessNotFound, + .AGAIN => return error.WouldBlock, + .BADF => |err| { + if (native_os == .wasi) return error.NotOpenForReading; // File operation on directory. + return errnoBug(err); // File descriptor used after closed. + }, + .IO => return error.InputOutput, + .ISDIR => return error.IsDir, + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + .NOTCONN => return error.SocketUnconnected, + .CONNRESET => return error.ConnectionResetByPeer, + .TIMEDOUT => return error.Timeout, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn fileReadStreamingWindows(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File.Reader.Error!usize { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + const DWORD = windows.DWORD; + var index: usize = 0; + while (data[index].len == 0) index += 1; + const buffer = data[index]; + const want_read_count: DWORD = @min(std.math.maxInt(DWORD), buffer.len); + + while (true) { + try t.checkCancel(); + var n: DWORD = undefined; + if (windows.kernel32.ReadFile(file.handle, buffer.ptr, want_read_count, &n, null) != 0) + return n; + switch (windows.GetLastError()) { + .IO_PENDING => |err| return windows.errorBug(err), + .OPERATION_ABORTED => continue, + .BROKEN_PIPE => return 0, + .HANDLE_EOF => return 0, + .NETNAME_DELETED => return error.ConnectionResetByPeer, + .LOCK_VIOLATION => return error.LockViolation, + .ACCESS_DENIED => return error.AccessDenied, + .INVALID_HANDLE => return error.NotOpenForReading, + else => |err| return windows.unexpectedError(err), + } + } +} + +fn fileReadPositionalPosix(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset: u64) Io.File.ReadPositionalError!usize { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + if (!have_preadv) @compileError("TODO"); + + var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined; + var i: usize = 0; + for (data) |buf| { + if (iovecs_buffer.len - i == 0) break; + if (buf.len != 0) { + iovecs_buffer[i] = .{ .base = buf.ptr, .len = buf.len }; + i += 1; + } + } + const dest = iovecs_buffer[0..i]; + assert(dest[0].len > 0); + + if (native_os == .wasi and !builtin.link_libc) while (true) { + try t.checkCancel(); + var nread: usize = undefined; + switch (std.os.wasi.fd_pread(file.handle, dest.ptr, dest.len, offset, &nread)) { + .SUCCESS => return nread, + .INTR => continue, + .CANCELED => return error.Canceled, + + .INVAL => |err| return errnoBug(err), + .FAULT => |err| return errnoBug(err), + .AGAIN => |err| return errnoBug(err), + .BADF => return error.NotOpenForReading, // File operation on directory. + .IO => return error.InputOutput, + .ISDIR => return error.IsDir, + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + .NOTCONN => return error.SocketUnconnected, + .CONNRESET => return error.ConnectionResetByPeer, + .TIMEDOUT => return error.Timeout, + .NXIO => return error.Unseekable, + .SPIPE => return error.Unseekable, + .OVERFLOW => return error.Unseekable, + .NOTCAPABLE => return error.AccessDenied, + else => |err| return posix.unexpectedErrno(err), + } + }; + + while (true) { + try t.checkCancel(); + const rc = preadv_sym(file.handle, dest.ptr, @intCast(dest.len), @bitCast(offset)); + switch (posix.errno(rc)) { + .SUCCESS => return @bitCast(rc), + .INTR => continue, + .CANCELED => return error.Canceled, + + .INVAL => |err| return errnoBug(err), + .FAULT => |err| return errnoBug(err), + .SRCH => return error.ProcessNotFound, + .AGAIN => return error.WouldBlock, + .BADF => |err| { + if (native_os == .wasi) return error.NotOpenForReading; // File operation on directory. + return errnoBug(err); // File descriptor used after closed. + }, + .IO => return error.InputOutput, + .ISDIR => return error.IsDir, + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + .NOTCONN => return error.SocketUnconnected, + .CONNRESET => return error.ConnectionResetByPeer, + .TIMEDOUT => return error.Timeout, + .NXIO => return error.Unseekable, + .SPIPE => return error.Unseekable, + .OVERFLOW => return error.Unseekable, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +const fileReadPositional = switch (native_os) { + .windows => fileReadPositionalWindows, + else => fileReadPositionalPosix, +}; + +fn fileReadPositionalWindows(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset: u64) Io.File.ReadPositionalError!usize { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + const DWORD = windows.DWORD; + + var index: usize = 0; + while (data[index].len == 0) index += 1; + const buffer = data[index]; + const want_read_count: DWORD = @min(std.math.maxInt(DWORD), buffer.len); + + var overlapped: windows.OVERLAPPED = .{ + .Internal = 0, + .InternalHigh = 0, + .DUMMYUNIONNAME = .{ + .DUMMYSTRUCTNAME = .{ + .Offset = @truncate(offset), + .OffsetHigh = @truncate(offset >> 32), + }, + }, + .hEvent = null, + }; + + while (true) { + try t.checkCancel(); + var n: DWORD = undefined; + if (windows.kernel32.ReadFile(file.handle, buffer.ptr, want_read_count, &n, &overlapped) != 0) + return n; + switch (windows.GetLastError()) { + .IO_PENDING => |err| return windows.errorBug(err), + .OPERATION_ABORTED => continue, + .BROKEN_PIPE => return 0, + .HANDLE_EOF => return 0, + .NETNAME_DELETED => return error.ConnectionResetByPeer, + .LOCK_VIOLATION => return error.LockViolation, + .ACCESS_DENIED => return error.AccessDenied, + .INVALID_HANDLE => return error.NotOpenForReading, + else => |err| return windows.unexpectedError(err), + } + } +} + +fn fileSeekBy(userdata: ?*anyopaque, file: Io.File, offset: i64) Io.File.SeekError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + try t.checkCancel(); + + _ = file; + _ = offset; + @panic("TODO implement fileSeekBy"); +} + +fn fileSeekTo(userdata: ?*anyopaque, file: Io.File, offset: u64) Io.File.SeekError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const fd = file.handle; + + if (native_os == .linux and !builtin.link_libc and @sizeOf(usize) == 4) while (true) { + try t.checkCancel(); + var result: u64 = undefined; + switch (posix.errno(posix.system.llseek(fd, offset, &result, posix.SEEK.SET))) { + .SUCCESS => return, + .INTR => continue, + .CANCELED => return error.Canceled, + + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .INVAL => return error.Unseekable, + .OVERFLOW => return error.Unseekable, + .SPIPE => return error.Unseekable, + .NXIO => return error.Unseekable, + else => |err| return posix.unexpectedErrno(err), + } + }; + + if (native_os == .windows) { + try t.checkCancel(); + return windows.SetFilePointerEx_BEGIN(fd, offset); + } + + if (native_os == .wasi and !builtin.link_libc) while (true) { + try t.checkCancel(); + var new_offset: std.os.wasi.filesize_t = undefined; + switch (std.os.wasi.fd_seek(fd, @bitCast(offset), .SET, &new_offset)) { + .SUCCESS => return, + .INTR => continue, + .CANCELED => return error.Canceled, + + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .INVAL => return error.Unseekable, + .OVERFLOW => return error.Unseekable, + .SPIPE => return error.Unseekable, + .NXIO => return error.Unseekable, + .NOTCAPABLE => return error.AccessDenied, + else => |err| return posix.unexpectedErrno(err), + } + }; + + if (posix.SEEK == void) return error.Unseekable; + + while (true) { + try t.checkCancel(); + switch (posix.errno(lseek_sym(fd, @bitCast(offset), posix.SEEK.SET))) { + .SUCCESS => return, + .INTR => continue, + .CANCELED => return error.Canceled, + + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .INVAL => return error.Unseekable, + .OVERFLOW => return error.Unseekable, + .SPIPE => return error.Unseekable, + .NXIO => return error.Unseekable, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn openSelfExe(userdata: ?*anyopaque, flags: Io.File.OpenFlags) Io.File.OpenSelfExeError!Io.File { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + switch (native_os) { + .linux, .serenity => return dirOpenFilePosix(t, .{ .handle = posix.AT.FDCWD }, "/proc/self/exe", flags), + .windows => { + // If ImagePathName is a symlink, then it will contain the path of the symlink, + // not the path that the symlink points to. However, because we are opening + // the file, we can let the openFileW call follow the symlink for us. + const image_path_unicode_string = &windows.peb().ProcessParameters.ImagePathName; + const image_path_name = image_path_unicode_string.Buffer.?[0 .. image_path_unicode_string.Length / 2 :0]; + const prefixed_path_w = try windows.wToPrefixedFileW(null, image_path_name); + return dirOpenFileWtf16(t, null, prefixed_path_w.span(), flags); + }, + else => @panic("TODO implement openSelfExe"), + } +} + +fn fileWritePositional( + userdata: ?*anyopaque, + file: Io.File, + buffer: [][]const u8, + offset: u64, +) Io.File.WritePositionalError!usize { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + while (true) { + try t.checkCancel(); + _ = file; + _ = buffer; + _ = offset; + @panic("TODO implement fileWritePositional"); + } +} + +fn fileWriteStreaming(userdata: ?*anyopaque, file: Io.File, buffer: [][]const u8) Io.File.WriteStreamingError!usize { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + while (true) { + try t.checkCancel(); + _ = file; + _ = buffer; + @panic("TODO implement fileWriteStreaming"); + } +} + +fn nowPosix(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + const clock_id: posix.clockid_t = clockToPosix(clock); + var tp: posix.timespec = undefined; + switch (posix.errno(posix.system.clock_gettime(clock_id, &tp))) { + .SUCCESS => return timestampFromPosix(&tp), + .INVAL => return error.UnsupportedClock, + else => |err| return posix.unexpectedErrno(err), + } +} + +const now = switch (native_os) { + .windows => nowWindows, + .wasi => nowWasi, + else => nowPosix, +}; + +fn nowWindows(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + switch (clock) { + .real => { + // RtlGetSystemTimePrecise() has a granularity of 100 nanoseconds + // and uses the NTFS/Windows epoch, which is 1601-01-01. + return .{ .nanoseconds = @as(i96, windows.ntdll.RtlGetSystemTimePrecise()) * 100 }; + }, + .awake, .boot => { + // QPC on windows doesn't fail on >= XP/2000 and includes time suspended. + return .{ .nanoseconds = windows.QueryPerformanceCounter() }; + }, + .cpu_process, + .cpu_thread, + => return error.UnsupportedClock, + } +} + +fn nowWasi(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + var ns: std.os.wasi.timestamp_t = undefined; + const err = std.os.wasi.clock_time_get(clockToWasi(clock), 1, &ns); + if (err != .SUCCESS) return error.Unexpected; + return .fromNanoseconds(ns); +} + +const sleep = switch (native_os) { + .windows => sleepWindows, + .wasi => sleepWasi, + .linux => sleepLinux, + else => sleepPosix, +}; + +fn sleepLinux(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const clock_id: posix.clockid_t = clockToPosix(switch (timeout) { + .none => .awake, + .duration => |d| d.clock, + .deadline => |d| d.clock, + }); + const deadline_nanoseconds: i96 = switch (timeout) { + .none => std.math.maxInt(i96), + .duration => |duration| duration.raw.nanoseconds, + .deadline => |deadline| deadline.raw.nanoseconds, + }; + var timespec: posix.timespec = timestampToPosix(deadline_nanoseconds); + while (true) { + try t.checkCancel(); + switch (std.os.linux.E.init(std.os.linux.clock_nanosleep(clock_id, .{ .ABSTIME = switch (timeout) { + .none, .duration => false, + .deadline => true, + } }, ×pec, ×pec))) { + .SUCCESS => return, + .INTR => continue, + .CANCELED => return error.Canceled, + .INVAL => return error.UnsupportedClock, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn sleepWindows(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const t_io = ioBasic(t); + try t.checkCancel(); + const ms = ms: { + const d = (try timeout.toDurationFromNow(t_io)) orelse + break :ms std.math.maxInt(windows.DWORD); + break :ms std.math.lossyCast(windows.DWORD, d.raw.toMilliseconds()); + }; + // TODO: alertable true with checkCancel in a loop plus deadline + _ = windows.kernel32.SleepEx(ms, windows.FALSE); +} + +fn sleepWasi(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const t_io = ioBasic(t); + try t.checkCancel(); + + const w = std.os.wasi; + + const clock: w.subscription_clock_t = if (try timeout.toDurationFromNow(t_io)) |d| .{ + .id = clockToWasi(d.clock), + .timeout = std.math.lossyCast(u64, d.raw.nanoseconds), + .precision = 0, + .flags = 0, + } else .{ + .id = .MONOTONIC, + .timeout = std.math.maxInt(u64), + .precision = 0, + .flags = 0, + }; + const in: w.subscription_t = .{ + .userdata = 0, + .u = .{ + .tag = .CLOCK, + .u = .{ .clock = clock }, + }, + }; + var event: w.event_t = undefined; + var nevents: usize = undefined; + _ = w.poll_oneoff(&in, &event, 1, &nevents); +} + +fn sleepPosix(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const t_io = ioBasic(t); + const sec_type = @typeInfo(posix.timespec).@"struct".fields[0].type; + const nsec_type = @typeInfo(posix.timespec).@"struct".fields[1].type; + + var timespec: posix.timespec = t: { + const d = (try timeout.toDurationFromNow(t_io)) orelse break :t .{ + .sec = std.math.maxInt(sec_type), + .nsec = std.math.maxInt(nsec_type), + }; + break :t timestampToPosix(d.raw.toNanoseconds()); + }; + while (true) { + try t.checkCancel(); + switch (posix.errno(posix.system.nanosleep(×pec, ×pec))) { + .INTR => continue, + .CANCELED => return error.Canceled, + else => return, // This prong handles success as well as unexpected errors. + } + } +} + +fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) Io.Cancelable!usize { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + var reset_event: ResetEvent = .unset; + + for (futures, 0..) |future, i| { + const closure: *AsyncClosure = @ptrCast(@alignCast(future)); + if (@atomicRmw(?*ResetEvent, &closure.select_condition, .Xchg, &reset_event, .seq_cst) == AsyncClosure.done_reset_event) { + for (futures[0..i]) |cleanup_future| { + const cleanup_closure: *AsyncClosure = @ptrCast(@alignCast(cleanup_future)); + if (@atomicRmw(?*ResetEvent, &cleanup_closure.select_condition, .Xchg, null, .seq_cst) == AsyncClosure.done_reset_event) { + cleanup_closure.reset_event.waitUncancelable(); // Ensure no reference to our stack-allocated reset_event. + } + } + return i; + } + } + + try reset_event.wait(t); + + var result: ?usize = null; + for (futures, 0..) |future, i| { + const closure: *AsyncClosure = @ptrCast(@alignCast(future)); + if (@atomicRmw(?*ResetEvent, &closure.select_condition, .Xchg, null, .seq_cst) == AsyncClosure.done_reset_event) { + closure.reset_event.waitUncancelable(); // Ensure no reference to our stack-allocated reset_event. + if (result == null) result = i; // In case multiple are ready, return first. + } + } + return result.?; +} + +fn netListenIpPosix( + userdata: ?*anyopaque, + address: IpAddress, + options: IpAddress.ListenOptions, +) IpAddress.ListenError!net.Server { + if (!have_networking) return error.NetworkDown; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const family = posixAddressFamily(&address); + const socket_fd = try openSocketPosix(t, family, .{ + .mode = options.mode, + .protocol = options.protocol, + }); + errdefer posix.close(socket_fd); + + if (options.reuse_address) { + try setSocketOption(t, socket_fd, posix.SOL.SOCKET, posix.SO.REUSEADDR, 1); + if (@hasDecl(posix.SO, "REUSEPORT")) + try setSocketOption(t, socket_fd, posix.SOL.SOCKET, posix.SO.REUSEPORT, 1); + } + + var storage: PosixAddress = undefined; + var addr_len = addressToPosix(&address, &storage); + try posixBind(t, socket_fd, &storage.any, addr_len); + + while (true) { + try t.checkCancel(); + switch (posix.errno(posix.system.listen(socket_fd, options.kernel_backlog))) { + .SUCCESS => break, + .ADDRINUSE => return error.AddressInUse, + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + else => |err| return posix.unexpectedErrno(err), + } + } + + try posixGetSockName(t, socket_fd, &storage.any, &addr_len); + return .{ + .socket = .{ + .handle = socket_fd, + .address = addressFromPosix(&storage), + }, + }; +} + +fn netListenIpWindows( + userdata: ?*anyopaque, + address: IpAddress, + options: IpAddress.ListenOptions, +) IpAddress.ListenError!net.Server { + if (!have_networking) return error.NetworkDown; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const family = posixAddressFamily(&address); + const socket_handle = try openSocketWsa(t, family, .{ + .mode = options.mode, + .protocol = options.protocol, + }); + errdefer closeSocketWindows(socket_handle); + + if (options.reuse_address) + try setSocketOptionWsa(t, socket_handle, posix.SOL.SOCKET, posix.SO.REUSEADDR, 1); + + var storage: WsaAddress = undefined; + var addr_len = addressToWsa(&address, &storage); + + while (true) { + try t.checkCancel(); + const rc = ws2_32.bind(socket_handle, &storage.any, addr_len); + if (rc != ws2_32.SOCKET_ERROR) break; + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + .EADDRINUSE => return error.AddressInUse, + .EADDRNOTAVAIL => return error.AddressUnavailable, + .ENOTSOCK => |err| return wsaErrorBug(err), + .EFAULT => |err| return wsaErrorBug(err), + .EINVAL => |err| return wsaErrorBug(err), + .ENOBUFS => return error.SystemResources, + .ENETDOWN => return error.NetworkDown, + else => |err| return windows.unexpectedWSAError(err), + } + } + + while (true) { + try t.checkCancel(); + const rc = ws2_32.listen(socket_handle, options.kernel_backlog); + if (rc != ws2_32.SOCKET_ERROR) break; + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + .ENETDOWN => return error.NetworkDown, + .EADDRINUSE => return error.AddressInUse, + .EISCONN => |err| return wsaErrorBug(err), + .EINVAL => |err| return wsaErrorBug(err), + .EMFILE, .ENOBUFS => return error.SystemResources, + .ENOTSOCK => |err| return wsaErrorBug(err), + .EOPNOTSUPP => |err| return wsaErrorBug(err), + .EINPROGRESS => |err| return wsaErrorBug(err), + else => |err| return windows.unexpectedWSAError(err), + } + } + + try wsaGetSockName(t, socket_handle, &storage.any, &addr_len); + + return .{ + .socket = .{ + .handle = socket_handle, + .address = addressFromWsa(&storage), + }, + }; +} + +fn netListenIpUnavailable( + userdata: ?*anyopaque, + address: IpAddress, + options: IpAddress.ListenOptions, +) IpAddress.ListenError!net.Server { + _ = userdata; + _ = address; + _ = options; + return error.NetworkDown; +} + +fn netListenUnixPosix( + userdata: ?*anyopaque, + address: *const net.UnixAddress, + options: net.UnixAddress.ListenOptions, +) net.UnixAddress.ListenError!net.Socket.Handle { + if (!net.has_unix_sockets) return error.AddressFamilyUnsupported; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const socket_fd = openSocketPosix(t, posix.AF.UNIX, .{ .mode = .stream }) catch |err| switch (err) { + error.ProtocolUnsupportedBySystem => return error.AddressFamilyUnsupported, + error.ProtocolUnsupportedByAddressFamily => return error.AddressFamilyUnsupported, + error.SocketModeUnsupported => return error.AddressFamilyUnsupported, + error.OptionUnsupported => return error.Unexpected, + else => |e| return e, + }; + errdefer posix.close(socket_fd); + + var storage: UnixAddress = undefined; + const addr_len = addressUnixToPosix(address, &storage); + try posixBindUnix(t, socket_fd, &storage.any, addr_len); + + while (true) { + try t.checkCancel(); + switch (posix.errno(posix.system.listen(socket_fd, options.kernel_backlog))) { + .SUCCESS => break, + .ADDRINUSE => return error.AddressInUse, + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + else => |err| return posix.unexpectedErrno(err), + } + } + + return socket_fd; +} + +fn netListenUnixWindows( + userdata: ?*anyopaque, + address: *const net.UnixAddress, + options: net.UnixAddress.ListenOptions, +) net.UnixAddress.ListenError!net.Socket.Handle { + if (!net.has_unix_sockets) return error.AddressFamilyUnsupported; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + const socket_handle = openSocketWsa(t, posix.AF.UNIX, .{ .mode = .stream }) catch |err| switch (err) { + error.ProtocolUnsupportedByAddressFamily => return error.AddressFamilyUnsupported, + else => |e| return e, + }; + errdefer closeSocketWindows(socket_handle); + + var storage: WsaAddress = undefined; + const addr_len = addressUnixToWsa(address, &storage); + + while (true) { + try t.checkCancel(); + const rc = ws2_32.bind(socket_handle, &storage.any, addr_len); + if (rc != ws2_32.SOCKET_ERROR) break; + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + .EADDRINUSE => return error.AddressInUse, + .EADDRNOTAVAIL => return error.AddressUnavailable, + .ENOTSOCK => |err| return wsaErrorBug(err), + .EFAULT => |err| return wsaErrorBug(err), + .EINVAL => |err| return wsaErrorBug(err), + .ENOBUFS => return error.SystemResources, + .ENETDOWN => return error.NetworkDown, + else => |err| return windows.unexpectedWSAError(err), + } + } + + while (true) { + try t.checkCancel(); + const rc = ws2_32.listen(socket_handle, options.kernel_backlog); + if (rc != ws2_32.SOCKET_ERROR) break; + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + .ENETDOWN => return error.NetworkDown, + .EADDRINUSE => return error.AddressInUse, + .EISCONN => |err| return wsaErrorBug(err), + .EINVAL => |err| return wsaErrorBug(err), + .EMFILE, .ENOBUFS => return error.SystemResources, + .ENOTSOCK => |err| return wsaErrorBug(err), + .EOPNOTSUPP => |err| return wsaErrorBug(err), + .EINPROGRESS => |err| return wsaErrorBug(err), + else => |err| return windows.unexpectedWSAError(err), + } + } + + return socket_handle; +} + +fn netListenUnixUnavailable( + userdata: ?*anyopaque, + address: *const net.UnixAddress, + options: net.UnixAddress.ListenOptions, +) net.UnixAddress.ListenError!net.Socket.Handle { + _ = userdata; + _ = address; + _ = options; + return error.AddressFamilyUnsupported; +} + +fn posixBindUnix(t: *Threaded, fd: posix.socket_t, addr: *const posix.sockaddr, addr_len: posix.socklen_t) !void { + while (true) { + try t.checkCancel(); + switch (posix.errno(posix.system.bind(fd, addr, addr_len))) { + .SUCCESS => break, + .INTR => continue, + .CANCELED => return error.Canceled, + + .ACCES => return error.AccessDenied, + .ADDRINUSE => return error.AddressInUse, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .ADDRNOTAVAIL => return error.AddressUnavailable, + .NOMEM => return error.SystemResources, + + .LOOP => return error.SymLinkLoop, + .NOENT => return error.FileNotFound, + .NOTDIR => return error.NotDir, + .ROFS => return error.ReadOnlyFileSystem, + .PERM => return error.PermissionDenied, + + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .INVAL => |err| return errnoBug(err), // invalid parameters + .NOTSOCK => |err| return errnoBug(err), // invalid `sockfd` + .FAULT => |err| return errnoBug(err), // invalid `addr` pointer + .NAMETOOLONG => |err| return errnoBug(err), + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn posixBind(t: *Threaded, socket_fd: posix.socket_t, addr: *const posix.sockaddr, addr_len: posix.socklen_t) !void { + while (true) { + try t.checkCancel(); + switch (posix.errno(posix.system.bind(socket_fd, addr, addr_len))) { + .SUCCESS => break, + .INTR => continue, + .CANCELED => return error.Canceled, + + .ADDRINUSE => return error.AddressInUse, + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .INVAL => |err| return errnoBug(err), // invalid parameters + .NOTSOCK => |err| return errnoBug(err), // invalid `sockfd` + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .ADDRNOTAVAIL => return error.AddressUnavailable, + .FAULT => |err| return errnoBug(err), // invalid `addr` pointer + .NOMEM => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn posixConnect(t: *Threaded, socket_fd: posix.socket_t, addr: *const posix.sockaddr, addr_len: posix.socklen_t) !void { + while (true) { + try t.checkCancel(); + switch (posix.errno(posix.system.connect(socket_fd, addr, addr_len))) { + .SUCCESS => return, + .INTR => continue, + .CANCELED => return error.Canceled, + + .ADDRNOTAVAIL => return error.AddressUnavailable, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .AGAIN, .INPROGRESS => return error.WouldBlock, + .ALREADY => return error.ConnectionPending, + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .CONNREFUSED => return error.ConnectionRefused, + .CONNRESET => return error.ConnectionResetByPeer, + .FAULT => |err| return errnoBug(err), + .ISCONN => |err| return errnoBug(err), + .HOSTUNREACH => return error.HostUnreachable, + .NETUNREACH => return error.NetworkUnreachable, + .NOTSOCK => |err| return errnoBug(err), + .PROTOTYPE => |err| return errnoBug(err), + .TIMEDOUT => return error.Timeout, + .CONNABORTED => |err| return errnoBug(err), + .ACCES => return error.AccessDenied, + .PERM => |err| return errnoBug(err), + .NOENT => |err| return errnoBug(err), + .NETDOWN => return error.NetworkDown, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn posixConnectUnix(t: *Threaded, fd: posix.socket_t, addr: *const posix.sockaddr, addr_len: posix.socklen_t) !void { + while (true) { + try t.checkCancel(); + switch (posix.errno(posix.system.connect(fd, addr, addr_len))) { + .SUCCESS => return, + .INTR => continue, + .CANCELED => return error.Canceled, + + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .AGAIN => return error.WouldBlock, + .INPROGRESS => return error.WouldBlock, + .ACCES => return error.AccessDenied, + + .LOOP => return error.SymLinkLoop, + .NOENT => return error.FileNotFound, + .NOTDIR => return error.NotDir, + .ROFS => return error.ReadOnlyFileSystem, + .PERM => return error.PermissionDenied, + + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .CONNABORTED => |err| return errnoBug(err), + .FAULT => |err| return errnoBug(err), + .ISCONN => |err| return errnoBug(err), + .NOTSOCK => |err| return errnoBug(err), + .PROTOTYPE => |err| return errnoBug(err), + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn posixGetSockName(t: *Threaded, socket_fd: posix.fd_t, addr: *posix.sockaddr, addr_len: *posix.socklen_t) !void { + while (true) { + try t.checkCancel(); + switch (posix.errno(posix.system.getsockname(socket_fd, addr, addr_len))) { + .SUCCESS => break, + .INTR => continue, + .CANCELED => return error.Canceled, + + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .FAULT => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), // invalid parameters + .NOTSOCK => |err| return errnoBug(err), // always a race condition + .NOBUFS => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn wsaGetSockName(t: *Threaded, handle: ws2_32.SOCKET, addr: *ws2_32.sockaddr, addr_len: *i32) !void { + while (true) { + try t.checkCancel(); + const rc = ws2_32.getsockname(handle, addr, addr_len); + if (rc != ws2_32.SOCKET_ERROR) break; + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + .ENETDOWN => return error.NetworkDown, + .EFAULT => |err| return wsaErrorBug(err), + .ENOTSOCK => |err| return wsaErrorBug(err), + .EINVAL => |err| return wsaErrorBug(err), + else => |err| return windows.unexpectedWSAError(err), + } + } +} + +fn setSocketOption(t: *Threaded, fd: posix.fd_t, level: i32, opt_name: u32, option: u32) !void { + const o: []const u8 = @ptrCast(&option); + while (true) { + try t.checkCancel(); + switch (posix.errno(posix.system.setsockopt(fd, level, opt_name, o.ptr, @intCast(o.len)))) { + .SUCCESS => return, + .INTR => continue, + .CANCELED => return error.Canceled, + + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .NOTSOCK => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), + .FAULT => |err| return errnoBug(err), + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn setSocketOptionWsa(t: *Threaded, socket: Io.net.Socket.Handle, level: i32, opt_name: u32, option: u32) !void { + const o: []const u8 = @ptrCast(&option); + const rc = ws2_32.setsockopt(socket, level, @bitCast(opt_name), o.ptr, @intCast(o.len)); + while (true) { + if (rc != ws2_32.SOCKET_ERROR) return; + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + .ENETDOWN => return error.NetworkDown, + .EFAULT => |err| return wsaErrorBug(err), + .ENOTSOCK => |err| return wsaErrorBug(err), + .EINVAL => |err| return wsaErrorBug(err), + else => |err| return windows.unexpectedWSAError(err), + } + } +} + +fn netConnectIpPosix( + userdata: ?*anyopaque, + address: *const IpAddress, + options: IpAddress.ConnectOptions, +) IpAddress.ConnectError!net.Stream { + if (!have_networking) return error.NetworkDown; + if (options.timeout != .none) @panic("TODO implement netConnectIpPosix with timeout"); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const family = posixAddressFamily(address); + const socket_fd = try openSocketPosix(t, family, .{ + .mode = options.mode, + .protocol = options.protocol, + }); + errdefer posix.close(socket_fd); + var storage: PosixAddress = undefined; + var addr_len = addressToPosix(address, &storage); + try posixConnect(t, socket_fd, &storage.any, addr_len); + try posixGetSockName(t, socket_fd, &storage.any, &addr_len); + return .{ .socket = .{ + .handle = socket_fd, + .address = addressFromPosix(&storage), + } }; +} + +fn netConnectIpWindows( + userdata: ?*anyopaque, + address: *const IpAddress, + options: IpAddress.ConnectOptions, +) IpAddress.ConnectError!net.Stream { + if (!have_networking) return error.NetworkDown; + if (options.timeout != .none) @panic("TODO implement netConnectIpWindows with timeout"); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const family = posixAddressFamily(address); + const socket_handle = try openSocketWsa(t, family, .{ + .mode = options.mode, + .protocol = options.protocol, + }); + errdefer closeSocketWindows(socket_handle); + + var storage: WsaAddress = undefined; + var addr_len = addressToWsa(address, &storage); + + while (true) { + const rc = ws2_32.connect(socket_handle, &storage.any, addr_len); + if (rc != ws2_32.SOCKET_ERROR) break; + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + + .EADDRNOTAVAIL => return error.AddressUnavailable, + .ECONNREFUSED => return error.ConnectionRefused, + .ECONNRESET => return error.ConnectionResetByPeer, + .ETIMEDOUT => return error.Timeout, + .EHOSTUNREACH => return error.HostUnreachable, + .ENETUNREACH => return error.NetworkUnreachable, + .EFAULT => |err| return wsaErrorBug(err), + .EINVAL => |err| return wsaErrorBug(err), + .EISCONN => |err| return wsaErrorBug(err), + .ENOTSOCK => |err| return wsaErrorBug(err), + .EWOULDBLOCK => return error.WouldBlock, + .EACCES => return error.AccessDenied, + .ENOBUFS => return error.SystemResources, + .EAFNOSUPPORT => return error.AddressFamilyUnsupported, + else => |err| return windows.unexpectedWSAError(err), + } + } + + try wsaGetSockName(t, socket_handle, &storage.any, &addr_len); + + return .{ .socket = .{ + .handle = socket_handle, + .address = addressFromWsa(&storage), + } }; +} + +fn netConnectIpUnavailable( + userdata: ?*anyopaque, + address: *const IpAddress, + options: IpAddress.ConnectOptions, +) IpAddress.ConnectError!net.Stream { + _ = userdata; + _ = address; + _ = options; + return error.NetworkDown; +} + +fn netConnectUnixPosix( + userdata: ?*anyopaque, + address: *const net.UnixAddress, +) net.UnixAddress.ConnectError!net.Socket.Handle { + if (!net.has_unix_sockets) return error.AddressFamilyUnsupported; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const socket_fd = openSocketPosix(t, posix.AF.UNIX, .{ .mode = .stream }) catch |err| switch (err) { + error.OptionUnsupported => return error.Unexpected, + else => |e| return e, + }; + errdefer posix.close(socket_fd); + var storage: UnixAddress = undefined; + const addr_len = addressUnixToPosix(address, &storage); + try posixConnectUnix(t, socket_fd, &storage.any, addr_len); + return socket_fd; +} + +fn netConnectUnixWindows( + userdata: ?*anyopaque, + address: *const net.UnixAddress, +) net.UnixAddress.ConnectError!net.Socket.Handle { + if (!net.has_unix_sockets) return error.AddressFamilyUnsupported; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + const socket_handle = try openSocketWsa(t, posix.AF.UNIX, .{ .mode = .stream }); + errdefer closeSocketWindows(socket_handle); + var storage: WsaAddress = undefined; + const addr_len = addressUnixToWsa(address, &storage); + + while (true) { + const rc = ws2_32.connect(socket_handle, &storage.any, addr_len); + if (rc != ws2_32.SOCKET_ERROR) break; + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + + .ECONNREFUSED => return error.FileNotFound, + .EFAULT => |err| return wsaErrorBug(err), + .EINVAL => |err| return wsaErrorBug(err), + .EISCONN => |err| return wsaErrorBug(err), + .ENOTSOCK => |err| return wsaErrorBug(err), + .EWOULDBLOCK => return error.WouldBlock, + .EACCES => return error.AccessDenied, + .ENOBUFS => return error.SystemResources, + .EAFNOSUPPORT => return error.AddressFamilyUnsupported, + else => |err| return windows.unexpectedWSAError(err), + } + } + + return socket_handle; +} + +fn netConnectUnixUnavailable( + userdata: ?*anyopaque, + address: *const net.UnixAddress, +) net.UnixAddress.ConnectError!net.Socket.Handle { + _ = userdata; + _ = address; + return error.AddressFamilyUnsupported; +} + +fn netBindIpPosix( + userdata: ?*anyopaque, + address: *const IpAddress, + options: IpAddress.BindOptions, +) IpAddress.BindError!net.Socket { + if (!have_networking) return error.NetworkDown; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const family = posixAddressFamily(address); + const socket_fd = try openSocketPosix(t, family, options); + errdefer posix.close(socket_fd); + var storage: PosixAddress = undefined; + var addr_len = addressToPosix(address, &storage); + try posixBind(t, socket_fd, &storage.any, addr_len); + try posixGetSockName(t, socket_fd, &storage.any, &addr_len); + return .{ + .handle = socket_fd, + .address = addressFromPosix(&storage), + }; +} + +fn netBindIpWindows( + userdata: ?*anyopaque, + address: *const IpAddress, + options: IpAddress.BindOptions, +) IpAddress.BindError!net.Socket { + if (!have_networking) return error.NetworkDown; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const family = posixAddressFamily(address); + const socket_handle = try openSocketWsa(t, family, .{ + .mode = options.mode, + .protocol = options.protocol, + }); + errdefer closeSocketWindows(socket_handle); + + var storage: WsaAddress = undefined; + var addr_len = addressToWsa(address, &storage); + + while (true) { + try t.checkCancel(); + const rc = ws2_32.bind(socket_handle, &storage.any, addr_len); + if (rc != ws2_32.SOCKET_ERROR) break; + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + .EADDRINUSE => return error.AddressInUse, + .EADDRNOTAVAIL => return error.AddressUnavailable, + .ENOTSOCK => |err| return wsaErrorBug(err), + .EFAULT => |err| return wsaErrorBug(err), + .EINVAL => |err| return wsaErrorBug(err), + .ENOBUFS => return error.SystemResources, + .ENETDOWN => return error.NetworkDown, + else => |err| return windows.unexpectedWSAError(err), + } + } + + try wsaGetSockName(t, socket_handle, &storage.any, &addr_len); + + return .{ + .handle = socket_handle, + .address = addressFromWsa(&storage), + }; +} + +fn netBindIpUnavailable( + userdata: ?*anyopaque, + address: *const IpAddress, + options: IpAddress.BindOptions, +) IpAddress.BindError!net.Socket { + _ = userdata; + _ = address; + _ = options; + return error.NetworkDown; +} + +fn openSocketPosix( + t: *Threaded, + family: posix.sa_family_t, + options: IpAddress.BindOptions, +) error{ + AddressFamilyUnsupported, + ProtocolUnsupportedBySystem, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, + SystemResources, + ProtocolUnsupportedByAddressFamily, + SocketModeUnsupported, + OptionUnsupported, + Unexpected, + Canceled, +}!posix.socket_t { + const mode = posixSocketMode(options.mode); + const protocol = posixProtocol(options.protocol); + const socket_fd = while (true) { + try t.checkCancel(); + const flags: u32 = mode | if (socket_flags_unsupported) 0 else posix.SOCK.CLOEXEC; + const socket_rc = posix.system.socket(family, flags, protocol); + switch (posix.errno(socket_rc)) { + .SUCCESS => { + const fd: posix.fd_t = @intCast(socket_rc); + errdefer posix.close(fd); + if (socket_flags_unsupported) while (true) { + try t.checkCancel(); + switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFD, @as(usize, posix.FD_CLOEXEC)))) { + .SUCCESS => break, + .INTR => continue, + .CANCELED => return error.Canceled, + else => |err| return posix.unexpectedErrno(err), + } + }; + break fd; + }, + .INTR => continue, + .CANCELED => return error.Canceled, + + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .INVAL => return error.ProtocolUnsupportedBySystem, + .MFILE => return error.ProcessFdQuotaExceeded, + .NFILE => return error.SystemFdQuotaExceeded, + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + .PROTONOSUPPORT => return error.ProtocolUnsupportedByAddressFamily, + .PROTOTYPE => return error.SocketModeUnsupported, + else => |err| return posix.unexpectedErrno(err), + } + }; + errdefer posix.close(socket_fd); + + if (options.ip6_only) { + if (posix.IPV6 == void) return error.OptionUnsupported; + try setSocketOption(t, socket_fd, posix.IPPROTO.IPV6, posix.IPV6.V6ONLY, 0); + } + + return socket_fd; +} + +fn openSocketWsa(t: *Threaded, family: posix.sa_family_t, options: IpAddress.BindOptions) !ws2_32.SOCKET { + const mode = posixSocketMode(options.mode); + const protocol = posixProtocol(options.protocol); + const flags: u32 = ws2_32.WSA_FLAG_OVERLAPPED | ws2_32.WSA_FLAG_NO_HANDLE_INHERIT; + while (true) { + try t.checkCancel(); + const rc = ws2_32.WSASocketW(family, @bitCast(mode), @bitCast(protocol), null, 0, flags); + if (rc != ws2_32.INVALID_SOCKET) return rc; + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + .EAFNOSUPPORT => return error.AddressFamilyUnsupported, + .EMFILE => return error.ProcessFdQuotaExceeded, + .ENOBUFS => return error.SystemResources, + .EPROTONOSUPPORT => return error.ProtocolUnsupportedByAddressFamily, + else => |err| return windows.unexpectedWSAError(err), + } + } +} + +fn netAcceptPosix(userdata: ?*anyopaque, listen_fd: net.Socket.Handle) net.Server.AcceptError!net.Stream { + if (!have_networking) return error.NetworkDown; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + var storage: PosixAddress = undefined; + var addr_len: posix.socklen_t = @sizeOf(PosixAddress); + const fd = while (true) { + try t.checkCancel(); + const rc = if (have_accept4) + posix.system.accept4(listen_fd, &storage.any, &addr_len, posix.SOCK.CLOEXEC) + else + posix.system.accept(listen_fd, &storage.any, &addr_len); + switch (posix.errno(rc)) { + .SUCCESS => { + const fd: posix.fd_t = @intCast(rc); + errdefer posix.close(fd); + if (!have_accept4) while (true) { + try t.checkCancel(); + switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFD, @as(usize, posix.FD_CLOEXEC)))) { + .SUCCESS => break, + .INTR => continue, + .CANCELED => return error.Canceled, + else => |err| return posix.unexpectedErrno(err), + } + }; + break fd; + }, + .INTR => continue, + .CANCELED => return error.Canceled, + + .AGAIN => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .CONNABORTED => return error.ConnectionAborted, + .FAULT => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), + .NOTSOCK => |err| return errnoBug(err), + .MFILE => return error.ProcessFdQuotaExceeded, + .NFILE => return error.SystemFdQuotaExceeded, + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + .OPNOTSUPP => |err| return errnoBug(err), + .PROTO => return error.ProtocolFailure, + .PERM => return error.BlockedByFirewall, + else => |err| return posix.unexpectedErrno(err), + } + }; + return .{ .socket = .{ + .handle = fd, + .address = addressFromPosix(&storage), + } }; +} + +fn netAcceptWindows(userdata: ?*anyopaque, listen_handle: net.Socket.Handle) net.Server.AcceptError!net.Stream { + if (!have_networking) return error.NetworkDown; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + var storage: WsaAddress = undefined; + var addr_len: i32 = @sizeOf(WsaAddress); + while (true) { + try t.checkCancel(); + const rc = ws2_32.accept(listen_handle, &storage.any, &addr_len); + if (rc != ws2_32.INVALID_SOCKET) return .{ .socket = .{ + .handle = rc, + .address = addressFromWsa(&storage), + } }; + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + .ECONNRESET => return error.ConnectionAborted, + .EFAULT => |err| return wsaErrorBug(err), + .ENOTSOCK => |err| return wsaErrorBug(err), + .EINVAL => |err| return wsaErrorBug(err), + .EMFILE => return error.ProcessFdQuotaExceeded, + .ENETDOWN => return error.NetworkDown, + .ENOBUFS => return error.SystemResources, + .EOPNOTSUPP => |err| return wsaErrorBug(err), + else => |err| return windows.unexpectedWSAError(err), + } + } +} + +fn netAcceptUnavailable(userdata: ?*anyopaque, listen_handle: net.Socket.Handle) net.Server.AcceptError!net.Stream { + _ = userdata; + _ = listen_handle; + return error.NetworkDown; +} + +fn netReadPosix(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize { + if (!have_networking) return error.NetworkDown; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined; + var i: usize = 0; + for (data) |buf| { + if (iovecs_buffer.len - i == 0) break; + if (buf.len != 0) { + iovecs_buffer[i] = .{ .base = buf.ptr, .len = buf.len }; + i += 1; + } + } + const dest = iovecs_buffer[0..i]; + assert(dest[0].len > 0); + + if (native_os == .wasi and !builtin.link_libc) while (true) { + try t.checkCancel(); + var n: usize = undefined; + switch (std.os.wasi.fd_read(fd, dest.ptr, dest.len, &n)) { + .SUCCESS => return n, + .INTR => continue, + .CANCELED => return error.Canceled, + + .INVAL => |err| return errnoBug(err), + .FAULT => |err| return errnoBug(err), + .AGAIN => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + .NOTCONN => return error.SocketUnconnected, + .CONNRESET => return error.ConnectionResetByPeer, + .TIMEDOUT => return error.Timeout, + .NOTCAPABLE => return error.AccessDenied, + else => |err| return posix.unexpectedErrno(err), + } + }; + + while (true) { + try t.checkCancel(); + const rc = posix.system.readv(fd, dest.ptr, @intCast(dest.len)); + switch (posix.errno(rc)) { + .SUCCESS => return @intCast(rc), + .INTR => continue, + .CANCELED => return error.Canceled, + + .INVAL => |err| return errnoBug(err), + .FAULT => |err| return errnoBug(err), + .AGAIN => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + .NOTCONN => return error.SocketUnconnected, + .CONNRESET => return error.ConnectionResetByPeer, + .TIMEDOUT => return error.Timeout, + .PIPE => return error.SocketUnconnected, + .NETDOWN => return error.NetworkDown, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn netReadWindows(userdata: ?*anyopaque, handle: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize { + if (!have_networking) return error.NetworkDown; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + const bufs = b: { + var iovec_buffer: [max_iovecs_len]ws2_32.WSABUF = undefined; + var i: usize = 0; + var n: usize = 0; + for (data) |buf| { + if (iovec_buffer.len - i == 0) break; + if (buf.len == 0) continue; + if (std.math.cast(u32, buf.len)) |len| { + iovec_buffer[i] = .{ .buf = buf.ptr, .len = len }; + i += 1; + n += len; + continue; + } + iovec_buffer[i] = .{ .buf = buf.ptr, .len = std.math.maxInt(u32) }; + i += 1; + n += std.math.maxInt(u32); + break; + } + + const bufs = iovec_buffer[0..i]; + assert(bufs[0].len != 0); + + break :b bufs; + }; + + while (true) { + try t.checkCancel(); + + var flags: u32 = 0; + var overlapped: windows.OVERLAPPED = std.mem.zeroes(windows.OVERLAPPED); + var n: u32 = undefined; + const rc = ws2_32.WSARecv(handle, bufs.ptr, @intCast(bufs.len), &n, &flags, &overlapped, null); + if (rc != ws2_32.SOCKET_ERROR) return n; + const wsa_error: ws2_32.WinsockError = switch (ws2_32.WSAGetLastError()) { + .IO_PENDING => e: { + var result_flags: u32 = undefined; + const overlapped_rc = ws2_32.WSAGetOverlappedResult( + handle, + &overlapped, + &n, + windows.TRUE, + &result_flags, + ); + if (overlapped_rc == windows.FALSE) { + break :e ws2_32.WSAGetLastError(); + } else { + return n; + } + }, + else => |err| err, + }; + switch (wsa_error) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + + .ECONNRESET => return error.ConnectionResetByPeer, + .EFAULT => unreachable, // a pointer is not completely contained in user address space. + .EINVAL => |err| return wsaErrorBug(err), + .EMSGSIZE => |err| return wsaErrorBug(err), + .ENETDOWN => return error.NetworkDown, + .ENETRESET => return error.ConnectionResetByPeer, + .ENOTCONN => return error.SocketUnconnected, + else => |err| return windows.unexpectedWSAError(err), + } + } +} + +fn netReadUnavailable(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize { + _ = userdata; + _ = fd; + _ = data; + return error.NetworkDown; +} + +fn netSendPosix( + userdata: ?*anyopaque, + handle: net.Socket.Handle, + messages: []net.OutgoingMessage, + flags: net.SendFlags, +) struct { ?net.Socket.SendError, usize } { + if (!have_networking) return .{ error.NetworkDown, 0 }; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + const posix_flags: u32 = + @as(u32, if (@hasDecl(posix.MSG, "CONFIRM") and flags.confirm) posix.MSG.CONFIRM else 0) | + @as(u32, if (@hasDecl(posix.MSG, "DONTROUTE") and flags.dont_route) posix.MSG.DONTROUTE else 0) | + @as(u32, if (@hasDecl(posix.MSG, "EOR") and flags.eor) posix.MSG.EOR else 0) | + @as(u32, if (@hasDecl(posix.MSG, "OOB") and flags.oob) posix.MSG.OOB else 0) | + @as(u32, if (@hasDecl(posix.MSG, "FASTOPEN") and flags.fastopen) posix.MSG.FASTOPEN else 0) | + posix.MSG.NOSIGNAL; + + var i: usize = 0; + while (messages.len - i != 0) { + if (have_sendmmsg) { + i += netSendMany(t, handle, messages[i..], posix_flags) catch |err| return .{ err, i }; + continue; + } + netSendOne(t, handle, &messages[i], posix_flags) catch |err| return .{ err, i }; + i += 1; + } + return .{ null, i }; +} + +fn netSendWindows( + userdata: ?*anyopaque, + handle: net.Socket.Handle, + messages: []net.OutgoingMessage, + flags: net.SendFlags, +) struct { ?net.Socket.SendError, usize } { + if (!have_networking) return .{ error.NetworkDown, 0 }; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + _ = handle; + _ = messages; + _ = flags; + @panic("TODO netSendWindows"); +} + +fn netSendUnavailable( + userdata: ?*anyopaque, + handle: net.Socket.Handle, + messages: []net.OutgoingMessage, + flags: net.SendFlags, +) struct { ?net.Socket.SendError, usize } { + _ = userdata; + _ = handle; + _ = messages; + _ = flags; + return .{ error.NetworkDown, 0 }; +} + +fn netSendOne( + t: *Threaded, + handle: net.Socket.Handle, + message: *net.OutgoingMessage, + flags: u32, +) net.Socket.SendError!void { + var addr: PosixAddress = undefined; + var iovec: posix.iovec_const = .{ .base = @constCast(message.data_ptr), .len = message.data_len }; + const msg: posix.msghdr_const = .{ + .name = &addr.any, + .namelen = addressToPosix(message.address, &addr), + .iov = (&iovec)[0..1], + .iovlen = 1, + // OS returns EINVAL if this pointer is invalid even if controllen is zero. + .control = if (message.control.len == 0) null else @constCast(message.control.ptr), + .controllen = @intCast(message.control.len), + .flags = 0, + }; + while (true) { + try t.checkCancel(); + const rc = posix.system.sendmsg(handle, &msg, flags); + if (is_windows) { + if (rc == ws2_32.SOCKET_ERROR) { + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + .EACCES => return error.AccessDenied, + .EADDRNOTAVAIL => return error.AddressUnavailable, + .ECONNRESET => return error.ConnectionResetByPeer, + .EMSGSIZE => return error.MessageOversize, + .ENOBUFS => return error.SystemResources, + .ENOTSOCK => return error.FileDescriptorNotASocket, + .EAFNOSUPPORT => return error.AddressFamilyUnsupported, + .EDESTADDRREQ => unreachable, // A destination address is required. + .EFAULT => unreachable, // The lpBuffers, lpTo, lpOverlapped, lpNumberOfBytesSent, or lpCompletionRoutine parameters are not part of the user address space, or the lpTo parameter is too small. + .EHOSTUNREACH => return error.NetworkUnreachable, + .EINVAL => unreachable, + .ENETDOWN => return error.NetworkDown, + .ENETRESET => return error.ConnectionResetByPeer, + .ENETUNREACH => return error.NetworkUnreachable, + .ENOTCONN => return error.SocketUnconnected, + .ESHUTDOWN => |err| return wsaErrorBug(err), + else => |err| return windows.unexpectedWSAError(err), + } + } else { + message.data_len = @intCast(rc); + return; + } + } + switch (posix.errno(rc)) { + .SUCCESS => { + message.data_len = @intCast(rc); + return; + }, + .INTR => continue, + .CANCELED => return error.Canceled, + + .ACCES => return error.AccessDenied, + .ALREADY => return error.FastOpenAlreadyInProgress, + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .CONNRESET => return error.ConnectionResetByPeer, + .DESTADDRREQ => |err| return errnoBug(err), + .FAULT => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), + .ISCONN => |err| return errnoBug(err), + .MSGSIZE => return error.MessageOversize, + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + .NOTSOCK => |err| return errnoBug(err), + .OPNOTSUPP => |err| return errnoBug(err), + .PIPE => return error.SocketUnconnected, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .HOSTUNREACH => return error.HostUnreachable, + .NETUNREACH => return error.NetworkUnreachable, + .NOTCONN => return error.SocketUnconnected, + .NETDOWN => return error.NetworkDown, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn netSendMany( + t: *Threaded, + handle: net.Socket.Handle, + messages: []net.OutgoingMessage, + flags: u32, +) net.Socket.SendError!usize { + var msg_buffer: [64]std.os.linux.mmsghdr = undefined; + var addr_buffer: [msg_buffer.len]PosixAddress = undefined; + var iovecs_buffer: [msg_buffer.len]posix.iovec = undefined; + const min_len: usize = @min(messages.len, msg_buffer.len); + const clamped_messages = messages[0..min_len]; + const clamped_msgs = (&msg_buffer)[0..min_len]; + const clamped_addrs = (&addr_buffer)[0..min_len]; + const clamped_iovecs = (&iovecs_buffer)[0..min_len]; + + for (clamped_messages, clamped_msgs, clamped_addrs, clamped_iovecs) |*message, *msg, *addr, *iovec| { + iovec.* = .{ .base = @constCast(message.data_ptr), .len = message.data_len }; + msg.* = .{ + .hdr = .{ + .name = &addr.any, + .namelen = addressToPosix(message.address, addr), + .iov = iovec[0..1], + .iovlen = 1, + .control = @constCast(message.control.ptr), + .controllen = message.control.len, + .flags = 0, + }, + .len = undefined, // Populated by calling sendmmsg below. + }; + } + + while (true) { + try t.checkCancel(); + const rc = posix.system.sendmmsg(handle, clamped_msgs.ptr, @intCast(clamped_msgs.len), flags); + switch (posix.errno(rc)) { + .SUCCESS => { + const n: usize = @intCast(rc); + for (clamped_messages[0..n], clamped_msgs[0..n]) |*message, *msg| { + message.data_len = msg.len; + } + return n; + }, + .INTR => continue, + .CANCELED => return error.Canceled, + + .AGAIN => |err| return errnoBug(err), + .ALREADY => return error.FastOpenAlreadyInProgress, + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .CONNRESET => return error.ConnectionResetByPeer, + .DESTADDRREQ => |err| return errnoBug(err), // The socket is not connection-mode, and no peer address is set. + .FAULT => |err| return errnoBug(err), // An invalid user space address was specified for an argument. + .INVAL => |err| return errnoBug(err), // Invalid argument passed. + .ISCONN => |err| return errnoBug(err), // connection-mode socket was connected already but a recipient was specified + .MSGSIZE => return error.MessageOversize, + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + .NOTSOCK => |err| return errnoBug(err), // The file descriptor sockfd does not refer to a socket. + .OPNOTSUPP => |err| return errnoBug(err), // Some bit in the flags argument is inappropriate for the socket type. + .PIPE => return error.SocketUnconnected, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .HOSTUNREACH => return error.HostUnreachable, + .NETUNREACH => return error.NetworkUnreachable, + .NOTCONN => return error.SocketUnconnected, + .NETDOWN => return error.NetworkDown, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn netReceivePosix( + userdata: ?*anyopaque, + handle: net.Socket.Handle, + message_buffer: []net.IncomingMessage, + data_buffer: []u8, + flags: net.ReceiveFlags, + timeout: Io.Timeout, +) struct { ?net.Socket.ReceiveTimeoutError, usize } { + if (!have_networking) return .{ error.NetworkDown, 0 }; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const t_io = io(t); + + // recvmmsg is useless, here's why: + // * [timeout bug](https://bugzilla.kernel.org/show_bug.cgi?id=75371) + // * it wants iovecs for each message but we have a better API: one data + // buffer to handle all the messages. The better API cannot be lowered to + // the split vectors though because reducing the buffer size might make + // some messages unreceivable. + + // So the strategy instead is to use non-blocking recvmsg calls, calling + // poll() with timeout if the first one returns EAGAIN. + const posix_flags: u32 = + @as(u32, if (flags.oob) posix.MSG.OOB else 0) | + @as(u32, if (flags.peek) posix.MSG.PEEK else 0) | + @as(u32, if (flags.trunc) posix.MSG.TRUNC else 0) | + posix.MSG.DONTWAIT | posix.MSG.NOSIGNAL; + + var poll_fds: [1]posix.pollfd = .{ + .{ + .fd = handle, + .events = posix.POLL.IN, + .revents = undefined, + }, + }; + var message_i: usize = 0; + var data_i: usize = 0; + + const deadline = timeout.toDeadline(t_io) catch |err| return .{ err, message_i }; + + recv: while (true) { + t.checkCancel() catch |err| return .{ err, message_i }; + + if (message_buffer.len - message_i == 0) return .{ null, message_i }; + const message = &message_buffer[message_i]; + const remaining_data_buffer = data_buffer[data_i..]; + var storage: PosixAddress = undefined; + var iov: posix.iovec = .{ .base = remaining_data_buffer.ptr, .len = remaining_data_buffer.len }; + var msg: posix.msghdr = .{ + .name = &storage.any, + .namelen = @sizeOf(PosixAddress), + .iov = (&iov)[0..1], + .iovlen = 1, + .control = message.control.ptr, + .controllen = @intCast(message.control.len), + .flags = undefined, + }; + + const recv_rc = posix.system.recvmsg(handle, &msg, posix_flags); + switch (posix.errno(recv_rc)) { + .SUCCESS => { + const data = remaining_data_buffer[0..@intCast(recv_rc)]; + data_i += data.len; + message.* = .{ + .from = addressFromPosix(&storage), + .data = data, + .control = if (msg.control) |ptr| @as([*]u8, @ptrCast(ptr))[0..msg.controllen] else message.control, + .flags = .{ + .eor = (msg.flags & posix.MSG.EOR) != 0, + .trunc = (msg.flags & posix.MSG.TRUNC) != 0, + .ctrunc = (msg.flags & posix.MSG.CTRUNC) != 0, + .oob = (msg.flags & posix.MSG.OOB) != 0, + .errqueue = if (@hasDecl(posix.MSG, "ERRQUEUE")) (msg.flags & posix.MSG.ERRQUEUE) != 0 else false, + }, + }; + message_i += 1; + continue; + }, + .AGAIN => while (true) { + t.checkCancel() catch |err| return .{ err, message_i }; + if (message_i != 0) return .{ null, message_i }; + + const max_poll_ms = std.math.maxInt(u31); + const timeout_ms: u31 = if (deadline) |d| t: { + const duration = d.durationFromNow(t_io) catch |err| return .{ err, message_i }; + if (duration.raw.nanoseconds <= 0) return .{ error.Timeout, message_i }; + break :t @intCast(@min(max_poll_ms, duration.raw.toMilliseconds())); + } else max_poll_ms; + + const poll_rc = posix.system.poll(&poll_fds, poll_fds.len, timeout_ms); + switch (posix.errno(poll_rc)) { + .SUCCESS => { + if (poll_rc == 0) { + // Although spurious timeouts are OK, when no deadline + // is passed we must not return `error.Timeout`. + if (deadline == null) continue; + return .{ error.Timeout, message_i }; + } + continue :recv; + }, + .INTR => continue, + .CANCELED => return .{ error.Canceled, message_i }, + + .FAULT => |err| return .{ errnoBug(err), message_i }, + .INVAL => |err| return .{ errnoBug(err), message_i }, + .NOMEM => return .{ error.SystemResources, message_i }, + else => |err| return .{ posix.unexpectedErrno(err), message_i }, + } + }, + .INTR => continue, + .CANCELED => return .{ error.Canceled, message_i }, + + .BADF => |err| return .{ errnoBug(err), message_i }, + .NFILE => return .{ error.SystemFdQuotaExceeded, message_i }, + .MFILE => return .{ error.ProcessFdQuotaExceeded, message_i }, + .FAULT => |err| return .{ errnoBug(err), message_i }, + .INVAL => |err| return .{ errnoBug(err), message_i }, + .NOBUFS => return .{ error.SystemResources, message_i }, + .NOMEM => return .{ error.SystemResources, message_i }, + .NOTCONN => return .{ error.SocketUnconnected, message_i }, + .NOTSOCK => |err| return .{ errnoBug(err), message_i }, + .MSGSIZE => return .{ error.MessageOversize, message_i }, + .PIPE => return .{ error.SocketUnconnected, message_i }, + .OPNOTSUPP => |err| return .{ errnoBug(err), message_i }, + .CONNRESET => return .{ error.ConnectionResetByPeer, message_i }, + .NETDOWN => return .{ error.NetworkDown, message_i }, + else => |err| return .{ posix.unexpectedErrno(err), message_i }, + } + } +} + +fn netReceiveWindows( + userdata: ?*anyopaque, + handle: net.Socket.Handle, + message_buffer: []net.IncomingMessage, + data_buffer: []u8, + flags: net.ReceiveFlags, + timeout: Io.Timeout, +) struct { ?net.Socket.ReceiveTimeoutError, usize } { + if (!have_networking) return .{ error.NetworkDown, 0 }; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + _ = handle; + _ = message_buffer; + _ = data_buffer; + _ = flags; + _ = timeout; + @panic("TODO implement netReceiveWindows"); +} + +fn netReceiveUnavailable( + userdata: ?*anyopaque, + handle: net.Socket.Handle, + message_buffer: []net.IncomingMessage, + data_buffer: []u8, + flags: net.ReceiveFlags, + timeout: Io.Timeout, +) struct { ?net.Socket.ReceiveTimeoutError, usize } { + _ = userdata; + _ = handle; + _ = message_buffer; + _ = data_buffer; + _ = flags; + _ = timeout; + return .{ error.NetworkDown, 0 }; +} + +fn netWritePosix( + userdata: ?*anyopaque, + fd: net.Socket.Handle, + header: []const u8, + data: []const []const u8, + splat: usize, +) net.Stream.Writer.Error!usize { + if (!have_networking) return error.NetworkDown; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + var iovecs: [max_iovecs_len]posix.iovec_const = undefined; + var msg: posix.msghdr_const = .{ + .name = null, + .namelen = 0, + .iov = &iovecs, + .iovlen = 0, + .control = null, + .controllen = 0, + .flags = 0, + }; + addBuf(&iovecs, &msg.iovlen, header); + for (data[0 .. data.len - 1]) |bytes| addBuf(&iovecs, &msg.iovlen, bytes); + const pattern = data[data.len - 1]; + if (iovecs.len - msg.iovlen != 0) switch (splat) { + 0 => {}, + 1 => addBuf(&iovecs, &msg.iovlen, pattern), + else => switch (pattern.len) { + 0 => {}, + 1 => { + var backup_buffer: [splat_buffer_size]u8 = undefined; + const splat_buffer = &backup_buffer; + const memset_len = @min(splat_buffer.len, splat); + const buf = splat_buffer[0..memset_len]; + @memset(buf, pattern[0]); + addBuf(&iovecs, &msg.iovlen, buf); + var remaining_splat = splat - buf.len; + while (remaining_splat > splat_buffer.len and iovecs.len - msg.iovlen != 0) { + assert(buf.len == splat_buffer.len); + addBuf(&iovecs, &msg.iovlen, splat_buffer); + remaining_splat -= splat_buffer.len; + } + addBuf(&iovecs, &msg.iovlen, splat_buffer[0..remaining_splat]); + }, + else => for (0..@min(splat, iovecs.len - msg.iovlen)) |_| { + addBuf(&iovecs, &msg.iovlen, pattern); + }, + }, + }; + const flags = posix.MSG.NOSIGNAL; + while (true) { + try t.checkCancel(); + const rc = posix.system.sendmsg(fd, &msg, flags); + switch (posix.errno(rc)) { + .SUCCESS => return @intCast(rc), + .INTR => continue, + .CANCELED => return error.Canceled, + + .ACCES => |err| return errnoBug(err), + .AGAIN => |err| return errnoBug(err), + .ALREADY => return error.FastOpenAlreadyInProgress, + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .CONNRESET => return error.ConnectionResetByPeer, + .DESTADDRREQ => |err| return errnoBug(err), // The socket is not connection-mode, and no peer address is set. + .FAULT => |err| return errnoBug(err), // An invalid user space address was specified for an argument. + .INVAL => |err| return errnoBug(err), // Invalid argument passed. + .ISCONN => |err| return errnoBug(err), // connection-mode socket was connected already but a recipient was specified + .MSGSIZE => |err| return errnoBug(err), + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + .NOTSOCK => |err| return errnoBug(err), // The file descriptor sockfd does not refer to a socket. + .OPNOTSUPP => |err| return errnoBug(err), // Some bit in the flags argument is inappropriate for the socket type. + .PIPE => return error.SocketUnconnected, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .HOSTUNREACH => return error.HostUnreachable, + .NETUNREACH => return error.NetworkUnreachable, + .NOTCONN => return error.SocketUnconnected, + .NETDOWN => return error.NetworkDown, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn netWriteWindows( + userdata: ?*anyopaque, + handle: net.Socket.Handle, + header: []const u8, + data: []const []const u8, + splat: usize, +) net.Stream.Writer.Error!usize { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + comptime assert(native_os == .windows); + + var iovecs: [max_iovecs_len]ws2_32.WSABUF = undefined; + var len: u32 = 0; + addWsaBuf(&iovecs, &len, header); + for (data[0 .. data.len - 1]) |bytes| addWsaBuf(&iovecs, &len, bytes); + const pattern = data[data.len - 1]; + if (iovecs.len - len != 0) switch (splat) { + 0 => {}, + 1 => addWsaBuf(&iovecs, &len, pattern), + else => switch (pattern.len) { + 0 => {}, + 1 => { + var backup_buffer: [64]u8 = undefined; + const splat_buffer = &backup_buffer; + const memset_len = @min(splat_buffer.len, splat); + const buf = splat_buffer[0..memset_len]; + @memset(buf, pattern[0]); + addWsaBuf(&iovecs, &len, buf); + var remaining_splat = splat - buf.len; + while (remaining_splat > splat_buffer.len and len < iovecs.len) { + addWsaBuf(&iovecs, &len, splat_buffer); + remaining_splat -= splat_buffer.len; + } + addWsaBuf(&iovecs, &len, splat_buffer[0..remaining_splat]); + }, + else => for (0..@min(splat, iovecs.len - len)) |_| { + addWsaBuf(&iovecs, &len, pattern); + }, + }, + }; + + while (true) { + try t.checkCancel(); + + var n: u32 = undefined; + var overlapped: windows.OVERLAPPED = std.mem.zeroes(windows.OVERLAPPED); + const rc = ws2_32.WSASend(handle, &iovecs, len, &n, 0, &overlapped, null); + if (rc != ws2_32.SOCKET_ERROR) return n; + const wsa_error: ws2_32.WinsockError = switch (ws2_32.WSAGetLastError()) { + .IO_PENDING => e: { + var result_flags: u32 = undefined; + const overlapped_rc = ws2_32.WSAGetOverlappedResult( + handle, + &overlapped, + &n, + windows.TRUE, + &result_flags, + ); + if (overlapped_rc == windows.FALSE) { + break :e ws2_32.WSAGetLastError(); + } else { + return n; + } + }, + else => |err| err, + }; + switch (wsa_error) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + + .ECONNABORTED => return error.ConnectionResetByPeer, + .ECONNRESET => return error.ConnectionResetByPeer, + .EINVAL => return error.SocketUnconnected, + .ENETDOWN => return error.NetworkDown, + .ENETRESET => return error.ConnectionResetByPeer, + .ENOBUFS => return error.SystemResources, + .ENOTCONN => return error.SocketUnconnected, + .ENOTSOCK => |err| return wsaErrorBug(err), + .EOPNOTSUPP => |err| return wsaErrorBug(err), + .ESHUTDOWN => |err| return wsaErrorBug(err), + else => |err| return windows.unexpectedWSAError(err), + } + } +} + +fn addWsaBuf(v: []ws2_32.WSABUF, i: *u32, bytes: []const u8) void { + const cap = std.math.maxInt(u32); + var remaining = bytes; + while (remaining.len > cap) { + if (v.len - i.* == 0) return; + v[i.*] = .{ .buf = @constCast(remaining.ptr), .len = cap }; + i.* += 1; + remaining = remaining[cap..]; + } else { + @branchHint(.likely); + if (v.len - i.* == 0) return; + v[i.*] = .{ .buf = @constCast(remaining.ptr), .len = @intCast(remaining.len) }; + i.* += 1; + } +} + +fn netWriteUnavailable( + userdata: ?*anyopaque, + handle: net.Socket.Handle, + header: []const u8, + data: []const []const u8, + splat: usize, +) net.Stream.Writer.Error!usize { + _ = userdata; + _ = handle; + _ = header; + _ = data; + _ = splat; + return error.NetworkDown; +} + +fn addBuf(v: []posix.iovec_const, i: *@FieldType(posix.msghdr_const, "iovlen"), bytes: []const u8) void { + // OS checks ptr addr before length so zero length vectors must be omitted. + if (bytes.len == 0) return; + if (v.len - i.* == 0) return; + v[i.*] = .{ .base = bytes.ptr, .len = bytes.len }; + i.* += 1; +} + +fn netClose(userdata: ?*anyopaque, handle: net.Socket.Handle) void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + switch (native_os) { + .windows => closeSocketWindows(handle), + else => posix.close(handle), + } +} + +fn netCloseUnavailable(userdata: ?*anyopaque, handle: net.Socket.Handle) void { + _ = userdata; + _ = handle; + unreachable; // How you gonna close something that was impossible to open? +} + +fn netInterfaceNameResolve( + userdata: ?*anyopaque, + name: *const net.Interface.Name, +) net.Interface.Name.ResolveError!net.Interface { + if (!have_networking) return error.InterfaceNotFound; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + if (native_os == .linux) { + const sock_fd = openSocketPosix(t, posix.AF.UNIX, .{ .mode = .dgram }) catch |err| switch (err) { + error.ProcessFdQuotaExceeded => return error.SystemResources, + error.SystemFdQuotaExceeded => return error.SystemResources, + error.AddressFamilyUnsupported => return error.Unexpected, + error.ProtocolUnsupportedBySystem => return error.Unexpected, + error.ProtocolUnsupportedByAddressFamily => return error.Unexpected, + error.SocketModeUnsupported => return error.Unexpected, + error.OptionUnsupported => return error.Unexpected, + else => |e| return e, + }; + defer posix.close(sock_fd); + + var ifr: posix.ifreq = .{ + .ifrn = .{ .name = @bitCast(name.bytes) }, + .ifru = undefined, + }; + + while (true) { + try t.checkCancel(); + switch (posix.errno(posix.system.ioctl(sock_fd, posix.SIOCGIFINDEX, @intFromPtr(&ifr)))) { + .SUCCESS => return .{ .index = @bitCast(ifr.ifru.ivalue) }, + .INTR => continue, + .CANCELED => return error.Canceled, + + .INVAL => |err| return errnoBug(err), // Bad parameters. + .NOTTY => |err| return errnoBug(err), + .NXIO => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .FAULT => |err| return errnoBug(err), // Bad pointer parameter. + .IO => |err| return errnoBug(err), // sock_fd is not a file descriptor + .NODEV => return error.InterfaceNotFound, + else => |err| return posix.unexpectedErrno(err), + } + } + } + + if (native_os == .windows) { + try t.checkCancel(); + @panic("TODO implement netInterfaceNameResolve for Windows"); + } + + if (builtin.link_libc) { + try t.checkCancel(); + const index = std.c.if_nametoindex(&name.bytes); + if (index == 0) return error.InterfaceNotFound; + return .{ .index = @bitCast(index) }; + } + + @panic("unimplemented"); +} + +fn netInterfaceNameResolveUnavailable( + userdata: ?*anyopaque, + name: *const net.Interface.Name, +) net.Interface.Name.ResolveError!net.Interface { + _ = userdata; + _ = name; + return error.InterfaceNotFound; +} + +fn netInterfaceName(userdata: ?*anyopaque, interface: net.Interface) net.Interface.NameError!net.Interface.Name { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + try t.checkCancel(); + + if (native_os == .linux) { + _ = interface; + @panic("TODO implement netInterfaceName for linux"); + } + + if (native_os == .windows) { + @panic("TODO implement netInterfaceName for windows"); + } + + if (builtin.link_libc) { + @panic("TODO implement netInterfaceName for libc"); + } + + @panic("unimplemented"); +} + +fn netInterfaceNameUnavailable(userdata: ?*anyopaque, interface: net.Interface) net.Interface.NameError!net.Interface.Name { + _ = userdata; + _ = interface; + return error.Unexpected; +} + +fn netLookup( + userdata: ?*anyopaque, + host_name: HostName, + resolved: *Io.Queue(HostName.LookupResult), + options: HostName.LookupOptions, +) void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const t_io = io(t); + resolved.putOneUncancelable(t_io, .{ .end = netLookupFallible(t, host_name, resolved, options) }); +} + +fn netLookupUnavailable( + userdata: ?*anyopaque, + host_name: HostName, + resolved: *Io.Queue(HostName.LookupResult), + options: HostName.LookupOptions, +) void { + _ = host_name; + _ = options; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const t_io = ioBasic(t); + resolved.putOneUncancelable(t_io, .{ .end = error.NetworkDown }); +} + +fn netLookupFallible( + t: *Threaded, + host_name: HostName, + resolved: *Io.Queue(HostName.LookupResult), + options: HostName.LookupOptions, +) !void { + if (!have_networking) return error.NetworkDown; + const t_io = io(t); + const name = host_name.bytes; + assert(name.len <= HostName.max_len); + + if (is_windows) { + var name_buffer: [HostName.max_len + 1]u16 = undefined; + const name_len = std.unicode.wtf8ToWtf16Le(&name_buffer, host_name.bytes) catch + unreachable; // HostName is prevalidated. + name_buffer[name_len] = 0; + const name_w = name_buffer[0..name_len :0]; + + var port_buffer: [8]u8 = undefined; + var port_buffer_wide: [8]u16 = undefined; + const port = std.fmt.bufPrint(&port_buffer, "{d}", .{options.port}) catch + unreachable; // `port_buffer` is big enough for decimal u16. + for (port, port_buffer_wide[0..port.len]) |byte, *wide| + wide.* = std.mem.nativeToLittle(u16, byte); + port_buffer_wide[port.len] = 0; + const port_w = port_buffer_wide[0..port.len :0]; + + const hints: ws2_32.ADDRINFOEXW = .{ + .flags = .{ .NUMERICSERV = true }, + .family = if (options.family) |f| switch (f) { + .ip4 => posix.AF.INET, + .ip6 => posix.AF.INET6, + } else posix.AF.UNSPEC, + .socktype = posix.SOCK.STREAM, + .protocol = posix.IPPROTO.TCP, + .canonname = null, + .addr = null, + .addrlen = 0, + .blob = null, + .bloblen = 0, + .provider = null, + .next = null, + }; + const cancel_handle: ?*windows.HANDLE = null; + var res: *ws2_32.ADDRINFOEXW = undefined; + const timeout: ?*ws2_32.timeval = null; + while (true) { + try t.checkCancel(); // TODO make requestCancel call GetAddrInfoExCancel + // TODO make this append to the queue eagerly rather than blocking until + // the whole thing finishes + const rc: ws2_32.WinsockError = @enumFromInt(ws2_32.GetAddrInfoExW(name_w, port_w, .DNS, null, &hints, &res, timeout, null, null, cancel_handle)); + switch (rc) { + @as(ws2_32.WinsockError, @enumFromInt(0)) => break, + .EINTR => continue, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + .TRY_AGAIN => return error.NameServerFailure, + .EINVAL => |err| return wsaErrorBug(err), + .NO_RECOVERY => return error.NameServerFailure, + .EAFNOSUPPORT => return error.AddressFamilyUnsupported, + .NOT_ENOUGH_MEMORY => return error.SystemResources, + .HOST_NOT_FOUND => return error.UnknownHostName, + .TYPE_NOT_FOUND => return error.ProtocolUnsupportedByAddressFamily, + .ESOCKTNOSUPPORT => return error.ProtocolUnsupportedBySystem, + else => |err| return windows.unexpectedWSAError(err), + } + } + defer ws2_32.FreeAddrInfoExW(res); + + var it: ?*ws2_32.ADDRINFOEXW = res; + var canon_name: ?[*:0]const u16 = null; + while (it) |info| : (it = info.next) { + const addr = info.addr orelse continue; + const storage: WsaAddress = .{ .any = addr.* }; + try resolved.putOne(t_io, .{ .address = addressFromWsa(&storage) }); + + if (info.canonname) |n| { + if (canon_name == null) { + canon_name = n; + } + } + } + if (canon_name) |n| { + const len = std.unicode.wtf16LeToWtf8(options.canonical_name_buffer, std.mem.sliceTo(n, 0)); + try resolved.putOne(t_io, .{ .canonical_name = .{ + .bytes = options.canonical_name_buffer[0..len], + } }); + } + return; + } + + // On Linux, glibc provides getaddrinfo_a which is capable of supporting our semantics. + // However, musl's POSIX-compliant getaddrinfo is not, so we bypass it. + + if (builtin.target.isGnuLibC()) { + // TODO use getaddrinfo_a / gai_cancel + } + + if (native_os == .linux) { + if (options.family != .ip4) { + if (IpAddress.parseIp6(name, options.port)) |addr| { + try resolved.putAll(t_io, &.{ + .{ .address = addr }, + .{ .canonical_name = copyCanon(options.canonical_name_buffer, name) }, + }); + return; + } else |_| {} + } + + if (options.family != .ip6) { + if (IpAddress.parseIp4(name, options.port)) |addr| { + try resolved.putAll(t_io, &.{ + .{ .address = addr }, + .{ .canonical_name = copyCanon(options.canonical_name_buffer, name) }, + }); + return; + } else |_| {} + } + + lookupHosts(t, host_name, resolved, options) catch |err| switch (err) { + error.UnknownHostName => {}, + else => |e| return e, + }; + + // RFC 6761 Section 6.3.3 + // Name resolution APIs and libraries SHOULD recognize + // localhost names as special and SHOULD always return the IP + // loopback address for address queries and negative responses + // for all other query types. + + // Check for equal to "localhost(.)" or ends in ".localhost(.)" + const localhost = if (name[name.len - 1] == '.') "localhost." else "localhost"; + if (std.mem.endsWith(u8, name, localhost) and + (name.len == localhost.len or name[name.len - localhost.len] == '.')) + { + var results_buffer: [3]HostName.LookupResult = undefined; + var results_index: usize = 0; + if (options.family != .ip4) { + results_buffer[results_index] = .{ .address = .{ .ip6 = .loopback(options.port) } }; + results_index += 1; + } + if (options.family != .ip6) { + results_buffer[results_index] = .{ .address = .{ .ip4 = .loopback(options.port) } }; + results_index += 1; + } + const canon_name = "localhost"; + const canon_name_dest = options.canonical_name_buffer[0..canon_name.len]; + canon_name_dest.* = canon_name.*; + results_buffer[results_index] = .{ .canonical_name = .{ .bytes = canon_name_dest } }; + results_index += 1; + try resolved.putAll(t_io, results_buffer[0..results_index]); + return; + } + + return lookupDnsSearch(t, host_name, resolved, options); + } + + if (native_os == .openbsd) { + // TODO use getaddrinfo_async / asr_abort + } + + if (native_os == .freebsd) { + // TODO use dnsres_getaddrinfo + } + + if (native_os.isDarwin()) { + // TODO use CFHostStartInfoResolution / CFHostCancelInfoResolution + } + + if (builtin.link_libc) { + // This operating system lacks a way to resolve asynchronously. We are + // stuck with getaddrinfo. + var name_buffer: [HostName.max_len + 1]u8 = undefined; + @memcpy(name_buffer[0..host_name.bytes.len], host_name.bytes); + name_buffer[host_name.bytes.len] = 0; + const name_c = name_buffer[0..host_name.bytes.len :0]; + + var port_buffer: [8]u8 = undefined; + const port_c = std.fmt.bufPrintZ(&port_buffer, "{d}", .{options.port}) catch unreachable; + + const hints: posix.addrinfo = .{ + .flags = .{ .NUMERICSERV = true }, + .family = posix.AF.UNSPEC, + .socktype = posix.SOCK.STREAM, + .protocol = posix.IPPROTO.TCP, + .canonname = null, + .addr = null, + .addrlen = 0, + .next = null, + }; + var res: ?*posix.addrinfo = null; + while (true) { + try t.checkCancel(); + switch (posix.system.getaddrinfo(name_c.ptr, port_c.ptr, &hints, &res)) { + @as(posix.system.EAI, @enumFromInt(0)) => break, + .ADDRFAMILY => return error.AddressFamilyUnsupported, + .AGAIN => return error.NameServerFailure, + .FAIL => return error.NameServerFailure, + .FAMILY => return error.AddressFamilyUnsupported, + .MEMORY => return error.SystemResources, + .NODATA => return error.UnknownHostName, + .NONAME => return error.UnknownHostName, + .SYSTEM => switch (posix.errno(-1)) { + .INTR => continue, + .CANCELED => return error.Canceled, + else => |e| return posix.unexpectedErrno(e), + }, + else => return error.Unexpected, + } + } + defer if (res) |some| posix.system.freeaddrinfo(some); + + var it = res; + var canon_name: ?[*:0]const u8 = null; + while (it) |info| : (it = info.next) { + const addr = info.addr orelse continue; + const storage: PosixAddress = .{ .any = addr.* }; + try resolved.putOne(t_io, .{ .address = addressFromPosix(&storage) }); + + if (info.canonname) |n| { + if (canon_name == null) { + canon_name = n; + } + } + } + if (canon_name) |n| { + try resolved.putOne(t_io, .{ + .canonical_name = copyCanon(options.canonical_name_buffer, std.mem.sliceTo(n, 0)), + }); + } + return; + } + + return error.OptionUnsupported; +} + +pub const PosixAddress = extern union { + any: posix.sockaddr, + in: posix.sockaddr.in, + in6: posix.sockaddr.in6, +}; + +const UnixAddress = extern union { + any: posix.sockaddr, + un: posix.sockaddr.un, +}; + +const WsaAddress = extern union { + any: ws2_32.sockaddr, + in: ws2_32.sockaddr.in, + in6: ws2_32.sockaddr.in6, + un: ws2_32.sockaddr.un, +}; + +pub fn posixAddressFamily(a: *const IpAddress) posix.sa_family_t { + return switch (a.*) { + .ip4 => posix.AF.INET, + .ip6 => posix.AF.INET6, + }; +} + +pub fn addressFromPosix(posix_address: *const PosixAddress) IpAddress { + return switch (posix_address.any.family) { + posix.AF.INET => .{ .ip4 = address4FromPosix(&posix_address.in) }, + posix.AF.INET6 => .{ .ip6 = address6FromPosix(&posix_address.in6) }, + else => .{ .ip4 = .loopback(0) }, + }; +} + +fn addressFromWsa(wsa_address: *const WsaAddress) IpAddress { + return switch (wsa_address.any.family) { + posix.AF.INET => .{ .ip4 = address4FromWsa(&wsa_address.in) }, + posix.AF.INET6 => .{ .ip6 = address6FromWsa(&wsa_address.in6) }, + else => .{ .ip4 = .loopback(0) }, + }; +} + +pub fn addressToPosix(a: *const IpAddress, storage: *PosixAddress) posix.socklen_t { + return switch (a.*) { + .ip4 => |ip4| { + storage.in = address4ToPosix(ip4); + return @sizeOf(posix.sockaddr.in); + }, + .ip6 => |*ip6| { + storage.in6 = address6ToPosix(ip6); + return @sizeOf(posix.sockaddr.in6); + }, + }; +} + +fn addressToWsa(a: *const IpAddress, storage: *WsaAddress) i32 { + return switch (a.*) { + .ip4 => |ip4| { + storage.in = address4ToPosix(ip4); + return @sizeOf(posix.sockaddr.in); + }, + .ip6 => |*ip6| { + storage.in6 = address6ToPosix(ip6); + return @sizeOf(posix.sockaddr.in6); + }, + }; +} + +fn addressUnixToPosix(a: *const net.UnixAddress, storage: *UnixAddress) posix.socklen_t { + @memcpy(storage.un.path[0..a.path.len], a.path); + storage.un.family = posix.AF.UNIX; + storage.un.path[a.path.len] = 0; + return @sizeOf(posix.sockaddr.un); +} + +fn addressUnixToWsa(a: *const net.UnixAddress, storage: *WsaAddress) i32 { + @memcpy(storage.un.path[0..a.path.len], a.path); + storage.un.family = posix.AF.UNIX; + storage.un.path[a.path.len] = 0; + return @sizeOf(posix.sockaddr.un); +} + +fn address4FromPosix(in: *const posix.sockaddr.in) net.Ip4Address { + return .{ + .port = std.mem.bigToNative(u16, in.port), + .bytes = @bitCast(in.addr), + }; +} + +fn address6FromPosix(in6: *const posix.sockaddr.in6) net.Ip6Address { + return .{ + .port = std.mem.bigToNative(u16, in6.port), + .bytes = in6.addr, + .flow = in6.flowinfo, + .interface = .{ .index = in6.scope_id }, + }; +} + +fn address4FromWsa(in: *const ws2_32.sockaddr.in) net.Ip4Address { + return .{ + .port = std.mem.bigToNative(u16, in.port), + .bytes = @bitCast(in.addr), + }; +} + +fn address6FromWsa(in6: *const ws2_32.sockaddr.in6) net.Ip6Address { + return .{ + .port = std.mem.bigToNative(u16, in6.port), + .bytes = in6.addr, + .flow = in6.flowinfo, + .interface = .{ .index = in6.scope_id }, + }; +} + +fn address4ToPosix(a: net.Ip4Address) posix.sockaddr.in { + return .{ + .port = std.mem.nativeToBig(u16, a.port), + .addr = @bitCast(a.bytes), + }; +} + +fn address6ToPosix(a: *const net.Ip6Address) posix.sockaddr.in6 { + return .{ + .port = std.mem.nativeToBig(u16, a.port), + .flowinfo = a.flow, + .addr = a.bytes, + .scope_id = a.interface.index, + }; +} + +pub fn errnoBug(err: posix.E) Io.UnexpectedError { + if (is_debug) std.debug.panic("programmer bug caused syscall error: {t}", .{err}); + return error.Unexpected; +} + +fn wsaErrorBug(err: ws2_32.WinsockError) Io.UnexpectedError { + if (is_debug) std.debug.panic("programmer bug caused syscall error: {t}", .{err}); + return error.Unexpected; +} + +pub fn posixSocketMode(mode: net.Socket.Mode) u32 { + return switch (mode) { + .stream => posix.SOCK.STREAM, + .dgram => posix.SOCK.DGRAM, + .seqpacket => posix.SOCK.SEQPACKET, + .raw => posix.SOCK.RAW, + .rdm => posix.SOCK.RDM, + }; +} + +pub fn posixProtocol(protocol: ?net.Protocol) u32 { + return @intFromEnum(protocol orelse return 0); +} + +fn recoverableOsBugDetected() void { + if (is_debug) unreachable; +} + +fn clockToPosix(clock: Io.Clock) posix.clockid_t { + return switch (clock) { + .real => posix.CLOCK.REALTIME, + .awake => switch (native_os) { + .macos, .ios, .watchos, .tvos => posix.CLOCK.UPTIME_RAW, + else => posix.CLOCK.MONOTONIC, + }, + .boot => switch (native_os) { + .macos, .ios, .watchos, .tvos => posix.CLOCK.MONOTONIC_RAW, + // On freebsd derivatives, use MONOTONIC_FAST as currently there's + // no precision tradeoff. + .freebsd, .dragonfly => posix.CLOCK.MONOTONIC_FAST, + // On linux, use BOOTTIME instead of MONOTONIC as it ticks while + // suspended. + .linux => posix.CLOCK.BOOTTIME, + // On other posix systems, MONOTONIC is generally the fastest and + // ticks while suspended. + else => posix.CLOCK.MONOTONIC, + }, + .cpu_process => posix.CLOCK.PROCESS_CPUTIME_ID, + .cpu_thread => posix.CLOCK.THREAD_CPUTIME_ID, + }; +} + +fn clockToWasi(clock: Io.Clock) std.os.wasi.clockid_t { + return switch (clock) { + .real => .REALTIME, + .awake => .MONOTONIC, + .boot => .MONOTONIC, + .cpu_process => .PROCESS_CPUTIME_ID, + .cpu_thread => .THREAD_CPUTIME_ID, + }; +} + +fn statFromLinux(stx: *const std.os.linux.Statx) Io.File.Stat { + const atime = stx.atime; + const mtime = stx.mtime; + const ctime = stx.ctime; + return .{ + .inode = stx.ino, + .size = stx.size, + .mode = stx.mode, + .kind = switch (stx.mode & std.os.linux.S.IFMT) { + std.os.linux.S.IFDIR => .directory, + std.os.linux.S.IFCHR => .character_device, + std.os.linux.S.IFBLK => .block_device, + std.os.linux.S.IFREG => .file, + std.os.linux.S.IFIFO => .named_pipe, + std.os.linux.S.IFLNK => .sym_link, + std.os.linux.S.IFSOCK => .unix_domain_socket, + else => .unknown, + }, + .atime = .{ .nanoseconds = @intCast(@as(i128, atime.sec) * std.time.ns_per_s + atime.nsec) }, + .mtime = .{ .nanoseconds = @intCast(@as(i128, mtime.sec) * std.time.ns_per_s + mtime.nsec) }, + .ctime = .{ .nanoseconds = @intCast(@as(i128, ctime.sec) * std.time.ns_per_s + ctime.nsec) }, + }; +} + +fn statFromPosix(st: *const posix.Stat) Io.File.Stat { + const atime = st.atime(); + const mtime = st.mtime(); + const ctime = st.ctime(); + return .{ + .inode = st.ino, + .size = @bitCast(st.size), + .mode = st.mode, + .kind = k: { + const m = st.mode & posix.S.IFMT; + switch (m) { + posix.S.IFBLK => break :k .block_device, + posix.S.IFCHR => break :k .character_device, + posix.S.IFDIR => break :k .directory, + posix.S.IFIFO => break :k .named_pipe, + posix.S.IFLNK => break :k .sym_link, + posix.S.IFREG => break :k .file, + posix.S.IFSOCK => break :k .unix_domain_socket, + else => {}, + } + if (native_os == .illumos) switch (m) { + posix.S.IFDOOR => break :k .door, + posix.S.IFPORT => break :k .event_port, + else => {}, + }; + + break :k .unknown; + }, + .atime = timestampFromPosix(&atime), + .mtime = timestampFromPosix(&mtime), + .ctime = timestampFromPosix(&ctime), + }; +} + +fn statFromWasi(st: *const std.os.wasi.filestat_t) Io.File.Stat { + return .{ + .inode = st.ino, + .size = @bitCast(st.size), + .mode = 0, + .kind = switch (st.filetype) { + .BLOCK_DEVICE => .block_device, + .CHARACTER_DEVICE => .character_device, + .DIRECTORY => .directory, + .SYMBOLIC_LINK => .sym_link, + .REGULAR_FILE => .file, + .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket, + else => .unknown, + }, + .atime = .fromNanoseconds(st.atim), + .mtime = .fromNanoseconds(st.mtim), + .ctime = .fromNanoseconds(st.ctim), + }; +} + +fn timestampFromPosix(timespec: *const posix.timespec) Io.Timestamp { + return .{ .nanoseconds = @intCast(@as(i128, timespec.sec) * std.time.ns_per_s + timespec.nsec) }; +} + +fn timestampToPosix(nanoseconds: i96) posix.timespec { + return .{ + .sec = @intCast(@divFloor(nanoseconds, std.time.ns_per_s)), + .nsec = @intCast(@mod(nanoseconds, std.time.ns_per_s)), + }; +} + +fn pathToPosix(file_path: []const u8, buffer: *[posix.PATH_MAX]u8) Io.Dir.PathNameError![:0]u8 { + if (std.mem.containsAtLeastScalar2(u8, file_path, 0, 1)) return error.BadPathName; + // >= rather than > to make room for the null byte + if (file_path.len >= buffer.len) return error.NameTooLong; + @memcpy(buffer[0..file_path.len], file_path); + buffer[file_path.len] = 0; + return buffer[0..file_path.len :0]; +} + +fn lookupDnsSearch( + t: *Threaded, + host_name: HostName, + resolved: *Io.Queue(HostName.LookupResult), + options: HostName.LookupOptions, +) HostName.LookupError!void { + const t_io = io(t); + const rc = HostName.ResolvConf.init(t_io) catch return error.ResolvConfParseFailed; + + // Count dots, suppress search when >=ndots or name ends in + // a dot, which is an explicit request for global scope. + const dots = std.mem.countScalar(u8, host_name.bytes, '.'); + const search_len = if (dots >= rc.ndots or std.mem.endsWith(u8, host_name.bytes, ".")) 0 else rc.search_len; + const search = rc.search_buffer[0..search_len]; + + var canon_name = host_name.bytes; + + // Strip final dot for canon, fail if multiple trailing dots. + if (std.mem.endsWith(u8, canon_name, ".")) canon_name.len -= 1; + if (std.mem.endsWith(u8, canon_name, ".")) return error.UnknownHostName; + + // Name with search domain appended is set up in `canon_name`. This + // both provides the desired default canonical name (if the requested + // name is not a CNAME record) and serves as a buffer for passing the + // full requested name to `lookupDns`. + @memcpy(options.canonical_name_buffer[0..canon_name.len], canon_name); + options.canonical_name_buffer[canon_name.len] = '.'; + var it = std.mem.tokenizeAny(u8, search, " \t"); + while (it.next()) |token| { + @memcpy(options.canonical_name_buffer[canon_name.len + 1 ..][0..token.len], token); + const lookup_canon_name = options.canonical_name_buffer[0 .. canon_name.len + 1 + token.len]; + if (lookupDns(t, lookup_canon_name, &rc, resolved, options)) |result| { + return result; + } else |err| switch (err) { + error.UnknownHostName => continue, + else => |e| return e, + } + } + + const lookup_canon_name = options.canonical_name_buffer[0..canon_name.len]; + return lookupDns(t, lookup_canon_name, &rc, resolved, options); +} + +fn lookupDns( + t: *Threaded, + lookup_canon_name: []const u8, + rc: *const HostName.ResolvConf, + resolved: *Io.Queue(HostName.LookupResult), + options: HostName.LookupOptions, +) HostName.LookupError!void { + const t_io = io(t); + const family_records: [2]struct { af: IpAddress.Family, rr: HostName.DnsRecord } = .{ + .{ .af = .ip6, .rr = .A }, + .{ .af = .ip4, .rr = .AAAA }, + }; + var query_buffers: [2][280]u8 = undefined; + var answer_buffer: [2 * 512]u8 = undefined; + var queries_buffer: [2][]const u8 = undefined; + var answers_buffer: [2][]const u8 = undefined; + var nq: usize = 0; + var answer_buffer_i: usize = 0; + + for (family_records) |fr| { + if (options.family != fr.af) { + const entropy = std.crypto.random.array(u8, 2); + const len = writeResolutionQuery(&query_buffers[nq], 0, lookup_canon_name, 1, fr.rr, entropy); + queries_buffer[nq] = query_buffers[nq][0..len]; + nq += 1; + } + } + + var ip4_mapped_buffer: [HostName.ResolvConf.max_nameservers]IpAddress = undefined; + const ip4_mapped = ip4_mapped_buffer[0..rc.nameservers_len]; + var any_ip6 = false; + for (rc.nameservers(), ip4_mapped) |*ns, *m| { + m.* = .{ .ip6 = .fromAny(ns.*) }; + any_ip6 = any_ip6 or ns.* == .ip6; + } + var socket = s: { + if (any_ip6) ip6: { + const ip6_addr: IpAddress = .{ .ip6 = .unspecified(0) }; + const socket = ip6_addr.bind(t_io, .{ .ip6_only = true, .mode = .dgram }) catch |err| switch (err) { + error.AddressFamilyUnsupported => break :ip6, + else => |e| return e, + }; + break :s socket; + } + any_ip6 = false; + const ip4_addr: IpAddress = .{ .ip4 = .unspecified(0) }; + const socket = try ip4_addr.bind(t_io, .{ .mode = .dgram }); + break :s socket; + }; + defer socket.close(t_io); + + const mapped_nameservers = if (any_ip6) ip4_mapped else rc.nameservers(); + const queries = queries_buffer[0..nq]; + const answers = answers_buffer[0..queries.len]; + var answers_remaining = answers.len; + for (answers) |*answer| answer.len = 0; + + // boot clock is chosen because time the computer is suspended should count + // against time spent waiting for external messages to arrive. + const clock: Io.Clock = .boot; + var now_ts = try clock.now(t_io); + const final_ts = now_ts.addDuration(.fromSeconds(rc.timeout_seconds)); + const attempt_duration: Io.Duration = .{ + .nanoseconds = (std.time.ns_per_s / rc.attempts) * @as(i96, rc.timeout_seconds), + }; + + send: while (now_ts.nanoseconds < final_ts.nanoseconds) : (now_ts = try clock.now(t_io)) { + const max_messages = queries_buffer.len * HostName.ResolvConf.max_nameservers; + { + var message_buffer: [max_messages]Io.net.OutgoingMessage = undefined; + var message_i: usize = 0; + for (queries, answers) |query, *answer| { + if (answer.len != 0) continue; + for (mapped_nameservers) |*ns| { + message_buffer[message_i] = .{ + .address = ns, + .data_ptr = query.ptr, + .data_len = query.len, + }; + message_i += 1; + } + } + _ = netSendPosix(t, socket.handle, message_buffer[0..message_i], .{}); + } + + const timeout: Io.Timeout = .{ .deadline = .{ + .raw = now_ts.addDuration(attempt_duration), + .clock = clock, + } }; + + while (true) { + var message_buffer: [max_messages]Io.net.IncomingMessage = @splat(.init); + const buf = answer_buffer[answer_buffer_i..]; + const recv_err, const recv_n = socket.receiveManyTimeout(t_io, &message_buffer, buf, .{}, timeout); + for (message_buffer[0..recv_n]) |*received_message| { + const reply = received_message.data; + // Ignore non-identifiable packets. + if (reply.len < 4) continue; + + // Ignore replies from addresses we didn't send to. + const ns = for (mapped_nameservers) |*ns| { + if (received_message.from.eql(ns)) break ns; + } else { + continue; + }; + + // Find which query this answer goes with, if any. + const query, const answer = for (queries, answers) |query, *answer| { + if (reply[0] == query[0] and reply[1] == query[1]) break .{ query, answer }; + } else { + continue; + }; + if (answer.len != 0) continue; + + // Only accept positive or negative responses; retry immediately on + // server failure, and ignore all other codes such as refusal. + switch (reply[3] & 15) { + 0, 3 => { + answer.* = reply; + answer_buffer_i += reply.len; + answers_remaining -= 1; + if (answer_buffer.len - answer_buffer_i == 0) break :send; + if (answers_remaining == 0) break :send; + }, + 2 => { + var retry_message: Io.net.OutgoingMessage = .{ + .address = ns, + .data_ptr = query.ptr, + .data_len = query.len, + }; + _ = netSendPosix(t, socket.handle, (&retry_message)[0..1], .{}); + continue; + }, + else => continue, + } + } + if (recv_err) |err| switch (err) { + error.Canceled => return error.Canceled, + error.Timeout => continue :send, + else => continue, + }; + } + } else { + return error.NameServerFailure; + } + + var addresses_len: usize = 0; + var canonical_name: ?HostName = null; + + for (answers) |answer| { + var it = HostName.DnsResponse.init(answer) catch { + // Here we could potentially add diagnostics to the results queue. + continue; + }; + while (it.next() catch { + // Here we could potentially add diagnostics to the results queue. + continue; + }) |record| switch (record.rr) { + .A => { + const data = record.packet[record.data_off..][0..record.data_len]; + if (data.len != 4) return error.InvalidDnsARecord; + try resolved.putOne(t_io, .{ .address = .{ .ip4 = .{ + .bytes = data[0..4].*, + .port = options.port, + } } }); + addresses_len += 1; + }, + .AAAA => { + const data = record.packet[record.data_off..][0..record.data_len]; + if (data.len != 16) return error.InvalidDnsAAAARecord; + try resolved.putOne(t_io, .{ .address = .{ .ip6 = .{ + .bytes = data[0..16].*, + .port = options.port, + } } }); + addresses_len += 1; + }, + .CNAME => { + _, canonical_name = HostName.expand(record.packet, record.data_off, options.canonical_name_buffer) catch + return error.InvalidDnsCnameRecord; + }, + _ => continue, + }; + } + + try resolved.putOne(t_io, .{ .canonical_name = canonical_name orelse .{ .bytes = lookup_canon_name } }); + if (addresses_len == 0) return error.NameServerFailure; +} + +fn lookupHosts( + t: *Threaded, + host_name: HostName, + resolved: *Io.Queue(HostName.LookupResult), + options: HostName.LookupOptions, +) !void { + const t_io = io(t); + const file = Io.File.openAbsolute(t_io, "/etc/hosts", .{}) catch |err| switch (err) { + error.FileNotFound, + error.NotDir, + error.AccessDenied, + => return error.UnknownHostName, + + error.Canceled => |e| return e, + + else => { + // Here we could add more detailed diagnostics to the results queue. + return error.DetectingNetworkConfigurationFailed; + }, + }; + defer file.close(t_io); + + var line_buf: [512]u8 = undefined; + var file_reader = file.reader(t_io, &line_buf); + return lookupHostsReader(t, host_name, resolved, options, &file_reader.interface) catch |err| switch (err) { + error.ReadFailed => switch (file_reader.err.?) { + error.Canceled => |e| return e, + else => { + // Here we could add more detailed diagnostics to the results queue. + return error.DetectingNetworkConfigurationFailed; + }, + }, + error.Canceled => |e| return e, + error.UnknownHostName => |e| return e, + }; +} + +fn lookupHostsReader( + t: *Threaded, + host_name: HostName, + resolved: *Io.Queue(HostName.LookupResult), + options: HostName.LookupOptions, + reader: *Io.Reader, +) error{ ReadFailed, Canceled, UnknownHostName }!void { + const t_io = io(t); + var addresses_len: usize = 0; + var canonical_name: ?HostName = null; + while (true) { + const line = reader.takeDelimiterExclusive('\n') catch |err| switch (err) { + error.StreamTooLong => { + // Skip lines that are too long. + _ = reader.discardDelimiterInclusive('\n') catch |e| switch (e) { + error.EndOfStream => break, + error.ReadFailed => return error.ReadFailed, + }; + continue; + }, + error.ReadFailed => return error.ReadFailed, + error.EndOfStream => break, + }; + reader.toss(1); + var split_it = std.mem.splitScalar(u8, line, '#'); + const no_comment_line = split_it.first(); + + var line_it = std.mem.tokenizeAny(u8, no_comment_line, " \t"); + const ip_text = line_it.next() orelse continue; + var first_name_text: ?[]const u8 = null; + while (line_it.next()) |name_text| { + if (std.mem.eql(u8, name_text, host_name.bytes)) { + if (first_name_text == null) first_name_text = name_text; + break; + } + } else continue; + + if (canonical_name == null) { + if (HostName.init(first_name_text.?)) |name_text| { + if (name_text.bytes.len <= options.canonical_name_buffer.len) { + const canonical_name_dest = options.canonical_name_buffer[0..name_text.bytes.len]; + @memcpy(canonical_name_dest, name_text.bytes); + canonical_name = .{ .bytes = canonical_name_dest }; + } + } else |_| {} + } + + if (options.family != .ip6) { + if (IpAddress.parseIp4(ip_text, options.port)) |addr| { + try resolved.putOne(t_io, .{ .address = addr }); + addresses_len += 1; + } else |_| {} + } + if (options.family != .ip4) { + if (IpAddress.parseIp6(ip_text, options.port)) |addr| { + try resolved.putOne(t_io, .{ .address = addr }); + addresses_len += 1; + } else |_| {} + } + } + + if (canonical_name) |canon_name| try resolved.putOne(t_io, .{ .canonical_name = canon_name }); + if (addresses_len == 0) return error.UnknownHostName; +} + +/// Writes DNS resolution query packet data to `w`; at most 280 bytes. +fn writeResolutionQuery(q: *[280]u8, op: u4, dname: []const u8, class: u8, ty: HostName.DnsRecord, entropy: [2]u8) usize { + // This implementation is ported from musl libc. + // A more idiomatic "ziggy" implementation would be welcome. + var name = dname; + if (std.mem.endsWith(u8, name, ".")) name.len -= 1; + assert(name.len <= 253); + const n = 17 + name.len + @intFromBool(name.len != 0); + + // Construct query template - ID will be filled later + q[0..2].* = entropy; + @memset(q[2..n], 0); + q[2] = @as(u8, op) * 8 + 1; + q[5] = 1; + @memcpy(q[13..][0..name.len], name); + var i: usize = 13; + var j: usize = undefined; + while (q[i] != 0) : (i = j + 1) { + j = i; + while (q[j] != 0 and q[j] != '.') : (j += 1) {} + // TODO determine the circumstances for this and whether or + // not this should be an error. + if (j - i - 1 > 62) unreachable; + q[i - 1] = @intCast(j - i); + } + q[i + 1] = @intFromEnum(ty); + q[i + 3] = class; + return n; +} + +fn copyCanon(canonical_name_buffer: *[HostName.max_len]u8, name: []const u8) HostName { + const dest = canonical_name_buffer[0..name.len]; + @memcpy(dest, name); + return .{ .bytes = dest }; +} + +/// Darwin XNU 7195.50.7.100.1 introduced __ulock_wait2 and migrated code paths (notably pthread_cond_t) towards it: +/// https://github.com/apple/darwin-xnu/commit/d4061fb0260b3ed486147341b72468f836ed6c8f#diff-08f993cc40af475663274687b7c326cc6c3031e0db3ac8de7b24624610616be6 +/// +/// This XNU version appears to correspond to 11.0.1: +/// https://kernelshaman.blogspot.com/2021/01/building-xnu-for-macos-big-sur-1101.html +/// +/// ulock_wait() uses 32-bit micro-second timeouts where 0 = INFINITE or no-timeout +/// ulock_wait2() uses 64-bit nano-second timeouts (with the same convention) +const darwin_supports_ulock_wait2 = builtin.os.version_range.semver.min.major >= 11; + +fn futexWait(t: *Threaded, ptr: *const std.atomic.Value(u32), expect: u32) Io.Cancelable!void { + @branchHint(.cold); + + if (builtin.cpu.arch.isWasm()) { + comptime assert(builtin.cpu.has(.wasm, .atomics)); + try t.checkCancel(); + const timeout: i64 = -1; + const signed_expect: i32 = @bitCast(expect); + const result = asm volatile ( + \\local.get %[ptr] + \\local.get %[expected] + \\local.get %[timeout] + \\memory.atomic.wait32 0 + \\local.set %[ret] + : [ret] "=r" (-> u32), + : [ptr] "r" (&ptr.raw), + [expected] "r" (signed_expect), + [timeout] "r" (timeout), + ); + switch (result) { + 0 => {}, // ok + 1 => {}, // expected != loaded + 2 => assert(!is_debug), // timeout + else => assert(!is_debug), + } + } else switch (native_os) { + .linux => { + const linux = std.os.linux; + try t.checkCancel(); + const rc = linux.futex_4arg(ptr, .{ .cmd = .WAIT, .private = true }, expect, null); + if (is_debug) switch (linux.E.init(rc)) { + .SUCCESS => {}, // notified by `wake()` + .INTR => {}, // gives caller a chance to check cancellation + .AGAIN => {}, // ptr.* != expect + .INVAL => {}, // possibly timeout overflow + .TIMEDOUT => unreachable, + .FAULT => unreachable, // ptr was invalid + else => unreachable, + }; + }, + .driverkit, .ios, .macos, .tvos, .visionos, .watchos => { + const c = std.c; + const flags: c.UL = .{ + .op = .COMPARE_AND_WAIT, + .NO_ERRNO = true, + }; + try t.checkCancel(); + const status = if (darwin_supports_ulock_wait2) + c.__ulock_wait2(flags, ptr, expect, 0, 0) + else + c.__ulock_wait(flags, ptr, expect, 0); + + if (status >= 0) return; + + if (is_debug) switch (@as(c.E, @enumFromInt(-status))) { + .INTR => {}, // spurious wake + // Address of the futex was paged out. This is unlikely, but possible in theory, and + // pthread/libdispatch on darwin bother to handle it. In this case we'll return + // without waiting, but the caller should retry anyway. + .FAULT => {}, + .TIMEDOUT => unreachable, + else => unreachable, + }; + }, + .windows => { + try t.checkCancel(); + switch (windows.ntdll.RtlWaitOnAddress(ptr, &expect, @sizeOf(@TypeOf(expect)), null)) { + .SUCCESS => {}, + .CANCELLED => return error.Canceled, + else => recoverableOsBugDetected(), + } + }, + .freebsd => { + const flags = @intFromEnum(std.c.UMTX_OP.WAIT_UINT_PRIVATE); + try t.checkCancel(); + const rc = std.c._umtx_op(@intFromPtr(&ptr.raw), flags, @as(c_ulong, expect), 0, 0); + if (is_debug) switch (posix.errno(rc)) { + .SUCCESS => {}, + .FAULT => unreachable, // one of the args points to invalid memory + .INVAL => unreachable, // arguments should be correct + .TIMEDOUT => unreachable, // no timeout provided + .INTR => {}, // spurious wake + else => unreachable, + }; + }, + else => @compileError("unimplemented: futexWait"), + } +} + +pub fn futexWaitUncancelable(ptr: *const std.atomic.Value(u32), expect: u32) void { + @branchHint(.cold); + + if (builtin.cpu.arch.isWasm()) { + comptime assert(builtin.cpu.has(.wasm, .atomics)); + const timeout: i64 = -1; + const signed_expect: i32 = @bitCast(expect); + const result = asm volatile ( + \\local.get %[ptr] + \\local.get %[expected] + \\local.get %[timeout] + \\memory.atomic.wait32 0 + \\local.set %[ret] + : [ret] "=r" (-> u32), + : [ptr] "r" (&ptr.raw), + [expected] "r" (signed_expect), + [timeout] "r" (timeout), + ); + switch (result) { + 0 => {}, // ok + 1 => {}, // expected != loaded + 2 => recoverableOsBugDetected(), // timeout + else => recoverableOsBugDetected(), + } + } else switch (native_os) { + .linux => { + const linux = std.os.linux; + const rc = linux.futex_4arg(ptr, .{ .cmd = .WAIT, .private = true }, expect, null); + switch (linux.E.init(rc)) { + .SUCCESS => {}, // notified by `wake()` + .INTR => {}, // gives caller a chance to check cancellation + .AGAIN => {}, // ptr.* != expect + .INVAL => {}, // possibly timeout overflow + .TIMEDOUT => recoverableOsBugDetected(), + .FAULT => recoverableOsBugDetected(), // ptr was invalid + else => recoverableOsBugDetected(), + } + }, + .driverkit, .ios, .macos, .tvos, .visionos, .watchos => { + const c = std.c; + const flags: c.UL = .{ + .op = .COMPARE_AND_WAIT, + .NO_ERRNO = true, + }; + const status = if (darwin_supports_ulock_wait2) + c.__ulock_wait2(flags, ptr, expect, 0, 0) + else + c.__ulock_wait(flags, ptr, expect, 0); + + if (status >= 0) return; + + switch (@as(c.E, @enumFromInt(-status))) { + // Wait was interrupted by the OS or other spurious signalling. + .INTR => {}, + // Address of the futex was paged out. This is unlikely, but possible in theory, and + // pthread/libdispatch on darwin bother to handle it. In this case we'll return + // without waiting, but the caller should retry anyway. + .FAULT => {}, + .TIMEDOUT => recoverableOsBugDetected(), + else => recoverableOsBugDetected(), + } + }, + .windows => { + switch (windows.ntdll.RtlWaitOnAddress(ptr, &expect, @sizeOf(@TypeOf(expect)), null)) { + .SUCCESS, .CANCELLED => {}, + else => recoverableOsBugDetected(), + } + }, + .freebsd => { + const flags = @intFromEnum(std.c.UMTX_OP.WAIT_UINT_PRIVATE); + const rc = std.c._umtx_op(@intFromPtr(&ptr.raw), flags, @as(c_ulong, expect), 0, 0); + switch (posix.errno(rc)) { + .SUCCESS => {}, + .INTR => {}, // spurious wake + .FAULT => recoverableOsBugDetected(), // one of the args points to invalid memory + .INVAL => recoverableOsBugDetected(), // arguments should be correct + .TIMEDOUT => recoverableOsBugDetected(), // no timeout provided + else => recoverableOsBugDetected(), + } + }, + else => @compileError("unimplemented: futexWaitUncancelable"), + } +} + +pub fn futexWaitDurationUncancelable(ptr: *const std.atomic.Value(u32), expect: u32, timeout: Io.Duration) void { + @branchHint(.cold); + + if (native_os == .linux) { + const linux = std.os.linux; + var ts = timestampToPosix(timeout.toNanoseconds()); + const rc = linux.futex_4arg(ptr, .{ .cmd = .WAIT, .private = true }, expect, &ts); + if (is_debug) switch (linux.E.init(rc)) { + .SUCCESS => {}, // notified by `wake()` + .INTR => {}, // gives caller a chance to check cancellation + .AGAIN => {}, // ptr.* != expect + .TIMEDOUT => {}, + .INVAL => {}, // possibly timeout overflow + .FAULT => unreachable, // ptr was invalid + else => unreachable, + }; + return; + } else { + @compileError("TODO"); + } +} + +pub fn futexWake(ptr: *const std.atomic.Value(u32), max_waiters: u32) void { + @branchHint(.cold); + + if (builtin.cpu.arch.isWasm()) { + comptime assert(builtin.cpu.has(.wasm, .atomics)); + assert(max_waiters != 0); + const woken_count = asm volatile ( + \\local.get %[ptr] + \\local.get %[waiters] + \\memory.atomic.notify 0 + \\local.set %[ret] + : [ret] "=r" (-> u32), + : [ptr] "r" (&ptr.raw), + [waiters] "r" (max_waiters), + ); + _ = woken_count; // can be 0 when linker flag 'shared-memory' is not enabled + } else switch (native_os) { + .linux => { + const linux = std.os.linux; + switch (linux.E.init(linux.futex_3arg( + &ptr.raw, + .{ .cmd = .WAKE, .private = true }, + @min(max_waiters, std.math.maxInt(i32)), + ))) { + .SUCCESS => return, // successful wake up + .INVAL => return, // invalid futex_wait() on ptr done elsewhere + .FAULT => return, // pointer became invalid while doing the wake + else => return recoverableOsBugDetected(), // deadlock due to operating system bug + } + }, + .driverkit, .ios, .macos, .tvos, .visionos, .watchos => { + const c = std.c; + const flags: c.UL = .{ + .op = .COMPARE_AND_WAIT, + .NO_ERRNO = true, + .WAKE_ALL = max_waiters > 1, + }; + while (true) { + const status = c.__ulock_wake(flags, ptr, 0); + if (status >= 0) return; + switch (@as(c.E, @enumFromInt(-status))) { + .INTR, .CANCELED => continue, // spurious wake() + .FAULT => unreachable, // __ulock_wake doesn't generate EFAULT according to darwin pthread_cond_t + .NOENT => return, // nothing was woken up + .ALREADY => unreachable, // only for UL.Op.WAKE_THREAD + else => unreachable, // deadlock due to operating system bug + } + } + }, + .windows => { + assert(max_waiters != 0); + switch (max_waiters) { + 1 => windows.ntdll.RtlWakeAddressSingle(ptr), + else => windows.ntdll.RtlWakeAddressAll(ptr), + } + }, + .freebsd => { + const rc = std.c._umtx_op( + @intFromPtr(&ptr.raw), + @intFromEnum(std.c.UMTX_OP.WAKE_PRIVATE), + @as(c_ulong, max_waiters), + 0, // there is no timeout struct + 0, // there is no timeout struct pointer + ); + switch (posix.errno(rc)) { + .SUCCESS => {}, + .FAULT => {}, // it's ok if the ptr doesn't point to valid memory + .INVAL => unreachable, // arguments should be correct + else => unreachable, // deadlock due to operating system bug + } + }, + else => @compileError("unimplemented: futexWake"), + } +} + +/// A thread-safe logical boolean value which can be `set` and `unset`. +/// +/// It can also block threads until the value is set with cancelation via timed +/// waits. Statically initializable; four bytes on all targets. +pub const ResetEvent = switch (native_os) { + .netbsd => ResetEventPosix, + else => ResetEventFutex, +}; + +/// A `ResetEvent` implementation based on futexes. +const ResetEventFutex = enum(u32) { + unset = 0, + waiting = 1, + is_set = 2, + + /// Returns whether the logical boolean is `set`. + /// + /// Once `reset` is called, this returns false until the next `set`. + /// + /// The memory accesses before the `set` can be said to happen before + /// `isSet` returns true. + pub fn isSet(ref: *const ResetEventFutex) bool { + if (builtin.single_threaded) return switch (ref.*) { + .unset => false, + .waiting => unreachable, + .is_set => true, + }; + // Acquire barrier ensures memory accesses before `set` happen before + // returning true. + return @atomicLoad(ResetEventFutex, ref, .acquire) == .is_set; + } + + /// Blocks the calling thread until `set` is called. + /// + /// This is effectively a more efficient version of `while (!isSet()) {}`. + /// + /// The memory accesses before the `set` can be said to happen before `wait` returns. + pub fn wait(ref: *ResetEventFutex, t: *Threaded) Io.Cancelable!void { + if (builtin.single_threaded) switch (ref.*) { + .unset => unreachable, // Deadlock, no other threads to wake us up. + .waiting => unreachable, // Invalid state. + .is_set => return, + }; + // Try to set the state from `unset` to `waiting` to indicate to the + // `set` thread that others are blocked on the ResetEventFutex. Avoid using + // any strict barriers until we know the ResetEventFutex is set. + var state = @atomicLoad(ResetEventFutex, ref, .acquire); + if (state == .is_set) { + @branchHint(.likely); + return; + } + if (state == .unset) { + state = @cmpxchgStrong(ResetEventFutex, ref, state, .waiting, .acquire, .acquire) orelse .waiting; + } + while (state == .waiting) { + try futexWait(t, @ptrCast(ref), @intFromEnum(ResetEventFutex.waiting)); + state = @atomicLoad(ResetEventFutex, ref, .acquire); + } + assert(state == .is_set); + } + + /// Same as `wait` except uninterruptible. + pub fn waitUncancelable(ref: *ResetEventFutex) void { + if (builtin.single_threaded) switch (ref.*) { + .unset => unreachable, // Deadlock, no other threads to wake us up. + .waiting => unreachable, // Invalid state. + .is_set => return, + }; + // Try to set the state from `unset` to `waiting` to indicate to the + // `set` thread that others are blocked on the ResetEventFutex. Avoid using + // any strict barriers until we know the ResetEventFutex is set. + var state = @atomicLoad(ResetEventFutex, ref, .acquire); + if (state == .is_set) { + @branchHint(.likely); + return; + } + if (state == .unset) { + state = @cmpxchgStrong(ResetEventFutex, ref, state, .waiting, .acquire, .acquire) orelse .waiting; + } + while (state == .waiting) { + futexWaitUncancelable(@ptrCast(ref), @intFromEnum(ResetEventFutex.waiting)); + state = @atomicLoad(ResetEventFutex, ref, .acquire); + } + assert(state == .is_set); + } + + /// Marks the logical boolean as `set` and unblocks any threads in `wait` + /// or `timedWait` to observe the new state. + /// + /// The logical boolean stays `set` until `reset` is called, making future + /// `set` calls do nothing semantically. + /// + /// The memory accesses before `set` can be said to happen before `isSet` + /// returns true or `wait`/`timedWait` return successfully. + pub fn set(ref: *ResetEventFutex) void { + if (builtin.single_threaded) { + ref.* = .is_set; + return; + } + if (@atomicRmw(ResetEventFutex, ref, .Xchg, .is_set, .release) == .waiting) { + futexWake(@ptrCast(ref), std.math.maxInt(u32)); + } + } + + /// Unmarks the ResetEventFutex as if `set` was never called. + /// + /// Assumes no threads are blocked in `wait` or `timedWait`. Concurrent + /// calls to `set`, `isSet` and `reset` are allowed. + pub fn reset(ref: *ResetEventFutex) void { + if (builtin.single_threaded) { + ref.* = .unset; + return; + } + @atomicStore(ResetEventFutex, ref, .unset, .monotonic); + } +}; + +/// A `ResetEvent` implementation based on pthreads API. +const ResetEventPosix = struct { + cond: std.c.pthread_cond_t, + mutex: std.c.pthread_mutex_t, + state: ResetEventFutex, + + pub const unset: ResetEventPosix = .{ + .cond = std.c.PTHREAD_COND_INITIALIZER, + .mutex = std.c.PTHREAD_MUTEX_INITIALIZER, + .state = .unset, + }; + + pub fn isSet(rep: *const ResetEventPosix) bool { + if (builtin.single_threaded) return switch (rep.state) { + .unset => false, + .waiting => unreachable, + .is_set => true, + }; + return @atomicLoad(ResetEventFutex, &rep.state, .acquire) == .is_set; + } + + pub fn wait(rep: *ResetEventPosix, t: *Threaded) Io.Cancelable!void { + if (builtin.single_threaded) switch (rep.*) { + .unset => unreachable, // Deadlock, no other threads to wake us up. + .waiting => unreachable, // Invalid state. + .is_set => return, + }; + assert(std.c.pthread_mutex_lock(&rep.mutex) == .SUCCESS); + defer assert(std.c.pthread_mutex_unlock(&rep.mutex) == .SUCCESS); + sw: switch (rep.state) { + .unset => { + rep.state = .waiting; + continue :sw .waiting; + }, + .waiting => { + try t.checkCancel(); + assert(std.c.pthread_cond_wait(&rep.cond, &rep.mutex) == .SUCCESS); + continue :sw rep.state; + }, + .is_set => return, + } + } + + pub fn waitUncancelable(rep: *ResetEventPosix) void { + if (builtin.single_threaded) switch (rep.*) { + .unset => unreachable, // Deadlock, no other threads to wake us up. + .waiting => unreachable, // Invalid state. + .is_set => return, + }; + assert(std.c.pthread_mutex_lock(&rep.mutex) == .SUCCESS); + defer assert(std.c.pthread_mutex_unlock(&rep.mutex) == .SUCCESS); + sw: switch (rep.state) { + .unset => { + rep.state = .waiting; + continue :sw .waiting; + }, + .waiting => { + assert(std.c.pthread_cond_wait(&rep.cond, &rep.mutex) == .SUCCESS); + continue :sw rep.state; + }, + .is_set => return, + } + } + + pub fn set(rep: *ResetEventPosix) void { + if (builtin.single_threaded) { + rep.* = .is_set; + return; + } + if (@atomicRmw(ResetEventFutex, &rep.state, .Xchg, .is_set, .release) == .waiting) { + assert(std.c.pthread_cond_broadcast(&rep.cond) == .SUCCESS); + } + } + + pub fn reset(rep: *ResetEventPosix) void { + if (builtin.single_threaded) { + rep.* = .unset; + return; + } + @atomicStore(ResetEventFutex, &rep.state, .unset, .monotonic); + } +}; + +fn closeSocketWindows(s: ws2_32.SOCKET) void { + const rc = ws2_32.closesocket(s); + if (is_debug) switch (rc) { + 0 => {}, + ws2_32.SOCKET_ERROR => switch (ws2_32.WSAGetLastError()) { + else => recoverableOsBugDetected(), + }, + else => recoverableOsBugDetected(), + }; +} + +const Wsa = struct { + status: Status = .uninitialized, + mutex: Io.Mutex = .init, + init_error: ?Wsa.InitError = null, + + const Status = enum { uninitialized, initialized, failure }; + + const InitError = error{ + ProcessFdQuotaExceeded, + NetworkDown, + VersionUnsupported, + BlockingOperationInProgress, + } || Io.UnexpectedError; +}; + +fn initializeWsa(t: *Threaded) error{NetworkDown}!void { + const t_io = io(t); + const wsa = &t.wsa; + wsa.mutex.lockUncancelable(t_io); + defer wsa.mutex.unlock(t_io); + switch (wsa.status) { + .uninitialized => { + var wsa_data: ws2_32.WSADATA = undefined; + const minor_version = 2; + const major_version = 2; + switch (ws2_32.WSAStartup((@as(windows.WORD, minor_version) << 8) | major_version, &wsa_data)) { + 0 => { + wsa.status = .initialized; + return; + }, + else => |err_int| switch (@as(ws2_32.WinsockError, @enumFromInt(@as(u16, @intCast(err_int))))) { + .SYSNOTREADY => wsa.init_error = error.NetworkDown, + .VERNOTSUPPORTED => wsa.init_error = error.VersionUnsupported, + .EINPROGRESS => wsa.init_error = error.BlockingOperationInProgress, + .EPROCLIM => wsa.init_error = error.ProcessFdQuotaExceeded, + else => |err| wsa.init_error = windows.unexpectedWSAError(err), + }, + } + }, + .initialized => return, + .failure => {}, + } + return error.NetworkDown; +} + +fn doNothingSignalHandler(_: posix.SIG) callconv(.c) void {} + +test { + _ = @import("Threaded/test.zig"); +} diff --git a/lib/std/Io/Threaded/test.zig b/lib/std/Io/Threaded/test.zig new file mode 100644 index 0000000000..ef24b25f34 --- /dev/null +++ b/lib/std/Io/Threaded/test.zig @@ -0,0 +1,58 @@ +const builtin = @import("builtin"); + +const std = @import("std"); +const Io = std.Io; +const testing = std.testing; +const assert = std.debug.assert; + +test "concurrent vs main prevents deadlock via oversubscription" { + var threaded: Io.Threaded = .init(std.testing.allocator); + defer threaded.deinit(); + const io = threaded.io(); + + threaded.cpu_count = 1; + + var queue: Io.Queue(u8) = .init(&.{}); + + var putter = io.concurrent(put, .{ io, &queue }) catch |err| switch (err) { + error.ConcurrencyUnavailable => { + try testing.expect(builtin.single_threaded); + return; + }, + }; + defer putter.cancel(io); + + try testing.expectEqual(42, queue.getOneUncancelable(io)); +} + +fn put(io: Io, queue: *Io.Queue(u8)) void { + queue.putOneUncancelable(io, 42); +} + +fn get(io: Io, queue: *Io.Queue(u8)) void { + assert(queue.getOneUncancelable(io) == 42); +} + +test "concurrent vs concurrent prevents deadlock via oversubscription" { + var threaded: Io.Threaded = .init(std.testing.allocator); + defer threaded.deinit(); + const io = threaded.io(); + + threaded.cpu_count = 1; + + var queue: Io.Queue(u8) = .init(&.{}); + + var putter = io.concurrent(put, .{ io, &queue }) catch |err| switch (err) { + error.ConcurrencyUnavailable => { + try testing.expect(builtin.single_threaded); + return; + }, + }; + defer putter.cancel(io); + + var getter = try io.concurrent(get, .{ io, &queue }); + defer getter.cancel(io); + + getter.await(io); + putter.await(io); +} diff --git a/lib/std/Io/Writer.zig b/lib/std/Io/Writer.zig index a525a028d7..b41aa24ccb 100644 --- a/lib/std/Io/Writer.zig +++ b/lib/std/Io/Writer.zig @@ -5,7 +5,7 @@ const Writer = @This(); const std = @import("../std.zig"); const assert = std.debug.assert; const Limit = std.Io.Limit; -const File = std.fs.File; +const File = std.Io.File; const testing = std.testing; const Allocator = std.mem.Allocator; const ArrayList = std.ArrayList; @@ -2827,6 +2827,8 @@ pub const Allocating = struct { }; test "discarding sendFile" { + const io = testing.io; + var tmp_dir = testing.tmpDir(.{}); defer tmp_dir.cleanup(); @@ -2837,7 +2839,7 @@ test "discarding sendFile" { try file_writer.interface.writeByte('h'); try file_writer.interface.flush(); - var file_reader = file_writer.moveToReader(); + var file_reader = file_writer.moveToReader(io); try file_reader.seekTo(0); var w_buffer: [256]u8 = undefined; @@ -2847,6 +2849,8 @@ test "discarding sendFile" { } test "allocating sendFile" { + const io = testing.io; + var tmp_dir = testing.tmpDir(.{}); defer tmp_dir.cleanup(); @@ -2857,7 +2861,7 @@ test "allocating sendFile" { try file_writer.interface.writeAll("abcd"); try file_writer.interface.flush(); - var file_reader = file_writer.moveToReader(); + var file_reader = file_writer.moveToReader(io); try file_reader.seekTo(0); try file_reader.interface.fill(2); @@ -2869,6 +2873,8 @@ test "allocating sendFile" { } test sendFileReading { + const io = testing.io; + var tmp_dir = testing.tmpDir(.{}); defer tmp_dir.cleanup(); @@ -2879,7 +2885,7 @@ test sendFileReading { try file_writer.interface.writeAll("abcd"); try file_writer.interface.flush(); - var file_reader = file_writer.moveToReader(); + var file_reader = file_writer.moveToReader(io); try file_reader.seekTo(0); try file_reader.interface.fill(2); diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig new file mode 100644 index 0000000000..b691b1501e --- /dev/null +++ b/lib/std/Io/net.zig @@ -0,0 +1,1379 @@ +const builtin = @import("builtin"); +const native_os = builtin.os.tag; +const std = @import("../std.zig"); +const Io = std.Io; +const assert = std.debug.assert; + +pub const HostName = @import("net/HostName.zig"); + +/// Source of truth: Internet Assigned Numbers Authority (IANA) +pub const Protocol = enum(u32) { + hopopts = 0, + icmp = 1, + igmp = 2, + ipip = 4, + tcp = 6, + egp = 8, + pup = 12, + udp = 17, + idp = 22, + tp = 29, + dccp = 33, + ipv6 = 41, + routing = 43, + fragment = 44, + rsvp = 46, + gre = 47, + esp = 50, + ah = 51, + icmpv6 = 58, + none = 59, + dstopts = 60, + mtp = 92, + beetph = 94, + encap = 98, + pim = 103, + comp = 108, + sctp = 132, + mh = 135, + udplite = 136, + mpls = 137, + ethernet = 143, + raw = 255, + mptcp = 262, +}; + +/// Windows 10 added support for unix sockets in build 17063, redstone 4 is the +/// first release to support them. +pub const has_unix_sockets = switch (native_os) { + .windows => builtin.os.version_range.windows.isAtLeast(.win10_rs4) orelse false, + .wasi => false, + else => true, +}; + +pub const default_kernel_backlog = 128; + +pub const IpAddress = union(enum) { + ip4: Ip4Address, + ip6: Ip6Address, + + pub const Family = @typeInfo(IpAddress).@"union".tag_type.?; + + pub const ParseLiteralError = error{ InvalidAddress, InvalidPort }; + + /// Parse an IP address which may include a port. + /// + /// For IPv4, this is written `address:port`. + /// + /// For IPv6, RFC 3986 defines this as an "IP literal", and the port is + /// differentiated from the address by surrounding the address part in + /// brackets "[addr]:port". Even if the port is not given, the brackets are + /// mandatory. + pub fn parseLiteral(text: []const u8) ParseLiteralError!IpAddress { + if (text.len == 0) return error.InvalidAddress; + if (text[0] == '[') { + const addr_end = std.mem.findScalar(u8, text, ']') orelse + return error.InvalidAddress; + const addr_text = text[1..addr_end]; + const port: u16 = p: { + if (addr_end == text.len - 1) break :p 0; + if (text[addr_end + 1] != ':') return error.InvalidAddress; + break :p std.fmt.parseInt(u16, text[addr_end + 2 ..], 10) catch return error.InvalidPort; + }; + return parseIp6(addr_text, port) catch error.InvalidAddress; + } + if (std.mem.findScalar(u8, text, ':')) |i| { + const addr = Ip4Address.parse(text[0..i], 0) catch return error.InvalidAddress; + return .{ .ip4 = .{ + .bytes = addr.bytes, + .port = std.fmt.parseInt(u16, text[i + 1 ..], 10) catch return error.InvalidPort, + } }; + } + return parseIp4(text, 0) catch error.InvalidAddress; + } + + /// Parse the given IP address string into an `IpAddress` value. + /// + /// This is a pure function but it cannot handle IPv6 addresses that have + /// scope ids ("%foo" at the end). To also handle those, `resolve` must be + /// called instead. + pub fn parse(text: []const u8, port: u16) !IpAddress { + if (parseIp4(text, port)) |ip4| return ip4 else |err| switch (err) { + error.Overflow, + error.InvalidEnd, + error.InvalidCharacter, + error.Incomplete, + error.NonCanonical, + => {}, + } + + return parseIp6(text, port); + } + + pub fn parseIp4(text: []const u8, port: u16) Ip4Address.ParseError!IpAddress { + return .{ .ip4 = try Ip4Address.parse(text, port) }; + } + + /// This is a pure function but it cannot handle IPv6 addresses that have + /// scope ids ("%foo" at the end). To also handle those, `resolveIp6` must be + /// called instead. + pub fn parseIp6(text: []const u8, port: u16) Ip6Address.ParseError!IpAddress { + return .{ .ip6 = try Ip6Address.parse(text, port) }; + } + + /// This function requires an `Io` parameter because it must query the operating + /// system to convert interface name to index. For example, in + /// "fe80::e0e:76ff:fed4:cf22%eno1", "eno1" must be resolved to an index by + /// creating a socket and then using an `ioctl` syscall. + /// + /// For a pure function that cannot handle scopes, see `parse`. + pub fn resolve(io: Io, text: []const u8, port: u16) !IpAddress { + if (parseIp4(text, port)) |ip4| return ip4 else |err| switch (err) { + error.Overflow, + error.InvalidEnd, + error.InvalidCharacter, + error.Incomplete, + error.NonCanonical, + => {}, + } + + return resolveIp6(io, text, port); + } + + pub fn resolveIp6(io: Io, text: []const u8, port: u16) Ip6Address.ResolveError!IpAddress { + return .{ .ip6 = try Ip6Address.resolve(io, text, port) }; + } + + /// Returns the port in native endian. + pub fn getPort(a: IpAddress) u16 { + return switch (a) { + inline .ip4, .ip6 => |x| x.port, + }; + } + + /// `port` is native-endian. + pub fn setPort(a: *IpAddress, port: u16) void { + switch (a) { + inline .ip4, .ip6 => |*x| x.port = port, + } + } + + /// Includes the optional scope ("%foo" at the end) in IPv6 addresses. + /// + /// See `format` for an alternative that omits scopes and does + /// not require an `Io` parameter. + pub fn formatResolved(a: IpAddress, io: Io, w: *Io.Writer) Ip6Address.FormatError!void { + switch (a) { + .ip4 => |x| return x.format(w), + .ip6 => |x| return x.formatResolved(io, w), + } + } + + /// See `formatResolved` for an alternative that additionally prints the optional + /// scope at the end of IPv6 addresses and requires an `Io` parameter. + pub fn format(a: IpAddress, w: *Io.Writer) Io.Writer.Error!void { + switch (a) { + inline .ip4, .ip6 => |x| return x.format(w), + } + } + + pub fn eql(a: *const IpAddress, b: *const IpAddress) bool { + return switch (a.*) { + .ip4 => |a_ip4| switch (b.*) { + .ip4 => |b_ip4| a_ip4.eql(b_ip4), + else => false, + }, + .ip6 => |a_ip6| switch (b.*) { + .ip6 => |b_ip6| a_ip6.eql(b_ip6), + else => false, + }, + }; + } + + pub const ListenError = error{ + /// The address is already taken. Can occur when bound port is 0 but + /// all ephemeral ports are already in use. + AddressInUse, + /// A nonexistent interface was requested or the requested address was not local. + AddressUnavailable, + /// The local network interface used to reach the destination is offline. + NetworkDown, + /// Insufficient memory or other resource internal to the operating system. + SystemResources, + /// Per-process limit on the number of open file descriptors has been reached. + ProcessFdQuotaExceeded, + /// System-wide limit on the total number of open files has been reached. + SystemFdQuotaExceeded, + /// The requested address family (IPv4 or IPv6) is not supported by the operating system. + AddressFamilyUnsupported, + ProtocolUnsupportedBySystem, + ProtocolUnsupportedByAddressFamily, + SocketModeUnsupported, + /// One of the `ListenOptions` is not supported by the Io + /// implementation. + OptionUnsupported, + } || Io.UnexpectedError || Io.Cancelable; + + pub const ListenOptions = struct { + /// How many connections the kernel will accept on the application's behalf. + /// If more than this many connections pool in the kernel, clients will start + /// seeing "Connection refused". + kernel_backlog: u31 = default_kernel_backlog, + /// Sets SO_REUSEADDR and SO_REUSEPORT on POSIX. + /// Sets SO_REUSEADDR on Windows, which is roughly equivalent. + reuse_address: bool = false, + /// Only connection-oriented modes may be used here, which includes: + /// * `Socket.Mode.stream` + /// * `Socket.Mode.seqpacket` + mode: Socket.Mode = .stream, + /// Only connection-oriented protocols may be used here, which includes: + /// * `Protocol.tcp` + /// * `Protocol.tp` + /// * `Protocol.dccp` + /// * `Protocol.sctp` + protocol: Protocol = .tcp, + }; + + /// Waits for a TCP connection. When using this API, `bind` does not need + /// to be called. The returned `Server` has an open `stream`. + pub fn listen(address: IpAddress, io: Io, options: ListenOptions) ListenError!Server { + return io.vtable.netListenIp(io.userdata, address, options); + } + + pub const BindError = error{ + /// The address is already taken. Can occur when bound port is 0 but + /// all ephemeral ports are already in use. + AddressInUse, + /// A nonexistent interface was requested or the requested address was not local. + AddressUnavailable, + /// The address is not valid for the address family of socket. + AddressFamilyUnsupported, + /// Insufficient memory or other resource internal to the operating system. + SystemResources, + /// The local network interface used to reach the destination is offline. + NetworkDown, + ProtocolUnsupportedBySystem, + ProtocolUnsupportedByAddressFamily, + /// Per-process limit on the number of open file descriptors has been reached. + ProcessFdQuotaExceeded, + /// System-wide limit on the total number of open files has been reached. + SystemFdQuotaExceeded, + SocketModeUnsupported, + /// One of the `BindOptions` is not supported by the Io + /// implementation. + OptionUnsupported, + } || Io.UnexpectedError || Io.Cancelable; + + pub const BindOptions = struct { + /// The socket is restricted to sending and receiving IPv6 packets only. + /// In this case, an IPv4 and an IPv6 application can bind to a single port + /// at the same time. + ip6_only: bool = false, + mode: Socket.Mode, + protocol: ?Protocol = null, + }; + + /// Associates an address with a `Socket` which can be used to receive UDP + /// packets and other kinds of non-streaming messages. See `listen` for a + /// streaming alternative. + /// + /// One bound `Socket` can be used to receive messages from multiple + /// different addresses. + pub fn bind(address: *const IpAddress, io: Io, options: BindOptions) BindError!Socket { + return io.vtable.netBindIp(io.userdata, address, options); + } + + pub const ConnectError = error{ + AddressUnavailable, + AddressFamilyUnsupported, + /// Insufficient memory or other resource internal to the operating system. + SystemResources, + ConnectionPending, + ConnectionRefused, + ConnectionResetByPeer, + HostUnreachable, + NetworkUnreachable, + Timeout, + /// One of the `ConnectOptions` is not supported by the Io + /// implementation. + OptionUnsupported, + /// Per-process limit on the number of open file descriptors has been reached. + ProcessFdQuotaExceeded, + /// System-wide limit on the total number of open files has been reached. + SystemFdQuotaExceeded, + ProtocolUnsupportedBySystem, + ProtocolUnsupportedByAddressFamily, + SocketModeUnsupported, + /// The user tried to connect to a broadcast address without having the socket broadcast flag enabled or + /// the connection request failed because of a local firewall rule. + AccessDenied, + /// Non-blocking was requested and the operation cannot return immediately. + WouldBlock, + NetworkDown, + } || Io.Timeout.Error || Io.UnexpectedError || Io.Cancelable; + + pub const ConnectOptions = struct { + mode: Socket.Mode, + protocol: ?Protocol = null, + timeout: Io.Timeout = .none, + }; + + /// Initiates a connection-oriented network stream. + pub fn connect(address: IpAddress, io: Io, options: ConnectOptions) ConnectError!Stream { + return io.vtable.netConnectIp(io.userdata, &address, options); + } +}; + +/// An IPv4 address in binary memory layout. +pub const Ip4Address = struct { + bytes: [4]u8, + port: u16, + + pub fn loopback(port: u16) Ip4Address { + return .{ + .bytes = .{ 127, 0, 0, 1 }, + .port = port, + }; + } + + pub fn unspecified(port: u16) Ip4Address { + return .{ + .bytes = .{ 0, 0, 0, 0 }, + .port = port, + }; + } + + pub const ParseError = error{ + Overflow, + InvalidEnd, + InvalidCharacter, + Incomplete, + NonCanonical, + }; + + pub fn parse(buffer: []const u8, port: u16) ParseError!Ip4Address { + var bytes: [4]u8 = @splat(0); + var index: u8 = 0; + var saw_any_digits = false; + var has_zero_prefix = false; + for (buffer) |c| switch (c) { + '.' => { + if (!saw_any_digits) return error.InvalidCharacter; + if (index == 3) return error.InvalidEnd; + index += 1; + saw_any_digits = false; + has_zero_prefix = false; + }, + '0'...'9' => { + if (c == '0' and !saw_any_digits) { + has_zero_prefix = true; + } else if (has_zero_prefix) { + return error.NonCanonical; + } + saw_any_digits = true; + bytes[index] = try std.math.mul(u8, bytes[index], 10); + bytes[index] = try std.math.add(u8, bytes[index], c - '0'); + }, + else => return error.InvalidCharacter, + }; + if (index == 3 and saw_any_digits) return .{ + .bytes = bytes, + .port = port, + }; + return error.Incomplete; + } + + pub fn format(a: Ip4Address, w: *Io.Writer) Io.Writer.Error!void { + const bytes = &a.bytes; + try w.print("{d}.{d}.{d}.{d}:{d}", .{ bytes[0], bytes[1], bytes[2], bytes[3], a.port }); + } + + pub fn eql(a: Ip4Address, b: Ip4Address) bool { + const a_int: u32 = @bitCast(a.bytes); + const b_int: u32 = @bitCast(b.bytes); + return a.port == b.port and a_int == b_int; + } +}; + +/// An IPv6 address in binary memory layout. +pub const Ip6Address = struct { + /// Native endian + port: u16, + /// Big endian + bytes: [16]u8, + flow: u32 = 0, + interface: Interface = .none, + + pub const Policy = struct { + addr: [16]u8, + len: u8, + mask: u8, + prec: u8, + label: u8, + }; + + pub fn loopback(port: u16) Ip6Address { + return .{ + .bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }, + .port = port, + }; + } + + pub fn unspecified(port: u16) Ip6Address { + return .{ + .bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + .port = port, + }; + } + + /// Constructs an IPv4-mapped IPv6 address. + pub fn fromIp4(ip4: Ip4Address) Ip6Address { + const b = &ip4.bytes; + return .{ + .bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, b[0], b[1], b[2], b[3] }, + .port = ip4.port, + }; + } + + /// Given an `IpAddress`, converts it to an `Ip6Address` directly, or via + /// constructing an IPv4-mapped IPv6 address. + pub fn fromAny(addr: IpAddress) Ip6Address { + return switch (addr) { + .ip4 => |ip4| fromIp4(ip4), + .ip6 => |ip6| ip6, + }; + } + + /// An IPv6 address but with `Interface` as a name rather than index. + pub const Unresolved = struct { + /// Big endian + bytes: [16]u8, + /// Has not been checked to be a valid native interface name. + /// Externally managed memory. + interface_name: ?[]const u8, + + pub const Parsed = union(enum) { + success: Unresolved, + invalid_byte: usize, + incomplete, + junk_after_end: usize, + interface_name_oversized: usize, + invalid_ip4_mapping: usize, + overflow: usize, + }; + + pub fn parse(text: []const u8) Parsed { + if (text.len < 2) return .incomplete; + const ip4_prefix = "::ffff:"; + if (std.ascii.startsWithIgnoreCase(text, ip4_prefix)) { + const parsed = Ip4Address.parse(text[ip4_prefix.len..], 0) catch + return .{ .invalid_ip4_mapping = ip4_prefix.len }; + const b = parsed.bytes; + return .{ .success = .{ + .bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, b[0], b[1], b[2], b[3] }, + .interface_name = null, + } }; + } + // Has to be u16 elements to handle 3-digit hex numbers from compression. + var parts: [8]u16 = @splat(0); + var parts_i: u8 = 0; + var text_i: u8 = 0; + var digit_i: u8 = 0; + var compress_start: ?u8 = null; + var interface_name_text: ?[]const u8 = null; + const State = union(enum) { digit, end }; + state: switch (State.digit) { + .digit => c: switch (text[text_i]) { + 'a'...'f' => |c| { + const digit = c - 'a' + 10; + parts[parts_i] = (std.math.mul(u16, parts[parts_i], 16) catch return .{ + .overflow = text_i, + }) + digit; + if (digit_i == 4) return .{ .invalid_byte = text_i }; + digit_i += 1; + text_i += 1; + if (text.len - text_i == 0) { + parts_i += 1; + continue :state .end; + } + continue :c text[text_i]; + }, + 'A'...'F' => |c| continue :c c - 'A' + 'a', + '0'...'9' => |c| { + const digit = c - '0'; + parts[parts_i] = (std.math.mul(u16, parts[parts_i], 16) catch return .{ + .overflow = text_i, + }) + digit; + if (digit_i == 4) return .{ .invalid_byte = text_i }; + digit_i += 1; + text_i += 1; + if (text.len - text_i == 0) { + parts_i += 1; + continue :state .end; + } + continue :c text[text_i]; + }, + ':' => { + if (digit_i == 0) { + if (compress_start != null) return .{ .invalid_byte = text_i }; + if (text_i == 0) { + text_i += 1; + if (text[text_i] != ':') return .{ .invalid_byte = text_i }; + assert(parts_i == 0); + } + compress_start = parts_i; + text_i += 1; + if (text.len - text_i == 0) continue :state .end; + continue :c text[text_i]; + } else { + parts_i += 1; + if (parts.len - parts_i == 0) continue :state .end; + digit_i = 0; + text_i += 1; + if (text.len - text_i == 0) return .incomplete; + continue :c text[text_i]; + } + }, + '%' => { + if (digit_i == 0) return .{ .invalid_byte = text_i }; + parts_i += 1; + text_i += 1; + const name = text[text_i..]; + if (name.len == 0) return .incomplete; + interface_name_text = name; + text_i = @intCast(text.len); + continue :state .end; + }, + else => return .{ .invalid_byte = text_i }, + }, + .end => { + if (text.len - text_i != 0) return .{ .junk_after_end = text_i }; + const remaining = parts.len - parts_i; + if (compress_start) |s| { + const src = parts[s..parts_i]; + @memmove(parts[parts.len - src.len ..], src); + @memset(parts[s..][0..remaining], 0); + } else { + if (remaining != 0) return .incomplete; + } + + // Workaround that can be removed when this proposal is + // implemented https://github.com/ziglang/zig/issues/19755 + if ((comptime @import("builtin").cpu.arch.endian()) != .big) { + for (&parts) |*part| part.* = @byteSwap(part.*); + } + + return .{ .success = .{ + .bytes = @bitCast(parts), + .interface_name = interface_name_text, + } }; + }, + } + } + + pub const FromAddressError = Interface.NameError; + + pub fn fromAddress(a: *const Ip6Address, io: Io) FromAddressError!Unresolved { + if (a.interface.isNone()) return .{ + .bytes = a.bytes, + .interface_name = null, + }; + return .{ + .bytes = a.bytes, + .interface_name = try a.interface.name(io), + }; + } + + pub fn format(u: *const Unresolved, w: *Io.Writer) Io.Writer.Error!void { + const bytes = &u.bytes; + if (std.mem.eql(u8, bytes[0..12], &[_]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff })) { + try w.print("::ffff:{d}.{d}.{d}.{d}", .{ bytes[12], bytes[13], bytes[14], bytes[15] }); + } else { + const parts: [8]u16 = .{ + std.mem.readInt(u16, bytes[0..2], .big), + std.mem.readInt(u16, bytes[2..4], .big), + std.mem.readInt(u16, bytes[4..6], .big), + std.mem.readInt(u16, bytes[6..8], .big), + std.mem.readInt(u16, bytes[8..10], .big), + std.mem.readInt(u16, bytes[10..12], .big), + std.mem.readInt(u16, bytes[12..14], .big), + std.mem.readInt(u16, bytes[14..16], .big), + }; + + // Find the longest zero run + var longest_start: usize = 8; + var longest_len: usize = 0; + var current_start: usize = 0; + var current_len: usize = 0; + + for (parts, 0..) |part, i| { + if (part == 0) { + if (current_len == 0) { + current_start = i; + } + current_len += 1; + if (current_len > longest_len) { + longest_start = current_start; + longest_len = current_len; + } + } else { + current_len = 0; + } + } + + // Only compress if the longest zero run is 2 or more + if (longest_len < 2) { + longest_start = 8; + longest_len = 0; + } + + var i: usize = 0; + var abbrv = false; + while (i < parts.len) : (i += 1) { + if (i == longest_start) { + // Emit "::" for the longest zero run + if (!abbrv) { + try w.writeAll(if (i == 0) "::" else ":"); + abbrv = true; + } + i += longest_len - 1; // Skip the compressed range + continue; + } + if (abbrv) { + abbrv = false; + } + try w.print("{x}", .{parts[i]}); + if (i != parts.len - 1) { + try w.writeAll(":"); + } + } + } + if (u.interface_name) |n| try w.print("%{s}", .{n}); + } + }; + + pub const ParseError = error{ + /// If this is returned, more detailed diagnostics can be obtained by + /// calling `Ip6Address.Parsed.init`. + ParseFailed, + /// If this is returned, the IPv6 address had a scope id on it ("%foo" + /// at the end) which requires calling `resolve`. + UnresolvedScope, + }; + + /// This is a pure function but it cannot handle IPv6 addresses that have + /// scope ids ("%foo" at the end). To also handle those, `resolve` must be + /// called instead, or the lower level `Unresolved` API may be used. + pub fn parse(buffer: []const u8, port: u16) ParseError!Ip6Address { + switch (Unresolved.parse(buffer)) { + .success => |p| return .{ + .bytes = p.bytes, + .port = port, + .interface = if (p.interface_name != null) return error.UnresolvedScope else .none, + }, + else => return error.ParseFailed, + } + return .{ .ip6 = try Ip6Address.parse(buffer, port) }; + } + + pub const ResolveError = error{ + /// If this is returned, more detailed diagnostics can be obtained by + /// calling the `Parsed.init` function. + ParseFailed, + /// The interface name is longer than the host operating system supports. + NameTooLong, + } || Interface.Name.ResolveError; + + /// This function requires an `Io` parameter because it must query the operating + /// system to convert interface name to index. For example, in + /// "fe80::e0e:76ff:fed4:cf22%eno1", "eno1" must be resolved to an index by + /// creating a socket and then using an `ioctl` syscall. + pub fn resolve(io: Io, buffer: []const u8, port: u16) ResolveError!Ip6Address { + return switch (Unresolved.parse(buffer)) { + .success => |p| return .{ + .bytes = p.bytes, + .port = port, + .interface = i: { + const text = p.interface_name orelse break :i .none; + const name: Interface.Name = try .fromSlice(text); + break :i try name.resolve(io); + }, + }, + else => return error.ParseFailed, + }; + } + + pub const FormatError = Io.Writer.Error || Unresolved.FromAddressError; + + /// Includes the optional scope ("%foo" at the end). + /// + /// See `format` for an alternative that omits scopes and does + /// not require an `Io` parameter. + pub fn formatResolved(a: Ip6Address, io: Io, w: *Io.Writer) FormatError!void { + const u: Unresolved = try .fromAddress(io); + try w.print("[{f}]:{d}", .{ u, a.port }); + } + + /// See `formatResolved` for an alternative that additionally prints the optional + /// scope at the end of addresses and requires an `Io` parameter. + pub fn format(a: Ip6Address, w: *Io.Writer) Io.Writer.Error!void { + const u: Unresolved = .{ + .bytes = a.bytes, + .interface_name = null, + }; + try w.print("[{f}]:{d}", .{ u, a.port }); + } + + pub fn eql(a: Ip6Address, b: Ip6Address) bool { + return a.port == b.port and std.mem.eql(u8, &a.bytes, &b.bytes); + } + + pub fn isMultiCast(a: Ip6Address) bool { + return a.bytes[0] == 0xff; + } + + pub fn isLinkLocal(a: Ip6Address) bool { + const b = &a.bytes; + return b[0] == 0xfe and (b[1] & 0xc0) == 0x80; + } + + pub fn isLoopBack(a: Ip6Address) bool { + const b = &a.bytes; + return b[0] == 0 and b[1] == 0 and + b[2] == 0 and + b[12] == 0 and b[13] == 0 and + b[14] == 0 and b[15] == 1; + } + + pub fn isSiteLocal(a: Ip6Address) bool { + const b = &a.bytes; + return b[0] == 0xfe and (b[1] & 0xc0) == 0xc0; + } + + pub fn policy(a: Ip6Address) *const Policy { + const b = &a.bytes; + for (&defined_policies) |*p| { + if (!std.mem.eql(u8, b[0..p.len], p.addr[0..p.len])) continue; + if ((b[p.len] & p.mask) != p.addr[p.len]) continue; + return p; + } + unreachable; + } + + pub fn scope(a: Ip6Address) u8 { + if (isMultiCast(a)) return a.bytes[1] & 15; + if (isLinkLocal(a)) return 2; + if (isLoopBack(a)) return 2; + if (isSiteLocal(a)) return 5; + return 14; + } + + const defined_policies = [_]Policy{ + .{ + .addr = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01".*, + .len = 15, + .mask = 0xff, + .prec = 50, + .label = 0, + }, + .{ + .addr = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x00\x00\x00\x00".*, + .len = 11, + .mask = 0xff, + .prec = 35, + .label = 4, + }, + .{ + .addr = "\x20\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00".*, + .len = 1, + .mask = 0xff, + .prec = 30, + .label = 2, + }, + .{ + .addr = "\x20\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00".*, + .len = 3, + .mask = 0xff, + .prec = 5, + .label = 5, + }, + .{ + .addr = "\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00".*, + .len = 0, + .mask = 0xfe, + .prec = 3, + .label = 13, + }, + // These are deprecated and/or returned to the address + // pool, so despite the RFC, treating them as special + // is probably wrong. + // { "", 11, 0xff, 1, 3 }, + // { "\xfe\xc0", 1, 0xc0, 1, 11 }, + // { "\x3f\xfe", 1, 0xff, 1, 12 }, + // Last rule must match all addresses to stop loop. + .{ + .addr = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00".*, + .len = 0, + .mask = 0, + .prec = 40, + .label = 1, + }, + }; +}; + +pub const UnixAddress = struct { + path: []const u8, + + pub const max_len = 108; + + pub const InitError = error{NameTooLong}; + + pub fn init(p: []const u8) InitError!UnixAddress { + if (p.len > max_len) return error.NameTooLong; + return .{ .path = p }; + } + + pub const ListenError = error{ + AddressFamilyUnsupported, + AddressInUse, + NetworkDown, + SystemResources, + SymLinkLoop, + FileNotFound, + NotDir, + ReadOnlyFileSystem, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, + AccessDenied, + PermissionDenied, + AddressUnavailable, + } || Io.Cancelable || Io.UnexpectedError; + + pub const ListenOptions = struct { + /// How many connections the kernel will accept on the application's behalf. + /// If more than this many connections pool in the kernel, clients will start + /// seeing "Connection refused". + kernel_backlog: u31 = default_kernel_backlog, + }; + + pub fn listen(ua: *const UnixAddress, io: Io, options: ListenOptions) ListenError!Server { + assert(ua.path.len <= max_len); + return .{ .socket = .{ + .handle = try io.vtable.netListenUnix(io.userdata, ua, options), + .address = .{ .ip4 = .loopback(0) }, + } }; + } + + pub const ConnectError = error{ + SystemResources, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, + AddressFamilyUnsupported, + ProtocolUnsupportedBySystem, + ProtocolUnsupportedByAddressFamily, + SocketModeUnsupported, + AccessDenied, + PermissionDenied, + SymLinkLoop, + FileNotFound, + NotDir, + ReadOnlyFileSystem, + WouldBlock, + NetworkDown, + } || Io.Cancelable || Io.UnexpectedError; + + pub fn connect(ua: *const UnixAddress, io: Io) ConnectError!Stream { + assert(ua.path.len <= max_len); + return .{ .socket = .{ + .handle = try io.vtable.netConnectUnix(io.userdata, ua), + .address = .{ .ip4 = .loopback(0) }, + } }; + } +}; + +pub const ReceiveFlags = packed struct(u8) { + oob: bool = false, + peek: bool = false, + trunc: bool = false, + _: u5 = 0, +}; + +pub const IncomingMessage = struct { + /// Populated by receive functions. + from: IpAddress, + /// Populated by receive functions, points into the caller-supplied buffer. + data: []u8, + /// Supplied by caller before calling receive functions; mutated by receive + /// functions. + control: []u8, + /// Populated by receive functions. + flags: Flags, + + /// Useful for initializing before calling `receiveManyTimeout`. + pub const init: IncomingMessage = .{ + .from = undefined, + .data = undefined, + .control = &.{}, + .flags = undefined, + }; + + pub const Flags = packed struct(u8) { + /// indicates end-of-record; the data returned completed a record + /// (generally used with sockets of type SOCK_SEQPACKET). + eor: bool, + /// indicates that the trailing portion of a datagram was discarded + /// because the datagram was larger than the buffer supplied. + trunc: bool, + /// indicates that some control data was discarded due to lack of + /// space in the buffer for ancil‐ lary data. + ctrunc: bool, + /// indicates expedited or out-of-band data was received. + oob: bool, + /// indicates that no data was received but an extended error from the + /// socket error queue. + errqueue: bool, + _: u3 = 0, + }; +}; + +pub const OutgoingMessage = struct { + address: *const IpAddress, + data_ptr: [*]const u8, + /// Initialized with how many bytes of `data_ptr` to send. After sending + /// succeeds, replaced with how many bytes were actually sent. + data_len: usize, + control: []const u8 = &.{}, +}; + +pub const SendFlags = packed struct(u8) { + confirm: bool = false, + dont_route: bool = false, + eor: bool = false, + oob: bool = false, + fastopen: bool = false, + _: u3 = 0, +}; + +pub const Interface = struct { + /// Value 0 indicates `none`. + index: u32, + + pub const none: Interface = .{ .index = 0 }; + + pub const Name = struct { + bytes: [max_len:0]u8, + + pub const max_len = if (@TypeOf(std.posix.IFNAMESIZE) == void) 0 else std.posix.IFNAMESIZE - 1; + + pub fn toSlice(n: *const Name) []const u8 { + return std.mem.sliceTo(&n.bytes, 0); + } + + pub fn fromSlice(bytes: []const u8) error{NameTooLong}!Name { + if (bytes.len > max_len) return error.NameTooLong; + return .fromSliceUnchecked(bytes); + } + + /// Asserts bytes.len fits in `max_len`. + pub fn fromSliceUnchecked(bytes: []const u8) Name { + assert(bytes.len <= max_len); + var result: Name = undefined; + @memcpy(result.bytes[0..bytes.len], bytes); + result.bytes[bytes.len] = 0; + return result; + } + + pub const ResolveError = error{ + InterfaceNotFound, + AccessDenied, + SystemResources, + } || Io.UnexpectedError || Io.Cancelable; + + /// Corresponds to "if_nametoindex" in libc. + pub fn resolve(n: *const Name, io: Io) ResolveError!Interface { + return io.vtable.netInterfaceNameResolve(io.userdata, n); + } + }; + + pub const NameError = Io.UnexpectedError || Io.Cancelable; + + /// Asserts not `none`. + /// + /// Corresponds to "if_indextoname" in libc. + pub fn name(i: Interface, io: Io) NameError!Name { + assert(i.index != 0); + return io.vtable.netInterfaceName(io.userdata, i); + } + + pub fn isNone(i: Interface) bool { + return i.index == 0; + } +}; + +/// An open port with unspecified protocol. +pub const Socket = struct { + handle: Handle, + /// Contains the resolved ephemeral port number if requested. + address: IpAddress, + + pub const Mode = enum { + /// Provides sequenced, reliable, two-way, connection-based byte + /// streams. An out-of-band data transmission mechanism may be + /// supported. + stream, + /// Supports datagrams (connectionless, unreliable messages of a fixed + /// maximum length). + dgram, + /// Provides a sequenced, reliable, two-way connection-based data + /// transmission path for datagrams of fixed maximum length; a consumer + /// is required to read an entire packet with each input system call. + seqpacket, + /// Provides raw network protocol access. + raw, + /// Provides a reliable datagram layer that does not guarantee ordering. + rdm, + }; + + /// Underlying platform-defined type which may or may not be + /// interchangeable with a file system file descriptor. + pub const Handle = switch (native_os) { + .windows => std.os.windows.ws2_32.SOCKET, + else => std.posix.fd_t, + }; + + /// Leaves `address` in a valid state. + pub fn close(s: *const Socket, io: Io) void { + io.vtable.netClose(io.userdata, s.handle); + } + + pub const SendError = error{ + /// The socket type requires that message be sent atomically, and the + /// size of the message to be sent made this impossible. The message + /// was not transmitted, or was partially transmitted. + MessageOversize, + /// The output queue for a network interface was full. This generally indicates that the + /// interface has stopped sending, but may be caused by transient congestion. (Normally, + /// this does not occur in Linux. Packets are just silently dropped when a device queue + /// overflows.) + /// + /// This is also caused when there is not enough kernel memory available. + SystemResources, + /// No route to network. + NetworkUnreachable, + /// Network reached but no route to host. + HostUnreachable, + /// The local network interface used to reach the destination is offline. + NetworkDown, + /// The destination address is not listening. Can still occur for + /// connectionless messages. + ConnectionRefused, + /// Operating system or protocol does not support the address family. + AddressFamilyUnsupported, + /// Another TCP Fast Open is already in progress. + FastOpenAlreadyInProgress, + /// Network session was unexpectedly closed by recipient. + ConnectionResetByPeer, + /// Local end has been shut down on a connection-oriented socket, or + /// the socket was never connected. + SocketUnconnected, + /// An attempt was made to send to a network/broadcast address as + /// though it was a unicast address. + AccessDenied, + } || Io.UnexpectedError || Io.Cancelable; + + /// Transfers `data` to `dest`, connectionless, in one packet. + pub fn send(s: *const Socket, io: Io, dest: *const IpAddress, data: []const u8) SendError!void { + var message: OutgoingMessage = .{ .address = dest, .data_ptr = data.ptr, .data_len = data.len }; + const err, const n = io.vtable.netSend(io.userdata, s.handle, (&message)[0..1], .{}); + if (n != 1) return err.?; + if (message.data_len != data.len) return error.MessageOversize; + } + + pub fn sendMany(s: *const Socket, io: Io, messages: []OutgoingMessage, flags: SendFlags) SendError!void { + return io.vtable.netSend(io.userdata, s.handle, messages, flags); + } + + pub const ReceiveError = error{ + /// Insufficient memory or other resource internal to the operating system. + SystemResources, + /// Per-process limit on the number of open file descriptors has been reached. + ProcessFdQuotaExceeded, + /// System-wide limit on the total number of open files has been reached. + SystemFdQuotaExceeded, + /// Local end has been shut down on a connection-oriented socket, or + /// the socket was never connected. + SocketUnconnected, + /// The socket type requires that message be sent atomically, and the + /// size of the message to be sent made this impossible. The message + /// was not transmitted, or was partially transmitted. + MessageOversize, + /// Network connection was unexpectedly closed by sender. + ConnectionResetByPeer, + /// The local network interface used to reach the destination is offline. + NetworkDown, + } || Io.UnexpectedError || Io.Cancelable; + + /// Waits for data. Connectionless. + /// + /// See also: + /// * `receiveTimeout` + pub fn receive(s: *const Socket, io: Io, buffer: []u8) ReceiveError!IncomingMessage { + var message: IncomingMessage = undefined; + assert(1 == try io.vtable.netReceive(io.userdata, s.handle, (&message)[0..1], buffer, .{}, .none)); + return message; + } + + pub const ReceiveTimeoutError = ReceiveError || Io.Timeout.Error; + + /// Waits for data. Connectionless. + /// + /// Returns `error.Timeout` if no message arrives early enough. + /// + /// See also: + /// * `receive` + /// * `receiveManyTimeout` + pub fn receiveTimeout( + s: *const Socket, + io: Io, + buffer: []u8, + timeout: Io.Timeout, + ) ReceiveTimeoutError!IncomingMessage { + var message: IncomingMessage = undefined; + assert(1 == try io.vtable.netReceive(io.userdata, s.handle, (&message)[0..1], buffer, .{}, timeout)); + return message; + } + + /// Waits until at least one message is delivered, possibly returning more + /// than one message. Connectionless. + /// + /// Returns number of messages received, or `error.Timeout` if no message + /// arrives early enough. + /// + /// See also: + /// * `receive` + /// * `receiveTimeout` + pub fn receiveManyTimeout( + s: *const Socket, + io: Io, + /// Function assumes each element has initialized `control` field. + /// Initializing with `IncomingMessage.init` may be helpful. + message_buffer: []IncomingMessage, + data_buffer: []u8, + flags: ReceiveFlags, + timeout: Io.Timeout, + ) struct { ?ReceiveTimeoutError, usize } { + return io.vtable.netReceive(io.userdata, s.handle, message_buffer, data_buffer, flags, timeout); + } +}; + +/// An open socket connection with a network protocol that guarantees +/// sequencing, delivery, and prevents repetition. Typically TCP or UNIX domain +/// socket. +pub const Stream = struct { + socket: Socket, + + const max_iovecs_len = 8; + + pub fn close(s: *const Stream, io: Io) void { + io.vtable.netClose(io.userdata, s.socket.handle); + } + + pub const Reader = struct { + io: Io, + interface: Io.Reader, + stream: Stream, + err: ?Error, + + pub const Error = error{ + SystemResources, + ConnectionResetByPeer, + Timeout, + SocketUnconnected, + /// The file descriptor does not hold the required rights to read + /// from it. + AccessDenied, + NetworkDown, + } || Io.Cancelable || Io.UnexpectedError; + + pub fn init(stream: Stream, io: Io, buffer: []u8) Reader { + return .{ + .io = io, + .interface = .{ + .vtable = &.{ + .stream = streamImpl, + .readVec = readVec, + }, + .buffer = buffer, + .seek = 0, + .end = 0, + }, + .stream = stream, + .err = null, + }; + } + + fn streamImpl(io_r: *Io.Reader, io_w: *Io.Writer, limit: Io.Limit) Io.Reader.StreamError!usize { + const dest = limit.slice(try io_w.writableSliceGreedy(1)); + var data: [1][]u8 = .{dest}; + const n = try readVec(io_r, &data); + io_w.advance(n); + return n; + } + + fn readVec(io_r: *Io.Reader, data: [][]u8) Io.Reader.Error!usize { + const r: *Reader = @alignCast(@fieldParentPtr("interface", io_r)); + const io = r.io; + var iovecs_buffer: [max_iovecs_len][]u8 = undefined; + const dest_n, const data_size = try io_r.writableVector(&iovecs_buffer, data); + const dest = iovecs_buffer[0..dest_n]; + assert(dest[0].len > 0); + const n = io.vtable.netRead(io.userdata, r.stream.socket.handle, dest) catch |err| { + r.err = err; + return error.ReadFailed; + }; + if (n == 0) { + return error.EndOfStream; + } + if (n > data_size) { + r.interface.end += n - data_size; + return data_size; + } + return n; + } + }; + + pub const Writer = struct { + io: Io, + interface: Io.Writer, + stream: Stream, + err: ?Error = null, + + pub const Error = error{ + /// Another TCP Fast Open is already in progress. + FastOpenAlreadyInProgress, + /// Network session was unexpectedly closed by recipient. + ConnectionResetByPeer, + /// The output queue for a network interface was full. This generally indicates that the + /// interface has stopped sending, but may be caused by transient congestion. (Normally, + /// this does not occur in Linux. Packets are just silently dropped when a device queue + /// overflows.) + /// + /// This is also caused when there is not enough kernel memory available. + SystemResources, + /// No route to network. + NetworkUnreachable, + /// Network reached but no route to host. + HostUnreachable, + /// The local network interface used to reach the destination is down. + NetworkDown, + /// The destination address is not listening. + ConnectionRefused, + /// The passed address didn't have the correct address family in its sa_family field. + AddressFamilyUnsupported, + /// Local end has been shut down on a connection-oriented socket, or + /// the socket was never connected. + SocketUnconnected, + SocketNotBound, + } || Io.UnexpectedError || Io.Cancelable; + + pub fn init(stream: Stream, io: Io, buffer: []u8) Writer { + return .{ + .io = io, + .stream = stream, + .interface = .{ + .vtable = &.{ .drain = drain }, + .buffer = buffer, + }, + }; + } + + fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize { + const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w)); + const io = w.io; + const buffered = io_w.buffered(); + const handle = w.stream.socket.handle; + const n = io.vtable.netWrite(io.userdata, handle, buffered, data, splat) catch |err| { + w.err = err; + return error.WriteFailed; + }; + return io_w.consume(n); + } + }; + + pub fn reader(stream: Stream, io: Io, buffer: []u8) Reader { + return .init(stream, io, buffer); + } + + pub fn writer(stream: Stream, io: Io, buffer: []u8) Writer { + return .init(stream, io, buffer); + } +}; + +pub const Server = struct { + socket: Socket, + + pub fn deinit(s: *Server, io: Io) void { + s.socket.close(io); + s.* = undefined; + } + + pub const AcceptError = error{ + /// The per-process limit on the number of open file descriptors has been reached. + ProcessFdQuotaExceeded, + /// The system-wide limit on the total number of open files has been reached. + SystemFdQuotaExceeded, + /// Not enough free memory. This often means that the memory allocation is limited + /// by the socket buffer limits, not by the system memory. + SystemResources, + /// The network subsystem has failed. + NetworkDown, + /// No connection is already queued and ready to be accepted, and + /// the socket is configured as non-blocking. + WouldBlock, + /// An incoming connection was indicated, but was subsequently terminated by the + /// remote peer prior to accepting the call. + ConnectionAborted, + /// Firewall rules forbid connection. + BlockedByFirewall, + ProtocolFailure, + } || Io.UnexpectedError || Io.Cancelable; + + /// Blocks until a client connects to the server. + pub fn accept(s: *Server, io: Io) AcceptError!Stream { + return io.vtable.netAccept(io.userdata, s.socket.handle); + } +}; + +test "parsing IPv6 addresses" { + try testIp6Parse("fe80::e0e:76ff:fed4:cf22%eno1"); + try testIp6Parse("2001:db8::1"); + try testIp6ParseTransform("2001:db8::1", "2001:0db8:0000:0000:0000:0000:0000:0001"); + try testIp6Parse("::1"); + try testIp6Parse("::"); + try testIp6Parse("fe80::1"); + try testIp6Parse("fe80::abcd:ef12%3"); + try testIp6Parse("ff02::"); + try testIp6Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"); +} + +fn testIp6Parse(input: []const u8) !void { + return testIp6ParseTransform(input, input); +} + +fn testIp6ParseTransform(expected: []const u8, input: []const u8) !void { + const ua = switch (Ip6Address.Unresolved.parse(input)) { + .success => |p| p, + else => |x| { + std.debug.print("failed to parse \"{s}\": {any}\n", .{ input, x }); + return error.TestFailed; + }, + }; + var buffer: [100]u8 = undefined; + const result = try std.fmt.bufPrint(&buffer, "{f}", .{ua}); + try std.testing.expectEqualStrings(expected, result); +} + +test { + _ = HostName; + _ = @import("net/test.zig"); +} diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig new file mode 100644 index 0000000000..35aeff6942 --- /dev/null +++ b/lib/std/Io/net/HostName.zig @@ -0,0 +1,433 @@ +//! An already-validated host name. A valid host name: +//! * Has length less than or equal to `max_len`. +//! * Is valid UTF-8. +//! * Lacks ASCII characters other than alphanumeric, '-', and '.'. +const HostName = @This(); + +const builtin = @import("builtin"); +const native_os = builtin.os.tag; + +const std = @import("../../std.zig"); +const Io = std.Io; +const IpAddress = Io.net.IpAddress; +const Ip6Address = Io.net.Ip6Address; +const assert = std.debug.assert; +const Stream = Io.net.Stream; + +/// Externally managed memory. Already checked to be valid. +bytes: []const u8, + +pub const max_len = 255; + +pub const ValidateError = error{ + NameTooLong, + InvalidHostName, +}; + +pub fn validate(bytes: []const u8) ValidateError!void { + if (bytes.len > max_len) return error.NameTooLong; + if (!std.unicode.utf8ValidateSlice(bytes)) return error.InvalidHostName; + for (bytes) |byte| { + if (!std.ascii.isAscii(byte) or byte == '.' or byte == '-' or std.ascii.isAlphanumeric(byte)) { + continue; + } + return error.InvalidHostName; + } +} + +pub fn init(bytes: []const u8) ValidateError!HostName { + try validate(bytes); + return .{ .bytes = bytes }; +} + +pub fn sameParentDomain(parent_host: HostName, child_host: HostName) bool { + const parent_bytes = parent_host.bytes; + const child_bytes = child_host.bytes; + if (!std.ascii.endsWithIgnoreCase(child_bytes, parent_bytes)) return false; + if (child_bytes.len == parent_bytes.len) return true; + if (parent_bytes.len > child_bytes.len) return false; + return child_bytes[child_bytes.len - parent_bytes.len - 1] == '.'; +} + +test sameParentDomain { + try std.testing.expect(!sameParentDomain(try .init("foo.com"), try .init("bar.com"))); + try std.testing.expect(sameParentDomain(try .init("foo.com"), try .init("foo.com"))); + try std.testing.expect(sameParentDomain(try .init("foo.com"), try .init("bar.foo.com"))); + try std.testing.expect(!sameParentDomain(try .init("bar.foo.com"), try .init("foo.com"))); +} + +/// Domain names are case-insensitive (RFC 5890, Section 2.3.2.4) +pub fn eql(a: HostName, b: HostName) bool { + return std.ascii.eqlIgnoreCase(a.bytes, b.bytes); +} + +pub const LookupOptions = struct { + port: u16, + canonical_name_buffer: *[max_len]u8, + /// `null` means either. + family: ?IpAddress.Family = null, +}; + +pub const LookupError = error{ + UnknownHostName, + ResolvConfParseFailed, + InvalidDnsARecord, + InvalidDnsAAAARecord, + InvalidDnsCnameRecord, + NameServerFailure, + /// Failed to open or read "/etc/hosts" or "/etc/resolv.conf". + DetectingNetworkConfigurationFailed, +} || Io.Clock.Error || IpAddress.BindError || Io.Cancelable; + +pub const LookupResult = union(enum) { + address: IpAddress, + canonical_name: HostName, + end: LookupError!void, +}; + +/// Adds any number of `IpAddress` into resolved, exactly one canonical_name, +/// and then always finishes by adding one `LookupResult.end` entry. +/// +/// Guaranteed not to block if provided queue has capacity at least 16. +pub fn lookup( + host_name: HostName, + io: Io, + resolved: *Io.Queue(LookupResult), + options: LookupOptions, +) void { + return io.vtable.netLookup(io.userdata, host_name, resolved, options); +} + +pub const ExpandError = error{InvalidDnsPacket} || ValidateError; + +/// Decompresses a DNS name. +/// +/// Returns number of bytes consumed from `packet` starting at `i`, +/// along with the expanded `HostName`. +/// +/// Asserts `buffer` is has length at least `max_len`. +pub fn expand(noalias packet: []const u8, start_i: usize, noalias dest_buffer: []u8) ExpandError!struct { usize, HostName } { + const dest = dest_buffer[0..max_len]; + + var i = start_i; + var dest_i: usize = 0; + var len: ?usize = null; + + // Detect reference loop using an iteration counter. + for (0..packet.len / 2) |_| { + if (i >= packet.len) return error.InvalidDnsPacket; + + const c = packet[i]; + if ((c & 0xc0) != 0) { + if (i + 1 >= packet.len) return error.InvalidDnsPacket; + const j: usize = (@as(usize, c & 0x3F) << 8) | packet[i + 1]; + if (j >= packet.len) return error.InvalidDnsPacket; + if (len == null) len = (i + 2) - start_i; + i = j; + } else if (c != 0) { + if (dest_i != 0) { + dest[dest_i] = '.'; + dest_i += 1; + } + const label_len: usize = c; + if (i + 1 + label_len > packet.len) return error.InvalidDnsPacket; + if (dest_i + label_len + 1 > dest.len) return error.InvalidDnsPacket; + @memcpy(dest[dest_i..][0..label_len], packet[i + 1 ..][0..label_len]); + dest_i += label_len; + i += 1 + label_len; + } else { + dest[dest_i] = 0; + dest_i += 1; + return .{ + len orelse i - start_i + 1, + try .init(dest[0..dest_i]), + }; + } + } + return error.InvalidDnsPacket; +} + +pub const DnsRecord = enum(u8) { + A = 1, + CNAME = 5, + AAAA = 28, + _, +}; + +pub const DnsResponse = struct { + bytes: []const u8, + bytes_index: u32, + answers_remaining: u16, + + pub const Answer = struct { + rr: DnsRecord, + packet: []const u8, + data_off: u32, + data_len: u16, + }; + + pub const Error = error{InvalidDnsPacket}; + + pub fn init(r: []const u8) Error!DnsResponse { + if (r.len < 12) return error.InvalidDnsPacket; + if ((r[3] & 15) != 0) return .{ .bytes = r, .bytes_index = 3, .answers_remaining = 0 }; + var i: u32 = 12; + var query_count = std.mem.readInt(u16, r[4..6], .big); + while (query_count != 0) : (query_count -= 1) { + while (i < r.len and r[i] -% 1 < 127) i += 1; + if (r.len - i < 6) return error.InvalidDnsPacket; + i = i + 5 + @intFromBool(r[i] != 0); + } + return .{ + .bytes = r, + .bytes_index = i, + .answers_remaining = std.mem.readInt(u16, r[6..8], .big), + }; + } + + pub fn next(dr: *DnsResponse) Error!?Answer { + if (dr.answers_remaining == 0) return null; + dr.answers_remaining -= 1; + const r = dr.bytes; + var i = dr.bytes_index; + while (i < r.len and r[i] -% 1 < 127) i += 1; + if (r.len - i < 12) return error.InvalidDnsPacket; + i = i + 1 + @intFromBool(r[i] != 0); + const len = std.mem.readInt(u16, r[i + 8 ..][0..2], .big); + if (i + 10 + len > r.len) return error.InvalidDnsPacket; + defer dr.bytes_index = i + 10 + len; + return .{ + .rr = @enumFromInt(r[i + 1]), + .packet = r, + .data_off = i + 10, + .data_len = len, + }; + } +}; + +pub const ConnectError = LookupError || IpAddress.ConnectError; + +pub fn connect( + host_name: HostName, + io: Io, + port: u16, + options: IpAddress.ConnectOptions, +) ConnectError!Stream { + var connect_many_buffer: [32]ConnectManyResult = undefined; + var connect_many_queue: Io.Queue(ConnectManyResult) = .init(&connect_many_buffer); + + var connect_many = io.async(connectMany, .{ host_name, io, port, &connect_many_queue, options }); + var saw_end = false; + defer { + connect_many.cancel(io); + if (!saw_end) while (true) switch (connect_many_queue.getOneUncancelable(io)) { + .connection => |loser| if (loser) |s| s.close(io) else |_| continue, + .end => break, + }; + } + + var aggregate_error: ConnectError = error.UnknownHostName; + + while (connect_many_queue.getOne(io)) |result| switch (result) { + .connection => |connection| if (connection) |stream| return stream else |err| switch (err) { + error.SystemResources, + error.OptionUnsupported, + error.ProcessFdQuotaExceeded, + error.SystemFdQuotaExceeded, + error.Canceled, + => |e| return e, + + error.WouldBlock => return error.Unexpected, + + else => |e| aggregate_error = e, + }, + .end => |end| { + saw_end = true; + try end; + return aggregate_error; + }, + } else |err| switch (err) { + error.Canceled => |e| return e, + } +} + +pub const ConnectManyResult = union(enum) { + connection: IpAddress.ConnectError!Stream, + end: ConnectError!void, +}; + +/// Asynchronously establishes a connection to all IP addresses associated with +/// a host name, adding them to a results queue upon completion. +pub fn connectMany( + host_name: HostName, + io: Io, + port: u16, + results: *Io.Queue(ConnectManyResult), + options: IpAddress.ConnectOptions, +) void { + var canonical_name_buffer: [max_len]u8 = undefined; + var lookup_buffer: [32]HostName.LookupResult = undefined; + var lookup_queue: Io.Queue(LookupResult) = .init(&lookup_buffer); + var group: Io.Group = .init; + defer group.cancel(io); + + group.async(io, lookup, .{ host_name, io, &lookup_queue, .{ + .port = port, + .canonical_name_buffer = &canonical_name_buffer, + } }); + + while (lookup_queue.getOne(io)) |dns_result| switch (dns_result) { + .address => |address| group.async(io, enqueueConnection, .{ address, io, results, options }), + .canonical_name => continue, + .end => |lookup_result| { + group.wait(io); + results.putOneUncancelable(io, .{ .end = lookup_result }); + return; + }, + } else |err| switch (err) { + error.Canceled => |e| { + group.cancel(io); + results.putOneUncancelable(io, .{ .end = e }); + }, + } +} + +fn enqueueConnection( + address: IpAddress, + io: Io, + queue: *Io.Queue(ConnectManyResult), + options: IpAddress.ConnectOptions, +) void { + queue.putOneUncancelable(io, .{ .connection = address.connect(io, options) }); +} + +pub const ResolvConf = struct { + attempts: u32, + ndots: u32, + timeout_seconds: u32, + nameservers_buffer: [max_nameservers]IpAddress, + nameservers_len: usize, + search_buffer: [max_len]u8, + search_len: usize, + + /// According to resolv.conf(5) there is a maximum of 3 nameservers in this + /// file. + pub const max_nameservers = 3; + + /// Returns `error.StreamTooLong` if a line is longer than 512 bytes. + pub fn init(io: Io) !ResolvConf { + var rc: ResolvConf = .{ + .nameservers_buffer = undefined, + .nameservers_len = 0, + .search_buffer = undefined, + .search_len = 0, + .ndots = 1, + .timeout_seconds = 5, + .attempts = 2, + }; + + const file = Io.File.openAbsolute(io, "/etc/resolv.conf", .{}) catch |err| switch (err) { + error.FileNotFound, + error.NotDir, + error.AccessDenied, + => { + try addNumeric(&rc, io, "127.0.0.1", 53); + return rc; + }, + + else => |e| return e, + }; + defer file.close(io); + + var line_buf: [512]u8 = undefined; + var file_reader = file.reader(io, &line_buf); + parse(&rc, io, &file_reader.interface) catch |err| switch (err) { + error.ReadFailed => return file_reader.err.?, + else => |e| return e, + }; + return rc; + } + + const Directive = enum { options, nameserver, domain, search }; + const Option = enum { ndots, attempts, timeout }; + + pub fn parse(rc: *ResolvConf, io: Io, reader: *Io.Reader) !void { + while (reader.takeSentinel('\n')) |line_with_comment| { + const line = line: { + var split = std.mem.splitScalar(u8, line_with_comment, '#'); + break :line split.first(); + }; + var line_it = std.mem.tokenizeAny(u8, line, " \t"); + + const token = line_it.next() orelse continue; + switch (std.meta.stringToEnum(Directive, token) orelse continue) { + .options => while (line_it.next()) |sub_tok| { + var colon_it = std.mem.splitScalar(u8, sub_tok, ':'); + const name = colon_it.first(); + const value_txt = colon_it.next() orelse continue; + const value = std.fmt.parseInt(u8, value_txt, 10) catch |err| switch (err) { + error.Overflow => 255, + error.InvalidCharacter => continue, + }; + switch (std.meta.stringToEnum(Option, name) orelse continue) { + .ndots => rc.ndots = @min(value, 15), + .attempts => rc.attempts = @min(value, 10), + .timeout => rc.timeout_seconds = @min(value, 60), + } + }, + .nameserver => { + const ip_txt = line_it.next() orelse continue; + try addNumeric(rc, io, ip_txt, 53); + }, + .domain, .search => { + const rest = line_it.rest(); + @memcpy(rc.search_buffer[0..rest.len], rest); + rc.search_len = rest.len; + }, + } + } else |err| switch (err) { + error.EndOfStream => if (reader.bufferedLen() != 0) return error.EndOfStream, + else => |e| return e, + } + + if (rc.nameservers_len == 0) { + try addNumeric(rc, io, "127.0.0.1", 53); + } + } + + fn addNumeric(rc: *ResolvConf, io: Io, name: []const u8, port: u16) !void { + if (rc.nameservers_len < rc.nameservers_buffer.len) { + rc.nameservers_buffer[rc.nameservers_len] = try .resolve(io, name, port); + rc.nameservers_len += 1; + } + } + + pub fn nameservers(rc: *const ResolvConf) []const IpAddress { + return rc.nameservers_buffer[0..rc.nameservers_len]; + } +}; + +test ResolvConf { + const input = + \\# Generated by resolvconf + \\nameserver 1.0.0.1 + \\nameserver 1.1.1.1 + \\nameserver fe80::e0e:76ff:fed4:cf22 + \\options edns0 + \\ + ; + var reader: Io.Reader = .fixed(input); + + var rc: ResolvConf = .{ + .nameservers_buffer = undefined, + .nameservers_len = 0, + .search_buffer = undefined, + .search_len = 0, + .ndots = 1, + .timeout_seconds = 5, + .attempts = 2, + }; + + try rc.parse(std.testing.io, &reader); + try std.testing.expectEqual(3, rc.nameservers().len); +} diff --git a/lib/std/Io/net/test.zig b/lib/std/Io/net/test.zig new file mode 100644 index 0000000000..60ca66349f --- /dev/null +++ b/lib/std/Io/net/test.zig @@ -0,0 +1,345 @@ +const builtin = @import("builtin"); + +const std = @import("std"); +const Io = std.Io; +const net = std.Io.net; +const mem = std.mem; +const testing = std.testing; + +test "parse and render IP addresses at comptime" { + comptime { + const ipv6addr = net.IpAddress.parse("::1", 0) catch unreachable; + try testing.expectFmt("[::1]:0", "{f}", .{ipv6addr}); + + const ipv4addr = net.IpAddress.parse("127.0.0.1", 0) catch unreachable; + try testing.expectFmt("127.0.0.1:0", "{f}", .{ipv4addr}); + + try testing.expectError(error.ParseFailed, net.IpAddress.parse("::123.123.123.123", 0)); + try testing.expectError(error.ParseFailed, net.IpAddress.parse("127.01.0.1", 0)); + } +} + +test "format IPv6 address with no zero runs" { + const addr = try net.IpAddress.parseIp6("2001:db8:1:2:3:4:5:6", 0); + try testing.expectFmt("[2001:db8:1:2:3:4:5:6]:0", "{f}", .{addr}); +} + +test "parse IPv6 addresses and check compressed form" { + try testing.expectFmt("[2001:db8::1:0:0:2]:0", "{f}", .{ + try net.IpAddress.parseIp6("2001:0db8:0000:0000:0001:0000:0000:0002", 0), + }); + try testing.expectFmt("[2001:db8::1:2]:0", "{f}", .{ + try net.IpAddress.parseIp6("2001:0db8:0000:0000:0000:0000:0001:0002", 0), + }); + try testing.expectFmt("[2001:db8:1:0:1::2]:0", "{f}", .{ + try net.IpAddress.parseIp6("2001:0db8:0001:0000:0001:0000:0000:0002", 0), + }); +} + +test "parse IPv6 address, check raw bytes" { + const expected_raw: [16]u8 = .{ + 0x20, 0x01, 0x0d, 0xb8, // 2001:db8 + 0x00, 0x00, 0x00, 0x00, // :0000:0000 + 0x00, 0x01, 0x00, 0x00, // :0001:0000 + 0x00, 0x00, 0x00, 0x02, // :0000:0002 + }; + const addr = try net.IpAddress.parseIp6("2001:db8:0000:0000:0001:0000:0000:0002", 0); + try testing.expectEqualSlices(u8, &expected_raw, &addr.ip6.bytes); +} + +test "parse and render IPv6 addresses" { + try testParseAndRenderIp6Address("FF01:0:0:0:0:0:0:FB", "ff01::fb"); + try testParseAndRenderIp6Address("FF01::Fb", "ff01::fb"); + try testParseAndRenderIp6Address("::1", "::1"); + try testParseAndRenderIp6Address("::", "::"); + try testParseAndRenderIp6Address("1::", "1::"); + try testParseAndRenderIp6Address("2001:db8::", "2001:db8::"); + try testParseAndRenderIp6Address("::1234:5678", "::1234:5678"); + try testParseAndRenderIp6Address("2001:db8::1234:5678", "2001:db8::1234:5678"); + try testParseAndRenderIp6Address("FF01::FB%1234", "ff01::fb%1234"); + try testParseAndRenderIp6Address("::ffff:123.5.123.5", "::ffff:123.5.123.5"); + try testParseAndRenderIp6Address("ff01::fb%12345678901234", "ff01::fb%12345678901234"); +} + +fn testParseAndRenderIp6Address(input: []const u8, expected_output: []const u8) !void { + var buffer: [100]u8 = undefined; + const parsed = net.Ip6Address.Unresolved.parse(input); + const actual_printed = try std.fmt.bufPrint(&buffer, "{f}", .{parsed.success}); + try testing.expectEqualStrings(expected_output, actual_printed); +} + +test "IPv6 address parse failures" { + try testing.expectError(error.ParseFailed, net.IpAddress.parseIp6(":::", 0)); + + const Unresolved = net.Ip6Address.Unresolved; + + try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 2 }, Unresolved.parse(":::")); + try testing.expectEqual(Unresolved.Parsed{ .overflow = 4 }, Unresolved.parse("FF001::FB")); + try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 9 }, Unresolved.parse("FF01::Fb:zig")); + try testing.expectEqual(Unresolved.Parsed{ .junk_after_end = 19 }, Unresolved.parse("FF01:0:0:0:0:0:0:FB:")); + try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("FF01:")); + try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 5 }, Unresolved.parse("::123.123.123.123")); + try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("1")); + try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("ff01::fb%")); +} + +test "invalid but parseable IPv6 scope ids" { + const io = testing.io; + + if (builtin.os.tag != .linux and comptime !builtin.os.tag.isDarwin()) { + return error.SkipZigTest; // TODO + } + + try testing.expectError(error.InterfaceNotFound, net.IpAddress.resolveIp6(io, "ff01::fb%123s45678901234", 0)); +} + +test "parse and render IPv4 addresses" { + var buffer: [18]u8 = undefined; + for ([_][]const u8{ + "0.0.0.0", + "255.255.255.255", + "1.2.3.4", + "123.255.0.91", + "127.0.0.1", + }) |ip| { + const addr = net.IpAddress.parseIp4(ip, 0) catch unreachable; + var newIp = std.fmt.bufPrint(buffer[0..], "{f}", .{addr}) catch unreachable; + try testing.expect(std.mem.eql(u8, ip, newIp[0 .. newIp.len - 2])); + } + + try testing.expectError(error.Overflow, net.IpAddress.parseIp4("256.0.0.1", 0)); + try testing.expectError(error.InvalidCharacter, net.IpAddress.parseIp4("x.0.0.1", 0)); + try testing.expectError(error.InvalidEnd, net.IpAddress.parseIp4("127.0.0.1.1", 0)); + try testing.expectError(error.Incomplete, net.IpAddress.parseIp4("127.0.0.", 0)); + try testing.expectError(error.InvalidCharacter, net.IpAddress.parseIp4("100..0.1", 0)); + try testing.expectError(error.NonCanonical, net.IpAddress.parseIp4("127.01.0.1", 0)); +} + +test "resolve DNS" { + if (builtin.os.tag == .wasi) return error.SkipZigTest; + + const io = testing.io; + + // Resolve localhost, this should not fail. + { + const localhost_v4 = try net.IpAddress.parse("127.0.0.1", 80); + const localhost_v6 = try net.IpAddress.parse("::2", 80); + + var canonical_name_buffer: [net.HostName.max_len]u8 = undefined; + var results_buffer: [32]net.HostName.LookupResult = undefined; + var results: Io.Queue(net.HostName.LookupResult) = .init(&results_buffer); + + net.HostName.lookup(try .init("localhost"), io, &results, .{ + .port = 80, + .canonical_name_buffer = &canonical_name_buffer, + }); + + var addresses_found: usize = 0; + + while (results.getOne(io)) |result| switch (result) { + .address => |address| { + if (address.eql(&localhost_v4) or address.eql(&localhost_v6)) + addresses_found += 1; + }, + .canonical_name => |canonical_name| try testing.expectEqualStrings("localhost", canonical_name.bytes), + .end => |end| { + try end; + break; + }, + } else |err| return err; + + try testing.expect(addresses_found != 0); + } + + { + // The tests are required to work even when there is no Internet connection, + // so some of these errors we must accept and skip the test. + var canonical_name_buffer: [net.HostName.max_len]u8 = undefined; + var results_buffer: [16]net.HostName.LookupResult = undefined; + var results: Io.Queue(net.HostName.LookupResult) = .init(&results_buffer); + + net.HostName.lookup(try .init("example.com"), io, &results, .{ + .port = 80, + .canonical_name_buffer = &canonical_name_buffer, + }); + + while (results.getOne(io)) |result| switch (result) { + .address => {}, + .canonical_name => {}, + .end => |end| { + end catch |err| switch (err) { + error.UnknownHostName => return error.SkipZigTest, + error.NameServerFailure => return error.SkipZigTest, + else => return err, + }; + break; + }, + } else |err| return err; + } +} + +test "listen on a port, send bytes, receive bytes" { + if (builtin.single_threaded) return error.SkipZigTest; + if (builtin.os.tag == .wasi) return error.SkipZigTest; + + const io = testing.io; + + // Try only the IPv4 variant as some CI builders have no IPv6 localhost + // configured. + const localhost: net.IpAddress = .{ .ip4 = .loopback(0) }; + + var server = try localhost.listen(io, .{}); + defer server.deinit(io); + + const S = struct { + fn clientFn(server_address: net.IpAddress) !void { + var stream = try server_address.connect(io, .{ .mode = .stream }); + defer stream.close(io); + + var stream_writer = stream.writer(io, &.{}); + try stream_writer.interface.writeAll("Hello world!"); + } + }; + + const t = try std.Thread.spawn(.{}, S.clientFn, .{server.socket.address}); + defer t.join(); + + var stream = try server.accept(io); + defer stream.close(io); + var buf: [16]u8 = undefined; + var stream_reader = stream.reader(io, &.{}); + const n = try stream_reader.interface.readSliceShort(&buf); + + try testing.expectEqual(@as(usize, 12), n); + try testing.expectEqualSlices(u8, "Hello world!", buf[0..n]); +} + +test "listen on an in use port" { + if (builtin.os.tag != .linux and comptime !builtin.os.tag.isDarwin() and builtin.os.tag != .windows) { + // TODO build abstractions for other operating systems + return error.SkipZigTest; + } + + const io = testing.io; + + const localhost: net.IpAddress = .{ .ip4 = .loopback(0) }; + + var server1 = try localhost.listen(io, .{ .reuse_address = true }); + defer server1.deinit(io); + + var server2 = try server1.socket.address.listen(io, .{ .reuse_address = true }); + defer server2.deinit(io); +} + +fn testClientToHost(allocator: mem.Allocator, name: []const u8, port: u16) anyerror!void { + if (builtin.os.tag == .wasi) return error.SkipZigTest; + + const connection = try net.tcpConnectToHost(allocator, name, port); + defer connection.close(); + + var buf: [100]u8 = undefined; + const len = try connection.read(&buf); + const msg = buf[0..len]; + try testing.expect(mem.eql(u8, msg, "hello from server\n")); +} + +fn testClient(addr: net.IpAddress) anyerror!void { + if (builtin.os.tag == .wasi) return error.SkipZigTest; + + const socket_file = try net.tcpConnectToAddress(addr); + defer socket_file.close(); + + var buf: [100]u8 = undefined; + const len = try socket_file.read(&buf); + const msg = buf[0..len]; + try testing.expect(mem.eql(u8, msg, "hello from server\n")); +} + +fn testServer(server: *net.Server) anyerror!void { + if (builtin.os.tag == .wasi) return error.SkipZigTest; + + const io = testing.io; + + var stream = try server.accept(io); + var writer = stream.writer(io, &.{}); + try writer.interface.print("hello from server\n", .{}); +} + +test "listen on a unix socket, send bytes, receive bytes" { + if (builtin.single_threaded) return error.SkipZigTest; + if (!net.has_unix_sockets) return error.SkipZigTest; + + const io = testing.io; + + const socket_path = try generateFileName("socket.unix"); + defer testing.allocator.free(socket_path); + + const socket_addr = try net.UnixAddress.init(socket_path); + defer std.fs.cwd().deleteFile(socket_path) catch {}; + + var server = try socket_addr.listen(io, .{}); + defer server.socket.close(io); + + const S = struct { + fn clientFn(path: []const u8) !void { + const server_path: net.UnixAddress = try .init(path); + var stream = try server_path.connect(io); + defer stream.close(io); + + var stream_writer = stream.writer(io, &.{}); + try stream_writer.interface.writeAll("Hello world!"); + } + }; + + const t = try std.Thread.spawn(.{}, S.clientFn, .{socket_path}); + defer t.join(); + + var stream = try server.accept(io); + defer stream.close(io); + var buf: [16]u8 = undefined; + var stream_reader = stream.reader(io, &.{}); + const n = try stream_reader.interface.readSliceShort(&buf); + + try testing.expectEqual(@as(usize, 12), n); + try testing.expectEqualSlices(u8, "Hello world!", buf[0..n]); +} + +fn generateFileName(base_name: []const u8) ![]const u8 { + const random_bytes_count = 12; + const sub_path_len = comptime std.fs.base64_encoder.calcSize(random_bytes_count); + var random_bytes: [12]u8 = undefined; + std.crypto.random.bytes(&random_bytes); + var sub_path: [sub_path_len]u8 = undefined; + _ = std.fs.base64_encoder.encode(&sub_path, &random_bytes); + return std.fmt.allocPrint(testing.allocator, "{s}-{s}", .{ sub_path[0..], base_name }); +} + +test "non-blocking tcp server" { + if (builtin.os.tag == .wasi) return error.SkipZigTest; + if (true) { + // https://github.com/ziglang/zig/issues/18315 + return error.SkipZigTest; + } + + const io = testing.io; + + const localhost: net.IpAddress = .{ .ip4 = .loopback(0) }; + var server = localhost.listen(io, .{ .force_nonblocking = true }); + defer server.deinit(io); + + const accept_err = server.accept(io); + try testing.expectError(error.WouldBlock, accept_err); + + const socket_file = try net.tcpConnectToAddress(server.socket.address); + defer socket_file.close(); + + var stream = try server.accept(io); + defer stream.close(io); + var writer = stream.writer(io, .{}); + try writer.interface.print("hello from server\n", .{}); + + var buf: [100]u8 = undefined; + const len = try socket_file.read(&buf); + const msg = buf[0..len]; + try testing.expect(mem.eql(u8, msg, "hello from server\n")); +} diff --git a/lib/std/Io/test.zig b/lib/std/Io/test.zig index 22dc163dc6..52aca5ddcf 100644 --- a/lib/std/Io/test.zig +++ b/lib/std/Io/test.zig @@ -1,21 +1,28 @@ +const builtin = @import("builtin"); +const native_endian = builtin.cpu.arch.endian(); + const std = @import("std"); -const DefaultPrng = std.Random.DefaultPrng; +const Io = std.Io; +const testing = std.testing; const expect = std.testing.expect; const expectEqual = std.testing.expectEqual; const expectError = std.testing.expectError; +const DefaultPrng = std.Random.DefaultPrng; const mem = std.mem; const fs = std.fs; const File = std.fs.File; -const native_endian = @import("builtin").target.cpu.arch.endian(); +const assert = std.debug.assert; const tmpDir = std.testing.tmpDir; test "write a file, read it, then delete it" { + const io = testing.io; + var tmp = tmpDir(.{}); defer tmp.cleanup(); var data: [1024]u8 = undefined; - var prng = DefaultPrng.init(std.testing.random_seed); + var prng = DefaultPrng.init(testing.random_seed); const random = prng.random(); random.bytes(data[0..]); const tmp_file_name = "temp_test_file.txt"; @@ -45,9 +52,9 @@ test "write a file, read it, then delete it" { try expectEqual(expected_file_size, file_size); var file_buffer: [1024]u8 = undefined; - var file_reader = file.reader(&file_buffer); - const contents = try file_reader.interface.allocRemaining(std.testing.allocator, .limited(2 * 1024)); - defer std.testing.allocator.free(contents); + var file_reader = file.reader(io, &file_buffer); + const contents = try file_reader.interface.allocRemaining(testing.allocator, .limited(2 * 1024)); + defer testing.allocator.free(contents); try expect(mem.eql(u8, contents[0.."begin".len], "begin")); try expect(mem.eql(u8, contents["begin".len .. contents.len - "end".len], &data)); @@ -89,18 +96,18 @@ test "setEndPos" { defer file.close(); // Verify that the file size changes and the file offset is not moved - try std.testing.expect((try file.getEndPos()) == 0); - try std.testing.expect((try file.getPos()) == 0); + try expect((try file.getEndPos()) == 0); + try expect((try file.getPos()) == 0); try file.setEndPos(8192); - try std.testing.expect((try file.getEndPos()) == 8192); - try std.testing.expect((try file.getPos()) == 0); + try expect((try file.getEndPos()) == 8192); + try expect((try file.getPos()) == 0); try file.seekTo(100); try file.setEndPos(4096); - try std.testing.expect((try file.getEndPos()) == 4096); - try std.testing.expect((try file.getPos()) == 100); + try expect((try file.getEndPos()) == 4096); + try expect((try file.getPos()) == 100); try file.setEndPos(0); - try std.testing.expect((try file.getEndPos()) == 0); - try std.testing.expect((try file.getPos()) == 100); + try expect((try file.getEndPos()) == 0); + try expect((try file.getPos()) == 100); } test "updateTimes" { @@ -114,10 +121,90 @@ test "updateTimes" { const stat_old = try file.stat(); // Set atime and mtime to 5s before try file.updateTimes( - stat_old.atime - 5 * std.time.ns_per_s, - stat_old.mtime - 5 * std.time.ns_per_s, + stat_old.atime.subDuration(.fromSeconds(5)), + stat_old.mtime.subDuration(.fromSeconds(5)), ); const stat_new = try file.stat(); - try expect(stat_new.atime < stat_old.atime); - try expect(stat_new.mtime < stat_old.mtime); + try expect(stat_new.atime.nanoseconds < stat_old.atime.nanoseconds); + try expect(stat_new.mtime.nanoseconds < stat_old.mtime.nanoseconds); +} + +test "Group" { + const io = testing.io; + + var group: Io.Group = .init; + var results: [2]usize = undefined; + + group.async(io, count, .{ 1, 10, &results[0] }); + group.async(io, count, .{ 20, 30, &results[1] }); + + group.wait(io); + + try testing.expectEqualSlices(usize, &.{ 45, 245 }, &results); +} + +fn count(a: usize, b: usize, result: *usize) void { + var sum: usize = 0; + for (a..b) |i| { + sum += i; + } + result.* = sum; +} + +test "Group cancellation" { + const io = testing.io; + + var group: Io.Group = .init; + var results: [2]usize = undefined; + + group.async(io, sleep, .{ io, &results[0] }); + group.async(io, sleep, .{ io, &results[1] }); + + group.cancel(io); + + try testing.expectEqualSlices(usize, &.{ 1, 1 }, &results); +} + +fn sleep(io: Io, result: *usize) void { + // TODO when cancellation race bug is fixed, make this timeout much longer so that + // it causes the unit test to be failed if not cancelled. + io.sleep(.fromMilliseconds(1), .awake) catch {}; + result.* = 1; +} + +test "select" { + const io = testing.io; + + var queue: Io.Queue(u8) = .init(&.{}); + + var get_a = io.concurrent(Io.Queue(u8).getOne, .{ &queue, io }) catch |err| switch (err) { + error.ConcurrencyUnavailable => { + try testing.expect(builtin.single_threaded); + return; + }, + }; + defer if (get_a.cancel(io)) |_| {} else |_| @panic("fail"); + + var get_b = try io.concurrent(Io.Queue(u8).getOne, .{ &queue, io }); + defer if (get_b.cancel(io)) |_| {} else |_| @panic("fail"); + + var timeout = io.async(Io.sleep, .{ io, .fromMilliseconds(1), .awake }); + defer timeout.cancel(io) catch {}; + + switch (try io.select(.{ + .get_a = &get_a, + .get_b = &get_b, + .timeout = &timeout, + })) { + .get_a => return error.TestFailure, + .get_b => return error.TestFailure, + .timeout => { + // Unblock the queues to avoid making this unit test depend on + // cancellation. + queue.putOneUncancelable(io, 1); + queue.putOneUncancelable(io, 1); + try testing.expectEqual(1, try get_a.await(io)); + try testing.expectEqual(1, try get_b.await(io)); + }, + } } diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index fb06fca2e9..d2e962a8c2 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -392,7 +392,7 @@ var global_progress: Progress = .{ .terminal = undefined, .terminal_mode = .off, .update_thread = null, - .redraw_event = .{}, + .redraw_event = .unset, .refresh_rate_ns = undefined, .initial_delay_ns = undefined, .rows = 0, @@ -493,7 +493,7 @@ pub fn start(options: Options) Node { .mask = posix.sigemptyset(), .flags = (posix.SA.SIGINFO | posix.SA.RESTART), }; - posix.sigaction(posix.SIG.WINCH, &act, null); + posix.sigaction(.WINCH, &act, null); } if (switch (global_progress.terminal_mode) { @@ -523,9 +523,7 @@ pub fn setStatus(new_status: Status) void { /// Returns whether a resize is needed to learn the terminal size. fn wait(timeout_ns: u64) bool { - const resize_flag = if (global_progress.redraw_event.timedWait(timeout_ns)) |_| - true - else |err| switch (err) { + const resize_flag = if (global_progress.redraw_event.timedWait(timeout_ns)) |_| true else |err| switch (err) { error.Timeout => false, }; global_progress.redraw_event.reset(); @@ -1537,10 +1535,10 @@ fn maybeUpdateSize(resize_flag: bool) void { } } -fn handleSigWinch(sig: i32, info: *const posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) void { +fn handleSigWinch(sig: posix.SIG, info: *const posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) void { _ = info; _ = ctx_ptr; - assert(sig == posix.SIG.WINCH); + assert(sig == .WINCH); global_progress.redraw_event.set(); } diff --git a/lib/std/Random.zig b/lib/std/Random.zig index ae88d8b4fe..b90c8e3958 100644 --- a/lib/std/Random.zig +++ b/lib/std/Random.zig @@ -58,6 +58,12 @@ pub fn bytes(r: Random, buf: []u8) void { r.fillFn(r.ptr, buf); } +pub fn array(r: Random, comptime E: type, comptime N: usize) [N]E { + var result: [N]E = undefined; + bytes(r, &result); + return result; +} + pub fn boolean(r: Random) bool { return r.int(u1) != 0; } diff --git a/lib/std/Target/Query.zig b/lib/std/Target/Query.zig index 90c127d800..f3f5155b06 100644 --- a/lib/std/Target/Query.zig +++ b/lib/std/Target/Query.zig @@ -612,6 +612,8 @@ fn versionEqualOpt(a: ?SemanticVersion, b: ?SemanticVersion) bool { } test parse { + const io = std.testing.io; + if (builtin.target.isGnuLibC()) { var query = try Query.parse(.{}); query.setGnuLibCVersion(2, 1, 1); @@ -654,7 +656,7 @@ test parse { .arch_os_abi = "x86_64-linux-gnu", .cpu_features = "x86_64-sse-sse2-avx-cx8", }); - const target = try std.zig.system.resolveTargetQuery(query); + const target = try std.zig.system.resolveTargetQuery(io, query); try std.testing.expect(target.os.tag == .linux); try std.testing.expect(target.abi == .gnu); @@ -679,7 +681,7 @@ test parse { .arch_os_abi = "arm-linux-musleabihf", .cpu_features = "generic+v8a", }); - const target = try std.zig.system.resolveTargetQuery(query); + const target = try std.zig.system.resolveTargetQuery(io, query); try std.testing.expect(target.os.tag == .linux); try std.testing.expect(target.abi == .musleabihf); @@ -696,7 +698,7 @@ test parse { .arch_os_abi = "aarch64-linux.3.10...4.4.1-gnu.2.27", .cpu_features = "generic+v8a", }); - const target = try std.zig.system.resolveTargetQuery(query); + const target = try std.zig.system.resolveTargetQuery(io, query); try std.testing.expect(target.cpu.arch == .aarch64); try std.testing.expect(target.os.tag == .linux); @@ -719,7 +721,7 @@ test parse { const query = try Query.parse(.{ .arch_os_abi = "aarch64-linux.3.10...4.4.1-android.30", }); - const target = try std.zig.system.resolveTargetQuery(query); + const target = try std.zig.system.resolveTargetQuery(io, query); try std.testing.expect(target.cpu.arch == .aarch64); try std.testing.expect(target.os.tag == .linux); @@ -740,7 +742,7 @@ test parse { const query = try Query.parse(.{ .arch_os_abi = "x86-windows.xp...win8-msvc", }); - const target = try std.zig.system.resolveTargetQuery(query); + const target = try std.zig.system.resolveTargetQuery(io, query); try std.testing.expect(target.cpu.arch == .x86); try std.testing.expect(target.os.tag == .windows); diff --git a/lib/std/Thread.zig b/lib/std/Thread.zig index f587b15f86..f3abe1e6cf 100644 --- a/lib/std/Thread.zig +++ b/lib/std/Thread.zig @@ -10,9 +10,9 @@ const target = builtin.target; const native_os = builtin.os.tag; const posix = std.posix; const windows = std.os.windows; +const testing = std.testing; pub const Futex = @import("Thread/Futex.zig"); -pub const ResetEvent = @import("Thread/ResetEvent.zig"); pub const Mutex = @import("Thread/Mutex.zig"); pub const Semaphore = @import("Thread/Semaphore.zig"); pub const Condition = @import("Thread/Condition.zig"); @@ -22,81 +22,122 @@ pub const WaitGroup = @import("Thread/WaitGroup.zig"); pub const use_pthreads = native_os != .windows and native_os != .wasi and builtin.link_libc; -/// Spurious wakeups are possible and no precision of timing is guaranteed. -pub fn sleep(nanoseconds: u64) void { - if (builtin.os.tag == .windows) { - const big_ms_from_ns = nanoseconds / std.time.ns_per_ms; - const ms = math.cast(windows.DWORD, big_ms_from_ns) orelse math.maxInt(windows.DWORD); - windows.kernel32.Sleep(ms); - return; +/// A thread-safe logical boolean value which can be `set` and `unset`. +/// +/// It can also block threads until the value is set with cancelation via timed +/// waits. Statically initializable; four bytes on all targets. +pub const ResetEvent = enum(u32) { + unset = 0, + waiting = 1, + is_set = 2, + + /// Returns whether the logical boolean is `set`. + /// + /// Once `reset` is called, this returns false until the next `set`. + /// + /// The memory accesses before the `set` can be said to happen before + /// `isSet` returns true. + pub fn isSet(re: *const ResetEvent) bool { + if (builtin.single_threaded) return switch (re.*) { + .unset => false, + .waiting => unreachable, + .is_set => true, + }; + // Acquire barrier ensures memory accesses before `set` happen before + // returning true. + return @atomicLoad(ResetEvent, re, .acquire) == .is_set; } - if (builtin.os.tag == .wasi) { - const w = std.os.wasi; - const userdata: w.userdata_t = 0x0123_45678; - const clock: w.subscription_clock_t = .{ - .id = .MONOTONIC, - .timeout = nanoseconds, - .precision = 0, - .flags = 0, + /// Blocks the calling thread until `set` is called. + /// + /// This is effectively a more efficient version of `while (!isSet()) {}`. + /// + /// The memory accesses before the `set` can be said to happen before `wait` returns. + pub fn wait(re: *ResetEvent) void { + if (builtin.single_threaded) switch (re.*) { + .unset => unreachable, // Deadlock, no other threads to wake us up. + .waiting => unreachable, // Invalid state. + .is_set => return, }; - const in: w.subscription_t = .{ - .userdata = userdata, - .u = .{ - .tag = .CLOCK, - .u = .{ .clock = clock }, - }, + if (!re.isSet()) return timedWaitInner(re, null) catch |err| switch (err) { + error.Timeout => unreachable, // No timeout specified. }; - - var event: w.event_t = undefined; - var nevents: usize = undefined; - _ = w.poll_oneoff(&in, &event, 1, &nevents); - return; } - if (builtin.os.tag == .uefi) { - const boot_services = std.os.uefi.system_table.boot_services.?; - const us_from_ns = nanoseconds / std.time.ns_per_us; - const us = math.cast(usize, us_from_ns) orelse math.maxInt(usize); - boot_services.stall(us) catch unreachable; - return; + /// Blocks the calling thread until `set` is called, or until the + /// corresponding timeout expires, returning `error.Timeout`. + /// + /// This is effectively a more efficient version of `while (!isSet()) {}`. + /// + /// The memory accesses before the set() can be said to happen before + /// timedWait() returns without error. + pub fn timedWait(re: *ResetEvent, timeout_ns: u64) error{Timeout}!void { + if (builtin.single_threaded) switch (re.*) { + .unset => return error.Timeout, + .waiting => unreachable, // Invalid state. + .is_set => return, + }; + if (!re.isSet()) return timedWaitInner(re, timeout_ns); } - const s = nanoseconds / std.time.ns_per_s; - const ns = nanoseconds % std.time.ns_per_s; + fn timedWaitInner(re: *ResetEvent, timeout: ?u64) error{Timeout}!void { + @branchHint(.cold); - // Newer kernel ports don't have old `nanosleep()` and `clock_nanosleep()` has been around - // since Linux 2.6 and glibc 2.1 anyway. - if (builtin.os.tag == .linux) { - const linux = std.os.linux; + // Try to set the state from `unset` to `waiting` to indicate to the + // `set` thread that others are blocked on the ResetEvent. Avoid using + // any strict barriers until we know the ResetEvent is set. + var state = @atomicLoad(ResetEvent, re, .acquire); + if (state == .unset) { + state = @cmpxchgStrong(ResetEvent, re, state, .waiting, .acquire, .acquire) orelse .waiting; + } - var req: linux.timespec = .{ - .sec = std.math.cast(linux.time_t, s) orelse std.math.maxInt(linux.time_t), - .nsec = std.math.cast(linux.time_t, ns) orelse std.math.maxInt(linux.time_t), - }; - var rem: linux.timespec = undefined; + // Wait until the ResetEvent is set since the state is waiting. + if (state == .waiting) { + var futex_deadline = Futex.Deadline.init(timeout); + while (true) { + const wait_result = futex_deadline.wait(@ptrCast(re), @intFromEnum(ResetEvent.waiting)); - while (true) { - switch (linux.E.init(linux.clock_nanosleep(.MONOTONIC, .{ .ABSTIME = false }, &req, &rem))) { - .SUCCESS => return, - .INTR => { - req = rem; - continue; - }, - .FAULT => unreachable, - .INVAL => unreachable, - .OPNOTSUPP => unreachable, - else => return, + // Check if the ResetEvent was set before possibly reporting error.Timeout below. + state = @atomicLoad(ResetEvent, re, .acquire); + if (state != .waiting) break; + + try wait_result; } } + + assert(state == .is_set); + } + + /// Marks the logical boolean as `set` and unblocks any threads in `wait` + /// or `timedWait` to observe the new state. + /// + /// The logical boolean stays `set` until `reset` is called, making future + /// `set` calls do nothing semantically. + /// + /// The memory accesses before `set` can be said to happen before `isSet` + /// returns true or `wait`/`timedWait` return successfully. + pub fn set(re: *ResetEvent) void { + if (builtin.single_threaded) { + re.* = .is_set; + return; + } + if (@atomicRmw(ResetEvent, re, .Xchg, .is_set, .release) == .waiting) { + Futex.wake(@ptrCast(re), std.math.maxInt(u32)); + } } - posix.nanosleep(s, ns); -} - -test sleep { - sleep(1); -} + /// Unmarks the ResetEvent as if `set` was never called. + /// + /// Assumes no threads are blocked in `wait` or `timedWait`. Concurrent + /// calls to `set`, `isSet` and `reset` are allowed. + pub fn reset(re: *ResetEvent) void { + if (builtin.single_threaded) { + re.* = .unset; + return; + } + @atomicStore(ResetEvent, re, .unset, .monotonic); + } +}; const Thread = @This(); const Impl = if (native_os == .windows) @@ -130,6 +171,7 @@ pub const SetNameError = error{ NameTooLong, Unsupported, Unexpected, + InvalidWtf8, } || posix.PrctlError || posix.WriteError || std.fs.File.OpenError || std.fmt.BufPrintError; pub fn setName(self: Thread, name: []const u8) SetNameError!void { @@ -277,10 +319,13 @@ pub fn getName(self: Thread, buffer_ptr: *[max_name_len:0]u8) GetNameError!?[]co var buf: [32]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/proc/self/task/{d}/comm", .{self.getHandle()}); + var threaded: std.Io.Threaded = .init_single_threaded; + const io = threaded.ioBasic(); + const file = try std.fs.cwd().openFile(path, .{}); defer file.close(); - var file_reader = file.readerStreaming(&.{}); + var file_reader = file.readerStreaming(io, &.{}); const data_len = file_reader.interface.readSliceShort(buffer_ptr[0 .. max_name_len + 1]) catch |err| switch (err) { error.ReadFailed => return file_reader.err.?, }; @@ -385,6 +430,8 @@ pub const CpuCountError = error{ }; /// Returns the platforms view on the number of logical CPU cores available. +/// +/// Returned value guaranteed to be >= 1. pub fn getCpuCount() CpuCountError!usize { return try Impl.getCpuCount(); } @@ -963,7 +1010,7 @@ const WasiThreadImpl = struct { @call(.auto, f, w.args) catch |err| { std.debug.print("error: {s}\n", .{@errorName(err)}); if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); + std.debug.dumpStackTrace(trace); } }; }, @@ -1652,9 +1699,9 @@ test "setName, getName" { if (builtin.single_threaded) return error.SkipZigTest; const Context = struct { - start_wait_event: ResetEvent = .{}, - test_done_event: ResetEvent = .{}, - thread_done_event: ResetEvent = .{}, + start_wait_event: ResetEvent = .unset, + test_done_event: ResetEvent = .unset, + thread_done_event: ResetEvent = .unset, done: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), thread: Thread = undefined, @@ -1721,7 +1768,7 @@ test join { if (builtin.single_threaded) return error.SkipZigTest; var value: usize = 0; - var event = ResetEvent{}; + var event: ResetEvent = .unset; const thread = try Thread.spawn(.{}, testIncrementNotify, .{ &value, &event }); thread.join(); @@ -1733,7 +1780,7 @@ test detach { if (builtin.single_threaded) return error.SkipZigTest; var value: usize = 0; - var event = ResetEvent{}; + var event: ResetEvent = .unset; const thread = try Thread.spawn(.{}, testIncrementNotify, .{ &value, &event }); thread.detach(); @@ -1778,3 +1825,124 @@ fn testTls() !void { x += 1; if (x != 1235) return error.TlsBadEndValue; } + +test "ResetEvent smoke test" { + var event: ResetEvent = .unset; + try testing.expectEqual(false, event.isSet()); + + // make sure the event gets set + event.set(); + try testing.expectEqual(true, event.isSet()); + + // make sure the event gets unset again + event.reset(); + try testing.expectEqual(false, event.isSet()); + + // waits should timeout as there's no other thread to set the event + try testing.expectError(error.Timeout, event.timedWait(0)); + try testing.expectError(error.Timeout, event.timedWait(std.time.ns_per_ms)); + + // set the event again and make sure waits complete + event.set(); + event.wait(); + try event.timedWait(std.time.ns_per_ms); + try testing.expectEqual(true, event.isSet()); +} + +test "ResetEvent signaling" { + // This test requires spawning threads + if (builtin.single_threaded) { + return error.SkipZigTest; + } + + const Context = struct { + in: ResetEvent = .unset, + out: ResetEvent = .unset, + value: usize = 0, + + fn input(self: *@This()) !void { + // wait for the value to become 1 + self.in.wait(); + self.in.reset(); + try testing.expectEqual(self.value, 1); + + // bump the value and wake up output() + self.value = 2; + self.out.set(); + + // wait for output to receive 2, bump the value and wake us up with 3 + self.in.wait(); + self.in.reset(); + try testing.expectEqual(self.value, 3); + + // bump the value and wake up output() for it to see 4 + self.value = 4; + self.out.set(); + } + + fn output(self: *@This()) !void { + // start with 0 and bump the value for input to see 1 + try testing.expectEqual(self.value, 0); + self.value = 1; + self.in.set(); + + // wait for input to receive 1, bump the value to 2 and wake us up + self.out.wait(); + self.out.reset(); + try testing.expectEqual(self.value, 2); + + // bump the value to 3 for input to see (rhymes) + self.value = 3; + self.in.set(); + + // wait for input to bump the value to 4 and receive no more (rhymes) + self.out.wait(); + self.out.reset(); + try testing.expectEqual(self.value, 4); + } + }; + + var ctx = Context{}; + + const thread = try std.Thread.spawn(.{}, Context.output, .{&ctx}); + defer thread.join(); + + try ctx.input(); +} + +test "ResetEvent broadcast" { + // This test requires spawning threads + if (builtin.single_threaded) { + return error.SkipZigTest; + } + + const num_threads = 10; + const Barrier = struct { + event: ResetEvent = .unset, + counter: std.atomic.Value(usize) = std.atomic.Value(usize).init(num_threads), + + fn wait(self: *@This()) void { + if (self.counter.fetchSub(1, .acq_rel) == 1) { + self.event.set(); + } + } + }; + + const Context = struct { + start_barrier: Barrier = .{}, + finish_barrier: Barrier = .{}, + + fn run(self: *@This()) void { + self.start_barrier.wait(); + self.finish_barrier.wait(); + } + }; + + var ctx = Context{}; + var threads: [num_threads - 1]std.Thread = undefined; + + for (&threads) |*t| t.* = try std.Thread.spawn(.{}, Context.run, .{&ctx}); + defer for (threads) |t| t.join(); + + ctx.run(); +} diff --git a/lib/std/Thread/Condition.zig b/lib/std/Thread/Condition.zig index 91974a44b4..b298e598df 100644 --- a/lib/std/Thread/Condition.zig +++ b/lib/std/Thread/Condition.zig @@ -123,14 +123,9 @@ const SingleThreadedImpl = struct { fn wait(self: *Impl, mutex: *Mutex, timeout: ?u64) error{Timeout}!void { _ = self; _ = mutex; - // There are no other threads to wake us up. // So if we wait without a timeout we would never wake up. - const timeout_ns = timeout orelse { - unreachable; // deadlock detected - }; - - std.Thread.sleep(timeout_ns); + assert(timeout != null); // Deadlock detected. return error.Timeout; } @@ -323,6 +318,8 @@ test "wait and signal" { return error.SkipZigTest; } + const io = testing.io; + const num_threads = 4; const MultiWait = struct { @@ -348,7 +345,7 @@ test "wait and signal" { } while (true) { - std.Thread.sleep(100 * std.time.ns_per_ms); + try std.Io.Clock.Duration.sleep(.{ .clock = .awake, .raw = .fromMilliseconds(100) }, io); multi_wait.mutex.lock(); defer multi_wait.mutex.unlock(); @@ -368,6 +365,8 @@ test signal { return error.SkipZigTest; } + const io = testing.io; + const num_threads = 4; const SignalTest = struct { @@ -405,7 +404,7 @@ test signal { } while (true) { - std.Thread.sleep(10 * std.time.ns_per_ms); + try std.Io.Clock.Duration.sleep(.{ .clock = .awake, .raw = .fromMilliseconds(10) }, io); signal_test.mutex.lock(); defer signal_test.mutex.unlock(); diff --git a/lib/std/Thread/Futex.zig b/lib/std/Thread/Futex.zig index bad34996f5..0047f5400e 100644 --- a/lib/std/Thread/Futex.zig +++ b/lib/std/Thread/Futex.zig @@ -116,7 +116,7 @@ const SingleThreadedImpl = struct { unreachable; // deadlock detected }; - std.Thread.sleep(delay); + _ = delay; return error.Timeout; } diff --git a/lib/std/Thread/ResetEvent.zig b/lib/std/Thread/ResetEvent.zig deleted file mode 100644 index 2f22d8456a..0000000000 --- a/lib/std/Thread/ResetEvent.zig +++ /dev/null @@ -1,278 +0,0 @@ -//! ResetEvent is a thread-safe bool which can be set to true/false ("set"/"unset"). -//! It can also block threads until the "bool" is set with cancellation via timed waits. -//! ResetEvent can be statically initialized and is at most `@sizeOf(u64)` large. - -const std = @import("../std.zig"); -const builtin = @import("builtin"); -const ResetEvent = @This(); - -const os = std.os; -const assert = std.debug.assert; -const testing = std.testing; -const Futex = std.Thread.Futex; - -impl: Impl = .{}, - -/// Returns if the ResetEvent was set(). -/// Once reset() is called, this returns false until the next set(). -/// The memory accesses before the set() can be said to happen before isSet() returns true. -pub fn isSet(self: *const ResetEvent) bool { - return self.impl.isSet(); -} - -/// Block's the callers thread until the ResetEvent is set(). -/// This is effectively a more efficient version of `while (!isSet()) {}`. -/// The memory accesses before the set() can be said to happen before wait() returns. -pub fn wait(self: *ResetEvent) void { - self.impl.wait(null) catch |err| switch (err) { - error.Timeout => unreachable, // no timeout provided so we shouldn't have timed-out - }; -} - -/// Block's the callers thread until the ResetEvent is set(), or until the corresponding timeout expires. -/// If the timeout expires before the ResetEvent is set, `error.Timeout` is returned. -/// This is effectively a more efficient version of `while (!isSet()) {}`. -/// The memory accesses before the set() can be said to happen before timedWait() returns without error. -pub fn timedWait(self: *ResetEvent, timeout_ns: u64) error{Timeout}!void { - return self.impl.wait(timeout_ns); -} - -/// Marks the ResetEvent as "set" and unblocks any threads in `wait()` or `timedWait()` to observe the new state. -/// The ResetEvent says "set" until reset() is called, making future set() calls do nothing semantically. -/// The memory accesses before set() can be said to happen before isSet() returns true or wait()/timedWait() return successfully. -pub fn set(self: *ResetEvent) void { - self.impl.set(); -} - -/// Unmarks the ResetEvent from its "set" state if set() was called previously. -/// It is undefined behavior is reset() is called while threads are blocked in wait() or timedWait(). -/// Concurrent calls to set(), isSet() and reset() are allowed. -pub fn reset(self: *ResetEvent) void { - self.impl.reset(); -} - -const Impl = if (builtin.single_threaded) - SingleThreadedImpl -else - FutexImpl; - -const SingleThreadedImpl = struct { - is_set: bool = false, - - fn isSet(self: *const Impl) bool { - return self.is_set; - } - - fn wait(self: *Impl, timeout: ?u64) error{Timeout}!void { - if (self.isSet()) { - return; - } - - // There are no other threads to wake us up. - // So if we wait without a timeout we would never wake up. - const timeout_ns = timeout orelse { - unreachable; // deadlock detected - }; - - std.Thread.sleep(timeout_ns); - return error.Timeout; - } - - fn set(self: *Impl) void { - self.is_set = true; - } - - fn reset(self: *Impl) void { - self.is_set = false; - } -}; - -const FutexImpl = struct { - state: std.atomic.Value(u32) = std.atomic.Value(u32).init(unset), - - const unset = 0; - const waiting = 1; - const is_set = 2; - - fn isSet(self: *const Impl) bool { - // Acquire barrier ensures memory accesses before set() happen before we return true. - return self.state.load(.acquire) == is_set; - } - - fn wait(self: *Impl, timeout: ?u64) error{Timeout}!void { - // Outline the slow path to allow isSet() to be inlined - if (!self.isSet()) { - return self.waitUntilSet(timeout); - } - } - - fn waitUntilSet(self: *Impl, timeout: ?u64) error{Timeout}!void { - @branchHint(.cold); - - // Try to set the state from `unset` to `waiting` to indicate - // to the set() thread that others are blocked on the ResetEvent. - // We avoid using any strict barriers until the end when we know the ResetEvent is set. - var state = self.state.load(.acquire); - if (state == unset) { - state = self.state.cmpxchgStrong(state, waiting, .acquire, .acquire) orelse waiting; - } - - // Wait until the ResetEvent is set since the state is waiting. - if (state == waiting) { - var futex_deadline = Futex.Deadline.init(timeout); - while (true) { - const wait_result = futex_deadline.wait(&self.state, waiting); - - // Check if the ResetEvent was set before possibly reporting error.Timeout below. - state = self.state.load(.acquire); - if (state != waiting) { - break; - } - - try wait_result; - } - } - - assert(state == is_set); - } - - fn set(self: *Impl) void { - // Quick check if the ResetEvent is already set before doing the atomic swap below. - // set() could be getting called quite often and multiple threads calling swap() increases contention unnecessarily. - if (self.state.load(.monotonic) == is_set) { - return; - } - - // Mark the ResetEvent as set and unblock all waiters waiting on it if any. - // Release barrier ensures memory accesses before set() happen before the ResetEvent is observed to be "set". - if (self.state.swap(is_set, .release) == waiting) { - Futex.wake(&self.state, std.math.maxInt(u32)); - } - } - - fn reset(self: *Impl) void { - self.state.store(unset, .monotonic); - } -}; - -test "smoke test" { - // make sure the event is unset - var event = ResetEvent{}; - try testing.expectEqual(false, event.isSet()); - - // make sure the event gets set - event.set(); - try testing.expectEqual(true, event.isSet()); - - // make sure the event gets unset again - event.reset(); - try testing.expectEqual(false, event.isSet()); - - // waits should timeout as there's no other thread to set the event - try testing.expectError(error.Timeout, event.timedWait(0)); - try testing.expectError(error.Timeout, event.timedWait(std.time.ns_per_ms)); - - // set the event again and make sure waits complete - event.set(); - event.wait(); - try event.timedWait(std.time.ns_per_ms); - try testing.expectEqual(true, event.isSet()); -} - -test "signaling" { - // This test requires spawning threads - if (builtin.single_threaded) { - return error.SkipZigTest; - } - - const Context = struct { - in: ResetEvent = .{}, - out: ResetEvent = .{}, - value: usize = 0, - - fn input(self: *@This()) !void { - // wait for the value to become 1 - self.in.wait(); - self.in.reset(); - try testing.expectEqual(self.value, 1); - - // bump the value and wake up output() - self.value = 2; - self.out.set(); - - // wait for output to receive 2, bump the value and wake us up with 3 - self.in.wait(); - self.in.reset(); - try testing.expectEqual(self.value, 3); - - // bump the value and wake up output() for it to see 4 - self.value = 4; - self.out.set(); - } - - fn output(self: *@This()) !void { - // start with 0 and bump the value for input to see 1 - try testing.expectEqual(self.value, 0); - self.value = 1; - self.in.set(); - - // wait for input to receive 1, bump the value to 2 and wake us up - self.out.wait(); - self.out.reset(); - try testing.expectEqual(self.value, 2); - - // bump the value to 3 for input to see (rhymes) - self.value = 3; - self.in.set(); - - // wait for input to bump the value to 4 and receive no more (rhymes) - self.out.wait(); - self.out.reset(); - try testing.expectEqual(self.value, 4); - } - }; - - var ctx = Context{}; - - const thread = try std.Thread.spawn(.{}, Context.output, .{&ctx}); - defer thread.join(); - - try ctx.input(); -} - -test "broadcast" { - // This test requires spawning threads - if (builtin.single_threaded) { - return error.SkipZigTest; - } - - const num_threads = 10; - const Barrier = struct { - event: ResetEvent = .{}, - counter: std.atomic.Value(usize) = std.atomic.Value(usize).init(num_threads), - - fn wait(self: *@This()) void { - if (self.counter.fetchSub(1, .acq_rel) == 1) { - self.event.set(); - } - } - }; - - const Context = struct { - start_barrier: Barrier = .{}, - finish_barrier: Barrier = .{}, - - fn run(self: *@This()) void { - self.start_barrier.wait(); - self.finish_barrier.wait(); - } - }; - - var ctx = Context{}; - var threads: [num_threads - 1]std.Thread = undefined; - - for (&threads) |*t| t.* = try std.Thread.spawn(.{}, Context.run, .{&ctx}); - defer for (threads) |t| t.join(); - - ctx.run(); -} diff --git a/lib/std/Thread/WaitGroup.zig b/lib/std/Thread/WaitGroup.zig index 52e9c379c2..a5970b7d69 100644 --- a/lib/std/Thread/WaitGroup.zig +++ b/lib/std/Thread/WaitGroup.zig @@ -7,11 +7,15 @@ const is_waiting: usize = 1 << 0; const one_pending: usize = 1 << 1; state: std.atomic.Value(usize) = std.atomic.Value(usize).init(0), -event: std.Thread.ResetEvent = .{}, +event: std.Thread.ResetEvent = .unset, pub fn start(self: *WaitGroup) void { - const state = self.state.fetchAdd(one_pending, .monotonic); - assert((state / one_pending) < (std.math.maxInt(usize) / one_pending)); + return startStateless(&self.state); +} + +pub fn startStateless(state: *std.atomic.Value(usize)) void { + const prev_state = state.fetchAdd(one_pending, .monotonic); + assert((prev_state / one_pending) < (std.math.maxInt(usize) / one_pending)); } pub fn startMany(self: *WaitGroup, n: usize) void { @@ -28,13 +32,20 @@ pub fn finish(self: *WaitGroup) void { } } -pub fn wait(self: *WaitGroup) void { - const state = self.state.fetchAdd(is_waiting, .acquire); - assert(state & is_waiting == 0); +pub fn finishStateless(state: *std.atomic.Value(usize), event: *std.Thread.ResetEvent) void { + const prev_state = state.fetchSub(one_pending, .acq_rel); + assert((prev_state / one_pending) > 0); + if (prev_state == (one_pending | is_waiting)) event.set(); +} - if ((state / one_pending) > 0) { - self.event.wait(); - } +pub fn wait(wg: *WaitGroup) void { + return waitStateless(&wg.state, &wg.event); +} + +pub fn waitStateless(state: *std.atomic.Value(usize), event: *std.Thread.ResetEvent) void { + const prev_state = state.fetchAdd(is_waiting, .acquire); + assert(prev_state & is_waiting == 0); + if ((prev_state / one_pending) > 0) event.wait(); } pub fn reset(self: *WaitGroup) void { diff --git a/lib/std/Uri.zig b/lib/std/Uri.zig index d3df0491bb..bf183cb09f 100644 --- a/lib/std/Uri.zig +++ b/lib/std/Uri.zig @@ -1,45 +1,48 @@ -//! Uniform Resource Identifier (URI) parsing roughly adhering to . -//! Does not do perfect grammar and character class checking, but should be robust against URIs in the wild. +//! Uniform Resource Identifier (URI) parsing roughly adhering to +//! . Does not do perfect grammar and +//! character class checking, but should be robust against URIs in the wild. const std = @import("std.zig"); const testing = std.testing; const Uri = @This(); const Allocator = std.mem.Allocator; const Writer = std.Io.Writer; +const HostName = std.Io.net.HostName; scheme: []const u8, user: ?Component = null, password: ?Component = null, +/// If non-null, already validated. host: ?Component = null, port: ?u16 = null, path: Component = Component.empty, query: ?Component = null, fragment: ?Component = null, -pub const host_name_max = 255; +pub const GetHostError = error{UriMissingHost}; /// Returned value may point into `buffer` or be the original string. /// -/// Suggested buffer length: `host_name_max`. -/// /// See also: /// * `getHostAlloc` -pub fn getHost(uri: Uri, buffer: []u8) error{ UriMissingHost, UriHostTooLong }![]const u8 { +pub fn getHost(uri: Uri, buffer: *[HostName.max_len]u8) GetHostError!HostName { const component = uri.host orelse return error.UriMissingHost; - return component.toRaw(buffer) catch |err| switch (err) { - error.NoSpaceLeft => return error.UriHostTooLong, + const bytes = component.toRaw(buffer) catch |err| switch (err) { + error.NoSpaceLeft => unreachable, // `host` already validated. }; + return .{ .bytes = bytes }; } +pub const GetHostAllocError = GetHostError || error{OutOfMemory}; + /// Returned value may point into `buffer` or be the original string. /// /// See also: /// * `getHost` -pub fn getHostAlloc(uri: Uri, arena: Allocator) error{ UriMissingHost, UriHostTooLong, OutOfMemory }![]const u8 { +pub fn getHostAlloc(uri: Uri, arena: Allocator) GetHostAllocError!HostName { const component = uri.host orelse return error.UriMissingHost; - const result = try component.toRawMaybeAlloc(arena); - if (result.len > host_name_max) return error.UriHostTooLong; - return result; + const bytes = try component.toRawMaybeAlloc(arena); + return .{ .bytes = bytes }; } pub const Component = union(enum) { @@ -194,7 +197,12 @@ pub fn percentDecodeInPlace(buffer: []u8) []u8 { return percentDecodeBackwards(buffer, buffer); } -pub const ParseError = error{ UnexpectedCharacter, InvalidFormat, InvalidPort }; +pub const ParseError = error{ + UnexpectedCharacter, + InvalidFormat, + InvalidPort, + InvalidHostName, +}; /// Parses the URI or returns an error. This function is not compliant, but is required to parse /// some forms of URIs in the wild, such as HTTP Location headers. @@ -397,7 +405,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE .scheme = new_parsed.scheme, .user = new_parsed.user, .password = new_parsed.password, - .host = new_parsed.host, + .host = try validateHostComponent(new_parsed.host), .port = new_parsed.port, .path = remove_dot_segments(new_path), .query = new_parsed.query, @@ -408,7 +416,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE .scheme = base.scheme, .user = new_parsed.user, .password = new_parsed.password, - .host = host, + .host = try validateHostComponent(host), .port = new_parsed.port, .path = remove_dot_segments(new_path), .query = new_parsed.query, @@ -430,7 +438,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE .scheme = base.scheme, .user = base.user, .password = base.password, - .host = base.host, + .host = try validateHostComponent(base.host), .port = base.port, .path = path, .query = query, @@ -438,6 +446,18 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE }; } +fn validateHostComponent(optional_component: ?Component) error{InvalidHostName}!?Component { + const component = optional_component orelse return null; + switch (component) { + .raw => |raw| HostName.validate(raw) catch return error.InvalidHostName, + .percent_encoded => |encoded| { + // TODO validate decoded name instead + HostName.validate(encoded) catch return error.InvalidHostName; + }, + } + return component; +} + /// In-place implementation of RFC 3986, Section 5.2.4. fn remove_dot_segments(path: []u8) Component { var in_i: usize = 0; diff --git a/lib/std/builtin.zig b/lib/std/builtin.zig index 2f013a1ea8..a646e9ad80 100644 --- a/lib/std/builtin.zig +++ b/lib/std/builtin.zig @@ -37,19 +37,6 @@ pub const subsystem: ?std.Target.SubSystem = blk: { pub const StackTrace = struct { index: usize, instruction_addresses: []usize, - - pub fn format(st: *const StackTrace, writer: *std.Io.Writer) std.Io.Writer.Error!void { - // TODO: re-evaluate whether to use format() methods at all. - // Until then, avoid an error when using GeneralPurposeAllocator with WebAssembly - // where it tries to call detectTTYConfig here. - if (builtin.os.tag == .freestanding) return; - - // TODO: why on earth are we using stderr's ttyconfig? - // If we want colored output, we should just make a formatter out of `writeStackTrace`. - const tty_config = std.Io.tty.detectConfig(.stderr()); - try writer.writeAll("\n"); - try std.debug.writeStackTrace(st, writer, tty_config); - } }; /// This data structure is used by the Zig language code generation and diff --git a/lib/std/c.zig b/lib/std/c.zig index 30e584c1a1..2f96404451 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -1,12 +1,14 @@ -const std = @import("std"); const builtin = @import("builtin"); +const native_abi = builtin.abi; +const native_arch = builtin.cpu.arch; +const native_os = builtin.os.tag; +const native_endian = builtin.cpu.arch.endian(); + +const std = @import("std"); const c = @This(); const maxInt = std.math.maxInt; const assert = std.debug.assert; const page_size = std.heap.page_size_min; -const native_abi = builtin.abi; -const native_arch = builtin.cpu.arch; -const native_os = builtin.os.tag; const linux = std.os.linux; const emscripten = std.os.emscripten; const wasi = std.os.wasi; @@ -2587,25 +2589,24 @@ pub const SHUT = switch (native_os) { /// Signal types pub const SIG = switch (native_os) { - .linux => linux.SIG, - .emscripten => emscripten.SIG, - .windows => struct { + .linux, .emscripten => linux.SIG, + .windows => enum(u32) { /// interrupt - pub const INT = 2; + INT = 2, /// illegal instruction - invalid function image - pub const ILL = 4; + ILL = 4, /// floating point exception - pub const FPE = 8; + FPE = 8, /// segment violation - pub const SEGV = 11; + SEGV = 11, /// Software termination signal from kill - pub const TERM = 15; + TERM = 15, /// Ctrl-Break sequence - pub const BREAK = 21; + BREAK = 21, /// abnormal termination triggered by abort call - pub const ABRT = 22; + ABRT = 22, /// SIGABRT compatible with other platforms, same as SIGABRT - pub const ABRT_COMPAT = 6; + ABRT_COMPAT = 6, // Signal action codes /// default signal action @@ -2621,7 +2622,7 @@ pub const SIG = switch (native_os) { /// Signal error value (returned by signal call on error) pub const ERR = -1; }, - .macos, .ios, .tvos, .watchos, .visionos => struct { + .macos, .ios, .tvos, .watchos, .visionos => enum(u32) { pub const ERR: ?Sigaction.handler_fn = @ptrFromInt(maxInt(usize)); pub const DFL: ?Sigaction.handler_fn = @ptrFromInt(0); pub const IGN: ?Sigaction.handler_fn = @ptrFromInt(1); @@ -2633,113 +2634,74 @@ pub const SIG = switch (native_os) { pub const UNBLOCK = 2; /// set specified signal set pub const SETMASK = 3; + + pub const IOT: SIG = .ABRT; + pub const POLL: SIG = .EMT; + /// hangup - pub const HUP = 1; + HUP = 1, /// interrupt - pub const INT = 2; + INT = 2, /// quit - pub const QUIT = 3; + QUIT = 3, /// illegal instruction (not reset when caught) - pub const ILL = 4; + ILL = 4, /// trace trap (not reset when caught) - pub const TRAP = 5; + TRAP = 5, /// abort() - pub const ABRT = 6; - /// pollable event ([XSR] generated, not supported) - pub const POLL = 7; - /// compatibility - pub const IOT = ABRT; + ABRT = 6, /// EMT instruction - pub const EMT = 7; + EMT = 7, /// floating point exception - pub const FPE = 8; + FPE = 8, /// kill (cannot be caught or ignored) - pub const KILL = 9; + KILL = 9, /// bus error - pub const BUS = 10; + BUS = 10, /// segmentation violation - pub const SEGV = 11; + SEGV = 11, /// bad argument to system call - pub const SYS = 12; + SYS = 12, /// write on a pipe with no one to read it - pub const PIPE = 13; + PIPE = 13, /// alarm clock - pub const ALRM = 14; + ALRM = 14, /// software termination signal from kill - pub const TERM = 15; + TERM = 15, /// urgent condition on IO channel - pub const URG = 16; + URG = 16, /// sendable stop signal not from tty - pub const STOP = 17; + STOP = 17, /// stop signal from tty - pub const TSTP = 18; + TSTP = 18, /// continue a stopped process - pub const CONT = 19; + CONT = 19, /// to parent on child stop or exit - pub const CHLD = 20; + CHLD = 20, /// to readers pgrp upon background tty read - pub const TTIN = 21; + TTIN = 21, /// like TTIN for output if (tp->t_local<OSTOP) - pub const TTOU = 22; + TTOU = 22, /// input/output possible signal - pub const IO = 23; + IO = 23, /// exceeded CPU time limit - pub const XCPU = 24; + XCPU = 24, /// exceeded file size limit - pub const XFSZ = 25; + XFSZ = 25, /// virtual time alarm - pub const VTALRM = 26; + VTALRM = 26, /// profiling time alarm - pub const PROF = 27; + PROF = 27, /// window size changes - pub const WINCH = 28; + WINCH = 28, /// information request - pub const INFO = 29; + INFO = 29, /// user defined signal 1 - pub const USR1 = 30; + USR1 = 30, /// user defined signal 2 - pub const USR2 = 31; + USR2 = 31, }, - .freebsd => struct { - pub const HUP = 1; - pub const INT = 2; - pub const QUIT = 3; - pub const ILL = 4; - pub const TRAP = 5; - pub const ABRT = 6; - pub const IOT = ABRT; - pub const EMT = 7; - pub const FPE = 8; - pub const KILL = 9; - pub const BUS = 10; - pub const SEGV = 11; - pub const SYS = 12; - pub const PIPE = 13; - pub const ALRM = 14; - pub const TERM = 15; - pub const URG = 16; - pub const STOP = 17; - pub const TSTP = 18; - pub const CONT = 19; - pub const CHLD = 20; - pub const TTIN = 21; - pub const TTOU = 22; - pub const IO = 23; - pub const XCPU = 24; - pub const XFSZ = 25; - pub const VTALRM = 26; - pub const PROF = 27; - pub const WINCH = 28; - pub const INFO = 29; - pub const USR1 = 30; - pub const USR2 = 31; - pub const THR = 32; - pub const LWP = THR; - pub const LIBRT = 33; - - pub const RTMIN = 65; - pub const RTMAX = 126; - + .freebsd => enum(u32) { pub const BLOCK = 1; pub const UNBLOCK = 2; pub const SETMASK = 3; @@ -2763,8 +2725,48 @@ pub const SIG = switch (native_os) { pub inline fn VALID(sig: usize) usize { return sig <= MAXSIG and sig > 0; } + + pub const IOT: SIG = .ABRT; + pub const LWP: SIG = .THR; + + pub const RTMIN = 65; + pub const RTMAX = 126; + + HUP = 1, + INT = 2, + QUIT = 3, + ILL = 4, + TRAP = 5, + ABRT = 6, + EMT = 7, + FPE = 8, + KILL = 9, + BUS = 10, + SEGV = 11, + SYS = 12, + PIPE = 13, + ALRM = 14, + TERM = 15, + URG = 16, + STOP = 17, + TSTP = 18, + CONT = 19, + CHLD = 20, + TTIN = 21, + TTOU = 22, + IO = 23, + XCPU = 24, + XFSZ = 25, + VTALRM = 26, + PROF = 27, + WINCH = 28, + INFO = 29, + USR1 = 30, + USR2 = 31, + THR = 32, + LIBRT = 33, }, - .illumos => struct { + .illumos => enum(u32) { pub const DFL: ?Sigaction.handler_fn = @ptrFromInt(0); pub const ERR: ?Sigaction.handler_fn = @ptrFromInt(maxInt(usize)); pub const IGN: ?Sigaction.handler_fn = @ptrFromInt(1); @@ -2773,54 +2775,9 @@ pub const SIG = switch (native_os) { pub const WORDS = 4; pub const MAXSIG = 75; - pub const SIG_BLOCK = 1; - pub const SIG_UNBLOCK = 2; - pub const SIG_SETMASK = 3; - - pub const HUP = 1; - pub const INT = 2; - pub const QUIT = 3; - pub const ILL = 4; - pub const TRAP = 5; - pub const IOT = 6; - pub const ABRT = 6; - pub const EMT = 7; - pub const FPE = 8; - pub const KILL = 9; - pub const BUS = 10; - pub const SEGV = 11; - pub const SYS = 12; - pub const PIPE = 13; - pub const ALRM = 14; - pub const TERM = 15; - pub const USR1 = 16; - pub const USR2 = 17; - pub const CLD = 18; - pub const CHLD = 18; - pub const PWR = 19; - pub const WINCH = 20; - pub const URG = 21; - pub const POLL = 22; - pub const IO = .POLL; - pub const STOP = 23; - pub const TSTP = 24; - pub const CONT = 25; - pub const TTIN = 26; - pub const TTOU = 27; - pub const VTALRM = 28; - pub const PROF = 29; - pub const XCPU = 30; - pub const XFSZ = 31; - pub const WAITING = 32; - pub const LWP = 33; - pub const FREEZE = 34; - pub const THAW = 35; - pub const CANCEL = 36; - pub const LOST = 37; - pub const XRES = 38; - pub const JVM1 = 39; - pub const JVM2 = 40; - pub const INFO = 41; + pub const BLOCK = 1; + pub const UNBLOCK = 2; + pub const SETMASK = 3; pub const RTMIN = 42; pub const RTMAX = 74; @@ -2837,8 +2794,54 @@ pub const SIG = switch (native_os) { pub inline fn VALID(sig: usize) usize { return sig <= MAXSIG and sig > 0; } + + pub const POLL: SIG = .IO; + + HUP = 1, + INT = 2, + QUIT = 3, + ILL = 4, + TRAP = 5, + IOT = 6, + ABRT = 6, + EMT = 7, + FPE = 8, + KILL = 9, + BUS = 10, + SEGV = 11, + SYS = 12, + PIPE = 13, + ALRM = 14, + TERM = 15, + USR1 = 16, + USR2 = 17, + CLD = 18, + CHLD = 18, + PWR = 19, + WINCH = 20, + URG = 21, + IO = 22, + STOP = 23, + TSTP = 24, + CONT = 25, + TTIN = 26, + TTOU = 27, + VTALRM = 28, + PROF = 29, + XCPU = 30, + XFSZ = 31, + WAITING = 32, + LWP = 33, + FREEZE = 34, + THAW = 35, + CANCEL = 36, + LOST = 37, + XRES = 38, + JVM1 = 39, + JVM2 = 40, + INFO = 41, }, - .netbsd => struct { + .netbsd => enum(u32) { pub const DFL: ?Sigaction.handler_fn = @ptrFromInt(0); pub const IGN: ?Sigaction.handler_fn = @ptrFromInt(1); pub const ERR: ?Sigaction.handler_fn = @ptrFromInt(maxInt(usize)); @@ -2850,40 +2853,6 @@ pub const SIG = switch (native_os) { pub const UNBLOCK = 2; pub const SETMASK = 3; - pub const HUP = 1; - pub const INT = 2; - pub const QUIT = 3; - pub const ILL = 4; - pub const TRAP = 5; - pub const ABRT = 6; - pub const IOT = ABRT; - pub const EMT = 7; - pub const FPE = 8; - pub const KILL = 9; - pub const BUS = 10; - pub const SEGV = 11; - pub const SYS = 12; - pub const PIPE = 13; - pub const ALRM = 14; - pub const TERM = 15; - pub const URG = 16; - pub const STOP = 17; - pub const TSTP = 18; - pub const CONT = 19; - pub const CHLD = 20; - pub const TTIN = 21; - pub const TTOU = 22; - pub const IO = 23; - pub const XCPU = 24; - pub const XFSZ = 25; - pub const VTALRM = 26; - pub const PROF = 27; - pub const WINCH = 28; - pub const INFO = 29; - pub const USR1 = 30; - pub const USR2 = 31; - pub const PWR = 32; - pub const RTMIN = 33; pub const RTMAX = 63; @@ -2899,8 +2868,43 @@ pub const SIG = switch (native_os) { pub inline fn VALID(sig: usize) usize { return sig <= MAXSIG and sig > 0; } + + pub const IOT: SIG = .ABRT; + + HUP = 1, + INT = 2, + QUIT = 3, + ILL = 4, + TRAP = 5, + ABRT = 6, + EMT = 7, + FPE = 8, + KILL = 9, + BUS = 10, + SEGV = 11, + SYS = 12, + PIPE = 13, + ALRM = 14, + TERM = 15, + URG = 16, + STOP = 17, + TSTP = 18, + CONT = 19, + CHLD = 20, + TTIN = 21, + TTOU = 22, + IO = 23, + XCPU = 24, + XFSZ = 25, + VTALRM = 26, + PROF = 27, + WINCH = 28, + INFO = 29, + USR1 = 30, + USR2 = 31, + PWR = 32, }, - .dragonfly => struct { + .dragonfly => enum(u32) { pub const DFL: ?Sigaction.handler_fn = @ptrFromInt(0); pub const IGN: ?Sigaction.handler_fn = @ptrFromInt(1); pub const ERR: ?Sigaction.handler_fn = @ptrFromInt(maxInt(usize)); @@ -2909,137 +2913,140 @@ pub const SIG = switch (native_os) { pub const UNBLOCK = 2; pub const SETMASK = 3; - pub const IOT = ABRT; - pub const HUP = 1; - pub const INT = 2; - pub const QUIT = 3; - pub const ILL = 4; - pub const TRAP = 5; - pub const ABRT = 6; - pub const EMT = 7; - pub const FPE = 8; - pub const KILL = 9; - pub const BUS = 10; - pub const SEGV = 11; - pub const SYS = 12; - pub const PIPE = 13; - pub const ALRM = 14; - pub const TERM = 15; - pub const URG = 16; - pub const STOP = 17; - pub const TSTP = 18; - pub const CONT = 19; - pub const CHLD = 20; - pub const TTIN = 21; - pub const TTOU = 22; - pub const IO = 23; - pub const XCPU = 24; - pub const XFSZ = 25; - pub const VTALRM = 26; - pub const PROF = 27; - pub const WINCH = 28; - pub const INFO = 29; - pub const USR1 = 30; - pub const USR2 = 31; - pub const THR = 32; - pub const CKPT = 33; - pub const CKPTEXIT = 34; - pub const WORDS = 4; + + pub const IOT: SIG = .ABRT; + + HUP = 1, + INT = 2, + QUIT = 3, + ILL = 4, + TRAP = 5, + ABRT = 6, + EMT = 7, + FPE = 8, + KILL = 9, + BUS = 10, + SEGV = 11, + SYS = 12, + PIPE = 13, + ALRM = 14, + TERM = 15, + URG = 16, + STOP = 17, + TSTP = 18, + CONT = 19, + CHLD = 20, + TTIN = 21, + TTOU = 22, + IO = 23, + XCPU = 24, + XFSZ = 25, + VTALRM = 26, + PROF = 27, + WINCH = 28, + INFO = 29, + USR1 = 30, + USR2 = 31, + THR = 32, + CKPT = 33, + CKPTEXIT = 34, }, - .haiku => struct { + .haiku => enum(u32) { pub const DFL: ?Sigaction.handler_fn = @ptrFromInt(0); pub const IGN: ?Sigaction.handler_fn = @ptrFromInt(1); pub const ERR: ?Sigaction.handler_fn = @ptrFromInt(maxInt(usize)); pub const HOLD: ?Sigaction.handler_fn = @ptrFromInt(3); - pub const HUP = 1; - pub const INT = 2; - pub const QUIT = 3; - pub const ILL = 4; - pub const CHLD = 5; - pub const ABRT = 6; - pub const IOT = ABRT; - pub const PIPE = 7; - pub const FPE = 8; - pub const KILL = 9; - pub const STOP = 10; - pub const SEGV = 11; - pub const CONT = 12; - pub const TSTP = 13; - pub const ALRM = 14; - pub const TERM = 15; - pub const TTIN = 16; - pub const TTOU = 17; - pub const USR1 = 18; - pub const USR2 = 19; - pub const WINCH = 20; - pub const KILLTHR = 21; - pub const TRAP = 22; - pub const POLL = 23; - pub const PROF = 24; - pub const SYS = 25; - pub const URG = 26; - pub const VTALRM = 27; - pub const XCPU = 28; - pub const XFSZ = 29; - pub const BUS = 30; - pub const RESERVED1 = 31; - pub const RESERVED2 = 32; - pub const BLOCK = 1; pub const UNBLOCK = 2; pub const SETMASK = 3; + + pub const IOT: SIG = .ABRT; + + HUP = 1, + INT = 2, + QUIT = 3, + ILL = 4, + CHLD = 5, + ABRT = 6, + PIPE = 7, + FPE = 8, + KILL = 9, + STOP = 10, + SEGV = 11, + CONT = 12, + TSTP = 13, + ALRM = 14, + TERM = 15, + TTIN = 16, + TTOU = 17, + USR1 = 18, + USR2 = 19, + WINCH = 20, + KILLTHR = 21, + TRAP = 22, + POLL = 23, + PROF = 24, + SYS = 25, + URG = 26, + VTALRM = 27, + XCPU = 28, + XFSZ = 29, + BUS = 30, + RESERVED1 = 31, + RESERVED2 = 32, }, - .openbsd => struct { + .openbsd => enum(u32) { pub const DFL: ?Sigaction.handler_fn = @ptrFromInt(0); pub const IGN: ?Sigaction.handler_fn = @ptrFromInt(1); pub const ERR: ?Sigaction.handler_fn = @ptrFromInt(maxInt(usize)); pub const CATCH: ?Sigaction.handler_fn = @ptrFromInt(2); pub const HOLD: ?Sigaction.handler_fn = @ptrFromInt(3); - pub const HUP = 1; - pub const INT = 2; - pub const QUIT = 3; - pub const ILL = 4; - pub const TRAP = 5; - pub const ABRT = 6; - pub const IOT = ABRT; - pub const EMT = 7; - pub const FPE = 8; - pub const KILL = 9; - pub const BUS = 10; - pub const SEGV = 11; - pub const SYS = 12; - pub const PIPE = 13; - pub const ALRM = 14; - pub const TERM = 15; - pub const URG = 16; - pub const STOP = 17; - pub const TSTP = 18; - pub const CONT = 19; - pub const CHLD = 20; - pub const TTIN = 21; - pub const TTOU = 22; - pub const IO = 23; - pub const XCPU = 24; - pub const XFSZ = 25; - pub const VTALRM = 26; - pub const PROF = 27; - pub const WINCH = 28; - pub const INFO = 29; - pub const USR1 = 30; - pub const USR2 = 31; - pub const PWR = 32; - pub const BLOCK = 1; pub const UNBLOCK = 2; pub const SETMASK = 3; + + pub const IOT: SIG = .ABRT; + + HUP = 1, + INT = 2, + QUIT = 3, + ILL = 4, + TRAP = 5, + ABRT = 6, + EMT = 7, + FPE = 8, + KILL = 9, + BUS = 10, + SEGV = 11, + SYS = 12, + PIPE = 13, + ALRM = 14, + TERM = 15, + URG = 16, + STOP = 17, + TSTP = 18, + CONT = 19, + CHLD = 20, + TTIN = 21, + TTOU = 22, + IO = 23, + XCPU = 24, + XFSZ = 25, + VTALRM = 26, + PROF = 27, + WINCH = 28, + INFO = 29, + USR1 = 30, + USR2 = 31, + PWR = 32, }, // https://github.com/SerenityOS/serenity/blob/046c23f567a17758d762a33bdf04bacbfd088f9f/Kernel/API/POSIX/signal.h // https://github.com/SerenityOS/serenity/blob/046c23f567a17758d762a33bdf04bacbfd088f9f/Kernel/API/POSIX/signal_numbers.h - .serenity => struct { + .serenity => enum(u32) { pub const DFL: ?Sigaction.handler_fn = @ptrFromInt(0); pub const ERR: ?Sigaction.handler_fn = @ptrFromInt(maxInt(usize)); pub const IGN: ?Sigaction.handler_fn = @ptrFromInt(1); @@ -3048,39 +3055,39 @@ pub const SIG = switch (native_os) { pub const UNBLOCK = 2; pub const SETMASK = 3; - pub const INVAL = 0; - pub const HUP = 1; - pub const INT = 2; - pub const QUIT = 3; - pub const ILL = 4; - pub const TRAP = 5; - pub const ABRT = 6; - pub const BUS = 7; - pub const FPE = 8; - pub const KILL = 9; - pub const USR1 = 10; - pub const SEGV = 11; - pub const USR2 = 12; - pub const PIPE = 13; - pub const ALRM = 14; - pub const TERM = 15; - pub const STKFLT = 16; - pub const CHLD = 17; - pub const CONT = 18; - pub const STOP = 19; - pub const TSTP = 20; - pub const TTIN = 21; - pub const TTOU = 22; - pub const URG = 23; - pub const XCPU = 24; - pub const XFSZ = 25; - pub const VTALRM = 26; - pub const PROF = 27; - pub const WINCH = 28; - pub const IO = 29; - pub const INFO = 30; - pub const SYS = 31; - pub const CANCEL = 32; + INVAL = 0, + HUP = 1, + INT = 2, + QUIT = 3, + ILL = 4, + TRAP = 5, + ABRT = 6, + BUS = 7, + FPE = 8, + KILL = 9, + USR1 = 10, + SEGV = 11, + USR2 = 12, + PIPE = 13, + ALRM = 14, + TERM = 15, + STKFLT = 16, + CHLD = 17, + CONT = 18, + STOP = 19, + TSTP = 20, + TTIN = 21, + TTOU = 22, + URG = 23, + XCPU = 24, + XFSZ = 25, + VTALRM = 26, + PROF = 27, + WINCH = 28, + IO = 29, + INFO = 30, + SYS = 31, + CANCEL = 32, }, else => void, }; @@ -3117,8 +3124,8 @@ pub const SYS = switch (native_os) { /// A common format for the Sigaction struct across a variety of Linux flavors. const common_linux_Sigaction = extern struct { - pub const handler_fn = *align(1) const fn (i32) callconv(.c) void; - pub const sigaction_fn = *const fn (i32, *const siginfo_t, ?*anyopaque) callconv(.c) void; + pub const handler_fn = *align(1) const fn (SIG) callconv(.c) void; + pub const sigaction_fn = *const fn (SIG, *const siginfo_t, ?*anyopaque) callconv(.c) void; handler: extern union { handler: ?handler_fn, @@ -3139,8 +3146,8 @@ pub const Sigaction = switch (native_os) { => if (builtin.target.abi.isMusl()) common_linux_Sigaction else if (builtin.target.ptrBitWidth() == 64) extern struct { - pub const handler_fn = *align(1) const fn (i32) callconv(.c) void; - pub const sigaction_fn = *const fn (i32, *const siginfo_t, ?*anyopaque) callconv(.c) void; + pub const handler_fn = *align(1) const fn (SIG) callconv(.c) void; + pub const sigaction_fn = *const fn (SIG, *const siginfo_t, ?*anyopaque) callconv(.c) void; flags: c_uint, handler: extern union { @@ -3150,8 +3157,8 @@ pub const Sigaction = switch (native_os) { mask: sigset_t, restorer: ?*const fn () callconv(.c) void = null, } else extern struct { - pub const handler_fn = *align(1) const fn (i32) callconv(.c) void; - pub const sigaction_fn = *const fn (i32, *const siginfo_t, ?*anyopaque) callconv(.c) void; + pub const handler_fn = *align(1) const fn (SIG) callconv(.c) void; + pub const sigaction_fn = *const fn (SIG, *const siginfo_t, ?*anyopaque) callconv(.c) void; flags: c_uint, handler: extern union { @@ -3163,8 +3170,8 @@ pub const Sigaction = switch (native_os) { __resv: [1]c_int = .{0}, }, .s390x => if (builtin.abi == .gnu) extern struct { - pub const handler_fn = *align(1) const fn (i32) callconv(.c) void; - pub const sigaction_fn = *const fn (i32, *const siginfo_t, ?*anyopaque) callconv(.c) void; + pub const handler_fn = *align(1) const fn (SIG) callconv(.c) void; + pub const sigaction_fn = *const fn (SIG, *const siginfo_t, ?*anyopaque) callconv(.c) void; handler: extern union { handler: ?handler_fn, @@ -3179,8 +3186,8 @@ pub const Sigaction = switch (native_os) { }, .emscripten => emscripten.Sigaction, .netbsd, .macos, .ios, .tvos, .watchos, .visionos => extern struct { - pub const handler_fn = *align(1) const fn (i32) callconv(.c) void; - pub const sigaction_fn = *const fn (i32, *const siginfo_t, ?*anyopaque) callconv(.c) void; + pub const handler_fn = *align(1) const fn (SIG) callconv(.c) void; + pub const sigaction_fn = *const fn (SIG, *const siginfo_t, ?*anyopaque) callconv(.c) void; handler: extern union { handler: ?handler_fn, @@ -3190,8 +3197,8 @@ pub const Sigaction = switch (native_os) { flags: c_uint, }, .dragonfly, .freebsd => extern struct { - pub const handler_fn = *align(1) const fn (i32) callconv(.c) void; - pub const sigaction_fn = *const fn (i32, *const siginfo_t, ?*anyopaque) callconv(.c) void; + pub const handler_fn = *align(1) const fn (SIG) callconv(.c) void; + pub const sigaction_fn = *const fn (SIG, *const siginfo_t, ?*anyopaque) callconv(.c) void; /// signal handler handler: extern union { @@ -3204,8 +3211,8 @@ pub const Sigaction = switch (native_os) { mask: sigset_t, }, .illumos => extern struct { - pub const handler_fn = *align(1) const fn (i32) callconv(.c) void; - pub const sigaction_fn = *const fn (i32, *const siginfo_t, ?*anyopaque) callconv(.c) void; + pub const handler_fn = *align(1) const fn (SIG) callconv(.c) void; + pub const sigaction_fn = *const fn (SIG, *const siginfo_t, ?*anyopaque) callconv(.c) void; /// signal options flags: c_uint, @@ -3218,8 +3225,8 @@ pub const Sigaction = switch (native_os) { mask: sigset_t, }, .haiku => extern struct { - pub const handler_fn = *align(1) const fn (i32) callconv(.c) void; - pub const sigaction_fn = *const fn (i32, *const siginfo_t, ?*anyopaque) callconv(.c) void; + pub const handler_fn = *align(1) const fn (SIG) callconv(.c) void; + pub const sigaction_fn = *const fn (SIG, *const siginfo_t, ?*anyopaque) callconv(.c) void; /// signal handler handler: extern union { @@ -3237,8 +3244,8 @@ pub const Sigaction = switch (native_os) { userdata: *allowzero anyopaque = undefined, }, .openbsd => extern struct { - pub const handler_fn = *align(1) const fn (i32) callconv(.c) void; - pub const sigaction_fn = *const fn (i32, *const siginfo_t, ?*anyopaque) callconv(.c) void; + pub const handler_fn = *align(1) const fn (SIG) callconv(.c) void; + pub const sigaction_fn = *const fn (SIG, *const siginfo_t, ?*anyopaque) callconv(.c) void; /// signal handler handler: extern union { @@ -3252,8 +3259,8 @@ pub const Sigaction = switch (native_os) { }, // https://github.com/SerenityOS/serenity/blob/ec492a1a0819e6239ea44156825c4ee7234ca3db/Kernel/API/POSIX/signal.h#L39-L46 .serenity => extern struct { - pub const handler_fn = *align(1) const fn (i32) callconv(.c) void; - pub const sigaction_fn = *const fn (i32, *const siginfo_t, ?*anyopaque) callconv(.c) void; + pub const handler_fn = *align(1) const fn (SIG) callconv(.c) void; + pub const sigaction_fn = *const fn (SIG, *const siginfo_t, ?*anyopaque) callconv(.c) void; handler: extern union { handler: ?handler_fn, @@ -4087,8 +4094,9 @@ pub const linger = switch (native_os) { }, else => void, }; + pub const msghdr = switch (native_os) { - .linux => linux.msghdr, + .linux => if (@bitSizeOf(usize) > @bitSizeOf(i32) and builtin.abi.isMusl()) posix_msghdr else linux.msghdr, .openbsd, .emscripten, .dragonfly, @@ -4102,36 +4110,28 @@ pub const msghdr = switch (native_os) { .tvos, .visionos, .watchos, - => extern struct { - /// optional address - name: ?*sockaddr, - /// size of address - namelen: socklen_t, - /// scatter/gather array - iov: [*]iovec, - /// # elements in iov - iovlen: i32, - /// ancillary data - control: ?*anyopaque, - /// ancillary data buffer len - controllen: socklen_t, - /// flags on received message - flags: i32, - }, - // https://github.com/SerenityOS/serenity/blob/ac44ec5ebc707f9dd0c3d4759a1e17e91db5d74f/Kernel/API/POSIX/sys/socket.h#L74-L82 - .serenity => extern struct { - name: ?*anyopaque, - namelen: socklen_t, - iov: [*]iovec, - iovlen: c_int, - control: ?*anyopaque, - controllen: socklen_t, - flags: c_int, - }, + .serenity, // https://github.com/SerenityOS/serenity/blob/ac44ec5ebc707f9dd0c3d4759a1e17e91db5d74f/Kernel/API/POSIX/sys/socket.h#L74-L82 + => posix_msghdr, else => void, }; + +/// https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/sys_socket.h.html +const posix_msghdr = extern struct { + name: ?*sockaddr, + namelen: socklen_t, + iov: [*]iovec, + pad0: if (@sizeOf(usize) == 8 and native_endian == .big) u32 else u0 = 0, + iovlen: u32, + pad1: if (@sizeOf(usize) == 8 and native_endian == .little) u32 else u0 = 0, + control: ?*anyopaque, + pad2: if (@sizeOf(usize) == 8 and native_endian == .big) u32 else u0 = 0, + controllen: socklen_t, + pad3: if (@sizeOf(usize) == 8 and native_endian == .little) u32 else u0 = 0, + flags: u32, +}; + pub const msghdr_const = switch (native_os) { - .linux => linux.msghdr_const, + .linux => if (@bitSizeOf(usize) > @bitSizeOf(i32) and builtin.abi.isMusl()) posix_msghdr_const else linux.msghdr_const, .openbsd, .emscripten, .dragonfly, @@ -4145,36 +4145,37 @@ pub const msghdr_const = switch (native_os) { .tvos, .visionos, .watchos, - => extern struct { - /// optional address - name: ?*const sockaddr, - /// size of address - namelen: socklen_t, - /// scatter/gather array - iov: [*]const iovec_const, - /// # elements in iov - iovlen: u32, - /// ancillary data - control: ?*const anyopaque, - /// ancillary data buffer len - controllen: socklen_t, - /// flags on received message - flags: i32, - }, - .serenity => extern struct { - name: ?*const anyopaque, - namelen: socklen_t, - iov: [*]const iovec_const, - iovlen: c_uint, - control: ?*const anyopaque, - controllen: socklen_t, - flags: c_int, - }, + .serenity, + => posix_msghdr_const, else => void, }; + +const posix_msghdr_const = extern struct { + name: ?*const sockaddr, + namelen: socklen_t, + iov: [*]const iovec_const, + pad0: if (@sizeOf(usize) == 8 and native_endian == .big) u32 else u0 = 0, + iovlen: u32, + pad1: if (@sizeOf(usize) == 8 and native_endian == .little) u32 else u0 = 0, + control: ?*const anyopaque, + pad2: if (@sizeOf(usize) == 8 and native_endian == .big) u32 else u0 = 0, + controllen: socklen_t, + pad3: if (@sizeOf(usize) == 8 and native_endian == .little) u32 else u0 = 0, + flags: u32, +}; + +pub const mmsghdr = switch (native_os) { + .linux => linux.mmsghdr, + else => extern struct { + hdr: msghdr, + len: u32, + }, +}; + pub const cmsghdr = switch (native_os) { + .linux => if (@bitSizeOf(usize) > @bitSizeOf(i32) and builtin.abi.isMusl()) posix_cmsghdr else linux.cmsghdr, // https://github.com/emscripten-core/emscripten/blob/96371ed7888fc78c040179f4d4faa82a6a07a116/system/lib/libc/musl/include/sys/socket.h#L44 - .linux, .emscripten => linux.cmsghdr, + .emscripten => linux.cmsghdr, // https://github.com/freebsd/freebsd-src/blob/b197d2abcb6895d78bc9df8404e374397aa44748/sys/sys/socket.h#L492 .freebsd, // https://github.com/DragonFlyBSD/DragonFlyBSD/blob/107c0518337ba90e7fa49e74845d8d44320c9a6d/sys/sys/socket.h#L452 @@ -4196,13 +4197,19 @@ pub const cmsghdr = switch (native_os) { .tvos, .visionos, .watchos, - => extern struct { - len: socklen_t, - level: c_int, - type: c_int, - }, + => posix_cmsghdr, + else => void, }; + +const posix_cmsghdr = extern struct { + pad0: if (@sizeOf(usize) == 8 and native_endian == .big) u32 else u0 = 0, + len: socklen_t, + pad1: if (@sizeOf(usize) == 8 and native_endian == .little) u32 else u0 = 0, + level: c_int, + type: c_int, +}; + pub const nfds_t = switch (native_os) { .linux => linux.nfds_t, .emscripten => emscripten.nfds_t, @@ -4443,7 +4450,7 @@ pub const siginfo_t = switch (native_os) { .linux => linux.siginfo_t, .emscripten => emscripten.siginfo_t, .driverkit, .macos, .ios, .tvos, .watchos, .visionos => extern struct { - signo: c_int, + signo: SIG, errno: c_int, code: c_int, pid: pid_t, @@ -4459,7 +4466,7 @@ pub const siginfo_t = switch (native_os) { }, .freebsd => extern struct { // Signal number. - signo: c_int, + signo: SIG, // Errno association. errno: c_int, /// Signal code. @@ -4502,7 +4509,7 @@ pub const siginfo_t = switch (native_os) { }, }, .illumos => extern struct { - signo: c_int, + signo: SIG, code: c_int, errno: c_int, // 64bit architectures insert 4bytes of padding here, this is done by @@ -4559,7 +4566,7 @@ pub const siginfo_t = switch (native_os) { info: netbsd._ksiginfo, }, .dragonfly => extern struct { - signo: c_int, + signo: SIG, errno: c_int, code: c_int, pid: c_int, @@ -4571,7 +4578,7 @@ pub const siginfo_t = switch (native_os) { __spare__: [7]c_int, }, .haiku => extern struct { - signo: i32, + signo: SIG, code: i32, errno: i32, @@ -4580,7 +4587,7 @@ pub const siginfo_t = switch (native_os) { addr: *allowzero anyopaque, }, .openbsd => extern struct { - signo: c_int, + signo: SIG, code: c_int, errno: c_int, data: extern union { @@ -4615,7 +4622,7 @@ pub const siginfo_t = switch (native_os) { }, // https://github.com/SerenityOS/serenity/blob/ec492a1a0819e6239ea44156825c4ee7234ca3db/Kernel/API/POSIX/signal.h#L27-L37 .serenity => extern struct { - signo: c_int, + signo: SIG, code: c_int, errno: c_int, pid: pid_t, @@ -6865,7 +6872,7 @@ pub const IFNAMESIZE = switch (native_os) { // https://github.com/SerenityOS/serenity/blob/9882848e0bf783dfc8e8a6d887a848d70d9c58f4/Kernel/API/POSIX/net/if.h#L50 .openbsd, .dragonfly, .netbsd, .freebsd, .macos, .ios, .tvos, .watchos, .visionos, .serenity => 16, .illumos => 32, - else => void, + else => {}, }; pub const stack_t = switch (native_os) { @@ -10591,7 +10598,7 @@ pub extern "c" fn lseek(fd: fd_t, offset: off_t, whence: whence_t) off_t; pub extern "c" fn open(path: [*:0]const u8, oflag: O, ...) c_int; pub extern "c" fn openat(fd: c_int, path: [*:0]const u8, oflag: O, ...) c_int; pub extern "c" fn ftruncate(fd: c_int, length: off_t) c_int; -pub extern "c" fn raise(sig: c_int) c_int; +pub extern "c" fn raise(sig: SIG) c_int; pub extern "c" fn read(fd: fd_t, buf: [*]u8, nbyte: usize) isize; pub extern "c" fn readv(fd: c_int, iov: [*]const iovec, iovcnt: c_uint) isize; pub extern "c" fn pread(fd: fd_t, buf: [*]u8, nbyte: usize, offset: off_t) isize; @@ -10683,6 +10690,7 @@ pub extern "c" fn sendto( addrlen: socklen_t, ) isize; pub extern "c" fn sendmsg(sockfd: fd_t, msg: *const msghdr_const, flags: u32) isize; +pub extern "c" fn sendmmsg(sockfd: fd_t, msgvec: [*]mmsghdr, n: c_uint, flags: u32) c_int; pub extern "c" fn recv( sockfd: fd_t, @@ -10708,7 +10716,7 @@ pub const recvmsg = switch (native_os) { else => private.recvmsg, }; -pub extern "c" fn kill(pid: pid_t, sig: c_int) c_int; +pub extern "c" fn kill(pid: pid_t, sig: SIG) c_int; pub extern "c" fn setuid(uid: uid_t) c_int; pub extern "c" fn setgid(gid: gid_t) c_int; @@ -10772,6 +10780,8 @@ pub const pthread_setname_np = switch (native_os) { }; pub extern "c" fn pthread_getname_np(thread: pthread_t, name: [*:0]u8, len: usize) c_int; +pub extern "c" fn pthread_kill(pthread_t, signal: SIG) c_int; + pub const pthread_threadid_np = switch (native_os) { .macos, .ios, .tvos, .watchos, .visionos => private.pthread_threadid_np, else => {}, @@ -10876,13 +10886,13 @@ pub extern "c" fn dn_expand( length: c_int, ) c_int; -pub const PTHREAD_MUTEX_INITIALIZER = pthread_mutex_t{}; +pub const PTHREAD_MUTEX_INITIALIZER: pthread_mutex_t = .{}; pub extern "c" fn pthread_mutex_lock(mutex: *pthread_mutex_t) E; pub extern "c" fn pthread_mutex_unlock(mutex: *pthread_mutex_t) E; pub extern "c" fn pthread_mutex_trylock(mutex: *pthread_mutex_t) E; pub extern "c" fn pthread_mutex_destroy(mutex: *pthread_mutex_t) E; -pub const PTHREAD_COND_INITIALIZER = pthread_cond_t{}; +pub const PTHREAD_COND_INITIALIZER: pthread_cond_t = .{}; pub extern "c" fn pthread_cond_wait(noalias cond: *pthread_cond_t, noalias mutex: *pthread_mutex_t) E; pub extern "c" fn pthread_cond_timedwait(noalias cond: *pthread_cond_t, noalias mutex: *pthread_mutex_t, noalias abstime: *const timespec) E; pub extern "c" fn pthread_cond_signal(cond: *pthread_cond_t) E; @@ -11363,12 +11373,12 @@ const private = struct { extern "c" fn recvmsg(sockfd: fd_t, msg: *msghdr, flags: u32) isize; extern "c" fn sched_yield() c_int; extern "c" fn sendfile(out_fd: fd_t, in_fd: fd_t, offset: ?*off_t, count: usize) isize; - extern "c" fn sigaction(sig: c_int, noalias act: ?*const Sigaction, noalias oact: ?*Sigaction) c_int; - extern "c" fn sigdelset(set: ?*sigset_t, signo: c_int) c_int; - extern "c" fn sigaddset(set: ?*sigset_t, signo: c_int) c_int; + extern "c" fn sigaction(sig: SIG, noalias act: ?*const Sigaction, noalias oact: ?*Sigaction) c_int; + extern "c" fn sigdelset(set: ?*sigset_t, signo: SIG) c_int; + extern "c" fn sigaddset(set: ?*sigset_t, signo: SIG) c_int; extern "c" fn sigfillset(set: ?*sigset_t) c_int; extern "c" fn sigemptyset(set: ?*sigset_t) c_int; - extern "c" fn sigismember(set: ?*const sigset_t, signo: c_int) c_int; + extern "c" fn sigismember(set: ?*const sigset_t, signo: SIG) c_int; extern "c" fn sigprocmask(how: c_int, noalias set: ?*const sigset_t, noalias oset: ?*sigset_t) c_int; extern "c" fn socket(domain: c_uint, sock_type: c_uint, protocol: c_uint) c_int; extern "c" fn socketpair(domain: c_uint, sock_type: c_uint, protocol: c_uint, sv: *[2]fd_t) c_int; @@ -11420,7 +11430,7 @@ const private = struct { extern "c" fn __libc_thr_yield() c_int; extern "c" fn __msync13(addr: *align(page_size) const anyopaque, len: usize, flags: c_int) c_int; extern "c" fn __nanosleep50(rqtp: *const timespec, rmtp: ?*timespec) c_int; - extern "c" fn __sigaction14(sig: c_int, noalias act: ?*const Sigaction, noalias oact: ?*Sigaction) c_int; + extern "c" fn __sigaction14(sig: SIG, noalias act: ?*const Sigaction, noalias oact: ?*Sigaction) c_int; extern "c" fn __sigemptyset14(set: ?*sigset_t) c_int; extern "c" fn __sigfillset14(set: ?*sigset_t) c_int; extern "c" fn __sigprocmask14(how: c_int, noalias set: ?*const sigset_t, noalias oset: ?*sigset_t) c_int; diff --git a/lib/std/crypto/Certificate/Bundle.zig b/lib/std/crypto/Certificate/Bundle.zig index dd65b4cc4a..cc52ce71d3 100644 --- a/lib/std/crypto/Certificate/Bundle.zig +++ b/lib/std/crypto/Certificate/Bundle.zig @@ -4,6 +4,20 @@ //! concatenated together in the `bytes` array. The `map` field contains an //! index from the DER-encoded subject name to the index of the containing //! certificate within `bytes`. +const Bundle = @This(); +const builtin = @import("builtin"); + +const std = @import("../../std.zig"); +const Io = std.Io; +const assert = std.debug.assert; +const fs = std.fs; +const mem = std.mem; +const crypto = std.crypto; +const Allocator = std.mem.Allocator; +const Certificate = std.crypto.Certificate; +const der = Certificate.der; + +const base64 = std.base64.standard.decoderWithIgnore(" \t\r\n"); /// The key is the contents slice of the subject. map: std.HashMapUnmanaged(der.Element.Slice, u32, MapContext, std.hash_map.default_max_load_percentage) = .empty, @@ -56,18 +70,18 @@ pub const RescanError = RescanLinuxError || RescanMacError || RescanWithPathErro /// file system standard locations for certificates. /// For operating systems that do not have standard CA installations to be /// found, this function clears the set of certificates. -pub fn rescan(cb: *Bundle, gpa: Allocator) RescanError!void { +pub fn rescan(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp) RescanError!void { switch (builtin.os.tag) { - .linux => return rescanLinux(cb, gpa), - .macos => return rescanMac(cb, gpa), - .freebsd, .openbsd => return rescanWithPath(cb, gpa, "/etc/ssl/cert.pem"), - .netbsd => return rescanWithPath(cb, gpa, "/etc/openssl/certs/ca-certificates.crt"), - .dragonfly => return rescanWithPath(cb, gpa, "/usr/local/etc/ssl/cert.pem"), - .illumos => return rescanWithPath(cb, gpa, "/etc/ssl/cacert.pem"), - .haiku => return rescanWithPath(cb, gpa, "/boot/system/data/ssl/CARootCertificates.pem"), + .linux => return rescanLinux(cb, gpa, io, now), + .macos => return rescanMac(cb, gpa, io, now), + .freebsd, .openbsd => return rescanWithPath(cb, gpa, io, now, "/etc/ssl/cert.pem"), + .netbsd => return rescanWithPath(cb, gpa, io, now, "/etc/openssl/certs/ca-certificates.crt"), + .dragonfly => return rescanWithPath(cb, gpa, io, now, "/usr/local/etc/ssl/cert.pem"), + .illumos => return rescanWithPath(cb, gpa, io, now, "/etc/ssl/cacert.pem"), + .haiku => return rescanWithPath(cb, gpa, io, now, "/boot/system/data/ssl/CARootCertificates.pem"), // https://github.com/SerenityOS/serenity/blob/222acc9d389bc6b490d4c39539761b043a4bfcb0/Ports/ca-certificates/package.sh#L19 - .serenity => return rescanWithPath(cb, gpa, "/etc/ssl/certs/ca-certificates.crt"), - .windows => return rescanWindows(cb, gpa), + .serenity => return rescanWithPath(cb, gpa, io, now, "/etc/ssl/certs/ca-certificates.crt"), + .windows => return rescanWindows(cb, gpa, io, now), else => {}, } } @@ -77,7 +91,7 @@ const RescanMacError = @import("Bundle/macos.zig").RescanMacError; const RescanLinuxError = AddCertsFromFilePathError || AddCertsFromDirPathError; -fn rescanLinux(cb: *Bundle, gpa: Allocator) RescanLinuxError!void { +fn rescanLinux(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp) RescanLinuxError!void { // Possible certificate files; stop after finding one. const cert_file_paths = [_][]const u8{ "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Gentoo etc. @@ -100,7 +114,7 @@ fn rescanLinux(cb: *Bundle, gpa: Allocator) RescanLinuxError!void { scan: { for (cert_file_paths) |cert_file_path| { - if (addCertsFromFilePathAbsolute(cb, gpa, cert_file_path)) |_| { + if (addCertsFromFilePathAbsolute(cb, gpa, io, now, cert_file_path)) |_| { break :scan; } else |err| switch (err) { error.FileNotFound => continue, @@ -109,7 +123,7 @@ fn rescanLinux(cb: *Bundle, gpa: Allocator) RescanLinuxError!void { } for (cert_dir_paths) |cert_dir_path| { - addCertsFromDirPathAbsolute(cb, gpa, cert_dir_path) catch |err| switch (err) { + addCertsFromDirPathAbsolute(cb, gpa, io, now, cert_dir_path) catch |err| switch (err) { error.FileNotFound => continue, else => |e| return e, }; @@ -121,19 +135,21 @@ fn rescanLinux(cb: *Bundle, gpa: Allocator) RescanLinuxError!void { const RescanWithPathError = AddCertsFromFilePathError; -fn rescanWithPath(cb: *Bundle, gpa: Allocator, cert_file_path: []const u8) RescanWithPathError!void { +fn rescanWithPath(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp, cert_file_path: []const u8) RescanWithPathError!void { cb.bytes.clearRetainingCapacity(); cb.map.clearRetainingCapacity(); - try addCertsFromFilePathAbsolute(cb, gpa, cert_file_path); + try addCertsFromFilePathAbsolute(cb, gpa, io, now, cert_file_path); cb.bytes.shrinkAndFree(gpa, cb.bytes.items.len); } const RescanWindowsError = Allocator.Error || ParseCertError || std.posix.UnexpectedError || error{FileNotFound}; -fn rescanWindows(cb: *Bundle, gpa: Allocator) RescanWindowsError!void { +fn rescanWindows(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp) RescanWindowsError!void { cb.bytes.clearRetainingCapacity(); cb.map.clearRetainingCapacity(); + _ = io; + const w = std.os.windows; const GetLastError = w.GetLastError; const root = [4:0]u16{ 'R', 'O', 'O', 'T' }; @@ -143,7 +159,7 @@ fn rescanWindows(cb: *Bundle, gpa: Allocator) RescanWindowsError!void { }; defer _ = w.crypt32.CertCloseStore(store, 0); - const now_sec = std.time.timestamp(); + const now_sec = now.toSeconds(); var ctx = w.crypt32.CertEnumCertificatesInStore(store, null); while (ctx) |context| : (ctx = w.crypt32.CertEnumCertificatesInStore(store, ctx)) { @@ -160,28 +176,31 @@ pub const AddCertsFromDirPathError = fs.File.OpenError || AddCertsFromDirError; pub fn addCertsFromDirPath( cb: *Bundle, gpa: Allocator, + io: Io, dir: fs.Dir, sub_dir_path: []const u8, ) AddCertsFromDirPathError!void { var iterable_dir = try dir.openDir(sub_dir_path, .{ .iterate = true }); defer iterable_dir.close(); - return addCertsFromDir(cb, gpa, iterable_dir); + return addCertsFromDir(cb, gpa, io, iterable_dir); } pub fn addCertsFromDirPathAbsolute( cb: *Bundle, gpa: Allocator, + io: Io, + now: Io.Timestamp, abs_dir_path: []const u8, ) AddCertsFromDirPathError!void { assert(fs.path.isAbsolute(abs_dir_path)); var iterable_dir = try fs.openDirAbsolute(abs_dir_path, .{ .iterate = true }); defer iterable_dir.close(); - return addCertsFromDir(cb, gpa, iterable_dir); + return addCertsFromDir(cb, gpa, io, now, iterable_dir); } pub const AddCertsFromDirError = AddCertsFromFilePathError; -pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, iterable_dir: fs.Dir) AddCertsFromDirError!void { +pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp, iterable_dir: fs.Dir) AddCertsFromDirError!void { var it = iterable_dir.iterate(); while (try it.next()) |entry| { switch (entry.kind) { @@ -189,32 +208,37 @@ pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, iterable_dir: fs.Dir) AddCer else => continue, } - try addCertsFromFilePath(cb, gpa, iterable_dir, entry.name); + try addCertsFromFilePath(cb, gpa, io, now, iterable_dir.adaptToNewApi(), entry.name); } } -pub const AddCertsFromFilePathError = fs.File.OpenError || AddCertsFromFileError; +pub const AddCertsFromFilePathError = fs.File.OpenError || AddCertsFromFileError || Io.Clock.Error; pub fn addCertsFromFilePathAbsolute( cb: *Bundle, gpa: Allocator, + io: Io, + now: Io.Timestamp, abs_file_path: []const u8, ) AddCertsFromFilePathError!void { - assert(fs.path.isAbsolute(abs_file_path)); var file = try fs.openFileAbsolute(abs_file_path, .{}); defer file.close(); - return addCertsFromFile(cb, gpa, file); + var file_reader = file.reader(io, &.{}); + return addCertsFromFile(cb, gpa, &file_reader, now.toSeconds()); } pub fn addCertsFromFilePath( cb: *Bundle, gpa: Allocator, - dir: fs.Dir, + io: Io, + now: Io.Timestamp, + dir: Io.Dir, sub_file_path: []const u8, ) AddCertsFromFilePathError!void { - var file = try dir.openFile(sub_file_path, .{}); - defer file.close(); - return addCertsFromFile(cb, gpa, file); + var file = try dir.openFile(io, sub_file_path, .{}); + defer file.close(io); + var file_reader = file.reader(io, &.{}); + return addCertsFromFile(cb, gpa, &file_reader, now.toSeconds()); } pub const AddCertsFromFileError = Allocator.Error || @@ -222,10 +246,10 @@ pub const AddCertsFromFileError = Allocator.Error || fs.File.ReadError || ParseCertError || std.base64.Error || - error{ CertificateAuthorityBundleTooBig, MissingEndCertificateMarker }; + error{ CertificateAuthorityBundleTooBig, MissingEndCertificateMarker, Streaming }; -pub fn addCertsFromFile(cb: *Bundle, gpa: Allocator, file: fs.File) AddCertsFromFileError!void { - const size = try file.getEndPos(); +pub fn addCertsFromFile(cb: *Bundle, gpa: Allocator, file_reader: *Io.File.Reader, now_sec: i64) AddCertsFromFileError!void { + const size = try file_reader.getSize(); // We borrow `bytes` as a temporary buffer for the base64-encoded data. // This is possible by computing the decoded length and reserving the space @@ -236,14 +260,14 @@ pub fn addCertsFromFile(cb: *Bundle, gpa: Allocator, file: fs.File) AddCertsFrom try cb.bytes.ensureUnusedCapacity(gpa, needed_capacity); const end_reserved: u32 = @intCast(cb.bytes.items.len + decoded_size_upper_bound); const buffer = cb.bytes.allocatedSlice()[end_reserved..]; - const end_index = try file.readAll(buffer); + const end_index = file_reader.interface.readSliceShort(buffer) catch |err| switch (err) { + error.ReadFailed => return file_reader.err.?, + }; const encoded_bytes = buffer[0..end_index]; const begin_marker = "-----BEGIN CERTIFICATE-----"; const end_marker = "-----END CERTIFICATE-----"; - const now_sec = std.time.timestamp(); - var start_index: usize = 0; while (mem.indexOfPos(u8, encoded_bytes, start_index, begin_marker)) |begin_marker_start| { const cert_start = begin_marker_start + begin_marker.len; @@ -288,19 +312,6 @@ pub fn parseCert(cb: *Bundle, gpa: Allocator, decoded_start: u32, now_sec: i64) } } -const builtin = @import("builtin"); -const std = @import("../../std.zig"); -const assert = std.debug.assert; -const fs = std.fs; -const mem = std.mem; -const crypto = std.crypto; -const Allocator = std.mem.Allocator; -const Certificate = std.crypto.Certificate; -const der = Certificate.der; -const Bundle = @This(); - -const base64 = std.base64.standard.decoderWithIgnore(" \t\r\n"); - const MapContext = struct { cb: *const Bundle, @@ -321,8 +332,13 @@ const MapContext = struct { test "scan for OS-provided certificates" { if (builtin.os.tag == .wasi) return error.SkipZigTest; - var bundle: Bundle = .{}; - defer bundle.deinit(std.testing.allocator); + const io = std.testing.io; + const gpa = std.testing.allocator; - try bundle.rescan(std.testing.allocator); + var bundle: Bundle = .{}; + defer bundle.deinit(gpa); + + const now = try Io.Clock.real.now(io); + + try bundle.rescan(gpa, io, now); } diff --git a/lib/std/crypto/Certificate/Bundle/macos.zig b/lib/std/crypto/Certificate/Bundle/macos.zig index 5aa842f245..d32f1be8e0 100644 --- a/lib/std/crypto/Certificate/Bundle/macos.zig +++ b/lib/std/crypto/Certificate/Bundle/macos.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const Io = std.Io; const assert = std.debug.assert; const fs = std.fs; const mem = std.mem; @@ -7,7 +8,7 @@ const Bundle = @import("../Bundle.zig"); pub const RescanMacError = Allocator.Error || fs.File.OpenError || fs.File.ReadError || fs.File.SeekError || Bundle.ParseCertError || error{EndOfStream}; -pub fn rescanMac(cb: *Bundle, gpa: Allocator) RescanMacError!void { +pub fn rescanMac(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp) RescanMacError!void { cb.bytes.clearRetainingCapacity(); cb.map.clearRetainingCapacity(); @@ -16,6 +17,7 @@ pub fn rescanMac(cb: *Bundle, gpa: Allocator) RescanMacError!void { "/Library/Keychains/System.keychain", }; + _ = io; // TODO migrate file system to use std.Io for (keychain_paths) |keychain_path| { const bytes = std.fs.cwd().readFileAlloc(keychain_path, gpa, .limited(std.math.maxInt(u32))) catch |err| switch (err) { error.StreamTooLong => return error.FileTooBig, @@ -23,8 +25,8 @@ pub fn rescanMac(cb: *Bundle, gpa: Allocator) RescanMacError!void { }; defer gpa.free(bytes); - var reader: std.Io.Reader = .fixed(bytes); - scanReader(cb, gpa, &reader) catch |err| switch (err) { + var reader: Io.Reader = .fixed(bytes); + scanReader(cb, gpa, &reader, now.toSeconds()) catch |err| switch (err) { error.ReadFailed => unreachable, // prebuffered else => |e| return e, }; @@ -33,7 +35,7 @@ pub fn rescanMac(cb: *Bundle, gpa: Allocator) RescanMacError!void { cb.bytes.shrinkAndFree(gpa, cb.bytes.items.len); } -fn scanReader(cb: *Bundle, gpa: Allocator, reader: *std.Io.Reader) !void { +fn scanReader(cb: *Bundle, gpa: Allocator, reader: *Io.Reader, now_sec: i64) !void { const db_header = try reader.takeStruct(ApplDbHeader, .big); assert(mem.eql(u8, &db_header.signature, "kych")); @@ -49,8 +51,6 @@ fn scanReader(cb: *Bundle, gpa: Allocator, reader: *std.Io.Reader) !void { table_list[table_idx] = try reader.takeInt(u32, .big); } - const now_sec = std.time.timestamp(); - for (table_list) |table_offset| { reader.seek = db_header.schema_offset + table_offset; diff --git a/lib/std/crypto/tls/Client.zig b/lib/std/crypto/tls/Client.zig index b697d624fa..f6e334af8e 100644 --- a/lib/std/crypto/tls/Client.zig +++ b/lib/std/crypto/tls/Client.zig @@ -105,6 +105,14 @@ pub const Options = struct { /// Verify that the server certificate is authorized by a given ca bundle. bundle: Certificate.Bundle, }, + write_buffer: []u8, + read_buffer: []u8, + /// Cryptographically secure random bytes. The pointer is not captured; data is only + /// read during `init`. + entropy: *const [176]u8, + /// Current time according to the wall clock / calendar, in seconds. + realtime_now_seconds: i64, + /// If non-null, ssl secrets are logged to this stream. Creating such a log file allows /// other programs with access to that file to decrypt all traffic over this connection. /// @@ -120,8 +128,6 @@ pub const Options = struct { /// application layer itself verifies that the amount of data received equals /// the amount of data expected, such as HTTP with the Content-Length header. allow_truncation_attacks: bool = false, - write_buffer: []u8, - read_buffer: []u8, /// Populated when `error.TlsAlert` is returned from `init`. alert: ?*tls.Alert = null, }; @@ -189,14 +195,12 @@ pub fn init(input: *Reader, output: *Writer, options: Options) InitError!Client }; const host_len: u16 = @intCast(host.len); - var random_buffer: [176]u8 = undefined; - crypto.random.bytes(&random_buffer); - const client_hello_rand = random_buffer[0..32].*; + const client_hello_rand = options.entropy[0..32].*; var key_seq: u64 = 0; var server_hello_rand: [32]u8 = undefined; - const legacy_session_id = random_buffer[32..64].*; + const legacy_session_id = options.entropy[32..64].*; - var key_share = KeyShare.init(random_buffer[64..176].*) catch |err| switch (err) { + var key_share = KeyShare.init(options.entropy[64..176].*) catch |err| switch (err) { // Only possible to happen if the seed is all zeroes. error.IdentityElement => return error.InsufficientEntropy, }; @@ -321,7 +325,7 @@ pub fn init(input: *Reader, output: *Writer, options: Options) InitError!Client var handshake_cipher: tls.HandshakeCipher = undefined; var main_cert_pub_key: CertificatePublicKey = undefined; var tls12_negotiated_group: ?tls.NamedGroup = null; - const now_sec = std.time.timestamp(); + const now_sec = options.realtime_now_seconds; var cleartext_fragment_start: usize = 0; var cleartext_fragment_end: usize = 0; diff --git a/lib/std/debug.zig b/lib/std/debug.zig index ad3f2160b1..3bb5d6d7ab 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -1,4 +1,7 @@ const std = @import("std.zig"); +const Io = std.Io; +const Writer = std.Io.Writer; +const tty = std.Io.tty; const math = std.math; const mem = std.mem; const posix = std.posix; @@ -7,12 +10,11 @@ const testing = std.testing; const Allocator = mem.Allocator; const File = std.fs.File; const windows = std.os.windows; -const Writer = std.Io.Writer; -const tty = std.Io.tty; const builtin = @import("builtin"); const native_arch = builtin.cpu.arch; const native_os = builtin.os.tag; +const StackTrace = std.builtin.StackTrace; const root = @import("root"); @@ -82,6 +84,7 @@ pub const SelfInfoError = error{ /// The required debug info could not be read from disk due to some IO error. ReadFailed, OutOfMemory, + Canceled, Unexpected, }; @@ -544,7 +547,7 @@ pub fn defaultPanic( stderr.print("panic: ", .{}) catch break :trace; } else { const current_thread_id = std.Thread.getCurrentId(); - stderr.print("thread {} panic: ", .{current_thread_id}) catch break :trace; + stderr.print("thread {d} panic: ", .{current_thread_id}) catch break :trace; } stderr.print("{s}\n", .{msg}) catch break :trace; @@ -606,8 +609,8 @@ pub const StackUnwindOptions = struct { /// the given buffer, so `addr_buf` must have a lifetime at least equal to the `StackTrace`. /// /// See `writeCurrentStackTrace` to immediately print the trace instead of capturing it. -pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf: []usize) std.builtin.StackTrace { - const empty_trace: std.builtin.StackTrace = .{ .index = 0, .instruction_addresses = &.{} }; +pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf: []usize) StackTrace { + const empty_trace: StackTrace = .{ .index = 0, .instruction_addresses = &.{} }; if (!std.options.allow_stack_tracing) return empty_trace; var it = StackIterator.init(options.context) catch return empty_trace; defer it.deinit(); @@ -645,6 +648,9 @@ pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf: /// /// See `captureCurrentStackTrace` to capture the trace addresses into a buffer instead of printing. pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Writer, tty_config: tty.Config) Writer.Error!void { + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.ioBasic(); + if (!std.options.allow_stack_tracing) { tty_config.setColor(writer, .dim) catch {}; try writer.print("Cannot print stack trace: stack tracing is disabled\n", .{}); @@ -691,6 +697,7 @@ pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Wri error.UnsupportedDebugInfo => "unwind info unsupported", error.ReadFailed => "filesystem error", error.OutOfMemory => "out of memory", + error.Canceled => "operation canceled", error.Unexpected => "unexpected error", }; if (it.stratOk(options.allow_unsafe_unwind)) { @@ -728,7 +735,7 @@ pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Wri } // `ret_addr` is the return address, which is *after* the function call. // Subtract 1 to get an address *in* the function call for a better source location. - try printSourceAtAddress(di_gpa, di, writer, ret_addr -| StackIterator.ra_call_offset, tty_config); + try printSourceAtAddress(di_gpa, io, di, writer, ret_addr -| StackIterator.ra_call_offset, tty_config); printed_any_frame = true; }, }; @@ -752,14 +759,29 @@ pub fn dumpCurrentStackTrace(options: StackUnwindOptions) void { }; } +pub const FormatStackTrace = struct { + stack_trace: StackTrace, + tty_config: tty.Config, + + pub fn format(context: @This(), writer: *Io.Writer) Io.Writer.Error!void { + try writer.writeAll("\n"); + try writeStackTrace(&context.stack_trace, writer, context.tty_config); + } +}; + /// Write a previously captured stack trace to `writer`, annotated with source locations. -pub fn writeStackTrace(st: *const std.builtin.StackTrace, writer: *Writer, tty_config: tty.Config) Writer.Error!void { +pub fn writeStackTrace(st: *const StackTrace, writer: *Writer, tty_config: tty.Config) Writer.Error!void { if (!std.options.allow_stack_tracing) { tty_config.setColor(writer, .dim) catch {}; try writer.print("Cannot print stack trace: stack tracing is disabled\n", .{}); tty_config.setColor(writer, .reset) catch {}; return; } + // We use an independent Io implementation here in case there was a problem + // with the application's Io implementation itself. + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.ioBasic(); + // Fetch `st.index` straight away. Aside from avoiding redundant loads, this prevents issues if // `st` is `@errorReturnTrace()` and errors are encountered while writing the stack trace. const n_frames = st.index; @@ -777,7 +799,7 @@ pub fn writeStackTrace(st: *const std.builtin.StackTrace, writer: *Writer, tty_c for (st.instruction_addresses[0..captured_frames]) |ret_addr| { // `ret_addr` is the return address, which is *after* the function call. // Subtract 1 to get an address *in* the function call for a better source location. - try printSourceAtAddress(di_gpa, di, writer, ret_addr -| StackIterator.ra_call_offset, tty_config); + try printSourceAtAddress(di_gpa, io, di, writer, ret_addr -| StackIterator.ra_call_offset, tty_config); } if (n_frames > captured_frames) { tty_config.setColor(writer, .bold) catch {}; @@ -786,7 +808,7 @@ pub fn writeStackTrace(st: *const std.builtin.StackTrace, writer: *Writer, tty_c } } /// A thin wrapper around `writeStackTrace` which writes to stderr and ignores write errors. -pub fn dumpStackTrace(st: *const std.builtin.StackTrace) void { +pub fn dumpStackTrace(st: *const StackTrace) void { const tty_config = tty.detectConfig(.stderr()); const stderr = lockStderrWriter(&.{}); defer unlockStderrWriter(); @@ -1073,13 +1095,13 @@ pub inline fn stripInstructionPtrAuthCode(ptr: usize) usize { return ptr; } -fn printSourceAtAddress(gpa: Allocator, debug_info: *SelfInfo, writer: *Writer, address: usize, tty_config: tty.Config) Writer.Error!void { - const symbol: Symbol = debug_info.getSymbol(gpa, address) catch |err| switch (err) { +fn printSourceAtAddress(gpa: Allocator, io: Io, debug_info: *SelfInfo, writer: *Writer, address: usize, tty_config: tty.Config) Writer.Error!void { + const symbol: Symbol = debug_info.getSymbol(gpa, io, address) catch |err| switch (err) { error.MissingDebugInfo, error.UnsupportedDebugInfo, error.InvalidDebugInfo, => .unknown, - error.ReadFailed, error.Unexpected => s: { + error.ReadFailed, error.Unexpected, error.Canceled => s: { tty_config.setColor(writer, .dim) catch {}; try writer.print("Failed to read debug info from filesystem, trace may be incomplete\n\n", .{}); tty_config.setColor(writer, .reset) catch {}; @@ -1387,10 +1409,10 @@ pub fn maybeEnableSegfaultHandler() void { var windows_segfault_handle: ?windows.HANDLE = null; pub fn updateSegfaultHandler(act: ?*const posix.Sigaction) void { - posix.sigaction(posix.SIG.SEGV, act, null); - posix.sigaction(posix.SIG.ILL, act, null); - posix.sigaction(posix.SIG.BUS, act, null); - posix.sigaction(posix.SIG.FPE, act, null); + posix.sigaction(.SEGV, act, null); + posix.sigaction(.ILL, act, null); + posix.sigaction(.BUS, act, null); + posix.sigaction(.FPE, act, null); } /// Attaches a global handler for several signals which, when triggered, prints output to stderr @@ -1435,7 +1457,7 @@ fn resetSegfaultHandler() void { updateSegfaultHandler(&act); } -fn handleSegfaultPosix(sig: i32, info: *const posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) noreturn { +fn handleSegfaultPosix(sig: posix.SIG, info: *const posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) noreturn { if (use_trap_panic) @trap(); const addr: ?usize, const name: []const u8 = info: { if (native_os == .linux and native_arch == .x86_64) { @@ -1447,7 +1469,7 @@ fn handleSegfaultPosix(sig: i32, info: *const posix.siginfo_t, ctx_ptr: ?*anyopa // for example when reading/writing model-specific registers // by executing `rdmsr` or `wrmsr` in user-space (unprivileged mode). const SI_KERNEL = 0x80; - if (sig == posix.SIG.SEGV and info.code == SI_KERNEL) { + if (sig == .SEGV and info.code == SI_KERNEL) { break :info .{ null, "General protection exception" }; } } @@ -1474,10 +1496,10 @@ fn handleSegfaultPosix(sig: i32, info: *const posix.siginfo_t, ctx_ptr: ?*anyopa else => comptime unreachable, }; const name = switch (sig) { - posix.SIG.SEGV => "Segmentation fault", - posix.SIG.ILL => "Illegal instruction", - posix.SIG.BUS => "Bus error", - posix.SIG.FPE => "Arithmetic exception", + .SEGV => "Segmentation fault", + .ILL => "Illegal instruction", + .BUS => "Bus error", + .FPE => "Arithmetic exception", else => unreachable, }; break :info .{ addr, name }; @@ -1579,11 +1601,14 @@ test "manage resources correctly" { } }; const gpa = std.testing.allocator; - var discarding: std.Io.Writer.Discarding = .init(&.{}); + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.ioBasic(); + var discarding: Io.Writer.Discarding = .init(&.{}); var di: SelfInfo = .init; defer di.deinit(gpa); try printSourceAtAddress( gpa, + io, &di, &discarding.writer, S.showMyTrace(), @@ -1657,7 +1682,7 @@ pub fn ConfigurableTrace(comptime size: usize, comptime stack_frame_count: usize stderr.print("{s}:\n", .{t.notes[i]}) catch return; var frames_array_mutable = frames_array; const frames = mem.sliceTo(frames_array_mutable[0..], 0); - const stack_trace: std.builtin.StackTrace = .{ + const stack_trace: StackTrace = .{ .index = frames.len, .instruction_addresses = frames, }; diff --git a/lib/std/debug/ElfFile.zig b/lib/std/debug/ElfFile.zig index 5be5ee55c5..427a8fd1b1 100644 --- a/lib/std/debug/ElfFile.zig +++ b/lib/std/debug/ElfFile.zig @@ -108,6 +108,8 @@ pub const LoadError = error{ LockedMemoryLimitExceeded, ProcessFdQuotaExceeded, SystemFdQuotaExceeded, + Streaming, + Canceled, Unexpected, }; @@ -408,7 +410,7 @@ fn loadInner( arena: Allocator, elf_file: std.fs.File, opt_crc: ?u32, -) (LoadError || error{CrcMismatch})!LoadInnerResult { +) (LoadError || error{ CrcMismatch, Streaming, Canceled })!LoadInnerResult { const mapped_mem: []align(std.heap.page_size_min) const u8 = mapped: { const file_len = std.math.cast( usize, diff --git a/lib/std/debug/SelfInfo/Elf.zig b/lib/std/debug/SelfInfo/Elf.zig index 0f3c46e980..21319b01d4 100644 --- a/lib/std/debug/SelfInfo/Elf.zig +++ b/lib/std/debug/SelfInfo/Elf.zig @@ -28,7 +28,8 @@ pub fn deinit(si: *SelfInfo, gpa: Allocator) void { if (si.unwind_cache) |cache| gpa.free(cache); } -pub fn getSymbol(si: *SelfInfo, gpa: Allocator, address: usize) Error!std.debug.Symbol { +pub fn getSymbol(si: *SelfInfo, gpa: Allocator, io: Io, address: usize) Error!std.debug.Symbol { + _ = io; const module = try si.findModule(gpa, address, .exclusive); defer si.rwlock.unlock(); @@ -336,6 +337,7 @@ const Module = struct { var elf_file = load_result catch |err| switch (err) { error.OutOfMemory, error.Unexpected, + error.Canceled, => |e| return e, error.Overflow, @@ -353,6 +355,7 @@ const Module = struct { error.LockedMemoryLimitExceeded, error.ProcessFdQuotaExceeded, error.SystemFdQuotaExceeded, + error.Streaming, => return error.ReadFailed, }; errdefer elf_file.deinit(gpa); @@ -487,6 +490,7 @@ const DlIterContext = struct { }; const std = @import("std"); +const Io = std.Io; const Allocator = std.mem.Allocator; const Dwarf = std.debug.Dwarf; const Error = std.debug.SelfInfoError; diff --git a/lib/std/debug/SelfInfo/MachO.zig b/lib/std/debug/SelfInfo/MachO.zig index a89a2f0fb5..f7eb4465c5 100644 --- a/lib/std/debug/SelfInfo/MachO.zig +++ b/lib/std/debug/SelfInfo/MachO.zig @@ -30,7 +30,8 @@ pub fn deinit(si: *SelfInfo, gpa: Allocator) void { si.ofiles.deinit(gpa); } -pub fn getSymbol(si: *SelfInfo, gpa: Allocator, address: usize) Error!std.debug.Symbol { +pub fn getSymbol(si: *SelfInfo, gpa: Allocator, io: Io, address: usize) Error!std.debug.Symbol { + _ = io; const module = try si.findModule(gpa, address); defer si.mutex.unlock(); @@ -117,11 +118,14 @@ pub fn unwindFrame(si: *SelfInfo, gpa: Allocator, context: *UnwindContext) Error error.ReadFailed, error.OutOfMemory, error.Unexpected, + error.Canceled, => |e| return e, + error.UnsupportedRegister, error.UnsupportedAddrSize, error.UnimplementedUserOpcode, => return error.UnsupportedDebugInfo, + error.Overflow, error.EndOfStream, error.StreamTooLong, @@ -967,6 +971,7 @@ fn loadOFile(gpa: Allocator, o_file_path: []const u8) !OFile { } const std = @import("std"); +const Io = std.Io; const Allocator = std.mem.Allocator; const Dwarf = std.debug.Dwarf; const Error = std.debug.SelfInfoError; diff --git a/lib/std/debug/SelfInfo/Windows.zig b/lib/std/debug/SelfInfo/Windows.zig index f84836a6d4..70009217db 100644 --- a/lib/std/debug/SelfInfo/Windows.zig +++ b/lib/std/debug/SelfInfo/Windows.zig @@ -20,11 +20,11 @@ pub fn deinit(si: *SelfInfo, gpa: Allocator) void { module_name_arena.deinit(); } -pub fn getSymbol(si: *SelfInfo, gpa: Allocator, address: usize) Error!std.debug.Symbol { +pub fn getSymbol(si: *SelfInfo, gpa: Allocator, io: Io, address: usize) Error!std.debug.Symbol { si.mutex.lock(); defer si.mutex.unlock(); const module = try si.findModule(gpa, address); - const di = try module.getDebugInfo(gpa); + const di = try module.getDebugInfo(gpa, io); return di.getSymbol(gpa, address - module.base_address); } pub fn getModuleName(si: *SelfInfo, gpa: Allocator, address: usize) Error![]const u8 { @@ -190,6 +190,7 @@ const Module = struct { const DebugInfo = struct { arena: std.heap.ArenaAllocator.State, + io: Io, coff_image_base: u64, mapped_file: ?MappedFile, dwarf: ?Dwarf, @@ -209,9 +210,10 @@ const Module = struct { }; fn deinit(di: *DebugInfo, gpa: Allocator) void { + const io = di.io; if (di.dwarf) |*dwarf| dwarf.deinit(gpa); if (di.pdb) |*pdb| { - pdb.file_reader.file.close(); + pdb.file_reader.file.close(io); pdb.deinit(); } if (di.mapped_file) |*mf| mf.deinit(); @@ -277,11 +279,11 @@ const Module = struct { } }; - fn getDebugInfo(module: *Module, gpa: Allocator) Error!*DebugInfo { - if (module.di == null) module.di = loadDebugInfo(module, gpa); + fn getDebugInfo(module: *Module, gpa: Allocator, io: Io) Error!*DebugInfo { + if (module.di == null) module.di = loadDebugInfo(module, gpa, io); return if (module.di.?) |*di| di else |err| err; } - fn loadDebugInfo(module: *const Module, gpa: Allocator) Error!DebugInfo { + fn loadDebugInfo(module: *const Module, gpa: Allocator, io: Io) Error!DebugInfo { const mapped_ptr: [*]const u8 = @ptrFromInt(module.base_address); const mapped = mapped_ptr[0..module.size]; var coff_obj = coff.Coff.init(mapped, true) catch return error.InvalidDebugInfo; @@ -305,7 +307,10 @@ const Module = struct { windows.PATH_MAX_WIDE, ); if (len == 0) return error.MissingDebugInfo; - const coff_file = fs.openFileAbsoluteW(name_buffer[0 .. len + 4 :0], .{}) catch |err| switch (err) { + const name_w = name_buffer[0 .. len + 4 :0]; + var threaded: Io.Threaded = .init_single_threaded; + const coff_file = threaded.dirOpenFileWtf16(null, name_w, .{}) catch |err| switch (err) { + error.Canceled => |e| return e, error.Unexpected => |e| return e, error.FileNotFound => return error.MissingDebugInfo, @@ -314,8 +319,6 @@ const Module = struct { error.NotDir, error.SymLinkLoop, error.NameTooLong, - error.InvalidUtf8, - error.InvalidWtf8, error.BadPathName, => return error.InvalidDebugInfo, @@ -338,7 +341,7 @@ const Module = struct { error.FileBusy, => return error.ReadFailed, }; - errdefer coff_file.close(); + errdefer coff_file.close(io); var section_handle: windows.HANDLE = undefined; const create_section_rc = windows.ntdll.NtCreateSection( §ion_handle, @@ -372,7 +375,7 @@ const Module = struct { const section_view = section_view_ptr.?[0..coff_len]; coff_obj = coff.Coff.init(section_view, false) catch return error.InvalidDebugInfo; break :mapped .{ - .file = coff_file, + .file = .adaptFromNewApi(coff_file), .section_handle = section_handle, .section_view = section_view, }; @@ -434,8 +437,8 @@ const Module = struct { }; errdefer pdb_file.close(); - const pdb_reader = try arena.create(std.fs.File.Reader); - pdb_reader.* = pdb_file.reader(try arena.alloc(u8, 4096)); + const pdb_reader = try arena.create(Io.File.Reader); + pdb_reader.* = pdb_file.reader(io, try arena.alloc(u8, 4096)); var pdb = Pdb.init(gpa, pdb_reader) catch |err| switch (err) { error.OutOfMemory, error.ReadFailed, error.Unexpected => |e| return e, @@ -473,7 +476,7 @@ const Module = struct { break :pdb pdb; }; errdefer if (opt_pdb) |*pdb| { - pdb.file_reader.file.close(); + pdb.file_reader.file.close(io); pdb.deinit(); }; @@ -483,6 +486,7 @@ const Module = struct { return .{ .arena = arena_instance.state, + .io = io, .coff_image_base = coff_image_base, .mapped_file = mapped_file, .dwarf = opt_dwarf, @@ -544,6 +548,7 @@ fn findModule(si: *SelfInfo, gpa: Allocator, address: usize) error{ MissingDebug } const std = @import("std"); +const Io = std.Io; const Allocator = std.mem.Allocator; const Dwarf = std.debug.Dwarf; const Pdb = std.debug.Pdb; diff --git a/lib/std/dynamic_library.zig b/lib/std/dynamic_library.zig index 92aea780bf..90f655b95b 100644 --- a/lib/std/dynamic_library.zig +++ b/lib/std/dynamic_library.zig @@ -137,6 +137,8 @@ const ElfDynLibError = error{ ElfStringSectionNotFound, ElfSymSectionNotFound, ElfHashTableNotFound, + Canceled, + Streaming, } || posix.OpenError || posix.MMapError; pub const ElfDynLib = struct { diff --git a/lib/std/elf.zig b/lib/std/elf.zig index 3b0c085003..c5f8a7ea80 100644 --- a/lib/std/elf.zig +++ b/lib/std/elf.zig @@ -1,9 +1,11 @@ //! Executable and Linkable Format. const std = @import("std.zig"); +const Io = std.Io; const math = std.math; const mem = std.mem; const assert = std.debug.assert; +const Endian = std.builtin.Endian; const native_endian = @import("builtin").target.cpu.arch.endian(); pub const AT_NULL = 0; @@ -568,7 +570,7 @@ pub const ET = enum(u16) { /// All integers are native endian. pub const Header = struct { is_64: bool, - endian: std.builtin.Endian, + endian: Endian, os_abi: OSABI, /// The meaning of this value depends on `os_abi`. abi_version: u8, @@ -583,48 +585,76 @@ pub const Header = struct { shnum: u16, shstrndx: u16, - pub fn iterateProgramHeaders(h: Header, file_reader: *std.fs.File.Reader) ProgramHeaderIterator { + pub fn iterateProgramHeaders(h: *const Header, file_reader: *Io.File.Reader) ProgramHeaderIterator { return .{ - .elf_header = h, + .is_64 = h.is_64, + .endian = h.endian, + .phnum = h.phnum, + .phoff = h.phoff, .file_reader = file_reader, }; } - pub fn iterateProgramHeadersBuffer(h: Header, buf: []const u8) ProgramHeaderBufferIterator { + pub fn iterateProgramHeadersBuffer(h: *const Header, buf: []const u8) ProgramHeaderBufferIterator { return .{ - .elf_header = h, + .is_64 = h.is_64, + .endian = h.endian, + .phnum = h.phnum, + .phoff = h.phoff, .buf = buf, }; } - pub fn iterateSectionHeaders(h: Header, file_reader: *std.fs.File.Reader) SectionHeaderIterator { + pub fn iterateSectionHeaders(h: *const Header, file_reader: *Io.File.Reader) SectionHeaderIterator { return .{ - .elf_header = h, + .is_64 = h.is_64, + .endian = h.endian, + .shnum = h.shnum, + .shoff = h.shoff, .file_reader = file_reader, }; } - pub fn iterateSectionHeadersBuffer(h: Header, buf: []const u8) SectionHeaderBufferIterator { + pub fn iterateSectionHeadersBuffer(h: *const Header, buf: []const u8) SectionHeaderBufferIterator { return .{ - .elf_header = h, + .is_64 = h.is_64, + .endian = h.endian, + .shnum = h.shnum, + .shoff = h.shoff, .buf = buf, }; } - pub const ReadError = std.Io.Reader.Error || error{ + pub fn iterateDynamicSection( + h: *const Header, + file_reader: *Io.File.Reader, + offset: u64, + size: u64, + ) DynamicSectionIterator { + return .{ + .is_64 = h.is_64, + .endian = h.endian, + .offset = offset, + .end_offset = offset + size, + .file_reader = file_reader, + }; + } + + pub const ReadError = Io.Reader.Error || error{ InvalidElfMagic, InvalidElfVersion, InvalidElfClass, InvalidElfEndian, }; - pub fn read(r: *std.Io.Reader) ReadError!Header { + /// If this function fails, seek position of `r` is unchanged. + pub fn read(r: *Io.Reader) ReadError!Header { const buf = try r.peek(@sizeOf(Elf64_Ehdr)); if (!mem.eql(u8, buf[0..4], MAGIC)) return error.InvalidElfMagic; if (buf[EI.VERSION] != 1) return error.InvalidElfVersion; - const endian: std.builtin.Endian = switch (buf[EI.DATA]) { + const endian: Endian = switch (buf[EI.DATA]) { ELFDATA2LSB => .little, ELFDATA2MSB => .big, else => return error.InvalidElfEndian, @@ -637,7 +667,7 @@ pub const Header = struct { }; } - pub fn init(hdr: anytype, endian: std.builtin.Endian) Header { + pub fn init(hdr: anytype, endian: Endian) Header { // Converting integers to exhaustive enums using `@enumFromInt` could cause a panic. comptime assert(!@typeInfo(OSABI).@"enum".is_exhaustive); return .{ @@ -664,46 +694,54 @@ pub const Header = struct { }; pub const ProgramHeaderIterator = struct { - elf_header: Header, - file_reader: *std.fs.File.Reader, + is_64: bool, + endian: Endian, + phnum: u16, + phoff: u64, + + file_reader: *Io.File.Reader, index: usize = 0, pub fn next(it: *ProgramHeaderIterator) !?Elf64_Phdr { - if (it.index >= it.elf_header.phnum) return null; + if (it.index >= it.phnum) return null; defer it.index += 1; - const size: u64 = if (it.elf_header.is_64) @sizeOf(Elf64_Phdr) else @sizeOf(Elf32_Phdr); - const offset = it.elf_header.phoff + size * it.index; + const size: u64 = if (it.is_64) @sizeOf(Elf64_Phdr) else @sizeOf(Elf32_Phdr); + const offset = it.phoff + size * it.index; try it.file_reader.seekTo(offset); - return takePhdr(&it.file_reader.interface, it.elf_header); + return try takeProgramHeader(&it.file_reader.interface, it.is_64, it.endian); } }; pub const ProgramHeaderBufferIterator = struct { - elf_header: Header, + is_64: bool, + endian: Endian, + phnum: u16, + phoff: u64, + buf: []const u8, index: usize = 0, pub fn next(it: *ProgramHeaderBufferIterator) !?Elf64_Phdr { - if (it.index >= it.elf_header.phnum) return null; + if (it.index >= it.phnum) return null; defer it.index += 1; - const size: u64 = if (it.elf_header.is_64) @sizeOf(Elf64_Phdr) else @sizeOf(Elf32_Phdr); - const offset = it.elf_header.phoff + size * it.index; - var reader = std.Io.Reader.fixed(it.buf[offset..]); + const size: u64 = if (it.is_64) @sizeOf(Elf64_Phdr) else @sizeOf(Elf32_Phdr); + const offset = it.phoff + size * it.index; + var reader = Io.Reader.fixed(it.buf[offset..]); - return takePhdr(&reader, it.elf_header); + return try takeProgramHeader(&reader, it.is_64, it.endian); } }; -fn takePhdr(reader: *std.Io.Reader, elf_header: Header) !?Elf64_Phdr { - if (elf_header.is_64) { - const phdr = try reader.takeStruct(Elf64_Phdr, elf_header.endian); +pub fn takeProgramHeader(reader: *Io.Reader, is_64: bool, endian: Endian) !Elf64_Phdr { + if (is_64) { + const phdr = try reader.takeStruct(Elf64_Phdr, endian); return phdr; } - const phdr = try reader.takeStruct(Elf32_Phdr, elf_header.endian); + const phdr = try reader.takeStruct(Elf32_Phdr, endian); return .{ .p_type = phdr.p_type, .p_offset = phdr.p_offset, @@ -717,47 +755,55 @@ fn takePhdr(reader: *std.Io.Reader, elf_header: Header) !?Elf64_Phdr { } pub const SectionHeaderIterator = struct { - elf_header: Header, - file_reader: *std.fs.File.Reader, + is_64: bool, + endian: Endian, + shnum: u16, + shoff: u64, + + file_reader: *Io.File.Reader, index: usize = 0, pub fn next(it: *SectionHeaderIterator) !?Elf64_Shdr { - if (it.index >= it.elf_header.shnum) return null; + if (it.index >= it.shnum) return null; defer it.index += 1; - const size: u64 = if (it.elf_header.is_64) @sizeOf(Elf64_Shdr) else @sizeOf(Elf32_Shdr); - const offset = it.elf_header.shoff + size * it.index; + const size: u64 = if (it.is_64) @sizeOf(Elf64_Shdr) else @sizeOf(Elf32_Shdr); + const offset = it.shoff + size * it.index; try it.file_reader.seekTo(offset); - return takeShdr(&it.file_reader.interface, it.elf_header); + return try takeSectionHeader(&it.file_reader.interface, it.is_64, it.endian); } }; pub const SectionHeaderBufferIterator = struct { - elf_header: Header, + is_64: bool, + endian: Endian, + shnum: u16, + shoff: u64, + buf: []const u8, index: usize = 0, pub fn next(it: *SectionHeaderBufferIterator) !?Elf64_Shdr { - if (it.index >= it.elf_header.shnum) return null; + if (it.index >= it.shnum) return null; defer it.index += 1; - const size: u64 = if (it.elf_header.is_64) @sizeOf(Elf64_Shdr) else @sizeOf(Elf32_Shdr); - const offset = it.elf_header.shoff + size * it.index; + const size: u64 = if (it.is_64) @sizeOf(Elf64_Shdr) else @sizeOf(Elf32_Shdr); + const offset = it.shoff + size * it.index; if (offset > it.buf.len) return error.EndOfStream; - var reader = std.Io.Reader.fixed(it.buf[@intCast(offset)..]); + var reader = Io.Reader.fixed(it.buf[@intCast(offset)..]); - return takeShdr(&reader, it.elf_header); + return try takeSectionHeader(&reader, it.is_64, it.endian); } }; -fn takeShdr(reader: *std.Io.Reader, elf_header: Header) !?Elf64_Shdr { - if (elf_header.is_64) { - const shdr = try reader.takeStruct(Elf64_Shdr, elf_header.endian); +pub fn takeSectionHeader(reader: *Io.Reader, is_64: bool, endian: Endian) !Elf64_Shdr { + if (is_64) { + const shdr = try reader.takeStruct(Elf64_Shdr, endian); return shdr; } - const shdr = try reader.takeStruct(Elf32_Shdr, elf_header.endian); + const shdr = try reader.takeStruct(Elf32_Shdr, endian); return .{ .sh_name = shdr.sh_name, .sh_type = shdr.sh_type, @@ -772,6 +818,36 @@ fn takeShdr(reader: *std.Io.Reader, elf_header: Header) !?Elf64_Shdr { }; } +pub const DynamicSectionIterator = struct { + is_64: bool, + endian: Endian, + offset: u64, + end_offset: u64, + + file_reader: *Io.File.Reader, + + pub fn next(it: *DynamicSectionIterator) !?Elf64_Dyn { + if (it.offset >= it.end_offset) return null; + const size: u64 = if (it.is_64) @sizeOf(Elf64_Dyn) else @sizeOf(Elf32_Dyn); + defer it.offset += size; + try it.file_reader.seekTo(it.offset); + return try takeDynamicSection(&it.file_reader.interface, it.is_64, it.endian); + } +}; + +pub fn takeDynamicSection(reader: *Io.Reader, is_64: bool, endian: Endian) !Elf64_Dyn { + if (is_64) { + const dyn = try reader.takeStruct(Elf64_Dyn, endian); + return dyn; + } + + const dyn = try reader.takeStruct(Elf32_Dyn, endian); + return .{ + .d_tag = dyn.d_tag, + .d_val = dyn.d_val, + }; +} + pub const EI = struct { pub const CLASS = 4; pub const DATA = 5; diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 652480914f..6db63b6e2b 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -1,14 +1,15 @@ //! File System. +const builtin = @import("builtin"); +const native_os = builtin.os.tag; const std = @import("std.zig"); -const builtin = @import("builtin"); +const Io = std.Io; const root = @import("root"); const mem = std.mem; const base64 = std.base64; const crypto = std.crypto; const Allocator = std.mem.Allocator; const assert = std.debug.assert; -const native_os = builtin.os.tag; const posix = std.posix; const windows = std.os.windows; @@ -97,23 +98,6 @@ pub const base64_encoder = base64.Base64Encoder.init(base64_alphabet, null); /// Base64 decoder, replacing the standard `+/` with `-_` so that it can be used in a file name on any filesystem. pub const base64_decoder = base64.Base64Decoder.init(base64_alphabet, null); -/// Same as `Dir.updateFile`, except asserts that both `source_path` and `dest_path` -/// are absolute. See `Dir.updateFile` for a function that operates on both -/// absolute and relative paths. -/// On Windows, both paths should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, both paths should be encoded as valid UTF-8. -/// On other platforms, both paths are an opaque sequence of bytes with no particular encoding. -pub fn updateFileAbsolute( - source_path: []const u8, - dest_path: []const u8, - args: Dir.CopyFileOptions, -) !Dir.PrevStatus { - assert(path.isAbsolute(source_path)); - assert(path.isAbsolute(dest_path)); - const my_cwd = cwd(); - return Dir.updateFile(my_cwd, source_path, my_cwd, dest_path, args); -} - /// Same as `Dir.copyFile`, except asserts that both `source_path` and `dest_path` /// are absolute. See `Dir.copyFile` for a function that operates on both /// absolute and relative paths. @@ -131,6 +115,8 @@ pub fn copyFileAbsolute( return Dir.copyFile(my_cwd, source_path, my_cwd, dest_path, args); } +test copyFileAbsolute {} + /// Create a new directory, based on an absolute path. /// Asserts that the path is absolute. See `Dir.makeDir` for a function that operates /// on both absolute and relative paths. @@ -142,17 +128,15 @@ pub fn makeDirAbsolute(absolute_path: []const u8) !void { return posix.mkdir(absolute_path, Dir.default_mode); } +test makeDirAbsolute {} + /// Same as `makeDirAbsolute` except the parameter is null-terminated. pub fn makeDirAbsoluteZ(absolute_path_z: [*:0]const u8) !void { assert(path.isAbsoluteZ(absolute_path_z)); return posix.mkdirZ(absolute_path_z, Dir.default_mode); } -/// Same as `makeDirAbsolute` except the parameter is a null-terminated WTF-16 LE-encoded string. -pub fn makeDirAbsoluteW(absolute_path_w: [*:0]const u16) !void { - assert(path.isAbsoluteWindowsW(absolute_path_w)); - return posix.mkdirW(mem.span(absolute_path_w), Dir.default_mode); -} +test makeDirAbsoluteZ {} /// Same as `Dir.deleteDir` except the path is absolute. /// On Windows, `dir_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). @@ -169,12 +153,6 @@ pub fn deleteDirAbsoluteZ(dir_path: [*:0]const u8) !void { return posix.rmdirZ(dir_path); } -/// Same as `deleteDirAbsolute` except the path parameter is WTF-16 and target OS is assumed Windows. -pub fn deleteDirAbsoluteW(dir_path: [*:0]const u16) !void { - assert(path.isAbsoluteWindowsW(dir_path)); - return posix.rmdirW(mem.span(dir_path)); -} - /// Same as `Dir.rename` except the paths are absolute. /// On Windows, both paths should be encoded as [WTF-8](https://wtf-8.codeberg.page/). /// On WASI, both paths should be encoded as valid UTF-8. @@ -192,13 +170,6 @@ pub fn renameAbsoluteZ(old_path: [*:0]const u8, new_path: [*:0]const u8) !void { return posix.renameZ(old_path, new_path); } -/// Same as `renameAbsolute` except the path parameters are WTF-16 and target OS is assumed Windows. -pub fn renameAbsoluteW(old_path: [*:0]const u16, new_path: [*:0]const u16) !void { - assert(path.isAbsoluteWindowsW(old_path)); - assert(path.isAbsoluteWindowsW(new_path)); - return posix.renameW(old_path, new_path); -} - /// Same as `Dir.rename`, except `new_sub_path` is relative to `new_dir` pub fn rename(old_dir: Dir, old_sub_path: []const u8, new_dir: Dir, new_sub_path: []const u8) !void { return posix.renameat(old_dir.fd, old_sub_path, new_dir.fd, new_sub_path); @@ -209,15 +180,7 @@ pub fn renameZ(old_dir: Dir, old_sub_path_z: [*:0]const u8, new_dir: Dir, new_su return posix.renameatZ(old_dir.fd, old_sub_path_z, new_dir.fd, new_sub_path_z); } -/// Same as `rename` except the parameters are WTF16LE, NT prefixed. -/// This function is Windows-only. -pub fn renameW(old_dir: Dir, old_sub_path_w: []const u16, new_dir: Dir, new_sub_path_w: []const u16) !void { - return posix.renameatW(old_dir.fd, old_sub_path_w, new_dir.fd, new_sub_path_w, windows.TRUE); -} - -/// Returns a handle to the current working directory. It is not opened with iteration capability. -/// Closing the returned `Dir` is checked illegal behavior. Iterating over the result is illegal behavior. -/// On POSIX targets, this function is comptime-callable. +/// Deprecated in favor of `Io.Dir.cwd`. pub fn cwd() Dir { if (native_os == .windows) { return .{ .fd = windows.peb().ProcessParameters.CurrentDirectory.Handle }; @@ -251,12 +214,6 @@ pub fn openDirAbsoluteZ(absolute_path_c: [*:0]const u8, flags: Dir.OpenOptions) assert(path.isAbsoluteZ(absolute_path_c)); return cwd().openDirZ(absolute_path_c, flags); } -/// Same as `openDirAbsolute` but the path parameter is null-terminated. -pub fn openDirAbsoluteW(absolute_path_c: [*:0]const u16, flags: Dir.OpenOptions) File.OpenError!Dir { - assert(path.isAbsoluteWindowsW(absolute_path_c)); - return cwd().openDirW(absolute_path_c, flags); -} - /// Opens a file for reading or writing, without attempting to create a new file, based on an absolute path. /// Call `File.close` to release the resource. /// Asserts that the path is absolute. See `Dir.openFile` for a function that @@ -271,18 +228,6 @@ pub fn openFileAbsolute(absolute_path: []const u8, flags: File.OpenFlags) File.O return cwd().openFile(absolute_path, flags); } -/// Same as `openFileAbsolute` but the path parameter is null-terminated. -pub fn openFileAbsoluteZ(absolute_path_c: [*:0]const u8, flags: File.OpenFlags) File.OpenError!File { - assert(path.isAbsoluteZ(absolute_path_c)); - return cwd().openFileZ(absolute_path_c, flags); -} - -/// Same as `openFileAbsolute` but the path parameter is WTF-16-encoded. -pub fn openFileAbsoluteW(absolute_path_w: []const u16, flags: File.OpenFlags) File.OpenError!File { - assert(path.isAbsoluteWindowsWTF16(absolute_path_w)); - return cwd().openFileW(absolute_path_w, flags); -} - /// Test accessing `path`. /// Be careful of Time-Of-Check-Time-Of-Use race conditions when using this function. /// For example, instead of testing if a file exists and then opening it, just @@ -291,21 +236,10 @@ pub fn openFileAbsoluteW(absolute_path_w: []const u16, flags: File.OpenFlags) Fi /// On Windows, `absolute_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). /// On WASI, `absolute_path` should be encoded as valid UTF-8. /// On other platforms, `absolute_path` is an opaque sequence of bytes with no particular encoding. -pub fn accessAbsolute(absolute_path: []const u8, flags: File.OpenFlags) Dir.AccessError!void { +pub fn accessAbsolute(absolute_path: []const u8, flags: Io.Dir.AccessOptions) Dir.AccessError!void { assert(path.isAbsolute(absolute_path)); try cwd().access(absolute_path, flags); } -/// Same as `accessAbsolute` but the path parameter is null-terminated. -pub fn accessAbsoluteZ(absolute_path: [*:0]const u8, flags: File.OpenFlags) Dir.AccessError!void { - assert(path.isAbsoluteZ(absolute_path)); - try cwd().accessZ(absolute_path, flags); -} -/// Same as `accessAbsolute` but the path parameter is WTF-16 encoded. -pub fn accessAbsoluteW(absolute_path: [*:0]const u16, flags: File.OpenFlags) Dir.AccessError!void { - assert(path.isAbsoluteWindowsW(absolute_path)); - try cwd().accessW(absolute_path, flags); -} - /// Creates, opens, or overwrites a file with write access, based on an absolute path. /// Call `File.close` to release the resource. /// Asserts that the path is absolute. See `Dir.createFile` for a function that @@ -320,18 +254,6 @@ pub fn createFileAbsolute(absolute_path: []const u8, flags: File.CreateFlags) Fi return cwd().createFile(absolute_path, flags); } -/// Same as `createFileAbsolute` but the path parameter is null-terminated. -pub fn createFileAbsoluteZ(absolute_path_c: [*:0]const u8, flags: File.CreateFlags) File.OpenError!File { - assert(path.isAbsoluteZ(absolute_path_c)); - return cwd().createFileZ(absolute_path_c, flags); -} - -/// Same as `createFileAbsolute` but the path parameter is WTF-16 encoded. -pub fn createFileAbsoluteW(absolute_path_w: [*:0]const u16, flags: File.CreateFlags) File.OpenError!File { - assert(path.isAbsoluteWindowsW(absolute_path_w)); - return cwd().createFileW(mem.span(absolute_path_w), flags); -} - /// Delete a file name and possibly the file it refers to, based on an absolute path. /// Asserts that the path is absolute. See `Dir.deleteFile` for a function that /// operates on both absolute and relative paths. @@ -344,18 +266,6 @@ pub fn deleteFileAbsolute(absolute_path: []const u8) Dir.DeleteFileError!void { return cwd().deleteFile(absolute_path); } -/// Same as `deleteFileAbsolute` except the parameter is null-terminated. -pub fn deleteFileAbsoluteZ(absolute_path_c: [*:0]const u8) Dir.DeleteFileError!void { - assert(path.isAbsoluteZ(absolute_path_c)); - return cwd().deleteFileZ(absolute_path_c); -} - -/// Same as `deleteFileAbsolute` except the parameter is WTF-16 encoded. -pub fn deleteFileAbsoluteW(absolute_path_w: [*:0]const u16) Dir.DeleteFileError!void { - assert(path.isAbsoluteWindowsW(absolute_path_w)); - return cwd().deleteFileW(mem.span(absolute_path_w)); -} - /// Removes a symlink, file, or directory. /// This is equivalent to `Dir.deleteTree` with the base directory. /// Asserts that the path is absolute. See `Dir.deleteTree` for a function that @@ -387,19 +297,6 @@ pub fn readLinkAbsolute(pathname: []const u8, buffer: *[max_path_bytes]u8) ![]u8 return posix.readlink(pathname, buffer); } -/// Windows-only. Same as `readlinkW`, except the path parameter is null-terminated, WTF16 -/// encoded. -pub fn readlinkAbsoluteW(pathname_w: [*:0]const u16, buffer: *[max_path_bytes]u8) ![]u8 { - assert(path.isAbsoluteWindowsW(pathname_w)); - return posix.readlinkW(mem.span(pathname_w), buffer); -} - -/// Same as `readLink`, except the path parameter is null-terminated. -pub fn readLinkAbsoluteZ(pathname_c: [*:0]const u8, buffer: *[max_path_bytes]u8) ![]u8 { - assert(path.isAbsoluteZ(pathname_c)); - return posix.readlinkZ(pathname_c, buffer); -} - /// Creates a symbolic link named `sym_link_path` which contains the string `target_path`. /// A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent /// one; the latter case is known as a dangling link. @@ -437,44 +334,21 @@ pub fn symLinkAbsoluteW( return windows.CreateSymbolicLink(null, mem.span(sym_link_path_w), mem.span(target_path_w), flags.is_directory); } -/// Same as `symLinkAbsolute` except the parameters are null-terminated pointers. -/// See also `symLinkAbsolute`. -pub fn symLinkAbsoluteZ( - target_path_c: [*:0]const u8, - sym_link_path_c: [*:0]const u8, - flags: Dir.SymLinkFlags, -) !void { - assert(path.isAbsoluteZ(target_path_c)); - assert(path.isAbsoluteZ(sym_link_path_c)); - if (native_os == .windows) { - const target_path_w = try windows.cStrToPrefixedFileW(null, target_path_c); - const sym_link_path_w = try windows.cStrToPrefixedFileW(null, sym_link_path_c); - return windows.CreateSymbolicLink(null, sym_link_path_w.span(), target_path_w.span(), flags.is_directory); - } - return posix.symlinkZ(target_path_c, sym_link_path_c); -} - -pub const OpenSelfExeError = posix.OpenError || SelfExePathError || posix.FlockError; +pub const OpenSelfExeError = Io.File.OpenSelfExeError; +/// Deprecated in favor of `Io.File.openSelfExe`. pub fn openSelfExe(flags: File.OpenFlags) OpenSelfExeError!File { - if (native_os == .linux or native_os == .serenity) { - return openFileAbsoluteZ("/proc/self/exe", flags); - } - if (native_os == .windows) { - // If ImagePathName is a symlink, then it will contain the path of the symlink, - // not the path that the symlink points to. However, because we are opening - // the file, we can let the openFileW call follow the symlink for us. - const image_path_unicode_string = &windows.peb().ProcessParameters.ImagePathName; - const image_path_name = image_path_unicode_string.Buffer.?[0 .. image_path_unicode_string.Length / 2 :0]; - const prefixed_path_w = try windows.wToPrefixedFileW(null, image_path_name); - return cwd().openFileW(prefixed_path_w.span(), flags); + if (native_os == .linux or native_os == .serenity or native_os == .windows) { + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.ioBasic(); + return .adaptFromNewApi(try Io.File.openSelfExe(io, flags)); } // Use of max_path_bytes here is valid as the resulting path is immediately // opened with no modification. var buf: [max_path_bytes]u8 = undefined; const self_exe_path = try selfExePath(&buf); buf[self_exe_path.len] = 0; - return openFileAbsoluteZ(buf[0..self_exe_path.len :0].ptr, flags); + return openFileAbsolute(buf[0..self_exe_path.len :0], flags); } // This is `posix.ReadLinkError || posix.RealPathError` with impossible errors excluded @@ -515,6 +389,8 @@ pub const SelfExePathError = error{ /// On Windows, the volume does not contain a recognized file system. File /// system drivers might not be loaded, or the volume may be corrupt. UnrecognizedVolume, + + Canceled, } || posix.SysCtlError; /// `selfExePath` except allocates the result on the heap. @@ -554,7 +430,6 @@ pub fn selfExePath(out_buffer: []u8) SelfExePathError![]u8 { var real_path_buf: [max_path_bytes]u8 = undefined; const real_path = std.posix.realpathZ(&symlink_path_buf, &real_path_buf) catch |err| switch (err) { - error.InvalidWtf8 => unreachable, // Windows-only error.NetworkNotFound => unreachable, // Windows-only else => |e| return e, }; @@ -565,15 +440,11 @@ pub fn selfExePath(out_buffer: []u8) SelfExePathError![]u8 { } switch (native_os) { .linux, .serenity => return posix.readlinkZ("/proc/self/exe", out_buffer) catch |err| switch (err) { - error.InvalidUtf8 => unreachable, // WASI-only - error.InvalidWtf8 => unreachable, // Windows-only error.UnsupportedReparsePointType => unreachable, // Windows-only error.NetworkNotFound => unreachable, // Windows-only else => |e| return e, }, .illumos => return posix.readlinkZ("/proc/self/path/a.out", out_buffer) catch |err| switch (err) { - error.InvalidUtf8 => unreachable, // WASI-only - error.InvalidWtf8 => unreachable, // Windows-only error.UnsupportedReparsePointType => unreachable, // Windows-only error.NetworkNotFound => unreachable, // Windows-only else => |e| return e, @@ -602,7 +473,6 @@ pub fn selfExePath(out_buffer: []u8) SelfExePathError![]u8 { // argv[0] is a path (relative or absolute): use realpath(3) directly var real_path_buf: [max_path_bytes]u8 = undefined; const real_path = posix.realpathZ(std.os.argv[0], &real_path_buf) catch |err| switch (err) { - error.InvalidWtf8 => unreachable, // Windows-only error.NetworkNotFound => unreachable, // Windows-only else => |e| return e, }; @@ -645,10 +515,7 @@ pub fn selfExePath(out_buffer: []u8) SelfExePathError![]u8 { // that the symlink points to, though, so we need to get the realpath. var pathname_w = try windows.wToPrefixedFileW(null, image_path_name); - const wide_slice = std.fs.cwd().realpathW2(pathname_w.span(), &pathname_w.data) catch |err| switch (err) { - error.InvalidWtf8 => unreachable, - else => |e| return e, - }; + const wide_slice = try std.fs.cwd().realpathW2(pathname_w.span(), &pathname_w.data); const len = std.unicode.calcWtf8Len(wide_slice); if (len > out_buffer.len) @@ -702,16 +569,10 @@ pub fn realpathAlloc(allocator: Allocator, pathname: []const u8) ![]u8 { } test { - if (native_os != .wasi) { - _ = &makeDirAbsolute; - _ = &makeDirAbsoluteZ; - _ = ©FileAbsolute; - _ = &updateFileAbsolute; - } - _ = &AtomicFile; - _ = &Dir; - _ = &File; - _ = &path; + _ = AtomicFile; + _ = Dir; + _ = File; + _ = path; _ = @import("fs/test.zig"); _ = @import("fs/get_app_data_dir.zig"); } diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index 14b26c89bd..c90eeef508 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -1,6 +1,11 @@ +//! Deprecated in favor of `Io.Dir`. const Dir = @This(); + const builtin = @import("builtin"); +const native_os = builtin.os.tag; + const std = @import("../std.zig"); +const Io = std.Io; const File = std.fs.File; const AtomicFile = std.fs.AtomicFile; const base64_encoder = fs.base64_encoder; @@ -12,7 +17,6 @@ const Allocator = std.mem.Allocator; const assert = std.debug.assert; const linux = std.os.linux; const windows = std.os.windows; -const native_os = builtin.os.tag; const have_flock = @TypeOf(posix.system.flock) != void; fd: Handle, @@ -32,10 +36,6 @@ const IteratorError = error{ AccessDenied, PermissionDenied, SystemResources, - /// WASI-only. The path of an entry could not be encoded as valid UTF-8. - /// WASI is unable to handle paths that cannot be encoded as well-formed UTF-8. - /// https://github.com/WebAssembly/wasi-filesystem/issues/17#issuecomment-1430639353 - InvalidUtf8, } || posix.UnexpectedError; pub const Iterator = switch (native_os) { @@ -549,7 +549,6 @@ pub const Iterator = switch (native_os) { .INVAL => unreachable, .NOENT => return error.DirNotFound, // The directory being iterated was deleted during iteration. .NOTCAPABLE => return error.AccessDenied, - .ILSEQ => return error.InvalidUtf8, // An entry's name cannot be encoded as UTF-8. else => |err| return posix.unexpectedErrno(err), } if (bufused == 0) return null; @@ -840,517 +839,73 @@ pub fn walk(self: Dir, allocator: Allocator) Allocator.Error!Walker { }; } -pub const OpenError = error{ - FileNotFound, - NotDir, - AccessDenied, - PermissionDenied, - SymLinkLoop, - ProcessFdQuotaExceeded, - NameTooLong, - SystemFdQuotaExceeded, - NoDevice, - SystemResources, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - /// Windows-only; file paths provided by the user must be valid WTF-8. - /// https://wtf-8.codeberg.page/ - InvalidWtf8, - BadPathName, - DeviceBusy, - /// On Windows, `\\server` or `\\server\share` was not found. - NetworkNotFound, - ProcessNotFound, -} || posix.UnexpectedError; +pub const OpenError = Io.Dir.OpenError; pub fn close(self: *Dir) void { posix.close(self.fd); self.* = undefined; } -/// Opens a file for reading or writing, without attempting to create a new file. -/// To create a new file, see `createFile`. -/// Call `File.close` to release the resource. -/// Asserts that the path parameter has no null bytes. -/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `sub_path` should be encoded as valid UTF-8. -/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. +/// Deprecated in favor of `Io.Dir.openFile`. pub fn openFile(self: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { - if (native_os == .windows) { - const path_w = try windows.sliceToPrefixedFileW(self.fd, sub_path); - return self.openFileW(path_w.span(), flags); - } - if (native_os == .wasi and !builtin.link_libc) { - var base: std.os.wasi.rights_t = .{}; - // POLL_FD_READWRITE only grants extra rights if the corresponding FD_READ and/or FD_WRITE - // is also set. - if (flags.isRead()) { - base.FD_READ = true; - base.FD_TELL = true; - base.FD_SEEK = true; - base.FD_FILESTAT_GET = true; - base.POLL_FD_READWRITE = true; - } - if (flags.isWrite()) { - base.FD_WRITE = true; - base.FD_TELL = true; - base.FD_SEEK = true; - base.FD_DATASYNC = true; - base.FD_FDSTAT_SET_FLAGS = true; - base.FD_SYNC = true; - base.FD_ALLOCATE = true; - base.FD_ADVISE = true; - base.FD_FILESTAT_SET_TIMES = true; - base.FD_FILESTAT_SET_SIZE = true; - base.POLL_FD_READWRITE = true; - } - const fd = try posix.openatWasi(self.fd, sub_path, .{}, .{}, .{}, base, .{}); - return .{ .handle = fd }; - } - const path_c = try posix.toPosixPath(sub_path); - return self.openFileZ(&path_c, flags); + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.ioBasic(); + return .adaptFromNewApi(try Io.Dir.openFile(self.adaptToNewApi(), io, sub_path, flags)); } -/// Same as `openFile` but the path parameter is null-terminated. -pub fn openFileZ(self: Dir, sub_path: [*:0]const u8, flags: File.OpenFlags) File.OpenError!File { - switch (native_os) { - .windows => { - const path_w = try windows.cStrToPrefixedFileW(self.fd, sub_path); - return self.openFileW(path_w.span(), flags); - }, - // Use the libc API when libc is linked because it implements things - // such as opening absolute file paths. - .wasi => if (!builtin.link_libc) { - return openFile(self, mem.sliceTo(sub_path, 0), flags); - }, - else => {}, - } - - var os_flags: posix.O = switch (native_os) { - .wasi => .{ - .read = flags.mode != .write_only, - .write = flags.mode != .read_only, - }, - else => .{ - .ACCMODE = switch (flags.mode) { - .read_only => .RDONLY, - .write_only => .WRONLY, - .read_write => .RDWR, - }, - }, - }; - if (@hasField(posix.O, "CLOEXEC")) os_flags.CLOEXEC = true; - if (@hasField(posix.O, "LARGEFILE")) os_flags.LARGEFILE = true; - if (@hasField(posix.O, "NOCTTY")) os_flags.NOCTTY = !flags.allow_ctty; - - // Use the O locking flags if the os supports them to acquire the lock - // atomically. - const has_flock_open_flags = @hasField(posix.O, "EXLOCK"); - if (has_flock_open_flags) { - // Note that the NONBLOCK flag is removed after the openat() call - // is successful. - switch (flags.lock) { - .none => {}, - .shared => { - os_flags.SHLOCK = true; - os_flags.NONBLOCK = flags.lock_nonblocking; - }, - .exclusive => { - os_flags.EXLOCK = true; - os_flags.NONBLOCK = flags.lock_nonblocking; - }, - } - } - const fd = try posix.openatZ(self.fd, sub_path, os_flags, 0); - errdefer posix.close(fd); - - if (have_flock and !has_flock_open_flags and flags.lock != .none) { - // TODO: integrate async I/O - const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0; - try posix.flock(fd, switch (flags.lock) { - .none => unreachable, - .shared => posix.LOCK.SH | lock_nonblocking, - .exclusive => posix.LOCK.EX | lock_nonblocking, - }); - } - - if (has_flock_open_flags and flags.lock_nonblocking) { - var fl_flags = posix.fcntl(fd, posix.F.GETFL, 0) catch |err| switch (err) { - error.FileBusy => unreachable, - error.Locked => unreachable, - error.PermissionDenied => unreachable, - error.DeadLock => unreachable, - error.LockedRegionLimitExceeded => unreachable, - else => |e| return e, - }; - fl_flags &= ~@as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK")); - _ = posix.fcntl(fd, posix.F.SETFL, fl_flags) catch |err| switch (err) { - error.FileBusy => unreachable, - error.Locked => unreachable, - error.PermissionDenied => unreachable, - error.DeadLock => unreachable, - error.LockedRegionLimitExceeded => unreachable, - else => |e| return e, - }; - } - - return .{ .handle = fd }; -} - -/// Same as `openFile` but Windows-only and the path parameter is -/// [WTF-16](https://wtf-8.codeberg.page/#potentially-ill-formed-utf-16) encoded. -pub fn openFileW(self: Dir, sub_path_w: []const u16, flags: File.OpenFlags) File.OpenError!File { - const w = windows; - const file: File = .{ - .handle = try w.OpenFile(sub_path_w, .{ - .dir = self.fd, - .access_mask = w.SYNCHRONIZE | - (if (flags.isRead()) @as(u32, w.GENERIC_READ) else 0) | - (if (flags.isWrite()) @as(u32, w.GENERIC_WRITE) else 0), - .creation = w.FILE_OPEN, - }), - }; - errdefer file.close(); - var io: w.IO_STATUS_BLOCK = undefined; - const range_off: w.LARGE_INTEGER = 0; - const range_len: w.LARGE_INTEGER = 1; - const exclusive = switch (flags.lock) { - .none => return file, - .shared => false, - .exclusive => true, - }; - try w.LockFile( - file.handle, - null, - null, - null, - &io, - &range_off, - &range_len, - null, - @intFromBool(flags.lock_nonblocking), - @intFromBool(exclusive), - ); - return file; -} - -/// Creates, opens, or overwrites a file with write access. -/// Call `File.close` on the result when done. -/// Asserts that the path parameter has no null bytes. -/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `sub_path` should be encoded as valid UTF-8. -/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. +/// Deprecated in favor of `Io.Dir.createFile`. pub fn createFile(self: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File { - if (native_os == .windows) { - const path_w = try windows.sliceToPrefixedFileW(self.fd, sub_path); - return self.createFileW(path_w.span(), flags); - } - if (native_os == .wasi) { - return .{ - .handle = try posix.openatWasi(self.fd, sub_path, .{}, .{ - .CREAT = true, - .TRUNC = flags.truncate, - .EXCL = flags.exclusive, - }, .{}, .{ - .FD_READ = flags.read, - .FD_WRITE = true, - .FD_DATASYNC = true, - .FD_SEEK = true, - .FD_TELL = true, - .FD_FDSTAT_SET_FLAGS = true, - .FD_SYNC = true, - .FD_ALLOCATE = true, - .FD_ADVISE = true, - .FD_FILESTAT_SET_TIMES = true, - .FD_FILESTAT_SET_SIZE = true, - .FD_FILESTAT_GET = true, - // POLL_FD_READWRITE only grants extra rights if the corresponding FD_READ and/or - // FD_WRITE is also set. - .POLL_FD_READWRITE = true, - }, .{}), - }; - } - const path_c = try posix.toPosixPath(sub_path); - return self.createFileZ(&path_c, flags); + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.ioBasic(); + const new_file = try Io.Dir.createFile(self.adaptToNewApi(), io, sub_path, flags); + return .adaptFromNewApi(new_file); } -/// Same as `createFile` but the path parameter is null-terminated. -pub fn createFileZ(self: Dir, sub_path_c: [*:0]const u8, flags: File.CreateFlags) File.OpenError!File { - switch (native_os) { - .windows => { - const path_w = try windows.cStrToPrefixedFileW(self.fd, sub_path_c); - return self.createFileW(path_w.span(), flags); - }, - .wasi => { - return createFile(self, mem.sliceTo(sub_path_c, 0), flags); - }, - else => {}, - } +/// Deprecated in favor of `Io.Dir.MakeError`. +pub const MakeError = Io.Dir.MakeError; - var os_flags: posix.O = .{ - .ACCMODE = if (flags.read) .RDWR else .WRONLY, - .CREAT = true, - .TRUNC = flags.truncate, - .EXCL = flags.exclusive, - }; - if (@hasField(posix.O, "LARGEFILE")) os_flags.LARGEFILE = true; - if (@hasField(posix.O, "CLOEXEC")) os_flags.CLOEXEC = true; - - // Use the O locking flags if the os supports them to acquire the lock - // atomically. Note that the NONBLOCK flag is removed after the openat() - // call is successful. - const has_flock_open_flags = @hasField(posix.O, "EXLOCK"); - if (has_flock_open_flags) switch (flags.lock) { - .none => {}, - .shared => { - os_flags.SHLOCK = true; - os_flags.NONBLOCK = flags.lock_nonblocking; - }, - .exclusive => { - os_flags.EXLOCK = true; - os_flags.NONBLOCK = flags.lock_nonblocking; - }, - }; - - const fd = try posix.openatZ(self.fd, sub_path_c, os_flags, flags.mode); - errdefer posix.close(fd); - - if (have_flock and !has_flock_open_flags and flags.lock != .none) { - // TODO: integrate async I/O - const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0; - try posix.flock(fd, switch (flags.lock) { - .none => unreachable, - .shared => posix.LOCK.SH | lock_nonblocking, - .exclusive => posix.LOCK.EX | lock_nonblocking, - }); - } - - if (has_flock_open_flags and flags.lock_nonblocking) { - var fl_flags = posix.fcntl(fd, posix.F.GETFL, 0) catch |err| switch (err) { - error.FileBusy => unreachable, - error.Locked => unreachable, - error.PermissionDenied => unreachable, - error.DeadLock => unreachable, - error.LockedRegionLimitExceeded => unreachable, - else => |e| return e, - }; - fl_flags &= ~@as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK")); - _ = posix.fcntl(fd, posix.F.SETFL, fl_flags) catch |err| switch (err) { - error.FileBusy => unreachable, - error.Locked => unreachable, - error.PermissionDenied => unreachable, - error.DeadLock => unreachable, - error.LockedRegionLimitExceeded => unreachable, - else => |e| return e, - }; - } - - return .{ .handle = fd }; -} - -/// Same as `createFile` but Windows-only and the path parameter is -/// [WTF-16](https://wtf-8.codeberg.page/#potentially-ill-formed-utf-16) encoded. -pub fn createFileW(self: Dir, sub_path_w: []const u16, flags: File.CreateFlags) File.OpenError!File { - const w = windows; - const read_flag = if (flags.read) @as(u32, w.GENERIC_READ) else 0; - const file: File = .{ - .handle = try w.OpenFile(sub_path_w, .{ - .dir = self.fd, - .access_mask = w.SYNCHRONIZE | w.GENERIC_WRITE | read_flag, - .creation = if (flags.exclusive) - @as(u32, w.FILE_CREATE) - else if (flags.truncate) - @as(u32, w.FILE_OVERWRITE_IF) - else - @as(u32, w.FILE_OPEN_IF), - }), - }; - errdefer file.close(); - var io: w.IO_STATUS_BLOCK = undefined; - const range_off: w.LARGE_INTEGER = 0; - const range_len: w.LARGE_INTEGER = 1; - const exclusive = switch (flags.lock) { - .none => return file, - .shared => false, - .exclusive => true, - }; - try w.LockFile( - file.handle, - null, - null, - null, - &io, - &range_off, - &range_len, - null, - @intFromBool(flags.lock_nonblocking), - @intFromBool(exclusive), - ); - return file; -} - -pub const MakeError = posix.MakeDirError; - -/// Creates a single directory with a relative or absolute path. -/// To create multiple directories to make an entire path, see `makePath`. -/// To operate on only absolute paths, see `makeDirAbsolute`. -/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `sub_path` should be encoded as valid UTF-8. -/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. +/// Deprecated in favor of `Io.Dir.makeDir`. pub fn makeDir(self: Dir, sub_path: []const u8) MakeError!void { - try posix.mkdirat(self.fd, sub_path, default_mode); + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.ioBasic(); + return Io.Dir.makeDir(.{ .handle = self.fd }, io, sub_path); } -/// Same as `makeDir`, but `sub_path` is null-terminated. -/// To create multiple directories to make an entire path, see `makePath`. -/// To operate on only absolute paths, see `makeDirAbsoluteZ`. +/// Deprecated in favor of `Io.Dir.makeDir`. pub fn makeDirZ(self: Dir, sub_path: [*:0]const u8) MakeError!void { try posix.mkdiratZ(self.fd, sub_path, default_mode); } -/// Creates a single directory with a relative or absolute null-terminated WTF-16 LE-encoded path. -/// To create multiple directories to make an entire path, see `makePath`. -/// To operate on only absolute paths, see `makeDirAbsoluteW`. +/// Deprecated in favor of `Io.Dir.makeDir`. pub fn makeDirW(self: Dir, sub_path: [*:0]const u16) MakeError!void { try posix.mkdiratW(self.fd, mem.span(sub_path), default_mode); } -/// Calls makeDir iteratively to make an entire path -/// (i.e. creating any parent directories that do not exist). -/// Returns success if the path already exists and is a directory. -/// This function is not atomic, and if it returns an error, the file system may -/// have been modified regardless. -/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `sub_path` should be encoded as valid UTF-8. -/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. -/// Fails on an empty path with `error.BadPathName` as that is not a path that can be created. -/// -/// Paths containing `..` components are handled differently depending on the platform: -/// - On Windows, `..` are resolved before the path is passed to NtCreateFile, meaning -/// a `sub_path` like "first/../second" will resolve to "second" and only a -/// `./second` directory will be created. -/// - On other platforms, `..` are not resolved before the path is passed to `mkdirat`, -/// meaning a `sub_path` like "first/../second" will create both a `./first` -/// and a `./second` directory. -pub fn makePath(self: Dir, sub_path: []const u8) (MakeError || StatFileError)!void { +/// Deprecated in favor of `Io.Dir.makePath`. +pub fn makePath(self: Dir, sub_path: []const u8) MakePathError!void { _ = try self.makePathStatus(sub_path); } -pub const MakePathStatus = enum { existed, created }; -/// Same as `makePath` except returns whether the path already existed or was successfully created. -pub fn makePathStatus(self: Dir, sub_path: []const u8) (MakeError || StatFileError)!MakePathStatus { - var it = try fs.path.componentIterator(sub_path); - var status: MakePathStatus = .existed; - var component = it.last() orelse return error.BadPathName; - while (true) { - if (self.makeDir(component.path)) |_| { - status = .created; - } else |err| switch (err) { - error.PathAlreadyExists => { - // stat the file and return an error if it's not a directory - // this is important because otherwise a dangling symlink - // could cause an infinite loop - check_dir: { - // workaround for windows, see https://github.com/ziglang/zig/issues/16738 - const fstat = self.statFile(component.path) catch |stat_err| switch (stat_err) { - error.IsDir => break :check_dir, - else => |e| return e, - }; - if (fstat.kind != .directory) return error.NotDir; - } - }, - error.FileNotFound => |e| { - component = it.previous() orelse return e; - continue; - }, - else => |e| return e, - } - component = it.next() orelse return status; - } +/// Deprecated in favor of `Io.Dir.MakePathStatus`. +pub const MakePathStatus = Io.Dir.MakePathStatus; +/// Deprecated in favor of `Io.Dir.MakePathError`. +pub const MakePathError = Io.Dir.MakePathError; + +/// Deprecated in favor of `Io.Dir.makePathStatus`. +pub fn makePathStatus(self: Dir, sub_path: []const u8) MakePathError!MakePathStatus { + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.ioBasic(); + return Io.Dir.makePathStatus(.{ .handle = self.fd }, io, sub_path); } -/// Windows only. Calls makeOpenDirAccessMaskW iteratively to make an entire path -/// (i.e. creating any parent directories that do not exist). -/// Opens the dir if the path already exists and is a directory. -/// This function is not atomic, and if it returns an error, the file system may -/// have been modified regardless. -/// `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -fn makeOpenPathAccessMaskW(self: Dir, sub_path: []const u8, access_mask: u32, no_follow: bool) (MakeError || OpenError || StatFileError)!Dir { - const w = windows; - var it = try fs.path.componentIterator(sub_path); - // If there are no components in the path, then create a dummy component with the full path. - var component = it.last() orelse fs.path.NativeComponentIterator.Component{ - .name = "", - .path = sub_path, - }; - - while (true) { - const sub_path_w = try w.sliceToPrefixedFileW(self.fd, component.path); - const is_last = it.peekNext() == null; - var result = self.makeOpenDirAccessMaskW(sub_path_w.span().ptr, access_mask, .{ - .no_follow = no_follow, - .create_disposition = if (is_last) w.FILE_OPEN_IF else w.FILE_CREATE, - }) catch |err| switch (err) { - error.FileNotFound => |e| { - component = it.previous() orelse return e; - continue; - }, - error.PathAlreadyExists => result: { - assert(!is_last); - // stat the file and return an error if it's not a directory - // this is important because otherwise a dangling symlink - // could cause an infinite loop - check_dir: { - // workaround for windows, see https://github.com/ziglang/zig/issues/16738 - const fstat = self.statFile(component.path) catch |stat_err| switch (stat_err) { - error.IsDir => break :check_dir, - else => |e| return e, - }; - if (fstat.kind != .directory) return error.NotDir; - } - break :result null; - }, - else => |e| return e, - }; - - component = it.next() orelse return result.?; - - // Don't leak the intermediate file handles - if (result) |*dir| { - dir.close(); - } - } +/// Deprecated in favor of `Io.Dir.makeOpenPath`. +pub fn makeOpenPath(dir: Dir, sub_path: []const u8, options: OpenOptions) Io.Dir.MakeOpenPathError!Dir { + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.ioBasic(); + return .adaptFromNewApi(try Io.Dir.makeOpenPath(dir.adaptToNewApi(), io, sub_path, options)); } -/// This function performs `makePath`, followed by `openDir`. -/// If supported by the OS, this operation is atomic. It is not atomic on -/// all operating systems. -/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `sub_path` should be encoded as valid UTF-8. -/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. -pub fn makeOpenPath(self: Dir, sub_path: []const u8, open_dir_options: OpenOptions) (MakeError || OpenError || StatFileError)!Dir { - return switch (native_os) { - .windows => { - const w = windows; - const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | - w.SYNCHRONIZE | w.FILE_TRAVERSE | - (if (open_dir_options.iterate) w.FILE_LIST_DIRECTORY else @as(u32, 0)); - - return self.makeOpenPathAccessMaskW(sub_path, base_flags, open_dir_options.no_follow); - }, - else => { - return self.openDir(sub_path, open_dir_options) catch |err| switch (err) { - error.FileNotFound => { - try self.makePath(sub_path); - return self.openDir(sub_path, open_dir_options); - }, - else => |e| return e, - }; - }, - }; -} - -pub const RealPathError = posix.RealPathError; +pub const RealPathError = posix.RealPathError || error{Canceled}; /// This function returns the canonicalized absolute pathname of /// `pathname` relative to this `Dir`. If `pathname` is absolute, ignores this @@ -1408,7 +963,6 @@ pub fn realpathZ(self: Dir, pathname: [*:0]const u8, out_buffer: []u8) RealPathE error.FileLocksNotSupported => return error.Unexpected, error.FileBusy => return error.Unexpected, error.WouldBlock => return error.Unexpected, - error.InvalidUtf8 => unreachable, // WASI-only else => |e| return e, }; defer posix.close(fd); @@ -1510,234 +1064,14 @@ pub fn setAsCwd(self: Dir) !void { try posix.fchdir(self.fd); } -pub const OpenOptions = struct { - /// `true` means the opened directory can be used as the `Dir` parameter - /// for functions which operate based on an open directory handle. When `false`, - /// such operations are Illegal Behavior. - access_sub_paths: bool = true, +/// Deprecated in favor of `Io.Dir.OpenOptions`. +pub const OpenOptions = Io.Dir.OpenOptions; - /// `true` means the opened directory can be scanned for the files and sub-directories - /// of the result. It means the `iterate` function can be called. - iterate: bool = false, - - /// `true` means it won't dereference the symlinks. - no_follow: bool = false, -}; - -/// Opens a directory at the given path. The directory is a system resource that remains -/// open until `close` is called on the result. -/// The directory cannot be iterated unless the `iterate` option is set to `true`. -/// -/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `sub_path` should be encoded as valid UTF-8. -/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. -/// Asserts that the path parameter has no null bytes. +/// Deprecated in favor of `Io.Dir.openDir`. pub fn openDir(self: Dir, sub_path: []const u8, args: OpenOptions) OpenError!Dir { - switch (native_os) { - .windows => { - const sub_path_w = try windows.sliceToPrefixedFileW(self.fd, sub_path); - return self.openDirW(sub_path_w.span().ptr, args); - }, - .wasi => if (!builtin.link_libc) { - var base: std.os.wasi.rights_t = .{ - .FD_FILESTAT_GET = true, - .FD_FDSTAT_SET_FLAGS = true, - .FD_FILESTAT_SET_TIMES = true, - }; - if (args.access_sub_paths) { - base.FD_READDIR = true; - base.PATH_CREATE_DIRECTORY = true; - base.PATH_CREATE_FILE = true; - base.PATH_LINK_SOURCE = true; - base.PATH_LINK_TARGET = true; - base.PATH_OPEN = true; - base.PATH_READLINK = true; - base.PATH_RENAME_SOURCE = true; - base.PATH_RENAME_TARGET = true; - base.PATH_FILESTAT_GET = true; - base.PATH_FILESTAT_SET_SIZE = true; - base.PATH_FILESTAT_SET_TIMES = true; - base.PATH_SYMLINK = true; - base.PATH_REMOVE_DIRECTORY = true; - base.PATH_UNLINK_FILE = true; - } - - const result = posix.openatWasi( - self.fd, - sub_path, - .{ .SYMLINK_FOLLOW = !args.no_follow }, - .{ .DIRECTORY = true }, - .{}, - base, - base, - ); - const fd = result catch |err| switch (err) { - error.FileTooBig => unreachable, // can't happen for directories - error.IsDir => unreachable, // we're setting DIRECTORY - error.NoSpaceLeft => unreachable, // not setting CREAT - error.PathAlreadyExists => unreachable, // not setting CREAT - error.FileLocksNotSupported => unreachable, // locking folders is not supported - error.WouldBlock => unreachable, // can't happen for directories - error.FileBusy => unreachable, // can't happen for directories - else => |e| return e, - }; - return .{ .fd = fd }; - }, - else => {}, - } - const sub_path_c = try posix.toPosixPath(sub_path); - return self.openDirZ(&sub_path_c, args); -} - -/// Same as `openDir` except the parameter is null-terminated. -pub fn openDirZ(self: Dir, sub_path_c: [*:0]const u8, args: OpenOptions) OpenError!Dir { - switch (native_os) { - .windows => { - const sub_path_w = try windows.cStrToPrefixedFileW(self.fd, sub_path_c); - return self.openDirW(sub_path_w.span().ptr, args); - }, - // Use the libc API when libc is linked because it implements things - // such as opening absolute directory paths. - .wasi => if (!builtin.link_libc) { - return openDir(self, mem.sliceTo(sub_path_c, 0), args); - }, - .haiku => { - const rc = posix.system._kern_open_dir(self.fd, sub_path_c); - if (rc >= 0) return .{ .fd = rc }; - switch (@as(posix.E, @enumFromInt(rc))) { - .FAULT => unreachable, - .INVAL => unreachable, - .BADF => unreachable, - .ACCES => return error.AccessDenied, - .LOOP => return error.SymLinkLoop, - .MFILE => return error.ProcessFdQuotaExceeded, - .NAMETOOLONG => return error.NameTooLong, - .NFILE => return error.SystemFdQuotaExceeded, - .NODEV => return error.NoDevice, - .NOENT => return error.FileNotFound, - .NOMEM => return error.SystemResources, - .NOTDIR => return error.NotDir, - .PERM => return error.PermissionDenied, - .BUSY => return error.DeviceBusy, - else => |err| return posix.unexpectedErrno(err), - } - }, - else => {}, - } - - var symlink_flags: posix.O = switch (native_os) { - .wasi => .{ - .read = true, - .NOFOLLOW = args.no_follow, - .DIRECTORY = true, - }, - else => .{ - .ACCMODE = .RDONLY, - .NOFOLLOW = args.no_follow, - .DIRECTORY = true, - .CLOEXEC = true, - }, - }; - - if (@hasField(posix.O, "PATH") and !args.iterate) - symlink_flags.PATH = true; - - return self.openDirFlagsZ(sub_path_c, symlink_flags); -} - -/// Same as `openDir` except the path parameter is WTF-16 LE encoded, NT-prefixed. -/// This function asserts the target OS is Windows. -pub fn openDirW(self: Dir, sub_path_w: [*:0]const u16, args: OpenOptions) OpenError!Dir { - const w = windows; - // TODO remove some of these flags if args.access_sub_paths is false - const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | - w.SYNCHRONIZE | w.FILE_TRAVERSE; - const flags: u32 = if (args.iterate) base_flags | w.FILE_LIST_DIRECTORY else base_flags; - const dir = self.makeOpenDirAccessMaskW(sub_path_w, flags, .{ - .no_follow = args.no_follow, - .create_disposition = w.FILE_OPEN, - }) catch |err| switch (err) { - error.ReadOnlyFileSystem => unreachable, - error.DiskQuota => unreachable, - error.NoSpaceLeft => unreachable, - error.PathAlreadyExists => unreachable, - error.LinkQuotaExceeded => unreachable, - else => |e| return e, - }; - return dir; -} - -/// Asserts `flags` has `DIRECTORY` set. -fn openDirFlagsZ(self: Dir, sub_path_c: [*:0]const u8, flags: posix.O) OpenError!Dir { - assert(flags.DIRECTORY); - const fd = posix.openatZ(self.fd, sub_path_c, flags, 0) catch |err| switch (err) { - error.FileTooBig => unreachable, // can't happen for directories - error.IsDir => unreachable, // we're setting DIRECTORY - error.NoSpaceLeft => unreachable, // not setting CREAT - error.PathAlreadyExists => unreachable, // not setting CREAT - error.FileLocksNotSupported => unreachable, // locking folders is not supported - error.WouldBlock => unreachable, // can't happen for directories - error.FileBusy => unreachable, // can't happen for directories - else => |e| return e, - }; - return Dir{ .fd = fd }; -} - -const MakeOpenDirAccessMaskWOptions = struct { - no_follow: bool, - create_disposition: u32, -}; - -fn makeOpenDirAccessMaskW(self: Dir, sub_path_w: [*:0]const u16, access_mask: u32, flags: MakeOpenDirAccessMaskWOptions) (MakeError || OpenError)!Dir { - const w = windows; - - var result = Dir{ - .fd = undefined, - }; - - const path_len_bytes = @as(u16, @intCast(mem.sliceTo(sub_path_w, 0).len * 2)); - var nt_name = w.UNICODE_STRING{ - .Length = path_len_bytes, - .MaximumLength = path_len_bytes, - .Buffer = @constCast(sub_path_w), - }; - var attr = w.OBJECT_ATTRIBUTES{ - .Length = @sizeOf(w.OBJECT_ATTRIBUTES), - .RootDirectory = if (fs.path.isAbsoluteWindowsW(sub_path_w)) null else self.fd, - .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. - .ObjectName = &nt_name, - .SecurityDescriptor = null, - .SecurityQualityOfService = null, - }; - const open_reparse_point: w.DWORD = if (flags.no_follow) w.FILE_OPEN_REPARSE_POINT else 0x0; - var io: w.IO_STATUS_BLOCK = undefined; - const rc = w.ntdll.NtCreateFile( - &result.fd, - access_mask, - &attr, - &io, - null, - w.FILE_ATTRIBUTE_NORMAL, - w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE, - flags.create_disposition, - w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point, - null, - 0, - ); - - switch (rc) { - .SUCCESS => return result, - .OBJECT_NAME_INVALID => return error.BadPathName, - .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, - .OBJECT_NAME_COLLISION => return error.PathAlreadyExists, - .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, - .NOT_A_DIRECTORY => return error.NotDir, - // This can happen if the directory has 'List folder contents' permission set to 'Deny' - // and the directory is trying to be opened for iteration. - .ACCESS_DENIED => return error.AccessDenied, - .INVALID_PARAMETER => unreachable, - else => return w.unexpectedStatus(rc), - } + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.ioBasic(); + return .adaptFromNewApi(try Io.Dir.openDir(.{ .handle = self.fd }, io, sub_path, args)); } pub const DeleteFileError = posix.UnlinkError; @@ -1801,11 +1135,9 @@ pub const DeleteDirError = error{ NotDir, SystemResources, ReadOnlyFileSystem, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - /// Windows-only; file paths provided by the user must be valid WTF-8. + /// WASI: file paths must be valid UTF-8. + /// Windows: file paths provided by the user must be valid WTF-8. /// https://wtf-8.codeberg.page/ - InvalidWtf8, BadPathName, /// On Windows, `\\server` or `\\server\share` was not found. NetworkNotFound, @@ -1906,10 +1238,7 @@ pub fn symLink( // when converting to an NT namespaced path. CreateSymbolicLink in // symLinkW will handle the necessary conversion. var target_path_w: windows.PathSpace = undefined; - if (try std.unicode.checkWtf8ToWtf16LeOverflow(target_path, &target_path_w.data)) { - return error.NameTooLong; - } - target_path_w.len = try std.unicode.wtf8ToWtf16Le(&target_path_w.data, target_path); + target_path_w.len = try windows.wtf8ToWtf16Le(&target_path_w.data, target_path); target_path_w.data[target_path_w.len] = 0; // However, we need to canonicalize any path separators to `\`, since if // the target path is relative, then it must use `\` as the path separator. @@ -2052,20 +1381,11 @@ pub fn readLinkW(self: Dir, sub_path_w: []const u16, buffer: []u8) ![]u8 { return windows.ReadLink(self.fd, sub_path_w, buffer); } -/// Read all of file contents using a preallocated buffer. -/// The returned slice has the same pointer as `buffer`. If the length matches `buffer.len` -/// the situation is ambiguous. It could either mean that the entire file was read, and -/// it exactly fits the buffer, or it could mean the buffer was not big enough for the -/// entire file. -/// On Windows, `file_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `file_path` should be encoded as valid UTF-8. -/// On other platforms, `file_path` is an opaque sequence of bytes with no particular encoding. +/// Deprecated in favor of `Io.Dir.readFile`. pub fn readFile(self: Dir, file_path: []const u8, buffer: []u8) ![]u8 { - var file = try self.openFile(file_path, .{}); - defer file.close(); - - const end_index = try file.readAll(buffer); - return buffer[0..end_index]; + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.ioBasic(); + return Io.Dir.readFile(.{ .handle = self.fd }, io, file_path, buffer); } pub const ReadFileAllocError = File.OpenError || File.ReadError || Allocator.Error || error{ @@ -2091,7 +1411,7 @@ pub fn readFileAlloc( /// Used to allocate the result. gpa: Allocator, /// If reached or exceeded, `error.StreamTooLong` is returned instead. - limit: std.Io.Limit, + limit: Io.Limit, ) ReadFileAllocError![]u8 { return readFileAllocOptions(dir, sub_path, gpa, limit, .of(u8), null); } @@ -2101,6 +1421,8 @@ pub fn readFileAlloc( /// /// If the file size is already known, a better alternative is to initialize a /// `File.Reader`. +/// +/// TODO move this function to Io.Dir pub fn readFileAllocOptions( dir: Dir, /// On Windows, should be encoded as [WTF-8](https://wtf-8.codeberg.page/). @@ -2110,13 +1432,16 @@ pub fn readFileAllocOptions( /// Used to allocate the result. gpa: Allocator, /// If reached or exceeded, `error.StreamTooLong` is returned instead. - limit: std.Io.Limit, + limit: Io.Limit, comptime alignment: std.mem.Alignment, comptime sentinel: ?u8, ) ReadFileAllocError!(if (sentinel) |s| [:s]align(alignment.toByteUnits()) u8 else []align(alignment.toByteUnits()) u8) { + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.ioBasic(); + var file = try dir.openFile(sub_path, .{}); defer file.close(); - var file_reader = file.reader(&.{}); + var file_reader = file.reader(io, &.{}); return file_reader.interface.allocRemainingAlignedSentinel(gpa, limit, alignment, sentinel) catch |err| switch (err) { error.ReadFailed => return file_reader.err.?, error.OutOfMemory, error.StreamTooLong => |e| return e, @@ -2138,24 +1463,19 @@ pub const DeleteTreeError = error{ FileBusy, DeviceBusy, ProcessNotFound, - /// One of the path components was not a directory. /// This error is unreachable if `sub_path` does not contain a path separator. NotDir, - - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - - /// Windows-only; file paths provided by the user must be valid WTF-8. + /// WASI: file paths must be valid UTF-8. + /// Windows: file paths provided by the user must be valid WTF-8. /// https://wtf-8.codeberg.page/ - InvalidWtf8, - /// On Windows, file paths cannot contain these characters: /// '/', '*', '?', '"', '<', '>', '|' BadPathName, - /// On Windows, `\\server` or `\\server\share` was not found. NetworkNotFound, + + Canceled, } || posix.UnexpectedError; /// Whether `sub_path` describes a symlink, file, or directory, this function @@ -2196,7 +1516,7 @@ pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void { if (treat_as_dir) { if (stack.unusedCapacitySlice().len >= 1) { var iterable_dir = top.iter.dir.openDir(entry.name, .{ - .no_follow = true, + .follow_symlinks = false, .iterate = true, }) catch |err| switch (err) { error.NotDir => { @@ -2212,17 +1532,15 @@ pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void { error.PermissionDenied, error.SymLinkLoop, error.ProcessFdQuotaExceeded, - error.ProcessNotFound, error.NameTooLong, error.SystemFdQuotaExceeded, error.NoDevice, error.SystemResources, error.Unexpected, - error.InvalidUtf8, - error.InvalidWtf8, error.BadPathName, error.NetworkNotFound, error.DeviceBusy, + error.Canceled, => |e| return e, }; stack.appendAssumeCapacity(.{ @@ -2251,8 +1569,6 @@ pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void { error.AccessDenied, error.PermissionDenied, - error.InvalidUtf8, - error.InvalidWtf8, error.SymLinkLoop, error.NameTooLong, error.SystemResources, @@ -2294,7 +1610,7 @@ pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void { handle_entry: while (true) { if (treat_as_dir) { break :iterable_dir parent_dir.openDir(name, .{ - .no_follow = true, + .follow_symlinks = false, .iterate = true, }) catch |err| switch (err) { error.NotDir => { @@ -2309,18 +1625,16 @@ pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void { error.AccessDenied, error.PermissionDenied, error.SymLinkLoop, - error.ProcessNotFound, error.ProcessFdQuotaExceeded, error.NameTooLong, error.SystemFdQuotaExceeded, error.NoDevice, error.SystemResources, error.Unexpected, - error.InvalidUtf8, - error.InvalidWtf8, error.BadPathName, error.NetworkNotFound, error.DeviceBusy, + error.Canceled, => |e| return e, }; } else { @@ -2339,8 +1653,6 @@ pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void { error.AccessDenied, error.PermissionDenied, - error.InvalidUtf8, - error.InvalidWtf8, error.SymLinkLoop, error.NameTooLong, error.SystemResources, @@ -2402,7 +1714,7 @@ fn deleteTreeMinStackSizeWithKindHint(self: Dir, sub_path: []const u8, kind_hint handle_entry: while (true) { if (treat_as_dir) { const new_dir = dir.openDir(entry.name, .{ - .no_follow = true, + .follow_symlinks = false, .iterate = true, }) catch |err| switch (err) { error.NotDir => { @@ -2417,18 +1729,16 @@ fn deleteTreeMinStackSizeWithKindHint(self: Dir, sub_path: []const u8, kind_hint error.AccessDenied, error.PermissionDenied, error.SymLinkLoop, - error.ProcessNotFound, error.ProcessFdQuotaExceeded, error.NameTooLong, error.SystemFdQuotaExceeded, error.NoDevice, error.SystemResources, error.Unexpected, - error.InvalidUtf8, - error.InvalidWtf8, error.BadPathName, error.NetworkNotFound, error.DeviceBusy, + error.Canceled, => |e| return e, }; if (cleanup_dir_parent) |*d| d.close(); @@ -2454,8 +1764,6 @@ fn deleteTreeMinStackSizeWithKindHint(self: Dir, sub_path: []const u8, kind_hint error.AccessDenied, error.PermissionDenied, - error.InvalidUtf8, - error.InvalidWtf8, error.SymLinkLoop, error.NameTooLong, error.SystemResources, @@ -2503,7 +1811,7 @@ fn deleteTreeOpenInitialSubpath(self: Dir, sub_path: []const u8, kind_hint: File handle_entry: while (true) { if (treat_as_dir) { break :iterable_dir self.openDir(sub_path, .{ - .no_follow = true, + .follow_symlinks = false, .iterate = true, }) catch |err| switch (err) { error.NotDir => { @@ -2519,17 +1827,15 @@ fn deleteTreeOpenInitialSubpath(self: Dir, sub_path: []const u8, kind_hint: File error.PermissionDenied, error.SymLinkLoop, error.ProcessFdQuotaExceeded, - error.ProcessNotFound, error.NameTooLong, error.SystemFdQuotaExceeded, error.NoDevice, error.SystemResources, error.Unexpected, - error.InvalidUtf8, - error.InvalidWtf8, error.BadPathName, error.DeviceBusy, error.NetworkNotFound, + error.Canceled, => |e| return e, }; } else { @@ -2545,8 +1851,6 @@ fn deleteTreeOpenInitialSubpath(self: Dir, sub_path: []const u8, kind_hint: File error.AccessDenied, error.PermissionDenied, - error.InvalidUtf8, - error.InvalidWtf8, error.SymLinkLoop, error.NameTooLong, error.SystemResources, @@ -2582,47 +1886,14 @@ pub fn writeFile(self: Dir, options: WriteFileOptions) WriteFileError!void { try file.writeAll(options.data); } -pub const AccessError = posix.AccessError; +/// Deprecated in favor of `Io.Dir.AccessError`. +pub const AccessError = Io.Dir.AccessError; -/// Test accessing `sub_path`. -/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `sub_path` should be encoded as valid UTF-8. -/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. -/// Be careful of Time-Of-Check-Time-Of-Use race conditions when using this function. -/// For example, instead of testing if a file exists and then opening it, just -/// open it and handle the error for file not found. -pub fn access(self: Dir, sub_path: []const u8, flags: File.OpenFlags) AccessError!void { - if (native_os == .windows) { - const sub_path_w = try windows.sliceToPrefixedFileW(self.fd, sub_path); - return self.accessW(sub_path_w.span().ptr, flags); - } - const path_c = try posix.toPosixPath(sub_path); - return self.accessZ(&path_c, flags); -} - -/// Same as `access` except the path parameter is null-terminated. -pub fn accessZ(self: Dir, sub_path: [*:0]const u8, flags: File.OpenFlags) AccessError!void { - if (native_os == .windows) { - const sub_path_w = try windows.cStrToPrefixedFileW(self.fd, sub_path); - return self.accessW(sub_path_w.span().ptr, flags); - } - const os_mode = switch (flags.mode) { - .read_only => @as(u32, posix.F_OK), - .write_only => @as(u32, posix.W_OK), - .read_write => @as(u32, posix.R_OK | posix.W_OK), - }; - const result = posix.faccessatZ(self.fd, sub_path, os_mode, 0); - return result; -} - -/// Same as `access` except asserts the target OS is Windows and the path parameter is -/// * WTF-16 LE encoded -/// * null-terminated -/// * relative or has the NT namespace prefix -/// TODO currently this ignores `flags`. -pub fn accessW(self: Dir, sub_path_w: [*:0]const u16, flags: File.OpenFlags) AccessError!void { - _ = flags; - return posix.faccessatW(self.fd, sub_path_w); +/// Deprecated in favor of `Io.Dir.access`. +pub fn access(self: Dir, sub_path: []const u8, options: Io.Dir.AccessOptions) AccessError!void { + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.ioBasic(); + return Io.Dir.access(self.adaptToNewApi(), io, sub_path, options); } pub const CopyFileOptions = struct { @@ -2630,77 +1901,9 @@ pub const CopyFileOptions = struct { override_mode: ?File.Mode = null, }; -pub const PrevStatus = enum { - stale, - fresh, -}; - -/// Check the file size, mtime, and mode of `source_path` and `dest_path`. If they are equal, does nothing. -/// Otherwise, atomically copies `source_path` to `dest_path`. The destination file gains the mtime, -/// atime, and mode of the source file so that the next call to `updateFile` will not need a copy. -/// Returns the previous status of the file before updating. -/// If any of the directories do not exist for dest_path, they are created. -/// On Windows, both paths should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, both paths should be encoded as valid UTF-8. -/// On other platforms, both paths are an opaque sequence of bytes with no particular encoding. -pub fn updateFile( - source_dir: Dir, - source_path: []const u8, - dest_dir: Dir, - dest_path: []const u8, - options: CopyFileOptions, -) !PrevStatus { - var src_file = try source_dir.openFile(source_path, .{}); - defer src_file.close(); - - const src_stat = try src_file.stat(); - const actual_mode = options.override_mode orelse src_stat.mode; - check_dest_stat: { - const dest_stat = blk: { - var dest_file = dest_dir.openFile(dest_path, .{}) catch |err| switch (err) { - error.FileNotFound => break :check_dest_stat, - else => |e| return e, - }; - defer dest_file.close(); - - break :blk try dest_file.stat(); - }; - - if (src_stat.size == dest_stat.size and - src_stat.mtime == dest_stat.mtime and - actual_mode == dest_stat.mode) - { - return PrevStatus.fresh; - } - } - - if (fs.path.dirname(dest_path)) |dirname| { - try dest_dir.makePath(dirname); - } - - var buffer: [1000]u8 = undefined; // Used only when direct fd-to-fd is not available. - var atomic_file = try dest_dir.atomicFile(dest_path, .{ - .mode = actual_mode, - .write_buffer = &buffer, - }); - defer atomic_file.deinit(); - - var src_reader: File.Reader = .initSize(src_file, &.{}, src_stat.size); - const dest_writer = &atomic_file.file_writer.interface; - - _ = dest_writer.sendFileAll(&src_reader, .unlimited) catch |err| switch (err) { - error.ReadFailed => return src_reader.err.?, - error.WriteFailed => return atomic_file.file_writer.err.?, - }; - try atomic_file.flush(); - try atomic_file.file_writer.file.updateTimes(src_stat.atime, src_stat.mtime); - try atomic_file.renameIntoPlace(); - return .stale; -} - pub const CopyFileError = File.OpenError || File.StatError || AtomicFile.InitError || AtomicFile.FinishError || - File.ReadError || File.WriteError; + File.ReadError || File.WriteError || error{InvalidFileName}; /// Atomically creates a new file at `dest_path` within `dest_dir` with the /// same contents as `source_path` within `source_dir`, overwriting any already @@ -2715,6 +1918,8 @@ pub const CopyFileError = File.OpenError || File.StatError || /// [WTF-8](https://wtf-8.codeberg.page/). On WASI, both paths should be /// encoded as valid UTF-8. On other platforms, both paths are an opaque /// sequence of bytes with no particular encoding. +/// +/// TODO move this function to Io.Dir pub fn copyFile( source_dir: Dir, source_path: []const u8, @@ -2722,11 +1927,15 @@ pub fn copyFile( dest_path: []const u8, options: CopyFileOptions, ) CopyFileError!void { - var file_reader: File.Reader = .init(try source_dir.openFile(source_path, .{}), &.{}); - defer file_reader.file.close(); + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.ioBasic(); + + const file = try source_dir.openFile(source_path, .{}); + var file_reader: File.Reader = .init(.{ .handle = file.handle }, io, &.{}); + defer file_reader.file.close(io); const mode = options.override_mode orelse blk: { - const st = try file_reader.file.stat(); + const st = try file_reader.file.stat(io); file_reader.size = st.size; break :blk st.mode; }; @@ -2776,6 +1985,7 @@ pub fn atomicFile(self: Dir, dest_path: []const u8, options: AtomicFileOptions) pub const Stat = File.Stat; pub const StatError = File.StatError; +/// Deprecated in favor of `Io.Dir.stat`. pub fn stat(self: Dir) StatError!Stat { const file: File = .{ .handle = self.fd }; return file.stat(); @@ -2783,54 +1993,11 @@ pub fn stat(self: Dir) StatError!Stat { pub const StatFileError = File.OpenError || File.StatError || posix.FStatAtError; -/// Returns metadata for a file inside the directory. -/// -/// On Windows, this requires three syscalls. On other operating systems, it -/// only takes one. -/// -/// Symlinks are followed. -/// -/// `sub_path` may be absolute, in which case `self` is ignored. -/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `sub_path` should be encoded as valid UTF-8. -/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. +/// Deprecated in favor of `Io.Dir.statPath`. pub fn statFile(self: Dir, sub_path: []const u8) StatFileError!Stat { - if (native_os == .windows) { - var file = try self.openFile(sub_path, .{}); - defer file.close(); - return file.stat(); - } - if (native_os == .wasi and !builtin.link_libc) { - const st = try std.os.fstatat_wasi(self.fd, sub_path, .{ .SYMLINK_FOLLOW = true }); - return Stat.fromWasi(st); - } - if (native_os == .linux) { - const sub_path_c = try posix.toPosixPath(sub_path); - var stx = std.mem.zeroes(linux.Statx); - - const rc = linux.statx( - self.fd, - &sub_path_c, - linux.AT.NO_AUTOMOUNT, - linux.STATX_TYPE | linux.STATX_MODE | linux.STATX_ATIME | linux.STATX_MTIME | linux.STATX_CTIME, - &stx, - ); - - return switch (linux.E.init(rc)) { - .SUCCESS => Stat.fromLinux(stx), - .ACCES => error.AccessDenied, - .BADF => unreachable, - .FAULT => unreachable, - .INVAL => unreachable, - .LOOP => error.SymLinkLoop, - .NAMETOOLONG => unreachable, // Handled by posix.toPosixPath() above. - .NOENT, .NOTDIR => error.FileNotFound, - .NOMEM => error.SystemResources, - else => |err| posix.unexpectedErrno(err), - }; - } - const st = try posix.fstatat(self.fd, sub_path, 0); - return Stat.fromPosix(st); + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.ioBasic(); + return Io.Dir.statPath(.{ .handle = self.fd }, io, sub_path, .{}); } pub const ChmodError = File.ChmodError; @@ -2867,3 +2034,11 @@ pub fn setPermissions(self: Dir, permissions: Permissions) SetPermissionsError!v const file: File = .{ .handle = self.fd }; try file.setPermissions(permissions); } + +pub fn adaptToNewApi(dir: Dir) Io.Dir { + return .{ .handle = dir.fd }; +} + +pub fn adaptFromNewApi(dir: Io.Dir) Dir { + return .{ .fd = dir.handle }; +} diff --git a/lib/std/fs/File.zig b/lib/std/fs/File.zig index ebc3809878..11d8e7471b 100644 --- a/lib/std/fs/File.zig +++ b/lib/std/fs/File.zig @@ -1,10 +1,12 @@ +const File = @This(); + const builtin = @import("builtin"); -const Os = std.builtin.Os; const native_os = builtin.os.tag; const is_windows = native_os == .windows; -const File = @This(); const std = @import("../std.zig"); +const Io = std.Io; +const Os = std.builtin.Os; const Allocator = std.mem.Allocator; const posix = std.posix; const math = std.math; @@ -17,25 +19,12 @@ const Alignment = std.mem.Alignment; /// The OS-specific file descriptor or file handle. handle: Handle, -pub const Handle = posix.fd_t; -pub const Mode = posix.mode_t; -pub const INode = posix.ino_t; +pub const Handle = Io.File.Handle; +pub const Mode = Io.File.Mode; +pub const INode = Io.File.INode; pub const Uid = posix.uid_t; pub const Gid = posix.gid_t; - -pub const Kind = enum { - block_device, - character_device, - directory, - named_pipe, - sym_link, - file, - unix_domain_socket, - whiteout, - door, - event_port, - unknown, -}; +pub const Kind = Io.File.Kind; /// This is the default mode given to POSIX operating systems for creating /// files. `0o666` is "-rw-rw-rw-" which is counter-intuitive at first, @@ -43,98 +32,16 @@ pub const Kind = enum { /// the `touch` command, which would correspond to `0o644`. However, POSIX /// libc implementations use `0o666` inside `fopen` and then rely on the /// process-scoped "umask" setting to adjust this number for file creation. -pub const default_mode = switch (builtin.os.tag) { - .windows => 0, - .wasi => 0, - else => 0o666, -}; +pub const default_mode: Mode = if (Mode == u0) 0 else 0o666; -pub const OpenError = error{ - SharingViolation, - PathAlreadyExists, - FileNotFound, - AccessDenied, - PipeBusy, - NoDevice, - NameTooLong, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - /// Windows-only; file paths provided by the user must be valid WTF-8. - /// https://wtf-8.codeberg.page/ - InvalidWtf8, - /// On Windows, file paths cannot contain these characters: - /// '/', '*', '?', '"', '<', '>', '|' - BadPathName, - Unexpected, - /// On Windows, `\\server` or `\\server\share` was not found. - NetworkNotFound, - ProcessNotFound, - /// On Windows, antivirus software is enabled by default. It can be - /// disabled, but Windows Update sometimes ignores the user's preference - /// and re-enables it. When enabled, antivirus software on Windows - /// intercepts file system operations and makes them significantly slower - /// in addition to possibly failing with this error code. - AntivirusInterference, -} || posix.OpenError || posix.FlockError; - -pub const OpenMode = enum { - read_only, - write_only, - read_write, -}; - -pub const Lock = enum { - none, - shared, - exclusive, -}; - -pub const OpenFlags = struct { - mode: OpenMode = .read_only, - - /// Open the file with an advisory lock to coordinate with other processes - /// accessing it at the same time. An exclusive lock will prevent other - /// processes from acquiring a lock. A shared lock will prevent other - /// processes from acquiring a exclusive lock, but does not prevent - /// other process from getting their own shared locks. - /// - /// The lock is advisory, except on Linux in very specific circumstances[1]. - /// This means that a process that does not respect the locking API can still get access - /// to the file, despite the lock. - /// - /// On these operating systems, the lock is acquired atomically with - /// opening the file: - /// * Darwin - /// * DragonFlyBSD - /// * FreeBSD - /// * Haiku - /// * NetBSD - /// * OpenBSD - /// On these operating systems, the lock is acquired via a separate syscall - /// after opening the file: - /// * Linux - /// * Windows - /// - /// [1]: https://www.kernel.org/doc/Documentation/filesystems/mandatory-locking.txt - lock: Lock = .none, - - /// Sets whether or not to wait until the file is locked to return. If set to true, - /// `error.WouldBlock` will be returned. Otherwise, the file will wait until the file - /// is available to proceed. - lock_nonblocking: bool = false, - - /// Set this to allow the opened file to automatically become the - /// controlling TTY for the current process. - allow_ctty: bool = false, - - pub fn isRead(self: OpenFlags) bool { - return self.mode != .write_only; - } - - pub fn isWrite(self: OpenFlags) bool { - return self.mode != .read_only; - } -}; +/// Deprecated in favor of `Io.File.OpenError`. +pub const OpenError = Io.File.OpenError || error{WouldBlock}; +/// Deprecated in favor of `Io.File.OpenMode`. +pub const OpenMode = Io.File.OpenMode; +/// Deprecated in favor of `Io.File.Lock`. +pub const Lock = Io.File.Lock; +/// Deprecated in favor of `Io.File.OpenFlags`. +pub const OpenFlags = Io.File.OpenFlags; pub const CreateFlags = struct { /// Whether the file will be created with read access. @@ -399,193 +306,15 @@ pub fn mode(self: File) ModeError!Mode { return (try self.stat()).mode; } -pub const Stat = struct { - /// A number that the system uses to point to the file metadata. This - /// number is not guaranteed to be unique across time, as some file - /// systems may reuse an inode after its file has been deleted. Some - /// systems may change the inode of a file over time. - /// - /// On Linux, the inode is a structure that stores the metadata, and - /// the inode _number_ is what you see here: the index number of the - /// inode. - /// - /// The FileIndex on Windows is similar. It is a number for a file that - /// is unique to each filesystem. - inode: INode, - size: u64, - /// This is available on POSIX systems and is always 0 otherwise. - mode: Mode, - kind: Kind, - - /// Last access time in nanoseconds, relative to UTC 1970-01-01. - atime: i128, - /// Last modification time in nanoseconds, relative to UTC 1970-01-01. - mtime: i128, - /// Last status/metadata change time in nanoseconds, relative to UTC 1970-01-01. - ctime: i128, - - pub fn fromPosix(st: posix.Stat) Stat { - const atime = st.atime(); - const mtime = st.mtime(); - const ctime = st.ctime(); - return .{ - .inode = st.ino, - .size = @bitCast(st.size), - .mode = st.mode, - .kind = k: { - const m = st.mode & posix.S.IFMT; - switch (m) { - posix.S.IFBLK => break :k .block_device, - posix.S.IFCHR => break :k .character_device, - posix.S.IFDIR => break :k .directory, - posix.S.IFIFO => break :k .named_pipe, - posix.S.IFLNK => break :k .sym_link, - posix.S.IFREG => break :k .file, - posix.S.IFSOCK => break :k .unix_domain_socket, - else => {}, - } - if (builtin.os.tag == .illumos) switch (m) { - posix.S.IFDOOR => break :k .door, - posix.S.IFPORT => break :k .event_port, - else => {}, - }; - - break :k .unknown; - }, - .atime = @as(i128, atime.sec) * std.time.ns_per_s + atime.nsec, - .mtime = @as(i128, mtime.sec) * std.time.ns_per_s + mtime.nsec, - .ctime = @as(i128, ctime.sec) * std.time.ns_per_s + ctime.nsec, - }; - } - - pub fn fromLinux(stx: linux.Statx) Stat { - const atime = stx.atime; - const mtime = stx.mtime; - const ctime = stx.ctime; - - return .{ - .inode = stx.ino, - .size = stx.size, - .mode = stx.mode, - .kind = switch (stx.mode & linux.S.IFMT) { - linux.S.IFDIR => .directory, - linux.S.IFCHR => .character_device, - linux.S.IFBLK => .block_device, - linux.S.IFREG => .file, - linux.S.IFIFO => .named_pipe, - linux.S.IFLNK => .sym_link, - linux.S.IFSOCK => .unix_domain_socket, - else => .unknown, - }, - .atime = @as(i128, atime.sec) * std.time.ns_per_s + atime.nsec, - .mtime = @as(i128, mtime.sec) * std.time.ns_per_s + mtime.nsec, - .ctime = @as(i128, ctime.sec) * std.time.ns_per_s + ctime.nsec, - }; - } - - pub fn fromWasi(st: std.os.wasi.filestat_t) Stat { - return .{ - .inode = st.ino, - .size = @bitCast(st.size), - .mode = 0, - .kind = switch (st.filetype) { - .BLOCK_DEVICE => .block_device, - .CHARACTER_DEVICE => .character_device, - .DIRECTORY => .directory, - .SYMBOLIC_LINK => .sym_link, - .REGULAR_FILE => .file, - .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket, - else => .unknown, - }, - .atime = st.atim, - .mtime = st.mtim, - .ctime = st.ctim, - }; - } -}; +pub const Stat = Io.File.Stat; pub const StatError = posix.FStatError; /// Returns `Stat` containing basic information about the `File`. -/// TODO: integrate with async I/O pub fn stat(self: File) StatError!Stat { - if (builtin.os.tag == .windows) { - var io_status_block: windows.IO_STATUS_BLOCK = undefined; - var info: windows.FILE_ALL_INFORMATION = undefined; - const rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation); - switch (rc) { - .SUCCESS => {}, - // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer - // size provided. This is treated as success because the type of variable-length information that this would be relevant for - // (name, volume name, etc) we don't care about. - .BUFFER_OVERFLOW => {}, - .INVALID_PARAMETER => unreachable, - .ACCESS_DENIED => return error.AccessDenied, - else => return windows.unexpectedStatus(rc), - } - return .{ - .inode = info.InternalInformation.IndexNumber, - .size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)), - .mode = 0, - .kind = if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) reparse_point: { - var tag_info: windows.FILE_ATTRIBUTE_TAG_INFO = undefined; - const tag_rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &tag_info, @sizeOf(windows.FILE_ATTRIBUTE_TAG_INFO), .FileAttributeTagInformation); - switch (tag_rc) { - .SUCCESS => {}, - // INFO_LENGTH_MISMATCH and ACCESS_DENIED are the only documented possible errors - // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/d295752f-ce89-4b98-8553-266d37c84f0e - .INFO_LENGTH_MISMATCH => unreachable, - .ACCESS_DENIED => return error.AccessDenied, - else => return windows.unexpectedStatus(rc), - } - if (tag_info.ReparseTag & windows.reparse_tag_name_surrogate_bit != 0) { - break :reparse_point .sym_link; - } - // Unknown reparse point - break :reparse_point .unknown; - } else if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) - .directory - else - .file, - .atime = windows.fromSysTime(info.BasicInformation.LastAccessTime), - .mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime), - .ctime = windows.fromSysTime(info.BasicInformation.ChangeTime), - }; - } - - if (builtin.os.tag == .wasi and !builtin.link_libc) { - const st = try std.os.fstat_wasi(self.handle); - return Stat.fromWasi(st); - } - - if (builtin.os.tag == .linux) { - var stx = std.mem.zeroes(linux.Statx); - - const rc = linux.statx( - self.handle, - "", - linux.AT.EMPTY_PATH, - linux.STATX_TYPE | linux.STATX_MODE | linux.STATX_ATIME | linux.STATX_MTIME | linux.STATX_CTIME, - &stx, - ); - - return switch (linux.E.init(rc)) { - .SUCCESS => Stat.fromLinux(stx), - .ACCES => unreachable, - .BADF => unreachable, - .FAULT => unreachable, - .INVAL => unreachable, - .LOOP => unreachable, - .NAMETOOLONG => unreachable, - .NOENT => unreachable, - .NOMEM => error.SystemResources, - .NOTDIR => unreachable, - else => |err| posix.unexpectedErrno(err), - }; - } - - const st = try posix.fstat(self.handle); - return Stat.fromPosix(st); + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.ioBasic(); + return Io.File.stat(.{ .handle = self.handle }, io); } pub const ChmodError = posix.FChmodError; @@ -782,9 +511,9 @@ pub const UpdateTimesError = posix.FutimensError || windows.SetFileTimeError; pub fn updateTimes( self: File, /// access timestamp in nanoseconds - atime: i128, + atime: Io.Timestamp, /// last modification timestamp in nanoseconds - mtime: i128, + mtime: Io.Timestamp, ) UpdateTimesError!void { if (builtin.os.tag == .windows) { const atime_ft = windows.nanoSecondsToFileTime(atime); @@ -793,12 +522,12 @@ pub fn updateTimes( } const times = [2]posix.timespec{ posix.timespec{ - .sec = math.cast(isize, @divFloor(atime, std.time.ns_per_s)) orelse maxInt(isize), - .nsec = math.cast(isize, @mod(atime, std.time.ns_per_s)) orelse maxInt(isize), + .sec = math.cast(isize, @divFloor(atime.nanoseconds, std.time.ns_per_s)) orelse maxInt(isize), + .nsec = math.cast(isize, @mod(atime.nanoseconds, std.time.ns_per_s)) orelse maxInt(isize), }, posix.timespec{ - .sec = math.cast(isize, @divFloor(mtime, std.time.ns_per_s)) orelse maxInt(isize), - .nsec = math.cast(isize, @mod(mtime, std.time.ns_per_s)) orelse maxInt(isize), + .sec = math.cast(isize, @divFloor(mtime.nanoseconds, std.time.ns_per_s)) orelse maxInt(isize), + .nsec = math.cast(isize, @mod(mtime.nanoseconds, std.time.ns_per_s)) orelse maxInt(isize), }, }; try posix.futimens(self.handle, ×); @@ -815,17 +544,6 @@ pub fn read(self: File, buffer: []u8) ReadError!usize { return posix.read(self.handle, buffer); } -/// Deprecated in favor of `Reader`. -pub fn readAll(self: File, buffer: []u8) ReadError!usize { - var index: usize = 0; - while (index != buffer.len) { - const amt = try self.read(buffer[index..]); - if (amt == 0) break; - index += amt; - } - return index; -} - /// On Windows, this function currently does alter the file pointer. /// https://github.com/ziglang/zig/issues/12783 pub fn pread(self: File, buffer: []u8, offset: u64) PReadError!usize { @@ -858,36 +576,6 @@ pub fn readv(self: File, iovecs: []const posix.iovec) ReadError!usize { return posix.readv(self.handle, iovecs); } -/// Deprecated in favor of `Reader`. -pub fn readvAll(self: File, iovecs: []posix.iovec) ReadError!usize { - if (iovecs.len == 0) return 0; - - // We use the address of this local variable for all zero-length - // vectors so that the OS does not complain that we are giving it - // addresses outside the application's address space. - var garbage: [1]u8 = undefined; - for (iovecs) |*v| { - if (v.len == 0) v.base = &garbage; - } - - var i: usize = 0; - var off: usize = 0; - while (true) { - var amt = try self.readv(iovecs[i..]); - var eof = amt == 0; - off += amt; - while (amt >= iovecs[i].len) { - amt -= iovecs[i].len; - i += 1; - if (i >= iovecs.len) return off; - eof = false; - } - if (eof) return off; - iovecs[i].base += amt; - iovecs[i].len -= amt; - } -} - /// See https://github.com/ziglang/zig/issues/7699 /// On Windows, this function currently does alter the file pointer. /// https://github.com/ziglang/zig/issues/12783 @@ -901,28 +589,6 @@ pub fn preadv(self: File, iovecs: []const posix.iovec, offset: u64) PReadError!u return posix.preadv(self.handle, iovecs, offset); } -/// Deprecated in favor of `Reader`. -pub fn preadvAll(self: File, iovecs: []posix.iovec, offset: u64) PReadError!usize { - if (iovecs.len == 0) return 0; - - var i: usize = 0; - var off: usize = 0; - while (true) { - var amt = try self.preadv(iovecs[i..], offset + off); - var eof = amt == 0; - off += amt; - while (amt >= iovecs[i].len) { - amt -= iovecs[i].len; - i += 1; - if (i >= iovecs.len) return off; - eof = false; - } - if (eof) return off; - iovecs[i].base += amt; - iovecs[i].len -= amt; - } -} - pub const WriteError = posix.WriteError; pub const PWriteError = posix.PWriteError; @@ -934,7 +600,6 @@ pub fn write(self: File, bytes: []const u8) WriteError!usize { return posix.write(self.handle, bytes); } -/// Deprecated in favor of `Writer`. pub fn writeAll(self: File, bytes: []const u8) WriteError!void { var index: usize = 0; while (index < bytes.len) { @@ -942,6 +607,14 @@ pub fn writeAll(self: File, bytes: []const u8) WriteError!void { } } +/// Deprecated in favor of `Writer`. +pub fn pwriteAll(self: File, bytes: []const u8, offset: u64) PWriteError!void { + var index: usize = 0; + while (index < bytes.len) { + index += try self.pwrite(bytes[index..], offset + index); + } +} + /// On Windows, this function currently does alter the file pointer. /// https://github.com/ziglang/zig/issues/12783 pub fn pwrite(self: File, bytes: []const u8, offset: u64) PWriteError!usize { @@ -952,14 +625,6 @@ pub fn pwrite(self: File, bytes: []const u8, offset: u64) PWriteError!usize { return posix.pwrite(self.handle, bytes, offset); } -/// Deprecated in favor of `Writer`. -pub fn pwriteAll(self: File, bytes: []const u8, offset: u64) PWriteError!void { - var index: usize = 0; - while (index < bytes.len) { - index += try self.pwrite(bytes[index..], offset + index); - } -} - /// See https://github.com/ziglang/zig/issues/7699 pub fn writev(self: File, iovecs: []const posix.iovec_const) WriteError!usize { if (is_windows) { @@ -972,31 +637,6 @@ pub fn writev(self: File, iovecs: []const posix.iovec_const) WriteError!usize { return posix.writev(self.handle, iovecs); } -/// Deprecated in favor of `Writer`. -pub fn writevAll(self: File, iovecs: []posix.iovec_const) WriteError!void { - if (iovecs.len == 0) return; - - // We use the address of this local variable for all zero-length - // vectors so that the OS does not complain that we are giving it - // addresses outside the application's address space. - var garbage: [1]u8 = undefined; - for (iovecs) |*v| { - if (v.len == 0) v.base = &garbage; - } - - var i: usize = 0; - while (true) { - var amt = try self.writev(iovecs[i..]); - while (amt >= iovecs[i].len) { - amt -= iovecs[i].len; - i += 1; - if (i >= iovecs.len) return; - } - iovecs[i].base += amt; - iovecs[i].len -= amt; - } -} - /// See https://github.com/ziglang/zig/issues/7699 /// On Windows, this function currently does alter the file pointer. /// https://github.com/ziglang/zig/issues/12783 @@ -1011,23 +651,6 @@ pub fn pwritev(self: File, iovecs: []posix.iovec_const, offset: u64) PWriteError } /// Deprecated in favor of `Writer`. -pub fn pwritevAll(self: File, iovecs: []posix.iovec_const, offset: u64) PWriteError!void { - if (iovecs.len == 0) return; - var i: usize = 0; - var off: u64 = 0; - while (true) { - var amt = try self.pwritev(iovecs[i..], offset + off); - off += amt; - while (amt >= iovecs[i].len) { - amt -= iovecs[i].len; - i += 1; - if (i >= iovecs.len) return; - } - iovecs[i].base += amt; - iovecs[i].len -= amt; - } -} - pub const CopyRangeError = posix.CopyFileRangeError; /// Deprecated in favor of `Writer`. @@ -1052,449 +675,8 @@ pub fn copyRangeAll(in: File, in_offset: u64, out: File, out_offset: u64, len: u return total_bytes_copied; } -/// Memoizes key information about a file handle such as: -/// * The size from calling stat, or the error that occurred therein. -/// * The current seek position. -/// * The error that occurred when trying to seek. -/// * Whether reading should be done positionally or streaming. -/// * Whether reading should be done via fd-to-fd syscalls (e.g. `sendfile`) -/// versus plain variants (e.g. `read`). -/// -/// Fulfills the `std.Io.Reader` interface. -pub const Reader = struct { - file: File, - err: ?ReadError = null, - mode: Reader.Mode = .positional, - /// Tracks the true seek position in the file. To obtain the logical - /// position, use `logicalPos`. - pos: u64 = 0, - size: ?u64 = null, - size_err: ?SizeError = null, - seek_err: ?Reader.SeekError = null, - interface: std.Io.Reader, - - pub const SizeError = std.os.windows.GetFileSizeError || StatError || error{ - /// Occurs if, for example, the file handle is a network socket and therefore does not have a size. - Streaming, - }; - - pub const SeekError = File.SeekError || error{ - /// Seeking fell back to reading, and reached the end before the requested seek position. - /// `pos` remains at the end of the file. - EndOfStream, - /// Seeking fell back to reading, which failed. - ReadFailed, - }; - - pub const Mode = enum { - streaming, - positional, - /// Avoid syscalls other than `read` and `readv`. - streaming_reading, - /// Avoid syscalls other than `pread` and `preadv`. - positional_reading, - /// Indicates reading cannot continue because of a seek failure. - failure, - - pub fn toStreaming(m: @This()) @This() { - return switch (m) { - .positional, .streaming => .streaming, - .positional_reading, .streaming_reading => .streaming_reading, - .failure => .failure, - }; - } - - pub fn toReading(m: @This()) @This() { - return switch (m) { - .positional, .positional_reading => .positional_reading, - .streaming, .streaming_reading => .streaming_reading, - .failure => .failure, - }; - } - }; - - pub fn initInterface(buffer: []u8) std.Io.Reader { - return .{ - .vtable = &.{ - .stream = Reader.stream, - .discard = Reader.discard, - .readVec = Reader.readVec, - }, - .buffer = buffer, - .seek = 0, - .end = 0, - }; - } - - pub fn init(file: File, buffer: []u8) Reader { - return .{ - .file = file, - .interface = initInterface(buffer), - }; - } - - pub fn initSize(file: File, buffer: []u8, size: ?u64) Reader { - return .{ - .file = file, - .interface = initInterface(buffer), - .size = size, - }; - } - - /// Positional is more threadsafe, since the global seek position is not - /// affected, but when such syscalls are not available, preemptively - /// initializing in streaming mode skips a failed syscall. - pub fn initStreaming(file: File, buffer: []u8) Reader { - return .{ - .file = file, - .interface = Reader.initInterface(buffer), - .mode = .streaming, - .seek_err = error.Unseekable, - .size_err = error.Streaming, - }; - } - - pub fn getSize(r: *Reader) SizeError!u64 { - return r.size orelse { - if (r.size_err) |err| return err; - if (is_windows) { - if (windows.GetFileSizeEx(r.file.handle)) |size| { - r.size = size; - return size; - } else |err| { - r.size_err = err; - return err; - } - } - if (posix.Stat == void) { - r.size_err = error.Streaming; - return error.Streaming; - } - if (stat(r.file)) |st| { - if (st.kind == .file) { - r.size = st.size; - return st.size; - } else { - r.mode = r.mode.toStreaming(); - r.size_err = error.Streaming; - return error.Streaming; - } - } else |err| { - r.size_err = err; - return err; - } - }; - } - - pub fn seekBy(r: *Reader, offset: i64) Reader.SeekError!void { - switch (r.mode) { - .positional, .positional_reading => { - setLogicalPos(r, @intCast(@as(i64, @intCast(logicalPos(r))) + offset)); - }, - .streaming, .streaming_reading => { - if (posix.SEEK == void) { - r.seek_err = error.Unseekable; - return error.Unseekable; - } - const seek_err = r.seek_err orelse e: { - if (posix.lseek_CUR(r.file.handle, offset)) |_| { - setLogicalPos(r, @intCast(@as(i64, @intCast(logicalPos(r))) + offset)); - return; - } else |err| { - r.seek_err = err; - break :e err; - } - }; - var remaining = std.math.cast(u64, offset) orelse return seek_err; - while (remaining > 0) { - remaining -= discard(&r.interface, .limited64(remaining)) catch |err| { - r.seek_err = err; - return err; - }; - } - r.interface.seek = 0; - r.interface.end = 0; - }, - .failure => return r.seek_err.?, - } - } - - pub fn seekTo(r: *Reader, offset: u64) Reader.SeekError!void { - switch (r.mode) { - .positional, .positional_reading => { - setLogicalPos(r, offset); - }, - .streaming, .streaming_reading => { - const logical_pos = logicalPos(r); - if (offset >= logical_pos) return Reader.seekBy(r, @intCast(offset - logical_pos)); - if (r.seek_err) |err| return err; - posix.lseek_SET(r.file.handle, offset) catch |err| { - r.seek_err = err; - return err; - }; - setLogicalPos(r, offset); - }, - .failure => return r.seek_err.?, - } - } - - pub fn logicalPos(r: *const Reader) u64 { - return r.pos - r.interface.bufferedLen(); - } - - fn setLogicalPos(r: *Reader, offset: u64) void { - const logical_pos = logicalPos(r); - if (offset < logical_pos or offset >= r.pos) { - r.interface.seek = 0; - r.interface.end = 0; - r.pos = offset; - } else { - const logical_delta: usize = @intCast(offset - logical_pos); - r.interface.seek += logical_delta; - } - } - - /// Number of slices to store on the stack, when trying to send as many byte - /// vectors through the underlying read calls as possible. - const max_buffers_len = 16; - - fn stream(io_reader: *std.Io.Reader, w: *std.Io.Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize { - const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader)); - switch (r.mode) { - .positional, .streaming => return w.sendFile(r, limit) catch |write_err| switch (write_err) { - error.Unimplemented => { - r.mode = r.mode.toReading(); - return 0; - }, - else => |e| return e, - }, - .positional_reading => { - const dest = limit.slice(try w.writableSliceGreedy(1)); - var data: [1][]u8 = .{dest}; - const n = try readVecPositional(r, &data); - w.advance(n); - return n; - }, - .streaming_reading => { - const dest = limit.slice(try w.writableSliceGreedy(1)); - var data: [1][]u8 = .{dest}; - const n = try readVecStreaming(r, &data); - w.advance(n); - return n; - }, - .failure => return error.ReadFailed, - } - } - - fn readVec(io_reader: *std.Io.Reader, data: [][]u8) std.Io.Reader.Error!usize { - const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader)); - switch (r.mode) { - .positional, .positional_reading => return readVecPositional(r, data), - .streaming, .streaming_reading => return readVecStreaming(r, data), - .failure => return error.ReadFailed, - } - } - - fn readVecPositional(r: *Reader, data: [][]u8) std.Io.Reader.Error!usize { - const io_reader = &r.interface; - if (is_windows) { - // Unfortunately, `ReadFileScatter` cannot be used since it - // requires page alignment. - if (io_reader.seek == io_reader.end) { - io_reader.seek = 0; - io_reader.end = 0; - } - const first = data[0]; - if (first.len >= io_reader.buffer.len - io_reader.end) { - return readPositional(r, first); - } else { - io_reader.end += try readPositional(r, io_reader.buffer[io_reader.end..]); - return 0; - } - } - var iovecs_buffer: [max_buffers_len]posix.iovec = undefined; - const dest_n, const data_size = try io_reader.writableVectorPosix(&iovecs_buffer, data); - const dest = iovecs_buffer[0..dest_n]; - assert(dest[0].len > 0); - const n = posix.preadv(r.file.handle, dest, r.pos) catch |err| switch (err) { - error.Unseekable => { - r.mode = r.mode.toStreaming(); - const pos = r.pos; - if (pos != 0) { - r.pos = 0; - r.seekBy(@intCast(pos)) catch { - r.mode = .failure; - return error.ReadFailed; - }; - } - return 0; - }, - else => |e| { - r.err = e; - return error.ReadFailed; - }, - }; - if (n == 0) { - r.size = r.pos; - return error.EndOfStream; - } - r.pos += n; - if (n > data_size) { - io_reader.end += n - data_size; - return data_size; - } - return n; - } - - fn readVecStreaming(r: *Reader, data: [][]u8) std.Io.Reader.Error!usize { - const io_reader = &r.interface; - if (is_windows) { - // Unfortunately, `ReadFileScatter` cannot be used since it - // requires page alignment. - if (io_reader.seek == io_reader.end) { - io_reader.seek = 0; - io_reader.end = 0; - } - const first = data[0]; - if (first.len >= io_reader.buffer.len - io_reader.end) { - return readStreaming(r, first); - } else { - io_reader.end += try readStreaming(r, io_reader.buffer[io_reader.end..]); - return 0; - } - } - var iovecs_buffer: [max_buffers_len]posix.iovec = undefined; - const dest_n, const data_size = try io_reader.writableVectorPosix(&iovecs_buffer, data); - const dest = iovecs_buffer[0..dest_n]; - assert(dest[0].len > 0); - const n = posix.readv(r.file.handle, dest) catch |err| { - r.err = err; - return error.ReadFailed; - }; - if (n == 0) { - r.size = r.pos; - return error.EndOfStream; - } - r.pos += n; - if (n > data_size) { - io_reader.end += n - data_size; - return data_size; - } - return n; - } - - fn discard(io_reader: *std.Io.Reader, limit: std.Io.Limit) std.Io.Reader.Error!usize { - const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader)); - const file = r.file; - const pos = r.pos; - switch (r.mode) { - .positional, .positional_reading => { - const size = r.getSize() catch { - r.mode = r.mode.toStreaming(); - return 0; - }; - const delta = @min(@intFromEnum(limit), size - pos); - r.pos = pos + delta; - return delta; - }, - .streaming, .streaming_reading => { - // Unfortunately we can't seek forward without knowing the - // size because the seek syscalls provided to us will not - // return the true end position if a seek would exceed the - // end. - fallback: { - if (r.size_err == null and r.seek_err == null) break :fallback; - var trash_buffer: [128]u8 = undefined; - if (is_windows) { - const n = windows.ReadFile(file.handle, limit.slice(&trash_buffer), null) catch |err| { - r.err = err; - return error.ReadFailed; - }; - if (n == 0) { - r.size = pos; - return error.EndOfStream; - } - r.pos = pos + n; - return n; - } - var iovecs: [max_buffers_len]std.posix.iovec = undefined; - var iovecs_i: usize = 0; - var remaining = @intFromEnum(limit); - while (remaining > 0 and iovecs_i < iovecs.len) { - iovecs[iovecs_i] = .{ .base = &trash_buffer, .len = @min(trash_buffer.len, remaining) }; - remaining -= iovecs[iovecs_i].len; - iovecs_i += 1; - } - const n = posix.readv(file.handle, iovecs[0..iovecs_i]) catch |err| { - r.err = err; - return error.ReadFailed; - }; - if (n == 0) { - r.size = pos; - return error.EndOfStream; - } - r.pos = pos + n; - return n; - } - const size = r.getSize() catch return 0; - const n = @min(size - pos, maxInt(i64), @intFromEnum(limit)); - file.seekBy(n) catch |err| { - r.seek_err = err; - return 0; - }; - r.pos = pos + n; - return n; - }, - .failure => return error.ReadFailed, - } - } - - fn readPositional(r: *Reader, dest: []u8) std.Io.Reader.Error!usize { - const n = r.file.pread(dest, r.pos) catch |err| switch (err) { - error.Unseekable => { - r.mode = r.mode.toStreaming(); - const pos = r.pos; - if (pos != 0) { - r.pos = 0; - r.seekBy(@intCast(pos)) catch { - r.mode = .failure; - return error.ReadFailed; - }; - } - return 0; - }, - else => |e| { - r.err = e; - return error.ReadFailed; - }, - }; - if (n == 0) { - r.size = r.pos; - return error.EndOfStream; - } - r.pos += n; - return n; - } - - fn readStreaming(r: *Reader, dest: []u8) std.Io.Reader.Error!usize { - const n = r.file.read(dest) catch |err| { - r.err = err; - return error.ReadFailed; - }; - if (n == 0) { - r.size = r.pos; - return error.EndOfStream; - } - r.pos += n; - return n; - } - - pub fn atEnd(r: *Reader) bool { - // Even if stat fails, size is set when end is encountered. - const size = r.size orelse return false; - return size - r.pos == 0; - } -}; +/// Deprecated in favor of `Io.File.Reader`. +pub const Reader = Io.File.Reader; pub const Writer = struct { file: File, @@ -1507,7 +689,7 @@ pub const Writer = struct { copy_file_range_err: ?CopyFileRangeError = null, fcopyfile_err: ?FcopyfileError = null, seek_err: ?Writer.SeekError = null, - interface: std.Io.Writer, + interface: Io.Writer, pub const Mode = Reader.Mode; @@ -1553,23 +735,25 @@ pub const Writer = struct { }; } - pub fn initInterface(buffer: []u8) std.Io.Writer { + pub fn initInterface(buffer: []u8) Io.Writer { return .{ .vtable = &.{ .drain = drain, .sendFile = switch (builtin.zig_backend) { else => sendFile, - .stage2_aarch64 => std.Io.Writer.unimplementedSendFile, + .stage2_aarch64 => Io.Writer.unimplementedSendFile, }, }, .buffer = buffer, }; } - pub fn moveToReader(w: *Writer) Reader { + /// TODO when this logic moves from fs.File to Io.File the io parameter should be deleted + pub fn moveToReader(w: *Writer, io: Io) Reader { defer w.* = undefined; return .{ - .file = w.file, + .io = io, + .file = .{ .handle = w.file.handle }, .mode = w.mode, .pos = w.pos, .interface = Reader.initInterface(w.interface.buffer), @@ -1577,7 +761,7 @@ pub const Writer = struct { }; } - pub fn drain(io_w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize { + pub fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize { const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w)); const handle = w.file.handle; const buffered = io_w.buffered(); @@ -1727,10 +911,10 @@ pub const Writer = struct { } pub fn sendFile( - io_w: *std.Io.Writer, - file_reader: *Reader, - limit: std.Io.Limit, - ) std.Io.Writer.FileError!usize { + io_w: *Io.Writer, + file_reader: *Io.File.Reader, + limit: Io.Limit, + ) Io.Writer.FileError!usize { const reader_buffered = file_reader.interface.buffered(); if (reader_buffered.len >= @intFromEnum(limit)) return sendFileBuffered(io_w, file_reader, limit.slice(reader_buffered)); @@ -1994,16 +1178,16 @@ pub const Writer = struct { } fn sendFileBuffered( - io_w: *std.Io.Writer, - file_reader: *Reader, + io_w: *Io.Writer, + file_reader: *Io.File.Reader, reader_buffered: []const u8, - ) std.Io.Writer.FileError!usize { + ) Io.Writer.FileError!usize { const n = try drain(io_w, &.{reader_buffered}, 1); file_reader.seekBy(@intCast(n)) catch return error.ReadFailed; return n; } - pub fn seekTo(w: *Writer, offset: u64) (Writer.SeekError || std.Io.Writer.Error)!void { + pub fn seekTo(w: *Writer, offset: u64) (Writer.SeekError || Io.Writer.Error)!void { try w.interface.flush(); try seekToUnbuffered(w, offset); } @@ -2027,7 +1211,7 @@ pub const Writer = struct { } } - pub const EndError = SetEndPosError || std.Io.Writer.Error; + pub const EndError = SetEndPosError || Io.Writer.Error; /// Flushes any buffered data and sets the end position of the file. /// @@ -2058,15 +1242,15 @@ pub const Writer = struct { /// /// Positional is more threadsafe, since the global seek position is not /// affected. -pub fn reader(file: File, buffer: []u8) Reader { - return .init(file, buffer); +pub fn reader(file: File, io: Io, buffer: []u8) Reader { + return .init(.{ .handle = file.handle }, io, buffer); } /// Positional is more threadsafe, since the global seek position is not /// affected, but when such syscalls are not available, preemptively /// initializing in streaming mode skips a failed syscall. -pub fn readerStreaming(file: File, buffer: []u8) Reader { - return .initStreaming(file, buffer); +pub fn readerStreaming(file: File, io: Io, buffer: []u8) Reader { + return .initStreaming(.{ .handle = file.handle }, io, buffer); } /// Defaults to positional reading; falls back to streaming. @@ -2246,3 +1430,11 @@ pub fn downgradeLock(file: File) LockError!void { }; } } + +pub fn adaptToNewApi(file: File) Io.File { + return .{ .handle = file.handle }; +} + +pub fn adaptFromNewApi(file: Io.File) File { + return .{ .handle = file.handle }; +} diff --git a/lib/std/fs/path.zig b/lib/std/fs/path.zig index ff69c21cdd..966c481c23 100644 --- a/lib/std/fs/path.zig +++ b/lib/std/fs/path.zig @@ -313,7 +313,7 @@ pub fn isAbsoluteWindowsW(path_w: [*:0]const u16) bool { return isAbsoluteWindowsImpl(u16, mem.sliceTo(path_w, 0)); } -pub fn isAbsoluteWindowsWTF16(path: []const u16) bool { +pub fn isAbsoluteWindowsWtf16(path: []const u16) bool { return isAbsoluteWindowsImpl(u16, path); } diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index 0949d71a2d..75f35a46da 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -1,10 +1,12 @@ -const std = @import("../std.zig"); const builtin = @import("builtin"); +const native_os = builtin.os.tag; + +const std = @import("../std.zig"); +const Io = std.Io; const testing = std.testing; const fs = std.fs; const mem = std.mem; const wasi = std.os.wasi; -const native_os = builtin.os.tag; const windows = std.os.windows; const posix = std.posix; @@ -73,6 +75,7 @@ const PathType = enum { }; const TestContext = struct { + io: Io, path_type: PathType, path_sep: u8, arena: ArenaAllocator, @@ -83,6 +86,7 @@ const TestContext = struct { pub fn init(path_type: PathType, path_sep: u8, allocator: mem.Allocator, transform_fn: *const PathType.TransformFn) TestContext { const tmp = tmpDir(.{ .iterate = true }); return .{ + .io = testing.io, .path_type = path_type, .path_sep = path_sep, .arena = ArenaAllocator.init(allocator), @@ -1319,6 +1323,8 @@ test "max file name component lengths" { } test "writev, readv" { + const io = testing.io; + var tmp = tmpDir(.{}); defer tmp.cleanup(); @@ -1327,78 +1333,55 @@ test "writev, readv" { var buf1: [line1.len]u8 = undefined; var buf2: [line2.len]u8 = undefined; - var write_vecs = [_]posix.iovec_const{ - .{ - .base = line1, - .len = line1.len, - }, - .{ - .base = line2, - .len = line2.len, - }, - }; - var read_vecs = [_]posix.iovec{ - .{ - .base = &buf2, - .len = buf2.len, - }, - .{ - .base = &buf1, - .len = buf1.len, - }, - }; + var write_vecs: [2][]const u8 = .{ line1, line2 }; + var read_vecs: [2][]u8 = .{ &buf2, &buf1 }; var src_file = try tmp.dir.createFile("test.txt", .{ .read = true }); defer src_file.close(); - try src_file.writevAll(&write_vecs); + var writer = src_file.writerStreaming(&.{}); + + try writer.interface.writeVecAll(&write_vecs); + try writer.interface.flush(); try testing.expectEqual(@as(u64, line1.len + line2.len), try src_file.getEndPos()); - try src_file.seekTo(0); - const read = try src_file.readvAll(&read_vecs); - try testing.expectEqual(@as(usize, line1.len + line2.len), read); + + var reader = writer.moveToReader(io); + try reader.seekTo(0); + try reader.interface.readVecAll(&read_vecs); try testing.expectEqualStrings(&buf1, "line2\n"); try testing.expectEqualStrings(&buf2, "line1\n"); + try testing.expectError(error.EndOfStream, reader.interface.readSliceAll(&buf1)); } test "pwritev, preadv" { + const io = testing.io; + var tmp = tmpDir(.{}); defer tmp.cleanup(); const line1 = "line1\n"; const line2 = "line2\n"; - + var lines: [2][]const u8 = .{ line1, line2 }; var buf1: [line1.len]u8 = undefined; var buf2: [line2.len]u8 = undefined; - var write_vecs = [_]posix.iovec_const{ - .{ - .base = line1, - .len = line1.len, - }, - .{ - .base = line2, - .len = line2.len, - }, - }; - var read_vecs = [_]posix.iovec{ - .{ - .base = &buf2, - .len = buf2.len, - }, - .{ - .base = &buf1, - .len = buf1.len, - }, - }; + var read_vecs: [2][]u8 = .{ &buf2, &buf1 }; var src_file = try tmp.dir.createFile("test.txt", .{ .read = true }); defer src_file.close(); - try src_file.pwritevAll(&write_vecs, 16); + var writer = src_file.writer(&.{}); + + try writer.seekTo(16); + try writer.interface.writeVecAll(&lines); + try writer.interface.flush(); try testing.expectEqual(@as(u64, 16 + line1.len + line2.len), try src_file.getEndPos()); - const read = try src_file.preadvAll(&read_vecs, 16); - try testing.expectEqual(@as(usize, line1.len + line2.len), read); + + var reader = writer.moveToReader(io); + try reader.seekTo(16); + try reader.interface.readVecAll(&read_vecs); try testing.expectEqualStrings(&buf1, "line2\n"); try testing.expectEqualStrings(&buf2, "line1\n"); + try testing.expectError(error.EndOfStream, reader.interface.readSliceAll(&buf1)); } test "setEndPos" { @@ -1406,6 +1389,8 @@ test "setEndPos" { if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest; if (builtin.cpu.arch.isMIPS64() and (builtin.abi == .gnuabin32 or builtin.abi == .muslabin32)) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/23806 + const io = testing.io; + var tmp = tmpDir(.{}); defer tmp.cleanup(); @@ -1416,11 +1401,13 @@ test "setEndPos" { const initial_size = try f.getEndPos(); var buffer: [32]u8 = undefined; + var reader = f.reader(io, &.{}); { try f.setEndPos(initial_size); try testing.expectEqual(initial_size, try f.getEndPos()); - try testing.expectEqual(initial_size, try f.preadAll(&buffer, 0)); + try reader.seekTo(0); + try testing.expectEqual(initial_size, try reader.interface.readSliceShort(&buffer)); try testing.expectEqualStrings("ninebytes", buffer[0..@intCast(initial_size)]); } @@ -1428,7 +1415,8 @@ test "setEndPos" { const larger = initial_size + 4; try f.setEndPos(larger); try testing.expectEqual(larger, try f.getEndPos()); - try testing.expectEqual(larger, try f.preadAll(&buffer, 0)); + try reader.seekTo(0); + try testing.expectEqual(larger, try reader.interface.readSliceShort(&buffer)); try testing.expectEqualStrings("ninebytes\x00\x00\x00\x00", buffer[0..@intCast(larger)]); } @@ -1436,27 +1424,15 @@ test "setEndPos" { const smaller = initial_size - 5; try f.setEndPos(smaller); try testing.expectEqual(smaller, try f.getEndPos()); - try testing.expectEqual(smaller, try f.preadAll(&buffer, 0)); + try reader.seekTo(0); + try testing.expectEqual(smaller, try reader.interface.readSliceShort(&buffer)); try testing.expectEqualStrings("nine", buffer[0..@intCast(smaller)]); } try f.setEndPos(0); try testing.expectEqual(0, try f.getEndPos()); - try testing.expectEqual(0, try f.preadAll(&buffer, 0)); - - // Invalid file length should error gracefully. Actual limit is host - // and file-system dependent, but 1PB should fail on filesystems like - // EXT4 and NTFS. But XFS or Btrfs support up to 8EiB files. - f.setEndPos(0x4_0000_0000_0000) catch |err| if (err != error.FileTooBig) { - return err; - }; - - f.setEndPos(std.math.maxInt(u63)) catch |err| if (err != error.FileTooBig) { - return err; - }; - - try testing.expectError(error.FileTooBig, f.setEndPos(std.math.maxInt(u63) + 1)); - try testing.expectError(error.FileTooBig, f.setEndPos(std.math.maxInt(u64))); + try reader.seekTo(0); + try testing.expectEqual(0, try reader.interface.readSliceShort(&buffer)); } test "access file" { @@ -1476,6 +1452,8 @@ test "access file" { } test "sendfile" { + const io = testing.io; + var tmp = tmpDir(.{}); defer tmp.cleanup(); @@ -1486,21 +1464,14 @@ test "sendfile" { const line1 = "line1\n"; const line2 = "second line\n"; - var vecs = [_]posix.iovec_const{ - .{ - .base = line1, - .len = line1.len, - }, - .{ - .base = line2, - .len = line2.len, - }, - }; + var vecs = [_][]const u8{ line1, line2 }; var src_file = try dir.createFile("sendfile1.txt", .{ .read = true }); defer src_file.close(); - - try src_file.writevAll(&vecs); + { + var fw = src_file.writer(&.{}); + try fw.interface.writeVecAll(&vecs); + } var dest_file = try dir.createFile("sendfile2.txt", .{ .read = true }); defer dest_file.close(); @@ -1513,7 +1484,7 @@ test "sendfile" { var trailers: [2][]const u8 = .{ trailer1, trailer2 }; var written_buf: [100]u8 = undefined; - var file_reader = src_file.reader(&.{}); + var file_reader = src_file.reader(io, &.{}); var fallback_buffer: [50]u8 = undefined; var file_writer = dest_file.writer(&fallback_buffer); try file_writer.interface.writeVecAll(&headers); @@ -1521,11 +1492,15 @@ test "sendfile" { try testing.expectEqual(10, try file_writer.interface.sendFileAll(&file_reader, .limited(10))); try file_writer.interface.writeVecAll(&trailers); try file_writer.interface.flush(); - const amt = try dest_file.preadAll(&written_buf, 0); + var fr = file_writer.moveToReader(io); + try fr.seekTo(0); + const amt = try fr.interface.readSliceShort(&written_buf); try testing.expectEqualStrings("header1\nsecond header\nine1\nsecontrailer1\nsecond trailer\n", written_buf[0..amt]); } test "sendfile with buffered data" { + const io = testing.io; + var tmp = tmpDir(.{}); defer tmp.cleanup(); @@ -1543,7 +1518,7 @@ test "sendfile with buffered data" { defer dest_file.close(); var src_buffer: [32]u8 = undefined; - var file_reader = src_file.reader(&src_buffer); + var file_reader = src_file.reader(io, &src_buffer); try file_reader.seekTo(0); try file_reader.interface.fill(8); @@ -1554,37 +1529,14 @@ test "sendfile with buffered data" { try std.testing.expectEqual(4, try file_writer.interface.sendFileAll(&file_reader, .limited(4))); var written_buf: [8]u8 = undefined; - const amt = try dest_file.preadAll(&written_buf, 0); + var fr = file_writer.moveToReader(io); + try fr.seekTo(0); + const amt = try fr.interface.readSliceShort(&written_buf); try std.testing.expectEqual(4, amt); try std.testing.expectEqualSlices(u8, "AAAA", written_buf[0..amt]); } -test "copyRangeAll" { - var tmp = tmpDir(.{}); - defer tmp.cleanup(); - - try tmp.dir.makePath("os_test_tmp"); - - var dir = try tmp.dir.openDir("os_test_tmp", .{}); - defer dir.close(); - - var src_file = try dir.createFile("file1.txt", .{ .read = true }); - defer src_file.close(); - - const data = "u6wj+JmdF3qHsFPE BUlH2g4gJCmEz0PP"; - try src_file.writeAll(data); - - var dest_file = try dir.createFile("file2.txt", .{ .read = true }); - defer dest_file.close(); - - var written_buf: [100]u8 = undefined; - _ = try src_file.copyRangeAll(0, dest_file, 0, data.len); - - const amt = try dest_file.preadAll(&written_buf, 0); - try testing.expectEqualStrings(data, written_buf[0..amt]); -} - test "copyFile" { try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { @@ -1708,8 +1660,8 @@ test "open file with exclusive lock twice, make sure second lock waits" { } }; - var started = std.Thread.ResetEvent{}; - var locked = std.Thread.ResetEvent{}; + var started: std.Thread.ResetEvent = .unset; + var locked: std.Thread.ResetEvent = .unset; const t = try std.Thread.spawn(.{}, S.checkFn, .{ &ctx.dir, @@ -1773,7 +1725,7 @@ test "read from locked file" { const f = try ctx.dir.createFile(filename, .{ .read = true }); defer f.close(); var buffer: [1]u8 = undefined; - _ = try f.readAll(&buffer); + _ = try f.read(&buffer); } { const f = try ctx.dir.createFile(filename, .{ @@ -1785,9 +1737,9 @@ test "read from locked file" { defer f2.close(); var buffer: [1]u8 = undefined; if (builtin.os.tag == .windows) { - try std.testing.expectError(error.LockViolation, f2.readAll(&buffer)); + try std.testing.expectError(error.LockViolation, f2.read(&buffer)); } else { - try std.testing.expectEqual(0, f2.readAll(&buffer)); + try std.testing.expectEqual(0, f2.read(&buffer)); } } } @@ -1944,6 +1896,7 @@ test "'.' and '..' in fs.Dir functions" { try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { + const io = ctx.io; const subdir_path = try ctx.transformPath("./subdir"); const file_path = try ctx.transformPath("./subdir/../file"); const copy_path = try ctx.transformPath("./subdir/../copy"); @@ -1966,8 +1919,9 @@ test "'.' and '..' in fs.Dir functions" { try ctx.dir.deleteFile(rename_path); try ctx.dir.writeFile(.{ .sub_path = update_path, .data = "something" }); - const prev_status = try ctx.dir.updateFile(file_path, ctx.dir, update_path, .{}); - try testing.expectEqual(fs.Dir.PrevStatus.stale, prev_status); + var dir = ctx.dir.adaptToNewApi(); + const prev_status = try dir.updateFile(io, file_path, dir, update_path, .{}); + try testing.expectEqual(Io.Dir.PrevStatus.stale, prev_status); try ctx.dir.deleteDir(subdir_path); } @@ -2005,13 +1959,6 @@ test "'.' and '..' in absolute functions" { renamed_file.close(); try fs.deleteFileAbsolute(renamed_file_path); - const update_file_path = try fs.path.join(allocator, &.{ subdir_path, "../update" }); - const update_file = try fs.createFileAbsolute(update_file_path, .{}); - try update_file.writeAll("something"); - update_file.close(); - const prev_status = try fs.updateFileAbsolute(created_file_path, update_file_path, .{}); - try testing.expectEqual(fs.Dir.PrevStatus.stale, prev_status); - try fs.deleteDirAbsolute(subdir_path); } @@ -2072,48 +2019,40 @@ test "delete a setAsCwd directory on Windows" { test "invalid UTF-8/WTF-8 paths" { const expected_err = switch (native_os) { - .wasi => error.InvalidUtf8, - .windows => error.InvalidWtf8, + .wasi => error.BadPathName, + .windows => error.BadPathName, else => return error.SkipZigTest, }; try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { + const io = ctx.io; // This is both invalid UTF-8 and WTF-8, since \xFF is an invalid start byte const invalid_path = try ctx.transformPath("\xFF"); try testing.expectError(expected_err, ctx.dir.openFile(invalid_path, .{})); - try testing.expectError(expected_err, ctx.dir.openFileZ(invalid_path, .{})); try testing.expectError(expected_err, ctx.dir.createFile(invalid_path, .{})); - try testing.expectError(expected_err, ctx.dir.createFileZ(invalid_path, .{})); try testing.expectError(expected_err, ctx.dir.makeDir(invalid_path)); - try testing.expectError(expected_err, ctx.dir.makeDirZ(invalid_path)); try testing.expectError(expected_err, ctx.dir.makePath(invalid_path)); try testing.expectError(expected_err, ctx.dir.makeOpenPath(invalid_path, .{})); try testing.expectError(expected_err, ctx.dir.openDir(invalid_path, .{})); - try testing.expectError(expected_err, ctx.dir.openDirZ(invalid_path, .{})); try testing.expectError(expected_err, ctx.dir.deleteFile(invalid_path)); - try testing.expectError(expected_err, ctx.dir.deleteFileZ(invalid_path)); try testing.expectError(expected_err, ctx.dir.deleteDir(invalid_path)); - try testing.expectError(expected_err, ctx.dir.deleteDirZ(invalid_path)); try testing.expectError(expected_err, ctx.dir.rename(invalid_path, invalid_path)); - try testing.expectError(expected_err, ctx.dir.renameZ(invalid_path, invalid_path)); try testing.expectError(expected_err, ctx.dir.symLink(invalid_path, invalid_path, .{})); - try testing.expectError(expected_err, ctx.dir.symLinkZ(invalid_path, invalid_path, .{})); if (native_os == .wasi) { try testing.expectError(expected_err, ctx.dir.symLinkWasi(invalid_path, invalid_path, .{})); } try testing.expectError(expected_err, ctx.dir.readLink(invalid_path, &[_]u8{})); - try testing.expectError(expected_err, ctx.dir.readLinkZ(invalid_path, &[_]u8{})); if (native_os == .wasi) { try testing.expectError(expected_err, ctx.dir.readLinkWasi(invalid_path, &[_]u8{})); } @@ -2127,47 +2066,34 @@ test "invalid UTF-8/WTF-8 paths" { try testing.expectError(expected_err, ctx.dir.writeFile(.{ .sub_path = invalid_path, .data = "" })); try testing.expectError(expected_err, ctx.dir.access(invalid_path, .{})); - try testing.expectError(expected_err, ctx.dir.accessZ(invalid_path, .{})); - try testing.expectError(expected_err, ctx.dir.updateFile(invalid_path, ctx.dir, invalid_path, .{})); + var dir = ctx.dir.adaptToNewApi(); + try testing.expectError(expected_err, dir.updateFile(io, invalid_path, dir, invalid_path, .{})); try testing.expectError(expected_err, ctx.dir.copyFile(invalid_path, ctx.dir, invalid_path, .{})); try testing.expectError(expected_err, ctx.dir.statFile(invalid_path)); if (native_os != .wasi) { try testing.expectError(expected_err, ctx.dir.realpath(invalid_path, &[_]u8{})); - try testing.expectError(expected_err, ctx.dir.realpathZ(invalid_path, &[_]u8{})); try testing.expectError(expected_err, ctx.dir.realpathAlloc(testing.allocator, invalid_path)); } try testing.expectError(expected_err, fs.rename(ctx.dir, invalid_path, ctx.dir, invalid_path)); - try testing.expectError(expected_err, fs.renameZ(ctx.dir, invalid_path, ctx.dir, invalid_path)); if (native_os != .wasi and ctx.path_type != .relative) { - try testing.expectError(expected_err, fs.updateFileAbsolute(invalid_path, invalid_path, .{})); try testing.expectError(expected_err, fs.copyFileAbsolute(invalid_path, invalid_path, .{})); try testing.expectError(expected_err, fs.makeDirAbsolute(invalid_path)); - try testing.expectError(expected_err, fs.makeDirAbsoluteZ(invalid_path)); try testing.expectError(expected_err, fs.deleteDirAbsolute(invalid_path)); - try testing.expectError(expected_err, fs.deleteDirAbsoluteZ(invalid_path)); try testing.expectError(expected_err, fs.renameAbsolute(invalid_path, invalid_path)); - try testing.expectError(expected_err, fs.renameAbsoluteZ(invalid_path, invalid_path)); try testing.expectError(expected_err, fs.openDirAbsolute(invalid_path, .{})); - try testing.expectError(expected_err, fs.openDirAbsoluteZ(invalid_path, .{})); try testing.expectError(expected_err, fs.openFileAbsolute(invalid_path, .{})); - try testing.expectError(expected_err, fs.openFileAbsoluteZ(invalid_path, .{})); try testing.expectError(expected_err, fs.accessAbsolute(invalid_path, .{})); - try testing.expectError(expected_err, fs.accessAbsoluteZ(invalid_path, .{})); try testing.expectError(expected_err, fs.createFileAbsolute(invalid_path, .{})); - try testing.expectError(expected_err, fs.createFileAbsoluteZ(invalid_path, .{})); try testing.expectError(expected_err, fs.deleteFileAbsolute(invalid_path)); - try testing.expectError(expected_err, fs.deleteFileAbsoluteZ(invalid_path)); try testing.expectError(expected_err, fs.deleteTreeAbsolute(invalid_path)); var readlink_buf: [fs.max_path_bytes]u8 = undefined; try testing.expectError(expected_err, fs.readLinkAbsolute(invalid_path, &readlink_buf)); - try testing.expectError(expected_err, fs.readLinkAbsoluteZ(invalid_path, &readlink_buf)); try testing.expectError(expected_err, fs.symLinkAbsolute(invalid_path, invalid_path, .{})); - try testing.expectError(expected_err, fs.symLinkAbsoluteZ(invalid_path, invalid_path, .{})); try testing.expectError(expected_err, fs.realpathAlloc(testing.allocator, invalid_path)); } } @@ -2175,6 +2101,8 @@ test "invalid UTF-8/WTF-8 paths" { } test "read file non vectored" { + const io = std.testing.io; + var tmp_dir = testing.tmpDir(.{}); defer tmp_dir.cleanup(); @@ -2188,7 +2116,7 @@ test "read file non vectored" { try file_writer.interface.flush(); } - var file_reader: std.fs.File.Reader = .init(file, &.{}); + var file_reader: std.Io.File.Reader = .initAdapted(file, io, &.{}); var write_buffer: [100]u8 = undefined; var w: std.Io.Writer = .fixed(&write_buffer); @@ -2205,6 +2133,8 @@ test "read file non vectored" { } test "seek keeping partial buffer" { + const io = std.testing.io; + var tmp_dir = testing.tmpDir(.{}); defer tmp_dir.cleanup(); @@ -2219,7 +2149,7 @@ test "seek keeping partial buffer" { } var read_buffer: [3]u8 = undefined; - var file_reader: std.fs.File.Reader = .init(file, &read_buffer); + var file_reader: Io.File.Reader = .initAdapted(file, io, &read_buffer); try testing.expectEqual(0, file_reader.logicalPos()); @@ -2246,13 +2176,15 @@ test "seek keeping partial buffer" { } test "seekBy" { + const io = testing.io; + var tmp_dir = testing.tmpDir(.{}); defer tmp_dir.cleanup(); try tmp_dir.dir.writeFile(.{ .sub_path = "blah.txt", .data = "let's test seekBy" }); const f = try tmp_dir.dir.openFile("blah.txt", .{ .mode = .read_only }); defer f.close(); - var reader = f.readerStreaming(&.{}); + var reader = f.readerStreaming(io, &.{}); try reader.seekBy(2); var buffer: [20]u8 = undefined; @@ -2265,6 +2197,8 @@ test "seekTo flushes buffered data" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); + const io = std.testing.io; + const contents = "data"; const file = try tmp.dir.createFile("seek.bin", .{ .read = true }); @@ -2279,7 +2213,7 @@ test "seekTo flushes buffered data" { } var read_buffer: [16]u8 = undefined; - var file_reader: std.fs.File.Reader = .init(file, &read_buffer); + var file_reader: std.Io.File.Reader = .initAdapted(file, io, &read_buffer); var buf: [4]u8 = undefined; try file_reader.interface.readSliceAll(&buf); @@ -2287,6 +2221,8 @@ test "seekTo flushes buffered data" { } test "File.Writer sendfile with buffered contents" { + const io = testing.io; + var tmp_dir = testing.tmpDir(.{}); defer tmp_dir.cleanup(); @@ -2298,7 +2234,7 @@ test "File.Writer sendfile with buffered contents" { defer out.close(); var in_buf: [2]u8 = undefined; - var in_r = in.reader(&in_buf); + var in_r = in.reader(io, &in_buf); _ = try in_r.getSize(); // Catch seeks past end by populating size try in_r.interface.fill(2); @@ -2312,7 +2248,7 @@ test "File.Writer sendfile with buffered contents" { var check = try tmp_dir.dir.openFile("b", .{}); defer check.close(); var check_buf: [4]u8 = undefined; - var check_r = check.reader(&check_buf); + var check_r = check.reader(io, &check_buf); try testing.expectEqualStrings("abcd", try check_r.interface.take(4)); try testing.expectError(error.EndOfStream, check_r.interface.takeByte()); } diff --git a/lib/std/hash_map.zig b/lib/std/hash_map.zig index 85424fe45e..83d4b5fae3 100644 --- a/lib/std/hash_map.zig +++ b/lib/std/hash_map.zig @@ -1827,9 +1827,9 @@ test "put and remove loop in random order" { } } -test "remove one million elements in random order" { +test "remove many elements in random order" { const Map = AutoHashMap(u32, u32); - const n = 1000 * 1000; + const n = 1000 * 100; var map = Map.init(std.heap.page_allocator); defer map.deinit(); @@ -2147,14 +2147,14 @@ test "getOrPut allocation failure" { try testing.expectError(error.OutOfMemory, map.getOrPut(std.testing.failing_allocator, "hello")); } -test "std.hash_map rehash" { +test "rehash" { var map = AutoHashMap(usize, usize).init(std.testing.allocator); defer map.deinit(); var prng = std.Random.DefaultPrng.init(0); const random = prng.random(); - const count = 6 * random.intRangeLessThan(u32, 100_000, 500_000); + const count = 4 * random.intRangeLessThan(u32, 100_000, 500_000); for (0..count) |i| { try map.put(i, i); diff --git a/lib/std/heap/debug_allocator.zig b/lib/std/heap/debug_allocator.zig index 8e66f722c3..4480009781 100644 --- a/lib/std/heap/debug_allocator.zig +++ b/lib/std/heap/debug_allocator.zig @@ -80,15 +80,15 @@ //! //! Resizing and remapping are forwarded directly to the backing allocator, //! except where such operations would change the category from large to small. +const builtin = @import("builtin"); +const StackTrace = std.builtin.StackTrace; const std = @import("std"); -const builtin = @import("builtin"); const log = std.log.scoped(.gpa); const math = std.math; const assert = std.debug.assert; const mem = std.mem; const Allocator = std.mem.Allocator; -const StackTrace = std.builtin.StackTrace; const default_page_size: usize = switch (builtin.os.tag) { // Makes `std.heap.PageAllocator` take the happy path. @@ -421,7 +421,12 @@ pub fn DebugAllocator(comptime config: Config) type { return usedBitsCount(slot_count) * @sizeOf(usize); } - fn detectLeaksInBucket(bucket: *BucketHeader, size_class_index: usize, used_bits_count: usize) usize { + fn detectLeaksInBucket( + bucket: *BucketHeader, + size_class_index: usize, + used_bits_count: usize, + tty_config: std.Io.tty.Config, + ) usize { const size_class = @as(usize, 1) << @as(Log2USize, @intCast(size_class_index)); const slot_count = slot_counts[size_class_index]; var leaks: usize = 0; @@ -436,7 +441,13 @@ pub fn DebugAllocator(comptime config: Config) type { const stack_trace = bucketStackTrace(bucket, slot_count, slot_index, .alloc); const page_addr = @intFromPtr(bucket) & ~(page_size - 1); const addr = page_addr + slot_index * size_class; - log.err("memory address 0x{x} leaked: {f}", .{ addr, stack_trace }); + log.err("memory address 0x{x} leaked: {f}", .{ + addr, + std.debug.FormatStackTrace{ + .stack_trace = stack_trace, + .tty_config = tty_config, + }, + }); leaks += 1; } } @@ -449,12 +460,14 @@ pub fn DebugAllocator(comptime config: Config) type { pub fn detectLeaks(self: *Self) usize { var leaks: usize = 0; + const tty_config = std.Io.tty.detectConfig(.stderr()); + for (self.buckets, 0..) |init_optional_bucket, size_class_index| { var optional_bucket = init_optional_bucket; const slot_count = slot_counts[size_class_index]; const used_bits_count = usedBitsCount(slot_count); while (optional_bucket) |bucket| { - leaks += detectLeaksInBucket(bucket, size_class_index, used_bits_count); + leaks += detectLeaksInBucket(bucket, size_class_index, used_bits_count, tty_config); optional_bucket = bucket.prev; } } @@ -464,7 +477,11 @@ pub fn DebugAllocator(comptime config: Config) type { if (config.retain_metadata and large_alloc.freed) continue; const stack_trace = large_alloc.getStackTrace(.alloc); log.err("memory address 0x{x} leaked: {f}", .{ - @intFromPtr(large_alloc.bytes.ptr), stack_trace, + @intFromPtr(large_alloc.bytes.ptr), + std.debug.FormatStackTrace{ + .stack_trace = stack_trace, + .tty_config = tty_config, + }, }); leaks += 1; } @@ -519,8 +536,20 @@ pub fn DebugAllocator(comptime config: Config) type { fn reportDoubleFree(ret_addr: usize, alloc_stack_trace: StackTrace, free_stack_trace: StackTrace) void { var addr_buf: [stack_n]usize = undefined; const second_free_stack_trace = std.debug.captureCurrentStackTrace(.{ .first_address = ret_addr }, &addr_buf); + const tty_config = std.Io.tty.detectConfig(.stderr()); log.err("Double free detected. Allocation: {f} First free: {f} Second free: {f}", .{ - alloc_stack_trace, free_stack_trace, second_free_stack_trace, + std.debug.FormatStackTrace{ + .stack_trace = alloc_stack_trace, + .tty_config = tty_config, + }, + std.debug.FormatStackTrace{ + .stack_trace = free_stack_trace, + .tty_config = tty_config, + }, + std.debug.FormatStackTrace{ + .stack_trace = second_free_stack_trace, + .tty_config = tty_config, + }, }); } @@ -561,11 +590,18 @@ pub fn DebugAllocator(comptime config: Config) type { if (config.safety and old_mem.len != entry.value_ptr.bytes.len) { var addr_buf: [stack_n]usize = undefined; const free_stack_trace = std.debug.captureCurrentStackTrace(.{ .first_address = ret_addr }, &addr_buf); + const tty_config = std.Io.tty.detectConfig(.stderr()); log.err("Allocation size {d} bytes does not match free size {d}. Allocation: {f} Free: {f}", .{ entry.value_ptr.bytes.len, old_mem.len, - entry.value_ptr.getStackTrace(.alloc), - free_stack_trace, + std.debug.FormatStackTrace{ + .stack_trace = entry.value_ptr.getStackTrace(.alloc), + .tty_config = tty_config, + }, + std.debug.FormatStackTrace{ + .stack_trace = free_stack_trace, + .tty_config = tty_config, + }, }); } @@ -667,11 +703,18 @@ pub fn DebugAllocator(comptime config: Config) type { if (config.safety and old_mem.len != entry.value_ptr.bytes.len) { var addr_buf: [stack_n]usize = undefined; const free_stack_trace = std.debug.captureCurrentStackTrace(.{ .first_address = ret_addr }, &addr_buf); + const tty_config = std.Io.tty.detectConfig(.stderr()); log.err("Allocation size {d} bytes does not match free size {d}. Allocation: {f} Free: {f}", .{ entry.value_ptr.bytes.len, old_mem.len, - entry.value_ptr.getStackTrace(.alloc), - free_stack_trace, + std.debug.FormatStackTrace{ + .stack_trace = entry.value_ptr.getStackTrace(.alloc), + .tty_config = tty_config, + }, + std.debug.FormatStackTrace{ + .stack_trace = free_stack_trace, + .tty_config = tty_config, + }, }); } @@ -892,19 +935,33 @@ pub fn DebugAllocator(comptime config: Config) type { var addr_buf: [stack_n]usize = undefined; const free_stack_trace = std.debug.captureCurrentStackTrace(.{ .first_address = return_address }, &addr_buf); if (old_memory.len != requested_size) { + const tty_config = std.Io.tty.detectConfig(.stderr()); log.err("Allocation size {d} bytes does not match free size {d}. Allocation: {f} Free: {f}", .{ requested_size, old_memory.len, - bucketStackTrace(bucket, slot_count, slot_index, .alloc), - free_stack_trace, + std.debug.FormatStackTrace{ + .stack_trace = bucketStackTrace(bucket, slot_count, slot_index, .alloc), + .tty_config = tty_config, + }, + std.debug.FormatStackTrace{ + .stack_trace = free_stack_trace, + .tty_config = tty_config, + }, }); } if (alignment != slot_alignment) { + const tty_config = std.Io.tty.detectConfig(.stderr()); log.err("Allocation alignment {d} does not match free alignment {d}. Allocation: {f} Free: {f}", .{ slot_alignment.toByteUnits(), alignment.toByteUnits(), - bucketStackTrace(bucket, slot_count, slot_index, .alloc), - free_stack_trace, + std.debug.FormatStackTrace{ + .stack_trace = bucketStackTrace(bucket, slot_count, slot_index, .alloc), + .tty_config = tty_config, + }, + std.debug.FormatStackTrace{ + .stack_trace = free_stack_trace, + .tty_config = tty_config, + }, }); } } @@ -987,19 +1044,33 @@ pub fn DebugAllocator(comptime config: Config) type { var addr_buf: [stack_n]usize = undefined; const free_stack_trace = std.debug.captureCurrentStackTrace(.{ .first_address = return_address }, &addr_buf); if (memory.len != requested_size) { + const tty_config = std.Io.tty.detectConfig(.stderr()); log.err("Allocation size {d} bytes does not match free size {d}. Allocation: {f} Free: {f}", .{ requested_size, memory.len, - bucketStackTrace(bucket, slot_count, slot_index, .alloc), - free_stack_trace, + std.debug.FormatStackTrace{ + .stack_trace = bucketStackTrace(bucket, slot_count, slot_index, .alloc), + .tty_config = tty_config, + }, + std.debug.FormatStackTrace{ + .stack_trace = free_stack_trace, + .tty_config = tty_config, + }, }); } if (alignment != slot_alignment) { + const tty_config = std.Io.tty.detectConfig(.stderr()); log.err("Allocation alignment {d} does not match free alignment {d}. Allocation: {f} Free: {f}", .{ slot_alignment.toByteUnits(), alignment.toByteUnits(), - bucketStackTrace(bucket, slot_count, slot_index, .alloc), - free_stack_trace, + std.debug.FormatStackTrace{ + .stack_trace = bucketStackTrace(bucket, slot_count, slot_index, .alloc), + .tty_config = tty_config, + }, + std.debug.FormatStackTrace{ + .stack_trace = free_stack_trace, + .tty_config = tty_config, + }, }); } } diff --git a/lib/std/http/Client.zig b/lib/std/http/Client.zig index 5d6a75cb28..431f239db3 100644 --- a/lib/std/http/Client.zig +++ b/lib/std/http/Client.zig @@ -9,12 +9,13 @@ const builtin = @import("builtin"); const testing = std.testing; const http = std.http; const mem = std.mem; -const net = std.net; const Uri = std.Uri; const Allocator = mem.Allocator; const assert = std.debug.assert; +const Io = std.Io; const Writer = std.Io.Writer; const Reader = std.Io.Reader; +const HostName = std.Io.net.HostName; const Client = @This(); @@ -22,6 +23,8 @@ pub const disable_tls = std.options.http_disable_tls; /// Used for all client allocations. Must be thread-safe. allocator: Allocator, +/// Used for opening TCP connections. +io: Io, ca_bundle: if (disable_tls) void else std.crypto.Certificate.Bundle = if (disable_tls) {} else .{}, ca_bundle_mutex: std.Thread.Mutex = .{}, @@ -32,9 +35,11 @@ tls_buffer_size: if (disable_tls) u0 else usize = if (disable_tls) 0 else std.cr /// traffic over connections created with this `Client`. ssl_key_log: ?*std.crypto.tls.Client.SslKeyLog = null, -/// When this is `true`, the next time this client performs an HTTPS request, -/// it will first rescan the system for root certificates. -next_https_rescan_certs: bool = true, +/// The time used to decide whether certificates are expired. +/// +/// When this is `null`, the next time this client performs an HTTPS request, +/// it will first check the time and rescan the system for root certificates. +now: ?Io.Timestamp = null, /// The pool of connections that can be reused (and currently in use). connection_pool: ConnectionPool = .{}, @@ -67,7 +72,7 @@ pub const ConnectionPool = struct { /// The criteria for a connection to be considered a match. pub const Criteria = struct { - host: []const u8, + host: HostName, port: u16, protocol: Protocol, }; @@ -87,7 +92,7 @@ pub const ConnectionPool = struct { if (connection.port != criteria.port) continue; // Domain names are case-insensitive (RFC 5890, Section 2.3.2.4) - if (!std.ascii.eqlIgnoreCase(connection.host(), criteria.host)) continue; + if (!connection.host().eql(criteria.host)) continue; pool.acquireUnsafe(connection); return connection; @@ -116,19 +121,19 @@ pub const ConnectionPool = struct { /// If the connection is marked as closing, it will be closed instead. /// /// Threadsafe. - pub fn release(pool: *ConnectionPool, connection: *Connection) void { + pub fn release(pool: *ConnectionPool, connection: *Connection, io: Io) void { pool.mutex.lock(); defer pool.mutex.unlock(); pool.used.remove(&connection.pool_node); - if (connection.closing or pool.free_size == 0) return connection.destroy(); + if (connection.closing or pool.free_size == 0) return connection.destroy(io); if (pool.free_len >= pool.free_size) { const popped: *Connection = @alignCast(@fieldParentPtr("pool_node", pool.free.popFirst().?)); pool.free_len -= 1; - popped.destroy(); + popped.destroy(io); } if (connection.proxied) { @@ -176,21 +181,21 @@ pub const ConnectionPool = struct { /// All future operations on the connection pool will deadlock. /// /// Threadsafe. - pub fn deinit(pool: *ConnectionPool) void { + pub fn deinit(pool: *ConnectionPool, io: Io) void { pool.mutex.lock(); var next = pool.free.first; while (next) |node| { const connection: *Connection = @alignCast(@fieldParentPtr("pool_node", node)); next = node.next; - connection.destroy(); + connection.destroy(io); } next = pool.used.first; while (next) |node| { const connection: *Connection = @alignCast(@fieldParentPtr("pool_node", node)); next = node.next; - connection.destroy(); + connection.destroy(io); } pool.* = undefined; @@ -225,8 +230,8 @@ pub const Protocol = enum { pub const Connection = struct { client: *Client, - stream_writer: net.Stream.Writer, - stream_reader: net.Stream.Reader, + stream_writer: Io.net.Stream.Writer, + stream_reader: Io.net.Stream.Reader, /// Entry in `ConnectionPool.used` or `ConnectionPool.free`. pool_node: std.DoublyLinkedList.Node, port: u16, @@ -240,28 +245,29 @@ pub const Connection = struct { fn create( client: *Client, - remote_host: []const u8, + remote_host: HostName, port: u16, - stream: net.Stream, + stream: Io.net.Stream, ) error{OutOfMemory}!*Plain { + const io = client.io; const gpa = client.allocator; - const alloc_len = allocLen(client, remote_host.len); + const alloc_len = allocLen(client, remote_host.bytes.len); const base = try gpa.alignedAlloc(u8, .of(Plain), alloc_len); errdefer gpa.free(base); - const host_buffer = base[@sizeOf(Plain)..][0..remote_host.len]; + const host_buffer = base[@sizeOf(Plain)..][0..remote_host.bytes.len]; const socket_read_buffer = host_buffer.ptr[host_buffer.len..][0..client.read_buffer_size]; const socket_write_buffer = socket_read_buffer.ptr[socket_read_buffer.len..][0..client.write_buffer_size]; assert(base.ptr + alloc_len == socket_write_buffer.ptr + socket_write_buffer.len); - @memcpy(host_buffer, remote_host); + @memcpy(host_buffer, remote_host.bytes); const plain: *Plain = @ptrCast(base); plain.* = .{ .connection = .{ .client = client, - .stream_writer = stream.writer(socket_write_buffer), - .stream_reader = stream.reader(socket_read_buffer), + .stream_writer = stream.writer(io, socket_write_buffer), + .stream_reader = stream.reader(io, socket_read_buffer), .pool_node = .{}, .port = port, - .host_len = @intCast(remote_host.len), + .host_len = @intCast(remote_host.bytes.len), .proxied = false, .closing = false, .protocol = .plain, @@ -281,9 +287,9 @@ pub const Connection = struct { return @sizeOf(Plain) + host_len + client.read_buffer_size + client.write_buffer_size; } - fn host(plain: *Plain) []u8 { + fn host(plain: *Plain) HostName { const base: [*]u8 = @ptrCast(plain); - return base[@sizeOf(Plain)..][0..plain.connection.host_len]; + return .{ .bytes = base[@sizeOf(Plain)..][0..plain.connection.host_len] }; } }; @@ -291,17 +297,19 @@ pub const Connection = struct { client: std.crypto.tls.Client, connection: Connection, + /// Asserts that `client.now` is non-null. fn create( client: *Client, - remote_host: []const u8, + remote_host: HostName, port: u16, - stream: net.Stream, - ) error{ OutOfMemory, TlsInitializationFailed }!*Tls { + stream: Io.net.Stream, + ) !*Tls { + const io = client.io; const gpa = client.allocator; - const alloc_len = allocLen(client, remote_host.len); + const alloc_len = allocLen(client, remote_host.bytes.len); const base = try gpa.alignedAlloc(u8, .of(Tls), alloc_len); errdefer gpa.free(base); - const host_buffer = base[@sizeOf(Tls)..][0..remote_host.len]; + const host_buffer = base[@sizeOf(Tls)..][0..remote_host.bytes.len]; // The TLS client wants enough buffer for the max encrypted frame // size, and the HTTP body reader wants enough buffer for the // entire HTTP header. This means we need a combined upper bound. @@ -311,35 +319,43 @@ pub const Connection = struct { const socket_write_buffer = tls_write_buffer.ptr[tls_write_buffer.len..][0..client.write_buffer_size]; const socket_read_buffer = socket_write_buffer.ptr[socket_write_buffer.len..][0..client.tls_buffer_size]; assert(base.ptr + alloc_len == socket_read_buffer.ptr + socket_read_buffer.len); - @memcpy(host_buffer, remote_host); + @memcpy(host_buffer, remote_host.bytes); const tls: *Tls = @ptrCast(base); + var random_buffer: [176]u8 = undefined; + std.crypto.random.bytes(&random_buffer); tls.* = .{ .connection = .{ .client = client, - .stream_writer = stream.writer(tls_write_buffer), - .stream_reader = stream.reader(socket_read_buffer), + .stream_writer = stream.writer(io, tls_write_buffer), + .stream_reader = stream.reader(io, socket_read_buffer), .pool_node = .{}, .port = port, - .host_len = @intCast(remote_host.len), + .host_len = @intCast(remote_host.bytes.len), .proxied = false, .closing = false, .protocol = .tls, }, - // TODO data race here on ca_bundle if the user sets next_https_rescan_certs to true + // TODO data race here on ca_bundle if the user sets `now` to null .client = std.crypto.tls.Client.init( - tls.connection.stream_reader.interface(), + &tls.connection.stream_reader.interface, &tls.connection.stream_writer.interface, .{ - .host = .{ .explicit = remote_host }, + .host = .{ .explicit = remote_host.bytes }, .ca = .{ .bundle = client.ca_bundle }, .ssl_key_log = client.ssl_key_log, .read_buffer = tls_read_buffer, .write_buffer = socket_write_buffer, + .entropy = &random_buffer, + .realtime_now_seconds = client.now.?.toSeconds(), // This is appropriate for HTTPS because the HTTP headers contain // the content length which is used to detect truncation attacks. .allow_truncation_attacks = true, }, - ) catch return error.TlsInitializationFailed, + ) catch |err| switch (err) { + error.WriteFailed => return tls.connection.stream_writer.err.?, + error.ReadFailed => return tls.connection.stream_reader.err.?, + else => |e| return e, + }, }; return tls; } @@ -357,32 +373,32 @@ pub const Connection = struct { client.write_buffer_size + client.tls_buffer_size; } - fn host(tls: *Tls) []u8 { + fn host(tls: *Tls) HostName { const base: [*]u8 = @ptrCast(tls); - return base[@sizeOf(Tls)..][0..tls.connection.host_len]; + return .{ .bytes = base[@sizeOf(Tls)..][0..tls.connection.host_len] }; } }; - pub const ReadError = std.crypto.tls.Client.ReadError || std.net.Stream.ReadError; + pub const ReadError = std.crypto.tls.Client.ReadError || Io.net.Stream.Reader.Error; pub fn getReadError(c: *const Connection) ?ReadError { return switch (c.protocol) { .tls => { if (disable_tls) unreachable; const tls: *const Tls = @alignCast(@fieldParentPtr("connection", c)); - return tls.client.read_err orelse c.stream_reader.getError(); + return tls.client.read_err orelse c.stream_reader.err.?; }, .plain => { - return c.stream_reader.getError(); + return c.stream_reader.err.?; }, }; } - fn getStream(c: *Connection) net.Stream { - return c.stream_reader.getStream(); + fn getStream(c: *Connection) Io.net.Stream { + return c.stream_reader.stream; } - pub fn host(c: *Connection) []u8 { + pub fn host(c: *Connection) HostName { return switch (c.protocol) { .tls => { if (disable_tls) unreachable; @@ -398,8 +414,8 @@ pub const Connection = struct { /// If this is called without calling `flush` or `end`, data will be /// dropped unsent. - pub fn destroy(c: *Connection) void { - c.getStream().close(); + pub fn destroy(c: *Connection, io: Io) void { + c.stream_reader.stream.close(io); switch (c.protocol) { .tls => { if (disable_tls) unreachable; @@ -435,7 +451,7 @@ pub const Connection = struct { const tls: *Tls = @alignCast(@fieldParentPtr("connection", c)); return &tls.client.reader; }, - .plain => c.stream_reader.interface(), + .plain => &c.stream_reader.interface, }; } @@ -864,6 +880,7 @@ pub const Request = struct { /// Returns the request's `Connection` back to the pool of the `Client`. pub fn deinit(r: *Request) void { + const io = r.client.io; if (r.connection) |connection| { connection.closing = connection.closing or switch (r.reader.state) { .ready => false, @@ -878,7 +895,7 @@ pub const Request = struct { }, else => true, }; - r.client.connection_pool.release(connection); + r.client.connection_pool.release(connection, io); } r.* = undefined; } @@ -1180,6 +1197,7 @@ pub const Request = struct { /// /// `aux_buf` must outlive accesses to `Request.uri`. fn redirect(r: *Request, head: *const Response.Head, aux_buf: *[]u8) !void { + const io = r.client.io; const new_location = head.location orelse return error.HttpRedirectLocationMissing; if (new_location.len > aux_buf.*.len) return error.HttpRedirectLocationOversize; const location = aux_buf.*[0..new_location.len]; @@ -1196,19 +1214,20 @@ pub const Request = struct { error.UnexpectedCharacter => return error.HttpRedirectLocationInvalid, error.InvalidFormat => return error.HttpRedirectLocationInvalid, error.InvalidPort => return error.HttpRedirectLocationInvalid, + error.InvalidHostName => return error.HttpRedirectLocationInvalid, error.NoSpaceLeft => return error.HttpRedirectLocationOversize, }; const protocol = Protocol.fromUri(new_uri) orelse return error.UnsupportedUriScheme; const old_connection = r.connection.?; const old_host = old_connection.host(); - var new_host_name_buffer: [Uri.host_name_max]u8 = undefined; + var new_host_name_buffer: [HostName.max_len]u8 = undefined; const new_host = try new_uri.getHost(&new_host_name_buffer); const keep_privileged_headers = std.ascii.eqlIgnoreCase(r.uri.scheme, new_uri.scheme) and - sameParentDomain(old_host, new_host); + old_host.sameParentDomain(new_host); - r.client.connection_pool.release(old_connection); + r.client.connection_pool.release(old_connection, io); r.connection = null; if (!keep_privileged_headers) { @@ -1264,7 +1283,7 @@ pub const Request = struct { pub const Proxy = struct { protocol: Protocol, - host: []const u8, + host: HostName, authorization: ?[]const u8, port: u16, supports_connect: bool, @@ -1275,9 +1294,10 @@ pub const Proxy = struct { /// All pending requests must be de-initialized and all active connections released /// before calling this function. pub fn deinit(client: *Client) void { + const io = client.io; assert(client.connection_pool.used.first == null); // There are still active requests. - client.connection_pool.deinit(); + client.connection_pool.deinit(io); if (!disable_tls) client.ca_bundle.deinit(client.allocator); client.* = undefined; @@ -1383,25 +1403,16 @@ pub const basic_authorization = struct { } }; -pub const ConnectTcpError = Allocator.Error || error{ - ConnectionRefused, - NetworkUnreachable, - ConnectionTimedOut, - ConnectionResetByPeer, - TemporaryNameServerFailure, - NameServerFailure, - UnknownHostName, - HostLacksNetworkAddresses, - UnexpectedConnectFailure, +pub const ConnectTcpError = error{ TlsInitializationFailed, -}; +} || Allocator.Error || HostName.ConnectError; /// Reuses a `Connection` if one matching `host` and `port` is already open. /// /// Threadsafe. pub fn connectTcp( client: *Client, - host: []const u8, + host: HostName, port: u16, protocol: Protocol, ) ConnectTcpError!*Connection { @@ -1409,15 +1420,17 @@ pub fn connectTcp( } pub const ConnectTcpOptions = struct { - host: []const u8, + host: HostName, port: u16, protocol: Protocol, - proxied_host: ?[]const u8 = null, + proxied_host: ?HostName = null, proxied_port: ?u16 = null, + timeout: Io.Timeout = .none, }; pub fn connectTcpOptions(client: *Client, options: ConnectTcpOptions) ConnectTcpError!*Connection { + const io = client.io; const host = options.host; const port = options.port; const protocol = options.protocol; @@ -1431,23 +1444,18 @@ pub fn connectTcpOptions(client: *Client, options: ConnectTcpOptions) ConnectTcp .protocol = protocol, })) |conn| return conn; - const stream = net.tcpConnectToHost(client.allocator, host, port) catch |err| switch (err) { - error.ConnectionRefused => return error.ConnectionRefused, - error.NetworkUnreachable => return error.NetworkUnreachable, - error.ConnectionTimedOut => return error.ConnectionTimedOut, - error.ConnectionResetByPeer => return error.ConnectionResetByPeer, - error.TemporaryNameServerFailure => return error.TemporaryNameServerFailure, - error.NameServerFailure => return error.NameServerFailure, - error.UnknownHostName => return error.UnknownHostName, - error.HostLacksNetworkAddresses => return error.HostLacksNetworkAddresses, - else => return error.UnexpectedConnectFailure, - }; - errdefer stream.close(); + var stream = try host.connect(io, port, .{ .mode = .stream }); + errdefer stream.close(io); switch (protocol) { .tls => { if (disable_tls) return error.TlsInitializationFailed; - const tc = try Connection.Tls.create(client, proxied_host, proxied_port, stream); + const tc = Connection.Tls.create(client, proxied_host, proxied_port, stream) catch |err| switch (err) { + error.OutOfMemory => |e| return e, + error.Unexpected => |e| return e, + error.Canceled => |e| return e, + else => return error.TlsInitializationFailed, + }; client.connection_pool.addUsed(&tc.connection); return &tc.connection; }, @@ -1476,7 +1484,7 @@ pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connecti errdefer client.allocator.destroy(conn); conn.* = .{ .data = undefined }; - const stream = try std.net.connectUnixSocket(path); + const stream = try Io.net.connectUnixSocket(path); errdefer stream.close(); conn.data = .{ @@ -1501,9 +1509,10 @@ pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connecti pub fn connectProxied( client: *Client, proxy: *Proxy, - proxied_host: []const u8, + proxied_host: HostName, proxied_port: u16, ) !*Connection { + const io = client.io; if (!proxy.supports_connect) return error.TunnelNotSupported; if (client.connection_pool.findConnection(.{ @@ -1523,12 +1532,12 @@ pub fn connectProxied( }); errdefer { connection.closing = true; - client.connection_pool.release(connection); + client.connection_pool.release(connection, io); } var req = client.request(.CONNECT, .{ .scheme = "http", - .host = .{ .raw = proxied_host }, + .host = .{ .raw = proxied_host.bytes }, .port = proxied_port, }, .{ .redirect_behavior = .unhandled, @@ -1573,7 +1582,7 @@ pub const ConnectError = ConnectTcpError || RequestError; /// This function is threadsafe. pub fn connect( client: *Client, - host: []const u8, + host: HostName, port: u16, protocol: Protocol, ) ConnectError!*Connection { @@ -1583,9 +1592,7 @@ pub fn connect( } orelse return client.connectTcp(host, port, protocol); // Prevent proxying through itself. - if (std.ascii.eqlIgnoreCase(proxy.host, host) and - proxy.port == port and proxy.protocol == protocol) - { + if (proxy.host.eql(host) and proxy.port == port and proxy.protocol == protocol) { return client.connectTcp(host, port, protocol); } @@ -1605,7 +1612,6 @@ pub fn connect( pub const RequestError = ConnectTcpError || error{ UnsupportedUriScheme, UriMissingHost, - UriHostTooLong, CertificateBundleLoadFailure, }; @@ -1663,6 +1669,8 @@ pub fn request( uri: Uri, options: RequestOptions, ) RequestError!Request { + const io = client.io; + if (std.debug.runtime_safety) { for (options.extra_headers) |header| { assert(header.name.len != 0); @@ -1681,20 +1689,21 @@ pub fn request( if (protocol == .tls) { if (disable_tls) unreachable; - if (@atomicLoad(bool, &client.next_https_rescan_certs, .acquire)) { + { client.ca_bundle_mutex.lock(); defer client.ca_bundle_mutex.unlock(); - if (client.next_https_rescan_certs) { - client.ca_bundle.rescan(client.allocator) catch + if (client.now == null) { + const now = try Io.Clock.real.now(io); + client.now = now; + client.ca_bundle.rescan(client.allocator, io, now) catch return error.CertificateBundleLoadFailure; - @atomicStore(bool, &client.next_https_rescan_certs, false, .release); } } } const connection = options.connection orelse c: { - var host_name_buffer: [Uri.host_name_max]u8 = undefined; + var host_name_buffer: [HostName.max_len]u8 = undefined; const host_name = try uri.getHost(&host_name_buffer); break :c try client.connect(host_name, uriPort(uri, protocol), protocol); }; @@ -1832,20 +1841,6 @@ pub fn fetch(client: *Client, options: FetchOptions) FetchError!FetchResult { return .{ .status = response.head.status }; } -pub fn sameParentDomain(parent_host: []const u8, child_host: []const u8) bool { - if (!std.ascii.endsWithIgnoreCase(child_host, parent_host)) return false; - if (child_host.len == parent_host.len) return true; - if (parent_host.len > child_host.len) return false; - return child_host[child_host.len - parent_host.len - 1] == '.'; -} - -test sameParentDomain { - try testing.expect(!sameParentDomain("foo.com", "bar.com")); - try testing.expect(sameParentDomain("foo.com", "foo.com")); - try testing.expect(sameParentDomain("foo.com", "bar.foo.com")); - try testing.expect(!sameParentDomain("bar.foo.com", "foo.com")); -} - test { _ = Response; } diff --git a/lib/std/http/Server.zig b/lib/std/http/Server.zig index b64253f975..084f2f5855 100644 --- a/lib/std/http/Server.zig +++ b/lib/std/http/Server.zig @@ -688,7 +688,7 @@ pub const WebSocket = struct { pub const ReadSmallTextMessageError = error{ ConnectionClose, UnexpectedOpCode, - MessageTooBig, + MessageOversize, MissingMaskBit, ReadFailed, EndOfStream, @@ -717,15 +717,15 @@ pub const WebSocket = struct { _ => return error.UnexpectedOpCode, } - if (!h0.fin) return error.MessageTooBig; + if (!h0.fin) return error.MessageOversize; if (!h1.mask) return error.MissingMaskBit; const len: usize = switch (h1.payload_len) { .len16 => try in.takeInt(u16, .big), - .len64 => std.math.cast(usize, try in.takeInt(u64, .big)) orelse return error.MessageTooBig, + .len64 => std.math.cast(usize, try in.takeInt(u64, .big)) orelse return error.MessageOversize, else => @intFromEnum(h1.payload_len), }; - if (len > in.buffer.len) return error.MessageTooBig; + if (len > in.buffer.len) return error.MessageOversize; const mask: u32 = @bitCast((try in.takeArray(4)).*); const payload = try in.take(len); diff --git a/lib/std/http/test.zig b/lib/std/http/test.zig index fac241ede4..08cafc4d5a 100644 --- a/lib/std/http/test.zig +++ b/lib/std/http/test.zig @@ -1,27 +1,36 @@ const builtin = @import("builtin"); +const native_endian = builtin.cpu.arch.endian(); + const std = @import("std"); const http = std.http; const mem = std.mem; -const native_endian = builtin.cpu.arch.endian(); +const net = std.Io.net; +const Io = std.Io; const expect = std.testing.expect; const expectEqual = std.testing.expectEqual; const expectEqualStrings = std.testing.expectEqualStrings; const expectError = std.testing.expectError; test "trailers" { - const test_server = try createTestServer(struct { + if (builtin.cpu.arch == .arm) { + // https://github.com/ziglang/zig/issues/25762 + return error.SkipZigTest; + } + + const io = std.testing.io; + const test_server = try createTestServer(io, struct { fn run(test_server: *TestServer) anyerror!void { const net_server = &test_server.net_server; var recv_buffer: [1024]u8 = undefined; var send_buffer: [1024]u8 = undefined; var remaining: usize = 1; while (remaining != 0) : (remaining -= 1) { - const connection = try net_server.accept(); - defer connection.stream.close(); + var stream = try net_server.accept(io); + defer stream.close(io); - var connection_br = connection.stream.reader(&recv_buffer); - var connection_bw = connection.stream.writer(&send_buffer); - var server = http.Server.init(connection_br.interface(), &connection_bw.interface); + var connection_br = stream.reader(io, &recv_buffer); + var connection_bw = stream.writer(io, &send_buffer); + var server = http.Server.init(&connection_br.interface, &connection_bw.interface); try expectEqual(.ready, server.reader.state); var request = try server.receiveHead(); @@ -49,7 +58,7 @@ test "trailers" { const gpa = std.testing.allocator; - var client: http.Client = .{ .allocator = gpa }; + var client: http.Client = .{ .allocator = gpa, .io = io }; defer client.deinit(); const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/trailer", .{ @@ -92,17 +101,18 @@ test "trailers" { } test "HTTP server handles a chunked transfer coding request" { - const test_server = try createTestServer(struct { + const io = std.testing.io; + const test_server = try createTestServer(io, struct { fn run(test_server: *TestServer) anyerror!void { const net_server = &test_server.net_server; var recv_buffer: [8192]u8 = undefined; var send_buffer: [500]u8 = undefined; - const connection = try net_server.accept(); - defer connection.stream.close(); + var stream = try net_server.accept(io); + defer stream.close(io); - var connection_br = connection.stream.reader(&recv_buffer); - var connection_bw = connection.stream.writer(&send_buffer); - var server = http.Server.init(connection_br.interface(), &connection_bw.interface); + var connection_br = stream.reader(io, &recv_buffer); + var connection_bw = stream.writer(io, &send_buffer); + var server = http.Server.init(&connection_br.interface, &connection_bw.interface); var request = try server.receiveHead(); try expect(request.head.transfer_encoding == .chunked); @@ -136,12 +146,13 @@ test "HTTP server handles a chunked transfer coding request" { "0\r\n" ++ "\r\n"; - const gpa = std.testing.allocator; - const stream = try std.net.tcpConnectToHost(gpa, "127.0.0.1", test_server.port()); - defer stream.close(); - var stream_writer = stream.writer(&.{}); + const host_name: net.HostName = try .init("127.0.0.1"); + var stream = try host_name.connect(io, test_server.port(), .{ .mode = .stream }); + defer stream.close(io); + var stream_writer = stream.writer(io, &.{}); try stream_writer.interface.writeAll(request_bytes); + const gpa = std.testing.allocator; const expected_response = "HTTP/1.1 200 OK\r\n" ++ "connection: close\r\n" ++ @@ -149,26 +160,27 @@ test "HTTP server handles a chunked transfer coding request" { "content-type: text/plain\r\n" ++ "\r\n" ++ "message from server!\n"; - var stream_reader = stream.reader(&.{}); - const response = try stream_reader.interface().allocRemaining(gpa, .limited(expected_response.len + 1)); + var stream_reader = stream.reader(io, &.{}); + const response = try stream_reader.interface.allocRemaining(gpa, .limited(expected_response.len + 1)); defer gpa.free(response); try expectEqualStrings(expected_response, response); } test "echo content server" { - const test_server = try createTestServer(struct { + const io = std.testing.io; + const test_server = try createTestServer(io, struct { fn run(test_server: *TestServer) anyerror!void { const net_server = &test_server.net_server; var recv_buffer: [1024]u8 = undefined; var send_buffer: [100]u8 = undefined; accept: while (!test_server.shutting_down) { - const connection = try net_server.accept(); - defer connection.stream.close(); + var stream = try net_server.accept(io); + defer stream.close(io); - var connection_br = connection.stream.reader(&recv_buffer); - var connection_bw = connection.stream.writer(&send_buffer); - var http_server = http.Server.init(connection_br.interface(), &connection_bw.interface); + var connection_br = stream.reader(io, &recv_buffer); + var connection_bw = stream.writer(io, &send_buffer); + var http_server = http.Server.init(&connection_br.interface, &connection_bw.interface); while (http_server.reader.state == .ready) { var request = http_server.receiveHead() catch |err| switch (err) { @@ -235,7 +247,7 @@ test "echo content server" { defer test_server.destroy(); { - var client: http.Client = .{ .allocator = std.testing.allocator }; + var client: http.Client = .{ .allocator = std.testing.allocator, .io = io }; defer client.deinit(); try echoTests(&client, test_server.port()); @@ -243,6 +255,8 @@ test "echo content server" { } test "Server.Request.respondStreaming non-chunked, unknown content-length" { + const io = std.testing.io; + if (builtin.os.tag == .windows) { // https://github.com/ziglang/zig/issues/21457 return error.SkipZigTest; @@ -250,19 +264,19 @@ test "Server.Request.respondStreaming non-chunked, unknown content-length" { // In this case, the response is expected to stream until the connection is // closed, indicating the end of the body. - const test_server = try createTestServer(struct { + const test_server = try createTestServer(io, struct { fn run(test_server: *TestServer) anyerror!void { const net_server = &test_server.net_server; var recv_buffer: [1000]u8 = undefined; var send_buffer: [500]u8 = undefined; var remaining: usize = 1; while (remaining != 0) : (remaining -= 1) { - const connection = try net_server.accept(); - defer connection.stream.close(); + var stream = try net_server.accept(io); + defer stream.close(io); - var connection_br = connection.stream.reader(&recv_buffer); - var connection_bw = connection.stream.writer(&send_buffer); - var server = http.Server.init(connection_br.interface(), &connection_bw.interface); + var connection_br = stream.reader(io, &recv_buffer); + var connection_bw = stream.writer(io, &send_buffer); + var server = http.Server.init(&connection_br.interface, &connection_bw.interface); try expectEqual(.ready, server.reader.state); var request = try server.receiveHead(); @@ -286,14 +300,15 @@ test "Server.Request.respondStreaming non-chunked, unknown content-length" { defer test_server.destroy(); const request_bytes = "GET /foo HTTP/1.1\r\n\r\n"; - const gpa = std.testing.allocator; - const stream = try std.net.tcpConnectToHost(gpa, "127.0.0.1", test_server.port()); - defer stream.close(); - var stream_writer = stream.writer(&.{}); + const host_name: net.HostName = try .init("127.0.0.1"); + var stream = try host_name.connect(io, test_server.port(), .{ .mode = .stream }); + defer stream.close(io); + var stream_writer = stream.writer(io, &.{}); try stream_writer.interface.writeAll(request_bytes); - var stream_reader = stream.reader(&.{}); - const response = try stream_reader.interface().allocRemaining(gpa, .unlimited); + var stream_reader = stream.reader(io, &.{}); + const gpa = std.testing.allocator; + const response = try stream_reader.interface.allocRemaining(gpa, .unlimited); defer gpa.free(response); var expected_response = std.array_list.Managed(u8).init(gpa); @@ -316,19 +331,21 @@ test "Server.Request.respondStreaming non-chunked, unknown content-length" { } test "receiving arbitrary http headers from the client" { - const test_server = try createTestServer(struct { + const io = std.testing.io; + + const test_server = try createTestServer(io, struct { fn run(test_server: *TestServer) anyerror!void { const net_server = &test_server.net_server; var recv_buffer: [666]u8 = undefined; var send_buffer: [777]u8 = undefined; var remaining: usize = 1; while (remaining != 0) : (remaining -= 1) { - const connection = try net_server.accept(); - defer connection.stream.close(); + var stream = try net_server.accept(io); + defer stream.close(io); - var connection_br = connection.stream.reader(&recv_buffer); - var connection_bw = connection.stream.writer(&send_buffer); - var server = http.Server.init(connection_br.interface(), &connection_bw.interface); + var connection_br = stream.reader(io, &recv_buffer); + var connection_bw = stream.writer(io, &send_buffer); + var server = http.Server.init(&connection_br.interface, &connection_bw.interface); try expectEqual(.ready, server.reader.state); var request = try server.receiveHead(); @@ -356,14 +373,15 @@ test "receiving arbitrary http headers from the client" { "CoNneCtIoN:close\r\n" ++ "aoeu: asdf \r\n" ++ "\r\n"; - const gpa = std.testing.allocator; - const stream = try std.net.tcpConnectToHost(gpa, "127.0.0.1", test_server.port()); - defer stream.close(); - var stream_writer = stream.writer(&.{}); + const host_name: net.HostName = try .init("127.0.0.1"); + var stream = try host_name.connect(io, test_server.port(), .{ .mode = .stream }); + defer stream.close(io); + var stream_writer = stream.writer(io, &.{}); try stream_writer.interface.writeAll(request_bytes); - var stream_reader = stream.reader(&.{}); - const response = try stream_reader.interface().allocRemaining(gpa, .unlimited); + var stream_reader = stream.reader(io, &.{}); + const gpa = std.testing.allocator; + const response = try stream_reader.interface.allocRemaining(gpa, .unlimited); defer gpa.free(response); var expected_response = std.array_list.Managed(u8).init(gpa); @@ -376,24 +394,26 @@ test "receiving arbitrary http headers from the client" { } test "general client/server API coverage" { + const io = std.testing.io; + if (builtin.os.tag == .windows) { // This test was never passing on Windows. return error.SkipZigTest; } - const test_server = try createTestServer(struct { + const test_server = try createTestServer(io, struct { fn run(test_server: *TestServer) anyerror!void { const net_server = &test_server.net_server; var recv_buffer: [1024]u8 = undefined; var send_buffer: [100]u8 = undefined; outer: while (!test_server.shutting_down) { - var connection = try net_server.accept(); - defer connection.stream.close(); + var stream = try net_server.accept(io); + defer stream.close(io); - var connection_br = connection.stream.reader(&recv_buffer); - var connection_bw = connection.stream.writer(&send_buffer); - var http_server = http.Server.init(connection_br.interface(), &connection_bw.interface); + var connection_br = stream.reader(io, &recv_buffer); + var connection_bw = stream.writer(io, &send_buffer); + var http_server = http.Server.init(&connection_br.interface, &connection_bw.interface); while (http_server.reader.state == .ready) { var request = http_server.receiveHead() catch |err| switch (err) { @@ -401,7 +421,7 @@ test "general client/server API coverage" { else => |e| return e, }; - try handleRequest(&request, net_server.listen_address.getPort()); + try handleRequest(&request, net_server.socket.address.getPort()); } } } @@ -530,10 +550,10 @@ test "general client/server API coverage" { } fn getUnusedTcpPort() !u16 { - const addr = try std.net.Address.parseIp("127.0.0.1", 0); - var s = try addr.listen(.{}); - defer s.deinit(); - return s.listen_address.in.getPort(); + const addr = try net.IpAddress.parse("127.0.0.1", 0); + var s = try addr.listen(io, .{}); + defer s.deinit(io); + return s.socket.address.getPort(); } }); defer test_server.destroy(); @@ -541,7 +561,7 @@ test "general client/server API coverage" { const log = std.log.scoped(.client); const gpa = std.testing.allocator; - var client: http.Client = .{ .allocator = gpa }; + var client: http.Client = .{ .allocator = gpa, .io = io }; defer client.deinit(); const port = test_server.port(); @@ -867,18 +887,20 @@ test "general client/server API coverage" { } test "Server streams both reading and writing" { - const test_server = try createTestServer(struct { + const io = std.testing.io; + + const test_server = try createTestServer(io, struct { fn run(test_server: *TestServer) anyerror!void { const net_server = &test_server.net_server; var recv_buffer: [1024]u8 = undefined; var send_buffer: [777]u8 = undefined; - const connection = try net_server.accept(); - defer connection.stream.close(); + var stream = try net_server.accept(io); + defer stream.close(io); - var connection_br = connection.stream.reader(&recv_buffer); - var connection_bw = connection.stream.writer(&send_buffer); - var server = http.Server.init(connection_br.interface(), &connection_bw.interface); + var connection_br = stream.reader(io, &recv_buffer); + var connection_bw = stream.writer(io, &send_buffer); + var server = http.Server.init(&connection_br.interface, &connection_bw.interface); var request = try server.receiveHead(); var read_buffer: [100]u8 = undefined; var br = try request.readerExpectContinue(&read_buffer); @@ -904,7 +926,10 @@ test "Server streams both reading and writing" { }); defer test_server.destroy(); - var client: http.Client = .{ .allocator = std.testing.allocator }; + var client: http.Client = .{ + .allocator = std.testing.allocator, + .io = io, + }; defer client.deinit(); var redirect_buffer: [555]u8 = undefined; @@ -1075,36 +1100,40 @@ fn echoTests(client: *http.Client, port: u16) !void { } const TestServer = struct { + io: Io, shutting_down: bool, server_thread: std.Thread, - net_server: std.net.Server, + net_server: net.Server, fn destroy(self: *@This()) void { + const io = self.io; self.shutting_down = true; - const conn = std.net.tcpConnectToAddress(self.net_server.listen_address) catch @panic("shutdown failure"); - conn.close(); + var stream = self.net_server.socket.address.connect(io, .{ .mode = .stream }) catch + @panic("shutdown failure"); + stream.close(io); self.server_thread.join(); - self.net_server.deinit(); + self.net_server.deinit(io); std.testing.allocator.destroy(self); } fn port(self: @This()) u16 { - return self.net_server.listen_address.in.getPort(); + return self.net_server.socket.address.getPort(); } }; -fn createTestServer(S: type) !*TestServer { +fn createTestServer(io: Io, S: type) !*TestServer { if (builtin.single_threaded) return error.SkipZigTest; if (builtin.zig_backend == .stage2_llvm and native_endian == .big) { // https://github.com/ziglang/zig/issues/13782 return error.SkipZigTest; } - const address = try std.net.Address.parseIp("127.0.0.1", 0); + const address = try net.IpAddress.parse("127.0.0.1", 0); const test_server = try std.testing.allocator.create(TestServer); test_server.* = .{ - .net_server = try address.listen(.{ .reuse_address = true }), + .io = io, + .net_server = try address.listen(io, .{ .reuse_address = true }), .shutting_down = false, .server_thread = try std.Thread.spawn(.{}, S.run, .{test_server}), }; @@ -1112,18 +1141,19 @@ fn createTestServer(S: type) !*TestServer { } test "redirect to different connection" { - const test_server_new = try createTestServer(struct { + const io = std.testing.io; + const test_server_new = try createTestServer(io, struct { fn run(test_server: *TestServer) anyerror!void { const net_server = &test_server.net_server; var recv_buffer: [888]u8 = undefined; var send_buffer: [777]u8 = undefined; - const connection = try net_server.accept(); - defer connection.stream.close(); + var stream = try net_server.accept(io); + defer stream.close(io); - var connection_br = connection.stream.reader(&recv_buffer); - var connection_bw = connection.stream.writer(&send_buffer); - var server = http.Server.init(connection_br.interface(), &connection_bw.interface); + var connection_br = stream.reader(io, &recv_buffer); + var connection_bw = stream.writer(io, &send_buffer); + var server = http.Server.init(&connection_br.interface, &connection_bw.interface); var request = try server.receiveHead(); try expectEqualStrings(request.head.target, "/ok"); try request.respond("good job, you pass", .{}); @@ -1136,23 +1166,23 @@ test "redirect to different connection" { }; global.other_port = test_server_new.port(); - const test_server_orig = try createTestServer(struct { + const test_server_orig = try createTestServer(io, struct { fn run(test_server: *TestServer) anyerror!void { const net_server = &test_server.net_server; var recv_buffer: [999]u8 = undefined; var send_buffer: [100]u8 = undefined; - const connection = try net_server.accept(); - defer connection.stream.close(); + var stream = try net_server.accept(io); + defer stream.close(io); var loc_buf: [50]u8 = undefined; const new_loc = try std.fmt.bufPrint(&loc_buf, "http://127.0.0.1:{d}/ok", .{ global.other_port.?, }); - var connection_br = connection.stream.reader(&recv_buffer); - var connection_bw = connection.stream.writer(&send_buffer); - var server = http.Server.init(connection_br.interface(), &connection_bw.interface); + var connection_br = stream.reader(io, &recv_buffer); + var connection_bw = stream.writer(io, &send_buffer); + var server = http.Server.init(&connection_br.interface, &connection_bw.interface); var request = try server.receiveHead(); try expectEqualStrings(request.head.target, "/help"); try request.respond("", .{ @@ -1167,7 +1197,10 @@ test "redirect to different connection" { const gpa = std.testing.allocator; - var client: http.Client = .{ .allocator = gpa }; + var client: http.Client = .{ + .allocator = gpa, + .io = io, + }; defer client.deinit(); var loc_buf: [100]u8 = undefined; diff --git a/lib/std/mem.zig b/lib/std/mem.zig index ed48132d9a..ade7d1e777 100644 --- a/lib/std/mem.zig +++ b/lib/std/mem.zig @@ -1678,6 +1678,7 @@ test "indexOfPos empty needle" { /// needle.len must be > 0 /// does not count overlapping needles pub fn count(comptime T: type, haystack: []const T, needle: []const T) usize { + if (needle.len == 1) return countScalar(T, haystack, needle[0]); assert(needle.len > 0); var i: usize = 0; var found: usize = 0; @@ -1704,9 +1705,9 @@ test count { try testing.expect(count(u8, "owowowu", "owowu") == 1); } -/// Returns the number of needles inside the haystack -pub fn countScalar(comptime T: type, haystack: []const T, needle: T) usize { - const n = haystack.len; +/// Returns the number of times `element` appears in a slice of memory. +pub fn countScalar(comptime T: type, list: []const T, element: T) usize { + const n = list.len; var i: usize = 0; var found: usize = 0; @@ -1716,16 +1717,16 @@ pub fn countScalar(comptime T: type, haystack: []const T, needle: T) usize { if (std.simd.suggestVectorLength(T)) |block_size| { const Block = @Vector(block_size, T); - const letter_mask: Block = @splat(needle); + const letter_mask: Block = @splat(element); while (n - i >= block_size) : (i += block_size) { - const haystack_block: Block = haystack[i..][0..block_size].*; + const haystack_block: Block = list[i..][0..block_size].*; found += std.simd.countTrues(letter_mask == haystack_block); } } } - for (haystack[i..n]) |item| { - found += @intFromBool(item == needle); + for (list[i..n]) |item| { + found += @intFromBool(item == element); } return found; @@ -1735,6 +1736,7 @@ test countScalar { try testing.expectEqual(0, countScalar(u8, "", 'h')); try testing.expectEqual(1, countScalar(u8, "h", 'h')); try testing.expectEqual(2, countScalar(u8, "hh", 'h')); + try testing.expectEqual(2, countScalar(u8, "ahhb", 'h')); try testing.expectEqual(3, countScalar(u8, " abcabc abc", 'b')); } @@ -1744,6 +1746,7 @@ test countScalar { // /// See also: `containsAtLeastScalar` pub fn containsAtLeast(comptime T: type, haystack: []const T, expected_count: usize, needle: []const T) bool { + if (needle.len == 1) return containsAtLeastScalar(T, haystack, expected_count, needle[0]); assert(needle.len > 0); if (expected_count == 0) return true; @@ -1774,32 +1777,52 @@ test containsAtLeast { try testing.expect(!containsAtLeast(u8, " radar radar ", 3, "radar")); } -/// Returns true if the haystack contains expected_count or more needles -// -/// See also: `containsAtLeast` -pub fn containsAtLeastScalar(comptime T: type, haystack: []const T, expected_count: usize, needle: T) bool { - if (expected_count == 0) return true; +/// Deprecated in favor of `containsAtLeastScalar2`. +pub fn containsAtLeastScalar(comptime T: type, list: []const T, minimum: usize, element: T) bool { + return containsAtLeastScalar2(T, list, element, minimum); +} +/// Returns true if `element` appears at least `minimum` number of times in `list`. +// +/// Related: +/// * `containsAtLeast` +/// * `countScalar` +pub fn containsAtLeastScalar2(comptime T: type, list: []const T, element: T, minimum: usize) bool { + const n = list.len; + var i: usize = 0; var found: usize = 0; - for (haystack) |item| { - if (item == needle) { - found += 1; - if (found == expected_count) return true; + if (use_vectors_for_comparison and + (@typeInfo(T) == .int or @typeInfo(T) == .float) and std.math.isPowerOfTwo(@bitSizeOf(T))) + { + if (std.simd.suggestVectorLength(T)) |block_size| { + const Block = @Vector(block_size, T); + + const letter_mask: Block = @splat(element); + while (n - i >= block_size) : (i += block_size) { + const haystack_block: Block = list[i..][0..block_size].*; + found += std.simd.countTrues(letter_mask == haystack_block); + if (found >= minimum) return true; + } } } + for (list[i..n]) |item| { + found += @intFromBool(item == element); + if (found >= minimum) return true; + } + return false; } -test containsAtLeastScalar { - try testing.expect(containsAtLeastScalar(u8, "aa", 0, 'a')); - try testing.expect(containsAtLeastScalar(u8, "aa", 1, 'a')); - try testing.expect(containsAtLeastScalar(u8, "aa", 2, 'a')); - try testing.expect(!containsAtLeastScalar(u8, "aa", 3, 'a')); +test containsAtLeastScalar2 { + try testing.expect(containsAtLeastScalar2(u8, "aa", 'a', 0)); + try testing.expect(containsAtLeastScalar2(u8, "aa", 'a', 1)); + try testing.expect(containsAtLeastScalar2(u8, "aa", 'a', 2)); + try testing.expect(!containsAtLeastScalar2(u8, "aa", 'a', 3)); - try testing.expect(containsAtLeastScalar(u8, "adadda", 3, 'd')); - try testing.expect(!containsAtLeastScalar(u8, "adadda", 4, 'd')); + try testing.expect(containsAtLeastScalar2(u8, "adadda", 'd', 3)); + try testing.expect(!containsAtLeastScalar2(u8, "adadda", 'd', 4)); } /// Reads an integer from memory with size equal to bytes.len. diff --git a/lib/std/net.zig b/lib/std/net.zig deleted file mode 100644 index 9d70515c2e..0000000000 --- a/lib/std/net.zig +++ /dev/null @@ -1,2430 +0,0 @@ -//! Cross-platform networking abstractions. - -const std = @import("std.zig"); -const builtin = @import("builtin"); -const assert = std.debug.assert; -const net = @This(); -const mem = std.mem; -const posix = std.posix; -const fs = std.fs; -const Io = std.Io; -const native_endian = builtin.target.cpu.arch.endian(); -const native_os = builtin.os.tag; -const windows = std.os.windows; -const Allocator = std.mem.Allocator; -const ArrayList = std.ArrayListUnmanaged; -const File = std.fs.File; - -// Windows 10 added support for unix sockets in build 17063, redstone 4 is the -// first release to support them. -pub const has_unix_sockets = switch (native_os) { - .windows => builtin.os.version_range.windows.isAtLeast(.win10_rs4) orelse false, - .wasi => false, - else => true, -}; - -pub const IPParseError = error{ - Overflow, - InvalidEnd, - InvalidCharacter, - Incomplete, -}; - -pub const IPv4ParseError = IPParseError || error{NonCanonical}; - -pub const IPv6ParseError = IPParseError || error{InvalidIpv4Mapping}; -pub const IPv6InterfaceError = posix.SocketError || posix.IoCtl_SIOCGIFINDEX_Error || error{NameTooLong}; -pub const IPv6ResolveError = IPv6ParseError || IPv6InterfaceError; - -pub const Address = extern union { - any: posix.sockaddr, - in: Ip4Address, - in6: Ip6Address, - un: if (has_unix_sockets) posix.sockaddr.un else void, - - /// Parse an IP address which may include a port. For IPv4, this is just written `address:port`. - /// For IPv6, RFC 3986 defines this as an "IP literal", and the port is differentiated from the - /// address by surrounding the address part in brackets '[addr]:port'. Even if the port is not - /// given, the brackets are mandatory. - pub fn parseIpAndPort(str: []const u8) error{ InvalidAddress, InvalidPort }!Address { - if (str.len == 0) return error.InvalidAddress; - if (str[0] == '[') { - const addr_end = std.mem.indexOfScalar(u8, str, ']') orelse - return error.InvalidAddress; - const addr_str = str[1..addr_end]; - const port: u16 = p: { - if (addr_end == str.len - 1) break :p 0; - if (str[addr_end + 1] != ':') return error.InvalidAddress; - break :p parsePort(str[addr_end + 2 ..]) orelse return error.InvalidPort; - }; - return parseIp6(addr_str, port) catch error.InvalidAddress; - } else { - if (std.mem.indexOfScalar(u8, str, ':')) |idx| { - // hold off on `error.InvalidPort` since `error.InvalidAddress` might make more sense - const port: ?u16 = parsePort(str[idx + 1 ..]); - const addr = parseIp4(str[0..idx], port orelse 0) catch return error.InvalidAddress; - if (port == null) return error.InvalidPort; - return addr; - } else { - return parseIp4(str, 0) catch error.InvalidAddress; - } - } - } - fn parsePort(str: []const u8) ?u16 { - var p: u16 = 0; - for (str) |c| switch (c) { - '0'...'9' => { - const shifted = std.math.mul(u16, p, 10) catch return null; - p = std.math.add(u16, shifted, c - '0') catch return null; - }, - else => return null, - }; - if (p == 0) return null; - return p; - } - - /// Parse the given IP address string into an Address value. - /// It is recommended to use `resolveIp` instead, to handle - /// IPv6 link-local unix addresses. - pub fn parseIp(name: []const u8, port: u16) !Address { - if (parseIp4(name, port)) |ip4| return ip4 else |err| switch (err) { - error.Overflow, - error.InvalidEnd, - error.InvalidCharacter, - error.Incomplete, - error.NonCanonical, - => {}, - } - - if (parseIp6(name, port)) |ip6| return ip6 else |err| switch (err) { - error.Overflow, - error.InvalidEnd, - error.InvalidCharacter, - error.Incomplete, - error.InvalidIpv4Mapping, - => {}, - } - - return error.InvalidIPAddressFormat; - } - - pub fn resolveIp(name: []const u8, port: u16) !Address { - if (parseIp4(name, port)) |ip4| return ip4 else |err| switch (err) { - error.Overflow, - error.InvalidEnd, - error.InvalidCharacter, - error.Incomplete, - error.NonCanonical, - => {}, - } - - if (resolveIp6(name, port)) |ip6| return ip6 else |err| switch (err) { - error.Overflow, - error.InvalidEnd, - error.InvalidCharacter, - error.Incomplete, - error.InvalidIpv4Mapping, - => {}, - else => return err, - } - - return error.InvalidIPAddressFormat; - } - - pub fn parseExpectingFamily(name: []const u8, family: posix.sa_family_t, port: u16) !Address { - switch (family) { - posix.AF.INET => return parseIp4(name, port), - posix.AF.INET6 => return parseIp6(name, port), - posix.AF.UNSPEC => return parseIp(name, port), - else => unreachable, - } - } - - pub fn parseIp6(buf: []const u8, port: u16) IPv6ParseError!Address { - return .{ .in6 = try Ip6Address.parse(buf, port) }; - } - - pub fn resolveIp6(buf: []const u8, port: u16) IPv6ResolveError!Address { - return .{ .in6 = try Ip6Address.resolve(buf, port) }; - } - - pub fn parseIp4(buf: []const u8, port: u16) IPv4ParseError!Address { - return .{ .in = try Ip4Address.parse(buf, port) }; - } - - pub fn initIp4(addr: [4]u8, port: u16) Address { - return .{ .in = Ip4Address.init(addr, port) }; - } - - pub fn initIp6(addr: [16]u8, port: u16, flowinfo: u32, scope_id: u32) Address { - return .{ .in6 = Ip6Address.init(addr, port, flowinfo, scope_id) }; - } - - pub fn initUnix(path: []const u8) !Address { - var sock_addr = posix.sockaddr.un{ - .family = posix.AF.UNIX, - .path = undefined, - }; - - // Add 1 to ensure a terminating 0 is present in the path array for maximum portability. - if (path.len + 1 > sock_addr.path.len) return error.NameTooLong; - - @memset(&sock_addr.path, 0); - @memcpy(sock_addr.path[0..path.len], path); - - return .{ .un = sock_addr }; - } - - /// Returns the port in native endian. - /// Asserts that the address is ip4 or ip6. - pub fn getPort(self: Address) u16 { - return switch (self.any.family) { - posix.AF.INET => self.in.getPort(), - posix.AF.INET6 => self.in6.getPort(), - else => unreachable, - }; - } - - /// `port` is native-endian. - /// Asserts that the address is ip4 or ip6. - pub fn setPort(self: *Address, port: u16) void { - switch (self.any.family) { - posix.AF.INET => self.in.setPort(port), - posix.AF.INET6 => self.in6.setPort(port), - else => unreachable, - } - } - - /// Asserts that `addr` is an IP address. - /// This function will read past the end of the pointer, with a size depending - /// on the address family. - pub fn initPosix(addr: *align(4) const posix.sockaddr) Address { - switch (addr.family) { - posix.AF.INET => return Address{ .in = Ip4Address{ .sa = @as(*const posix.sockaddr.in, @ptrCast(addr)).* } }, - posix.AF.INET6 => return Address{ .in6 = Ip6Address{ .sa = @as(*const posix.sockaddr.in6, @ptrCast(addr)).* } }, - else => unreachable, - } - } - - pub fn format(self: Address, w: *Io.Writer) Io.Writer.Error!void { - switch (self.any.family) { - posix.AF.INET => try self.in.format(w), - posix.AF.INET6 => try self.in6.format(w), - posix.AF.UNIX => { - if (!has_unix_sockets) unreachable; - try w.writeAll(std.mem.sliceTo(&self.un.path, 0)); - }, - else => unreachable, - } - } - - pub fn eql(a: Address, b: Address) bool { - const a_bytes = @as([*]const u8, @ptrCast(&a.any))[0..a.getOsSockLen()]; - const b_bytes = @as([*]const u8, @ptrCast(&b.any))[0..b.getOsSockLen()]; - return mem.eql(u8, a_bytes, b_bytes); - } - - pub fn getOsSockLen(self: Address) posix.socklen_t { - switch (self.any.family) { - posix.AF.INET => return self.in.getOsSockLen(), - posix.AF.INET6 => return self.in6.getOsSockLen(), - posix.AF.UNIX => { - if (!has_unix_sockets) { - unreachable; - } - - // Using the full length of the structure here is more portable than returning - // the number of bytes actually used by the currently stored path. - // This also is correct regardless if we are passing a socket address to the kernel - // (e.g. in bind, connect, sendto) since we ensure the path is 0 terminated in - // initUnix() or if we are receiving a socket address from the kernel and must - // provide the full buffer size (e.g. getsockname, getpeername, recvfrom, accept). - // - // To access the path, std.mem.sliceTo(&address.un.path, 0) should be used. - return @as(posix.socklen_t, @intCast(@sizeOf(posix.sockaddr.un))); - }, - - else => unreachable, - } - } - - pub const ListenError = posix.SocketError || posix.BindError || posix.ListenError || - posix.SetSockOptError || posix.GetSockNameError; - - pub const ListenOptions = struct { - /// How many connections the kernel will accept on the application's behalf. - /// If more than this many connections pool in the kernel, clients will start - /// seeing "Connection refused". - kernel_backlog: u31 = 128, - /// Sets SO_REUSEADDR and SO_REUSEPORT on POSIX. - /// Sets SO_REUSEADDR on Windows, which is roughly equivalent. - reuse_address: bool = false, - /// Sets O_NONBLOCK. - force_nonblocking: bool = false, - }; - - /// The returned `Server` has an open `stream`. - pub fn listen(address: Address, options: ListenOptions) ListenError!Server { - const nonblock: u32 = if (options.force_nonblocking) posix.SOCK.NONBLOCK else 0; - const sock_flags = posix.SOCK.STREAM | posix.SOCK.CLOEXEC | nonblock; - const proto: u32 = if (address.any.family == posix.AF.UNIX) 0 else posix.IPPROTO.TCP; - - const sockfd = try posix.socket(address.any.family, sock_flags, proto); - var s: Server = .{ - .listen_address = undefined, - .stream = .{ .handle = sockfd }, - }; - errdefer s.stream.close(); - - if (options.reuse_address) { - try posix.setsockopt( - sockfd, - posix.SOL.SOCKET, - posix.SO.REUSEADDR, - &mem.toBytes(@as(c_int, 1)), - ); - if (@hasDecl(posix.SO, "REUSEPORT") and address.any.family != posix.AF.UNIX) { - try posix.setsockopt( - sockfd, - posix.SOL.SOCKET, - posix.SO.REUSEPORT, - &mem.toBytes(@as(c_int, 1)), - ); - } - } - - var socklen = address.getOsSockLen(); - try posix.bind(sockfd, &address.any, socklen); - try posix.listen(sockfd, options.kernel_backlog); - try posix.getsockname(sockfd, &s.listen_address.any, &socklen); - return s; - } -}; - -pub const Ip4Address = extern struct { - sa: posix.sockaddr.in, - - pub fn parse(buf: []const u8, port: u16) IPv4ParseError!Ip4Address { - var result: Ip4Address = .{ - .sa = .{ - .port = mem.nativeToBig(u16, port), - .addr = undefined, - }, - }; - const out_ptr = mem.asBytes(&result.sa.addr); - - var x: u8 = 0; - var index: u8 = 0; - var saw_any_digits = false; - var has_zero_prefix = false; - for (buf) |c| { - if (c == '.') { - if (!saw_any_digits) { - return error.InvalidCharacter; - } - if (index == 3) { - return error.InvalidEnd; - } - out_ptr[index] = x; - index += 1; - x = 0; - saw_any_digits = false; - has_zero_prefix = false; - } else if (c >= '0' and c <= '9') { - if (c == '0' and !saw_any_digits) { - has_zero_prefix = true; - } else if (has_zero_prefix) { - return error.NonCanonical; - } - saw_any_digits = true; - x = try std.math.mul(u8, x, 10); - x = try std.math.add(u8, x, c - '0'); - } else { - return error.InvalidCharacter; - } - } - if (index == 3 and saw_any_digits) { - out_ptr[index] = x; - return result; - } - - return error.Incomplete; - } - - pub fn resolveIp(name: []const u8, port: u16) !Ip4Address { - if (parse(name, port)) |ip4| return ip4 else |err| switch (err) { - error.Overflow, - error.InvalidEnd, - error.InvalidCharacter, - error.Incomplete, - error.NonCanonical, - => {}, - } - return error.InvalidIPAddressFormat; - } - - pub fn init(addr: [4]u8, port: u16) Ip4Address { - return Ip4Address{ - .sa = posix.sockaddr.in{ - .port = mem.nativeToBig(u16, port), - .addr = @as(*align(1) const u32, @ptrCast(&addr)).*, - }, - }; - } - - /// Returns the port in native endian. - /// Asserts that the address is ip4 or ip6. - pub fn getPort(self: Ip4Address) u16 { - return mem.bigToNative(u16, self.sa.port); - } - - /// `port` is native-endian. - /// Asserts that the address is ip4 or ip6. - pub fn setPort(self: *Ip4Address, port: u16) void { - self.sa.port = mem.nativeToBig(u16, port); - } - - pub fn format(self: Ip4Address, w: *Io.Writer) Io.Writer.Error!void { - const bytes: *const [4]u8 = @ptrCast(&self.sa.addr); - try w.print("{d}.{d}.{d}.{d}:{d}", .{ bytes[0], bytes[1], bytes[2], bytes[3], self.getPort() }); - } - - pub fn getOsSockLen(self: Ip4Address) posix.socklen_t { - _ = self; - return @sizeOf(posix.sockaddr.in); - } -}; - -pub const Ip6Address = extern struct { - sa: posix.sockaddr.in6, - - /// Parse a given IPv6 address string into an Address. - /// Assumes the Scope ID of the address is fully numeric. - /// For non-numeric addresses, see `resolveIp6`. - pub fn parse(buf: []const u8, port: u16) IPv6ParseError!Ip6Address { - var result = Ip6Address{ - .sa = posix.sockaddr.in6{ - .scope_id = 0, - .port = mem.nativeToBig(u16, port), - .flowinfo = 0, - .addr = undefined, - }, - }; - var ip_slice: *[16]u8 = result.sa.addr[0..]; - - var tail: [16]u8 = undefined; - - var x: u16 = 0; - var saw_any_digits = false; - var index: u8 = 0; - var scope_id = false; - var abbrv = false; - for (buf, 0..) |c, i| { - if (scope_id) { - if (c >= '0' and c <= '9') { - const digit = c - '0'; - { - const ov = @mulWithOverflow(result.sa.scope_id, 10); - if (ov[1] != 0) return error.Overflow; - result.sa.scope_id = ov[0]; - } - { - const ov = @addWithOverflow(result.sa.scope_id, digit); - if (ov[1] != 0) return error.Overflow; - result.sa.scope_id = ov[0]; - } - } else { - return error.InvalidCharacter; - } - } else if (c == ':') { - if (!saw_any_digits) { - if (abbrv) return error.InvalidCharacter; // ':::' - if (i != 0) abbrv = true; - @memset(ip_slice[index..], 0); - ip_slice = tail[0..]; - index = 0; - continue; - } - if (index == 14) { - return error.InvalidEnd; - } - ip_slice[index] = @as(u8, @truncate(x >> 8)); - index += 1; - ip_slice[index] = @as(u8, @truncate(x)); - index += 1; - - x = 0; - saw_any_digits = false; - } else if (c == '%') { - if (!saw_any_digits) { - return error.InvalidCharacter; - } - scope_id = true; - saw_any_digits = false; - } else if (c == '.') { - if (!abbrv or ip_slice[0] != 0xff or ip_slice[1] != 0xff) { - // must start with '::ffff:' - return error.InvalidIpv4Mapping; - } - const start_index = mem.lastIndexOfScalar(u8, buf[0..i], ':').? + 1; - const addr = (Ip4Address.parse(buf[start_index..], 0) catch { - return error.InvalidIpv4Mapping; - }).sa.addr; - ip_slice = result.sa.addr[0..]; - ip_slice[10] = 0xff; - ip_slice[11] = 0xff; - - const ptr = mem.sliceAsBytes(@as(*const [1]u32, &addr)[0..]); - - ip_slice[12] = ptr[0]; - ip_slice[13] = ptr[1]; - ip_slice[14] = ptr[2]; - ip_slice[15] = ptr[3]; - return result; - } else { - const digit = try std.fmt.charToDigit(c, 16); - { - const ov = @mulWithOverflow(x, 16); - if (ov[1] != 0) return error.Overflow; - x = ov[0]; - } - { - const ov = @addWithOverflow(x, digit); - if (ov[1] != 0) return error.Overflow; - x = ov[0]; - } - saw_any_digits = true; - } - } - - if (!saw_any_digits and !abbrv) { - return error.Incomplete; - } - if (!abbrv and index < 14) { - return error.Incomplete; - } - - if (index == 14) { - ip_slice[14] = @as(u8, @truncate(x >> 8)); - ip_slice[15] = @as(u8, @truncate(x)); - return result; - } else { - ip_slice[index] = @as(u8, @truncate(x >> 8)); - index += 1; - ip_slice[index] = @as(u8, @truncate(x)); - index += 1; - @memcpy(result.sa.addr[16 - index ..][0..index], ip_slice[0..index]); - return result; - } - } - - pub fn resolve(buf: []const u8, port: u16) IPv6ResolveError!Ip6Address { - // TODO: Unify the implementations of resolveIp6 and parseIp6. - var result = Ip6Address{ - .sa = posix.sockaddr.in6{ - .scope_id = 0, - .port = mem.nativeToBig(u16, port), - .flowinfo = 0, - .addr = undefined, - }, - }; - var ip_slice: *[16]u8 = result.sa.addr[0..]; - - var tail: [16]u8 = undefined; - - var x: u16 = 0; - var saw_any_digits = false; - var index: u8 = 0; - var abbrv = false; - - var scope_id = false; - var scope_id_value: [posix.IFNAMESIZE - 1]u8 = undefined; - var scope_id_index: usize = 0; - - for (buf, 0..) |c, i| { - if (scope_id) { - // Handling of percent-encoding should be for an URI library. - if ((c >= '0' and c <= '9') or - (c >= 'A' and c <= 'Z') or - (c >= 'a' and c <= 'z') or - (c == '-') or (c == '.') or (c == '_') or (c == '~')) - { - if (scope_id_index >= scope_id_value.len) { - return error.Overflow; - } - - scope_id_value[scope_id_index] = c; - scope_id_index += 1; - } else { - return error.InvalidCharacter; - } - } else if (c == ':') { - if (!saw_any_digits) { - if (abbrv) return error.InvalidCharacter; // ':::' - if (i != 0) abbrv = true; - @memset(ip_slice[index..], 0); - ip_slice = tail[0..]; - index = 0; - continue; - } - if (index == 14) { - return error.InvalidEnd; - } - ip_slice[index] = @as(u8, @truncate(x >> 8)); - index += 1; - ip_slice[index] = @as(u8, @truncate(x)); - index += 1; - - x = 0; - saw_any_digits = false; - } else if (c == '%') { - if (!saw_any_digits) { - return error.InvalidCharacter; - } - scope_id = true; - saw_any_digits = false; - } else if (c == '.') { - if (!abbrv or ip_slice[0] != 0xff or ip_slice[1] != 0xff) { - // must start with '::ffff:' - return error.InvalidIpv4Mapping; - } - const start_index = mem.lastIndexOfScalar(u8, buf[0..i], ':').? + 1; - const addr = (Ip4Address.parse(buf[start_index..], 0) catch { - return error.InvalidIpv4Mapping; - }).sa.addr; - ip_slice = result.sa.addr[0..]; - ip_slice[10] = 0xff; - ip_slice[11] = 0xff; - - const ptr = mem.sliceAsBytes(@as(*const [1]u32, &addr)[0..]); - - ip_slice[12] = ptr[0]; - ip_slice[13] = ptr[1]; - ip_slice[14] = ptr[2]; - ip_slice[15] = ptr[3]; - return result; - } else { - const digit = try std.fmt.charToDigit(c, 16); - { - const ov = @mulWithOverflow(x, 16); - if (ov[1] != 0) return error.Overflow; - x = ov[0]; - } - { - const ov = @addWithOverflow(x, digit); - if (ov[1] != 0) return error.Overflow; - x = ov[0]; - } - saw_any_digits = true; - } - } - - if (!saw_any_digits and !abbrv) { - return error.Incomplete; - } - - if (scope_id and scope_id_index == 0) { - return error.Incomplete; - } - - var resolved_scope_id: u32 = 0; - if (scope_id_index > 0) { - const scope_id_str = scope_id_value[0..scope_id_index]; - resolved_scope_id = std.fmt.parseInt(u32, scope_id_str, 10) catch |err| blk: { - if (err != error.InvalidCharacter) return err; - break :blk try if_nametoindex(scope_id_str); - }; - } - - result.sa.scope_id = resolved_scope_id; - - if (index == 14) { - ip_slice[14] = @as(u8, @truncate(x >> 8)); - ip_slice[15] = @as(u8, @truncate(x)); - return result; - } else { - ip_slice[index] = @as(u8, @truncate(x >> 8)); - index += 1; - ip_slice[index] = @as(u8, @truncate(x)); - index += 1; - @memcpy(result.sa.addr[16 - index ..][0..index], ip_slice[0..index]); - return result; - } - } - - pub fn init(addr: [16]u8, port: u16, flowinfo: u32, scope_id: u32) Ip6Address { - return Ip6Address{ - .sa = posix.sockaddr.in6{ - .addr = addr, - .port = mem.nativeToBig(u16, port), - .flowinfo = flowinfo, - .scope_id = scope_id, - }, - }; - } - - /// Returns the port in native endian. - /// Asserts that the address is ip4 or ip6. - pub fn getPort(self: Ip6Address) u16 { - return mem.bigToNative(u16, self.sa.port); - } - - /// `port` is native-endian. - /// Asserts that the address is ip4 or ip6. - pub fn setPort(self: *Ip6Address, port: u16) void { - self.sa.port = mem.nativeToBig(u16, port); - } - - pub fn format(self: Ip6Address, w: *Io.Writer) Io.Writer.Error!void { - const port = mem.bigToNative(u16, self.sa.port); - if (mem.eql(u8, self.sa.addr[0..12], &[_]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff })) { - try w.print("[::ffff:{d}.{d}.{d}.{d}]:{d}", .{ - self.sa.addr[12], - self.sa.addr[13], - self.sa.addr[14], - self.sa.addr[15], - port, - }); - return; - } - const big_endian_parts = @as(*align(1) const [8]u16, @ptrCast(&self.sa.addr)); - const native_endian_parts = switch (native_endian) { - .big => big_endian_parts.*, - .little => blk: { - var buf: [8]u16 = undefined; - for (big_endian_parts, 0..) |part, i| { - buf[i] = mem.bigToNative(u16, part); - } - break :blk buf; - }, - }; - - // Find the longest zero run - var longest_start: usize = 8; - var longest_len: usize = 0; - var current_start: usize = 0; - var current_len: usize = 0; - - for (native_endian_parts, 0..) |part, i| { - if (part == 0) { - if (current_len == 0) { - current_start = i; - } - current_len += 1; - if (current_len > longest_len) { - longest_start = current_start; - longest_len = current_len; - } - } else { - current_len = 0; - } - } - - // Only compress if the longest zero run is 2 or more - if (longest_len < 2) { - longest_start = 8; - longest_len = 0; - } - - try w.writeAll("["); - var i: usize = 0; - var abbrv = false; - while (i < native_endian_parts.len) : (i += 1) { - if (i == longest_start) { - // Emit "::" for the longest zero run - if (!abbrv) { - try w.writeAll(if (i == 0) "::" else ":"); - abbrv = true; - } - i += longest_len - 1; // Skip the compressed range - continue; - } - if (abbrv) { - abbrv = false; - } - try w.print("{x}", .{native_endian_parts[i]}); - if (i != native_endian_parts.len - 1) { - try w.writeAll(":"); - } - } - if (self.sa.scope_id != 0) { - try w.print("%{}", .{self.sa.scope_id}); - } - try w.print("]:{}", .{port}); - } - - pub fn getOsSockLen(self: Ip6Address) posix.socklen_t { - _ = self; - return @sizeOf(posix.sockaddr.in6); - } -}; - -pub fn connectUnixSocket(path: []const u8) !Stream { - const opt_non_block = 0; - const sockfd = try posix.socket( - posix.AF.UNIX, - posix.SOCK.STREAM | posix.SOCK.CLOEXEC | opt_non_block, - 0, - ); - errdefer Stream.close(.{ .handle = sockfd }); - - var addr = try Address.initUnix(path); - try posix.connect(sockfd, &addr.any, addr.getOsSockLen()); - - return .{ .handle = sockfd }; -} - -fn if_nametoindex(name: []const u8) IPv6InterfaceError!u32 { - if (native_os == .linux) { - var ifr: posix.ifreq = undefined; - const sockfd = try posix.socket(posix.AF.UNIX, posix.SOCK.DGRAM | posix.SOCK.CLOEXEC, 0); - defer Stream.close(.{ .handle = sockfd }); - - @memcpy(ifr.ifrn.name[0..name.len], name); - ifr.ifrn.name[name.len] = 0; - - // TODO investigate if this needs to be integrated with evented I/O. - try posix.ioctl_SIOCGIFINDEX(sockfd, &ifr); - - return @bitCast(ifr.ifru.ivalue); - } - - if (native_os.isDarwin()) { - if (name.len >= posix.IFNAMESIZE) - return error.NameTooLong; - - var if_name: [posix.IFNAMESIZE:0]u8 = undefined; - @memcpy(if_name[0..name.len], name); - if_name[name.len] = 0; - const if_slice = if_name[0..name.len :0]; - const index = std.c.if_nametoindex(if_slice); - if (index == 0) - return error.InterfaceNotFound; - return @as(u32, @bitCast(index)); - } - - if (native_os == .windows) { - if (name.len >= posix.IFNAMESIZE) - return error.NameTooLong; - - var interface_name: [posix.IFNAMESIZE:0]u8 = undefined; - @memcpy(interface_name[0..name.len], name); - interface_name[name.len] = 0; - const index = std.os.windows.ws2_32.if_nametoindex(@as([*:0]const u8, &interface_name)); - if (index == 0) - return error.InterfaceNotFound; - return index; - } - - @compileError("std.net.if_nametoindex unimplemented for this OS"); -} - -pub const AddressList = struct { - arena: std.heap.ArenaAllocator, - addrs: []Address, - canon_name: ?[]u8, - - pub fn deinit(self: *AddressList) void { - // Here we copy the arena allocator into stack memory, because - // otherwise it would destroy itself while it was still working. - var arena = self.arena; - arena.deinit(); - // self is destroyed - } -}; - -pub const TcpConnectToHostError = GetAddressListError || TcpConnectToAddressError; - -/// All memory allocated with `allocator` will be freed before this function returns. -pub fn tcpConnectToHost(allocator: Allocator, name: []const u8, port: u16) TcpConnectToHostError!Stream { - const list = try getAddressList(allocator, name, port); - defer list.deinit(); - - if (list.addrs.len == 0) return error.UnknownHostName; - - for (list.addrs) |addr| { - return tcpConnectToAddress(addr) catch |err| switch (err) { - error.ConnectionRefused => { - continue; - }, - else => return err, - }; - } - return posix.ConnectError.ConnectionRefused; -} - -pub const TcpConnectToAddressError = posix.SocketError || posix.ConnectError; - -pub fn tcpConnectToAddress(address: Address) TcpConnectToAddressError!Stream { - const nonblock = 0; - const sock_flags = posix.SOCK.STREAM | nonblock | - (if (native_os == .windows) 0 else posix.SOCK.CLOEXEC); - const sockfd = try posix.socket(address.any.family, sock_flags, posix.IPPROTO.TCP); - errdefer Stream.close(.{ .handle = sockfd }); - - try posix.connect(sockfd, &address.any, address.getOsSockLen()); - - return Stream{ .handle = sockfd }; -} - -// TODO: Instead of having a massive error set, make the error set have categories, and then -// store the sub-error as a diagnostic value. -const GetAddressListError = Allocator.Error || File.OpenError || File.ReadError || posix.SocketError || posix.BindError || posix.SetSockOptError || error{ - TemporaryNameServerFailure, - NameServerFailure, - AddressFamilyNotSupported, - UnknownHostName, - ServiceUnavailable, - Unexpected, - - HostLacksNetworkAddresses, - - InvalidCharacter, - InvalidEnd, - NonCanonical, - Overflow, - Incomplete, - InvalidIpv4Mapping, - InvalidIPAddressFormat, - - InterfaceNotFound, - FileSystem, - ResolveConfParseFailed, -}; - -/// Call `AddressList.deinit` on the result. -pub fn getAddressList(gpa: Allocator, name: []const u8, port: u16) GetAddressListError!*AddressList { - const result = blk: { - var arena = std.heap.ArenaAllocator.init(gpa); - errdefer arena.deinit(); - - const result = try arena.allocator().create(AddressList); - result.* = AddressList{ - .arena = arena, - .addrs = undefined, - .canon_name = null, - }; - break :blk result; - }; - const arena = result.arena.allocator(); - errdefer result.deinit(); - - if (native_os == .windows) { - const name_c = try gpa.dupeZ(u8, name); - defer gpa.free(name_c); - - const port_c = try std.fmt.allocPrintSentinel(gpa, "{d}", .{port}, 0); - defer gpa.free(port_c); - - const ws2_32 = windows.ws2_32; - const hints: posix.addrinfo = .{ - .flags = .{ .NUMERICSERV = true }, - .family = posix.AF.UNSPEC, - .socktype = posix.SOCK.STREAM, - .protocol = posix.IPPROTO.TCP, - .canonname = null, - .addr = null, - .addrlen = 0, - .next = null, - }; - var res: ?*posix.addrinfo = null; - var first = true; - while (true) { - const rc = ws2_32.getaddrinfo(name_c.ptr, port_c.ptr, &hints, &res); - switch (@as(windows.ws2_32.WinsockError, @enumFromInt(@as(u16, @intCast(rc))))) { - @as(windows.ws2_32.WinsockError, @enumFromInt(0)) => break, - .WSATRY_AGAIN => return error.TemporaryNameServerFailure, - .WSANO_RECOVERY => return error.NameServerFailure, - .WSAEAFNOSUPPORT => return error.AddressFamilyNotSupported, - .WSA_NOT_ENOUGH_MEMORY => return error.OutOfMemory, - .WSAHOST_NOT_FOUND => return error.UnknownHostName, - .WSATYPE_NOT_FOUND => return error.ServiceUnavailable, - .WSAEINVAL => unreachable, - .WSAESOCKTNOSUPPORT => unreachable, - .WSANOTINITIALISED => { - if (!first) return error.Unexpected; - first = false; - try windows.callWSAStartup(); - continue; - }, - else => |err| return windows.unexpectedWSAError(err), - } - } - defer ws2_32.freeaddrinfo(res); - - const addr_count = blk: { - var count: usize = 0; - var it = res; - while (it) |info| : (it = info.next) { - if (info.addr != null) { - count += 1; - } - } - break :blk count; - }; - result.addrs = try arena.alloc(Address, addr_count); - - var it = res; - var i: usize = 0; - while (it) |info| : (it = info.next) { - const addr = info.addr orelse continue; - result.addrs[i] = Address.initPosix(@alignCast(addr)); - - if (info.canonname) |n| { - if (result.canon_name == null) { - result.canon_name = try arena.dupe(u8, mem.sliceTo(n, 0)); - } - } - i += 1; - } - - return result; - } - - if (builtin.link_libc) { - const name_c = try gpa.dupeZ(u8, name); - defer gpa.free(name_c); - - const port_c = try std.fmt.allocPrintSentinel(gpa, "{d}", .{port}, 0); - defer gpa.free(port_c); - - const hints: posix.addrinfo = .{ - .flags = .{ .NUMERICSERV = true }, - .family = posix.AF.UNSPEC, - .socktype = posix.SOCK.STREAM, - .protocol = posix.IPPROTO.TCP, - .canonname = null, - .addr = null, - .addrlen = 0, - .next = null, - }; - var res: ?*posix.addrinfo = null; - switch (posix.system.getaddrinfo(name_c.ptr, port_c.ptr, &hints, &res)) { - @as(posix.system.EAI, @enumFromInt(0)) => {}, - .ADDRFAMILY => return error.HostLacksNetworkAddresses, - .AGAIN => return error.TemporaryNameServerFailure, - .BADFLAGS => unreachable, // Invalid hints - .FAIL => return error.NameServerFailure, - .FAMILY => return error.AddressFamilyNotSupported, - .MEMORY => return error.OutOfMemory, - .NODATA => return error.HostLacksNetworkAddresses, - .NONAME => return error.UnknownHostName, - .SERVICE => return error.ServiceUnavailable, - .SOCKTYPE => unreachable, // Invalid socket type requested in hints - .SYSTEM => switch (posix.errno(-1)) { - else => |e| return posix.unexpectedErrno(e), - }, - else => unreachable, - } - defer if (res) |some| posix.system.freeaddrinfo(some); - - const addr_count = blk: { - var count: usize = 0; - var it = res; - while (it) |info| : (it = info.next) { - if (info.addr != null) { - count += 1; - } - } - break :blk count; - }; - result.addrs = try arena.alloc(Address, addr_count); - - var it = res; - var i: usize = 0; - while (it) |info| : (it = info.next) { - const addr = info.addr orelse continue; - result.addrs[i] = Address.initPosix(@alignCast(addr)); - - if (info.canonname) |n| { - if (result.canon_name == null) { - result.canon_name = try arena.dupe(u8, mem.sliceTo(n, 0)); - } - } - i += 1; - } - - return result; - } - - if (native_os == .linux) { - const family = posix.AF.UNSPEC; - var lookup_addrs: ArrayList(LookupAddr) = .empty; - defer lookup_addrs.deinit(gpa); - - var canon: ArrayList(u8) = .empty; - defer canon.deinit(gpa); - - try linuxLookupName(gpa, &lookup_addrs, &canon, name, family, .{ .NUMERICSERV = true }, port); - - result.addrs = try arena.alloc(Address, lookup_addrs.items.len); - if (canon.items.len != 0) { - result.canon_name = try arena.dupe(u8, canon.items); - } - - for (lookup_addrs.items, 0..) |lookup_addr, i| { - result.addrs[i] = lookup_addr.addr; - assert(result.addrs[i].getPort() == port); - } - - return result; - } - @compileError("std.net.getAddressList unimplemented for this OS"); -} - -const LookupAddr = struct { - addr: Address, - sortkey: i32 = 0, -}; - -const DAS_USABLE = 0x40000000; -const DAS_MATCHINGSCOPE = 0x20000000; -const DAS_MATCHINGLABEL = 0x10000000; -const DAS_PREC_SHIFT = 20; -const DAS_SCOPE_SHIFT = 16; -const DAS_PREFIX_SHIFT = 8; -const DAS_ORDER_SHIFT = 0; - -fn linuxLookupName( - gpa: Allocator, - addrs: *ArrayList(LookupAddr), - canon: *ArrayList(u8), - opt_name: ?[]const u8, - family: posix.sa_family_t, - flags: posix.AI, - port: u16, -) !void { - if (opt_name) |name| { - // reject empty name and check len so it fits into temp bufs - canon.items.len = 0; - try canon.appendSlice(gpa, name); - if (Address.parseExpectingFamily(name, family, port)) |addr| { - try addrs.append(gpa, .{ .addr = addr }); - } else |name_err| if (flags.NUMERICHOST) { - return name_err; - } else { - try linuxLookupNameFromHosts(gpa, addrs, canon, name, family, port); - if (addrs.items.len == 0) { - // RFC 6761 Section 6.3.3 - // Name resolution APIs and libraries SHOULD recognize localhost - // names as special and SHOULD always return the IP loopback address - // for address queries and negative responses for all other query - // types. - - // Check for equal to "localhost(.)" or ends in ".localhost(.)" - const localhost = if (name[name.len - 1] == '.') "localhost." else "localhost"; - if (mem.endsWith(u8, name, localhost) and (name.len == localhost.len or name[name.len - localhost.len] == '.')) { - try addrs.append(gpa, .{ .addr = .{ .in = Ip4Address.parse("127.0.0.1", port) catch unreachable } }); - try addrs.append(gpa, .{ .addr = .{ .in6 = Ip6Address.parse("::1", port) catch unreachable } }); - return; - } - - try linuxLookupNameFromDnsSearch(gpa, addrs, canon, name, family, port); - } - } - } else { - try canon.resize(gpa, 0); - try addrs.ensureUnusedCapacity(gpa, 2); - linuxLookupNameFromNull(addrs, family, flags, port); - } - if (addrs.items.len == 0) return error.UnknownHostName; - - // No further processing is needed if there are fewer than 2 - // results or if there are only IPv4 results. - if (addrs.items.len == 1 or family == posix.AF.INET) return; - const all_ip4 = for (addrs.items) |addr| { - if (addr.addr.any.family != posix.AF.INET) break false; - } else true; - if (all_ip4) return; - - // The following implements a subset of RFC 3484/6724 destination - // address selection by generating a single 31-bit sort key for - // each address. Rules 3, 4, and 7 are omitted for having - // excessive runtime and code size cost and dubious benefit. - // So far the label/precedence table cannot be customized. - // This implementation is ported from musl libc. - // A more idiomatic "ziggy" implementation would be welcome. - for (addrs.items, 0..) |*addr, i| { - var key: i32 = 0; - var sa6: posix.sockaddr.in6 = undefined; - @memset(@as([*]u8, @ptrCast(&sa6))[0..@sizeOf(posix.sockaddr.in6)], 0); - var da6 = posix.sockaddr.in6{ - .family = posix.AF.INET6, - .scope_id = addr.addr.in6.sa.scope_id, - .port = 65535, - .flowinfo = 0, - .addr = [1]u8{0} ** 16, - }; - var sa4: posix.sockaddr.in = undefined; - @memset(@as([*]u8, @ptrCast(&sa4))[0..@sizeOf(posix.sockaddr.in)], 0); - var da4 = posix.sockaddr.in{ - .family = posix.AF.INET, - .port = 65535, - .addr = 0, - .zero = [1]u8{0} ** 8, - }; - var sa: *align(4) posix.sockaddr = undefined; - var da: *align(4) posix.sockaddr = undefined; - var salen: posix.socklen_t = undefined; - var dalen: posix.socklen_t = undefined; - if (addr.addr.any.family == posix.AF.INET6) { - da6.addr = addr.addr.in6.sa.addr; - da = @ptrCast(&da6); - dalen = @sizeOf(posix.sockaddr.in6); - sa = @ptrCast(&sa6); - salen = @sizeOf(posix.sockaddr.in6); - } else { - sa6.addr[0..12].* = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff".*; - da6.addr[0..12].* = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff".*; - mem.writeInt(u32, da6.addr[12..], addr.addr.in.sa.addr, native_endian); - da4.addr = addr.addr.in.sa.addr; - da = @ptrCast(&da4); - dalen = @sizeOf(posix.sockaddr.in); - sa = @ptrCast(&sa4); - salen = @sizeOf(posix.sockaddr.in); - } - const dpolicy = policyOf(da6.addr); - const dscope: i32 = scopeOf(da6.addr); - const dlabel = dpolicy.label; - const dprec: i32 = dpolicy.prec; - const MAXADDRS = 3; - var prefixlen: i32 = 0; - const sock_flags = posix.SOCK.DGRAM | posix.SOCK.CLOEXEC; - if (posix.socket(addr.addr.any.family, sock_flags, posix.IPPROTO.UDP)) |fd| syscalls: { - defer Stream.close(.{ .handle = fd }); - posix.connect(fd, da, dalen) catch break :syscalls; - key |= DAS_USABLE; - posix.getsockname(fd, sa, &salen) catch break :syscalls; - if (addr.addr.any.family == posix.AF.INET) { - mem.writeInt(u32, sa6.addr[12..16], sa4.addr, native_endian); - } - if (dscope == @as(i32, scopeOf(sa6.addr))) key |= DAS_MATCHINGSCOPE; - if (dlabel == labelOf(sa6.addr)) key |= DAS_MATCHINGLABEL; - prefixlen = prefixMatch(sa6.addr, da6.addr); - } else |_| {} - key |= dprec << DAS_PREC_SHIFT; - key |= (15 - dscope) << DAS_SCOPE_SHIFT; - key |= prefixlen << DAS_PREFIX_SHIFT; - key |= (MAXADDRS - @as(i32, @intCast(i))) << DAS_ORDER_SHIFT; - addr.sortkey = key; - } - mem.sort(LookupAddr, addrs.items, {}, addrCmpLessThan); -} - -const Policy = struct { - addr: [16]u8, - len: u8, - mask: u8, - prec: u8, - label: u8, -}; - -const defined_policies = [_]Policy{ - Policy{ - .addr = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01".*, - .len = 15, - .mask = 0xff, - .prec = 50, - .label = 0, - }, - Policy{ - .addr = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x00\x00\x00\x00".*, - .len = 11, - .mask = 0xff, - .prec = 35, - .label = 4, - }, - Policy{ - .addr = "\x20\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00".*, - .len = 1, - .mask = 0xff, - .prec = 30, - .label = 2, - }, - Policy{ - .addr = "\x20\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00".*, - .len = 3, - .mask = 0xff, - .prec = 5, - .label = 5, - }, - Policy{ - .addr = "\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00".*, - .len = 0, - .mask = 0xfe, - .prec = 3, - .label = 13, - }, - // These are deprecated and/or returned to the address - // pool, so despite the RFC, treating them as special - // is probably wrong. - // { "", 11, 0xff, 1, 3 }, - // { "\xfe\xc0", 1, 0xc0, 1, 11 }, - // { "\x3f\xfe", 1, 0xff, 1, 12 }, - // Last rule must match all addresses to stop loop. - Policy{ - .addr = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00".*, - .len = 0, - .mask = 0, - .prec = 40, - .label = 1, - }, -}; - -fn policyOf(a: [16]u8) *const Policy { - for (&defined_policies) |*policy| { - if (!mem.eql(u8, a[0..policy.len], policy.addr[0..policy.len])) continue; - if ((a[policy.len] & policy.mask) != policy.addr[policy.len]) continue; - return policy; - } - unreachable; -} - -fn scopeOf(a: [16]u8) u8 { - if (IN6_IS_ADDR_MULTICAST(a)) return a[1] & 15; - if (IN6_IS_ADDR_LINKLOCAL(a)) return 2; - if (IN6_IS_ADDR_LOOPBACK(a)) return 2; - if (IN6_IS_ADDR_SITELOCAL(a)) return 5; - return 14; -} - -fn prefixMatch(s: [16]u8, d: [16]u8) u8 { - // TODO: This FIXME inherited from porting from musl libc. - // I don't want this to go into zig std lib 1.0.0. - - // FIXME: The common prefix length should be limited to no greater - // than the nominal length of the prefix portion of the source - // address. However the definition of the source prefix length is - // not clear and thus this limiting is not yet implemented. - var i: u8 = 0; - while (i < 128 and ((s[i / 8] ^ d[i / 8]) & (@as(u8, 128) >> @as(u3, @intCast(i % 8)))) == 0) : (i += 1) {} - return i; -} - -fn labelOf(a: [16]u8) u8 { - return policyOf(a).label; -} - -fn IN6_IS_ADDR_MULTICAST(a: [16]u8) bool { - return a[0] == 0xff; -} - -fn IN6_IS_ADDR_LINKLOCAL(a: [16]u8) bool { - return a[0] == 0xfe and (a[1] & 0xc0) == 0x80; -} - -fn IN6_IS_ADDR_LOOPBACK(a: [16]u8) bool { - return a[0] == 0 and a[1] == 0 and - a[2] == 0 and - a[12] == 0 and a[13] == 0 and - a[14] == 0 and a[15] == 1; -} - -fn IN6_IS_ADDR_SITELOCAL(a: [16]u8) bool { - return a[0] == 0xfe and (a[1] & 0xc0) == 0xc0; -} - -// Parameters `b` and `a` swapped to make this descending. -fn addrCmpLessThan(context: void, b: LookupAddr, a: LookupAddr) bool { - _ = context; - return a.sortkey < b.sortkey; -} - -fn linuxLookupNameFromNull( - addrs: *ArrayList(LookupAddr), - family: posix.sa_family_t, - flags: posix.AI, - port: u16, -) void { - if (flags.PASSIVE) { - if (family != posix.AF.INET6) { - addrs.appendAssumeCapacity(.{ - .addr = Address.initIp4([1]u8{0} ** 4, port), - }); - } - if (family != posix.AF.INET) { - addrs.appendAssumeCapacity(.{ - .addr = Address.initIp6([1]u8{0} ** 16, port, 0, 0), - }); - } - } else { - if (family != posix.AF.INET6) { - addrs.appendAssumeCapacity(.{ - .addr = Address.initIp4([4]u8{ 127, 0, 0, 1 }, port), - }); - } - if (family != posix.AF.INET) { - addrs.appendAssumeCapacity(.{ - .addr = Address.initIp6(([1]u8{0} ** 15) ++ [1]u8{1}, port, 0, 0), - }); - } - } -} - -fn linuxLookupNameFromHosts( - gpa: Allocator, - addrs: *ArrayList(LookupAddr), - canon: *ArrayList(u8), - name: []const u8, - family: posix.sa_family_t, - port: u16, -) !void { - const file = fs.openFileAbsoluteZ("/etc/hosts", .{}) catch |err| switch (err) { - error.FileNotFound, - error.NotDir, - error.AccessDenied, - => return, - else => |e| return e, - }; - defer file.close(); - - var line_buf: [512]u8 = undefined; - var file_reader = file.reader(&line_buf); - return parseHosts(gpa, addrs, canon, name, family, port, &file_reader.interface) catch |err| switch (err) { - error.OutOfMemory => return error.OutOfMemory, - error.ReadFailed => return file_reader.err.?, - }; -} - -fn parseHosts( - gpa: Allocator, - addrs: *ArrayList(LookupAddr), - canon: *ArrayList(u8), - name: []const u8, - family: posix.sa_family_t, - port: u16, - br: *Io.Reader, -) error{ OutOfMemory, ReadFailed }!void { - while (true) { - const line = br.takeDelimiter('\n') catch |err| switch (err) { - error.StreamTooLong => { - // Skip lines that are too long. - _ = br.discardDelimiterInclusive('\n') catch |e| switch (e) { - error.EndOfStream => break, - error.ReadFailed => return error.ReadFailed, - }; - continue; - }, - error.ReadFailed => return error.ReadFailed, - } orelse { - break; // end of stream - }; - var split_it = mem.splitScalar(u8, line, '#'); - const no_comment_line = split_it.first(); - - var line_it = mem.tokenizeAny(u8, no_comment_line, " \t"); - const ip_text = line_it.next() orelse continue; - var first_name_text: ?[]const u8 = null; - while (line_it.next()) |name_text| { - if (first_name_text == null) first_name_text = name_text; - if (mem.eql(u8, name_text, name)) { - break; - } - } else continue; - - const addr = Address.parseExpectingFamily(ip_text, family, port) catch |err| switch (err) { - error.Overflow, - error.InvalidEnd, - error.InvalidCharacter, - error.Incomplete, - error.InvalidIPAddressFormat, - error.InvalidIpv4Mapping, - error.NonCanonical, - => continue, - }; - try addrs.append(gpa, .{ .addr = addr }); - - // first name is canonical name - const name_text = first_name_text.?; - if (isValidHostName(name_text)) { - canon.items.len = 0; - try canon.appendSlice(gpa, name_text); - } - } -} - -test parseHosts { - if (builtin.os.tag == .wasi) { - // TODO parsing addresses should not have OS dependencies - return error.SkipZigTest; - } - var reader: Io.Reader = .fixed( - \\127.0.0.1 localhost - \\::1 localhost - \\127.0.0.2 abcd - ); - var addrs: ArrayList(LookupAddr) = .empty; - defer addrs.deinit(std.testing.allocator); - var canon: ArrayList(u8) = .empty; - defer canon.deinit(std.testing.allocator); - try parseHosts(std.testing.allocator, &addrs, &canon, "abcd", posix.AF.UNSPEC, 1234, &reader); - try std.testing.expectEqual(1, addrs.items.len); - try std.testing.expectFmt("127.0.0.2:1234", "{f}", .{addrs.items[0].addr}); -} - -pub fn isValidHostName(hostname: []const u8) bool { - if (hostname.len >= 254) return false; - if (!std.unicode.utf8ValidateSlice(hostname)) return false; - for (hostname) |byte| { - if (!std.ascii.isAscii(byte) or byte == '.' or byte == '-' or std.ascii.isAlphanumeric(byte)) { - continue; - } - return false; - } - return true; -} - -fn linuxLookupNameFromDnsSearch( - gpa: Allocator, - addrs: *ArrayList(LookupAddr), - canon: *ArrayList(u8), - name: []const u8, - family: posix.sa_family_t, - port: u16, -) !void { - var rc: ResolvConf = undefined; - rc.init(gpa) catch return error.ResolveConfParseFailed; - defer rc.deinit(); - - // Count dots, suppress search when >=ndots or name ends in - // a dot, which is an explicit request for global scope. - var dots: usize = 0; - for (name) |byte| { - if (byte == '.') dots += 1; - } - - const search = if (dots >= rc.ndots or mem.endsWith(u8, name, ".")) - "" - else - rc.search.items; - - var canon_name = name; - - // Strip final dot for canon, fail if multiple trailing dots. - if (mem.endsWith(u8, canon_name, ".")) canon_name.len -= 1; - if (mem.endsWith(u8, canon_name, ".")) return error.UnknownHostName; - - // Name with search domain appended is setup in canon[]. This both - // provides the desired default canonical name (if the requested - // name is not a CNAME record) and serves as a buffer for passing - // the full requested name to name_from_dns. - try canon.resize(gpa, canon_name.len); - @memcpy(canon.items, canon_name); - try canon.append(gpa, '.'); - - var tok_it = mem.tokenizeAny(u8, search, " \t"); - while (tok_it.next()) |tok| { - canon.shrinkRetainingCapacity(canon_name.len + 1); - try canon.appendSlice(gpa, tok); - try linuxLookupNameFromDns(gpa, addrs, canon, canon.items, family, rc, port); - if (addrs.items.len != 0) return; - } - - canon.shrinkRetainingCapacity(canon_name.len); - return linuxLookupNameFromDns(gpa, addrs, canon, name, family, rc, port); -} - -const dpc_ctx = struct { - gpa: Allocator, - addrs: *ArrayList(LookupAddr), - canon: *ArrayList(u8), - port: u16, -}; - -fn linuxLookupNameFromDns( - gpa: Allocator, - addrs: *ArrayList(LookupAddr), - canon: *ArrayList(u8), - name: []const u8, - family: posix.sa_family_t, - rc: ResolvConf, - port: u16, -) !void { - const ctx: dpc_ctx = .{ - .gpa = gpa, - .addrs = addrs, - .canon = canon, - .port = port, - }; - const AfRr = struct { - af: posix.sa_family_t, - rr: u8, - }; - const afrrs = [_]AfRr{ - .{ .af = posix.AF.INET6, .rr = posix.RR.A }, - .{ .af = posix.AF.INET, .rr = posix.RR.AAAA }, - }; - var qbuf: [2][280]u8 = undefined; - var abuf: [2][512]u8 = undefined; - var qp: [2][]const u8 = undefined; - const apbuf = [2][]u8{ &abuf[0], &abuf[1] }; - var nq: usize = 0; - - for (afrrs) |afrr| { - if (family != afrr.af) { - const len = posix.res_mkquery(0, name, 1, afrr.rr, &[_]u8{}, null, &qbuf[nq]); - qp[nq] = qbuf[nq][0..len]; - nq += 1; - } - } - - var ap = [2][]u8{ apbuf[0], apbuf[1] }; - ap[0].len = 0; - ap[1].len = 0; - - try rc.resMSendRc(qp[0..nq], ap[0..nq], apbuf[0..nq]); - - var i: usize = 0; - while (i < nq) : (i += 1) { - dnsParse(ap[i], ctx, dnsParseCallback) catch {}; - } - - if (addrs.items.len != 0) return; - if (ap[0].len < 4 or (ap[0][3] & 15) == 2) return error.TemporaryNameServerFailure; - if ((ap[0][3] & 15) == 0) return error.UnknownHostName; - if ((ap[0][3] & 15) == 3) return; - return error.NameServerFailure; -} - -const ResolvConf = struct { - gpa: Allocator, - attempts: u32, - ndots: u32, - timeout: u32, - search: ArrayList(u8), - /// TODO there are actually only allowed to be maximum 3 nameservers, no need - /// for an array list. - ns: ArrayList(LookupAddr), - - /// Returns `error.StreamTooLong` if a line is longer than 512 bytes. - /// TODO: https://github.com/ziglang/zig/issues/2765 and https://github.com/ziglang/zig/issues/2761 - fn init(rc: *ResolvConf, gpa: Allocator) !void { - rc.* = .{ - .gpa = gpa, - .ns = .empty, - .search = .empty, - .ndots = 1, - .timeout = 5, - .attempts = 2, - }; - errdefer rc.deinit(); - - const file = fs.openFileAbsoluteZ("/etc/resolv.conf", .{}) catch |err| switch (err) { - error.FileNotFound, - error.NotDir, - error.AccessDenied, - => return linuxLookupNameFromNumericUnspec(gpa, &rc.ns, "127.0.0.1", 53), - else => |e| return e, - }; - defer file.close(); - - var line_buf: [512]u8 = undefined; - var file_reader = file.reader(&line_buf); - return parse(rc, &file_reader.interface) catch |err| switch (err) { - error.ReadFailed => return file_reader.err.?, - else => |e| return e, - }; - } - - const Directive = enum { options, nameserver, domain, search }; - const Option = enum { ndots, attempts, timeout }; - - fn parse(rc: *ResolvConf, reader: *Io.Reader) !void { - const gpa = rc.gpa; - while (reader.takeSentinel('\n')) |line_with_comment| { - const line = line: { - var split = mem.splitScalar(u8, line_with_comment, '#'); - break :line split.first(); - }; - var line_it = mem.tokenizeAny(u8, line, " \t"); - - const token = line_it.next() orelse continue; - switch (std.meta.stringToEnum(Directive, token) orelse continue) { - .options => while (line_it.next()) |sub_tok| { - var colon_it = mem.splitScalar(u8, sub_tok, ':'); - const name = colon_it.first(); - const value_txt = colon_it.next() orelse continue; - const value = std.fmt.parseInt(u8, value_txt, 10) catch |err| switch (err) { - error.Overflow => 255, - error.InvalidCharacter => continue, - }; - switch (std.meta.stringToEnum(Option, name) orelse continue) { - .ndots => rc.ndots = @min(value, 15), - .attempts => rc.attempts = @min(value, 10), - .timeout => rc.timeout = @min(value, 60), - } - }, - .nameserver => { - const ip_txt = line_it.next() orelse continue; - try linuxLookupNameFromNumericUnspec(gpa, &rc.ns, ip_txt, 53); - }, - .domain, .search => { - rc.search.items.len = 0; - try rc.search.appendSlice(gpa, line_it.rest()); - }, - } - } else |err| switch (err) { - error.EndOfStream => if (reader.bufferedLen() != 0) return error.EndOfStream, - else => |e| return e, - } - - if (rc.ns.items.len == 0) { - return linuxLookupNameFromNumericUnspec(gpa, &rc.ns, "127.0.0.1", 53); - } - } - - fn resMSendRc( - rc: ResolvConf, - queries: []const []const u8, - answers: [][]u8, - answer_bufs: []const []u8, - ) !void { - const gpa = rc.gpa; - const timeout = 1000 * rc.timeout; - const attempts = rc.attempts; - - var sl: posix.socklen_t = @sizeOf(posix.sockaddr.in); - var family: posix.sa_family_t = posix.AF.INET; - - var ns_list: ArrayList(Address) = .empty; - defer ns_list.deinit(gpa); - - try ns_list.resize(gpa, rc.ns.items.len); - - for (ns_list.items, rc.ns.items) |*ns, iplit| { - ns.* = iplit.addr; - assert(ns.getPort() == 53); - if (iplit.addr.any.family != posix.AF.INET) { - family = posix.AF.INET6; - } - } - - const flags = posix.SOCK.DGRAM | posix.SOCK.CLOEXEC | posix.SOCK.NONBLOCK; - const fd = posix.socket(family, flags, 0) catch |err| switch (err) { - error.AddressFamilyNotSupported => blk: { - // Handle case where system lacks IPv6 support - if (family == posix.AF.INET6) { - family = posix.AF.INET; - break :blk try posix.socket(posix.AF.INET, flags, 0); - } - return err; - }, - else => |e| return e, - }; - defer Stream.close(.{ .handle = fd }); - - // Past this point, there are no errors. Each individual query will - // yield either no reply (indicated by zero length) or an answer - // packet which is up to the caller to interpret. - - // Convert any IPv4 addresses in a mixed environment to v4-mapped - if (family == posix.AF.INET6) { - try posix.setsockopt( - fd, - posix.SOL.IPV6, - std.os.linux.IPV6.V6ONLY, - &mem.toBytes(@as(c_int, 0)), - ); - for (ns_list.items) |*ns| { - if (ns.any.family != posix.AF.INET) continue; - mem.writeInt(u32, ns.in6.sa.addr[12..], ns.in.sa.addr, native_endian); - ns.in6.sa.addr[0..12].* = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff".*; - ns.any.family = posix.AF.INET6; - ns.in6.sa.flowinfo = 0; - ns.in6.sa.scope_id = 0; - } - sl = @sizeOf(posix.sockaddr.in6); - } - - // Get local address and open/bind a socket - var sa: Address = undefined; - @memset(@as([*]u8, @ptrCast(&sa))[0..@sizeOf(Address)], 0); - sa.any.family = family; - try posix.bind(fd, &sa.any, sl); - - var pfd = [1]posix.pollfd{posix.pollfd{ - .fd = fd, - .events = posix.POLL.IN, - .revents = undefined, - }}; - const retry_interval = timeout / attempts; - var next: u32 = 0; - var t2: u64 = @bitCast(std.time.milliTimestamp()); - const t0 = t2; - var t1 = t2 - retry_interval; - - var servfail_retry: usize = undefined; - - outer: while (t2 - t0 < timeout) : (t2 = @as(u64, @bitCast(std.time.milliTimestamp()))) { - if (t2 - t1 >= retry_interval) { - // Query all configured nameservers in parallel - var i: usize = 0; - while (i < queries.len) : (i += 1) { - if (answers[i].len == 0) { - for (ns_list.items) |*ns| { - _ = posix.sendto(fd, queries[i], posix.MSG.NOSIGNAL, &ns.any, sl) catch undefined; - } - } - } - t1 = t2; - servfail_retry = 2 * queries.len; - } - - // Wait for a response, or until time to retry - const clamped_timeout = @min(@as(u31, std.math.maxInt(u31)), t1 + retry_interval - t2); - const nevents = posix.poll(&pfd, clamped_timeout) catch 0; - if (nevents == 0) continue; - - while (true) { - var sl_copy = sl; - const rlen = posix.recvfrom(fd, answer_bufs[next], 0, &sa.any, &sl_copy) catch break; - - // Ignore non-identifiable packets - if (rlen < 4) continue; - - // Ignore replies from addresses we didn't send to - const ns = for (ns_list.items) |*ns| { - if (ns.eql(sa)) break ns; - } else continue; - - // Find which query this answer goes with, if any - var i: usize = next; - while (i < queries.len and (answer_bufs[next][0] != queries[i][0] or - answer_bufs[next][1] != queries[i][1])) : (i += 1) - {} - - if (i == queries.len) continue; - if (answers[i].len != 0) continue; - - // Only accept positive or negative responses; - // retry immediately on server failure, and ignore - // all other codes such as refusal. - switch (answer_bufs[next][3] & 15) { - 0, 3 => {}, - 2 => if (servfail_retry != 0) { - servfail_retry -= 1; - _ = posix.sendto(fd, queries[i], posix.MSG.NOSIGNAL, &ns.any, sl) catch undefined; - }, - else => continue, - } - - // Store answer in the right slot, or update next - // available temp slot if it's already in place. - answers[i].len = rlen; - if (i == next) { - while (next < queries.len and answers[next].len != 0) : (next += 1) {} - } else { - @memcpy(answer_bufs[i][0..rlen], answer_bufs[next][0..rlen]); - } - - if (next == queries.len) break :outer; - } - } - } - - fn deinit(rc: *ResolvConf) void { - const gpa = rc.gpa; - rc.ns.deinit(gpa); - rc.search.deinit(gpa); - rc.* = undefined; - } -}; - -fn linuxLookupNameFromNumericUnspec( - gpa: Allocator, - addrs: *ArrayList(LookupAddr), - name: []const u8, - port: u16, -) !void { - const addr = try Address.resolveIp(name, port); - try addrs.append(gpa, .{ .addr = addr }); -} - -fn dnsParse( - r: []const u8, - ctx: anytype, - comptime callback: anytype, -) !void { - // This implementation is ported from musl libc. - // A more idiomatic "ziggy" implementation would be welcome. - if (r.len < 12) return error.InvalidDnsPacket; - if ((r[3] & 15) != 0) return; - var p = r.ptr + 12; - var qdcount = r[4] * @as(usize, 256) + r[5]; - var ancount = r[6] * @as(usize, 256) + r[7]; - if (qdcount + ancount > 64) return error.InvalidDnsPacket; - while (qdcount != 0) { - qdcount -= 1; - while (@intFromPtr(p) - @intFromPtr(r.ptr) < r.len and p[0] -% 1 < 127) p += 1; - if (p[0] > 193 or (p[0] == 193 and p[1] > 254) or @intFromPtr(p) > @intFromPtr(r.ptr) + r.len - 6) - return error.InvalidDnsPacket; - p += @as(usize, 5) + @intFromBool(p[0] != 0); - } - while (ancount != 0) { - ancount -= 1; - while (@intFromPtr(p) - @intFromPtr(r.ptr) < r.len and p[0] -% 1 < 127) p += 1; - if (p[0] > 193 or (p[0] == 193 and p[1] > 254) or @intFromPtr(p) > @intFromPtr(r.ptr) + r.len - 6) - return error.InvalidDnsPacket; - p += @as(usize, 1) + @intFromBool(p[0] != 0); - const len = p[8] * @as(usize, 256) + p[9]; - if (@intFromPtr(p) + len > @intFromPtr(r.ptr) + r.len) return error.InvalidDnsPacket; - try callback(ctx, p[1], p[10..][0..len], r); - p += 10 + len; - } -} - -fn dnsParseCallback(ctx: dpc_ctx, rr: u8, data: []const u8, packet: []const u8) !void { - const gpa = ctx.gpa; - switch (rr) { - posix.RR.A => { - if (data.len != 4) return error.InvalidDnsARecord; - try ctx.addrs.append(gpa, .{ - .addr = Address.initIp4(data[0..4].*, ctx.port), - }); - }, - posix.RR.AAAA => { - if (data.len != 16) return error.InvalidDnsAAAARecord; - try ctx.addrs.append(gpa, .{ - .addr = Address.initIp6(data[0..16].*, ctx.port, 0, 0), - }); - }, - posix.RR.CNAME => { - var tmp: [256]u8 = undefined; - // Returns len of compressed name. strlen to get canon name. - _ = try posix.dn_expand(packet, data, &tmp); - const canon_name = mem.sliceTo(&tmp, 0); - if (isValidHostName(canon_name)) { - ctx.canon.items.len = 0; - try ctx.canon.appendSlice(gpa, canon_name); - } - }, - else => return, - } -} - -pub const Stream = struct { - /// Underlying platform-defined type which may or may not be - /// interchangeable with a file system file descriptor. - handle: Handle, - - pub const Handle = switch (native_os) { - .windows => windows.ws2_32.SOCKET, - else => posix.fd_t, - }; - - pub fn close(s: Stream) void { - switch (native_os) { - .windows => windows.closesocket(s.handle) catch unreachable, - else => posix.close(s.handle), - } - } - - pub const ReadError = posix.ReadError || error{ - SocketNotBound, - MessageTooBig, - NetworkSubsystemFailed, - ConnectionResetByPeer, - SocketNotConnected, - }; - - pub const WriteError = posix.SendMsgError || error{ - ConnectionResetByPeer, - SocketNotBound, - MessageTooBig, - NetworkSubsystemFailed, - SystemResources, - SocketNotConnected, - Unexpected, - }; - - pub const Reader = switch (native_os) { - .windows => struct { - /// Use `interface` for portable code. - interface_state: Io.Reader, - /// Use `getStream` for portable code. - net_stream: Stream, - /// Use `getError` for portable code. - error_state: ?Error, - - pub const Error = ReadError; - - pub fn getStream(r: *const Reader) Stream { - return r.net_stream; - } - - pub fn getError(r: *const Reader) ?Error { - return r.error_state; - } - - pub fn interface(r: *Reader) *Io.Reader { - return &r.interface_state; - } - - pub fn init(net_stream: Stream, buffer: []u8) Reader { - return .{ - .interface_state = .{ - .vtable = &.{ - .stream = stream, - .readVec = readVec, - }, - .buffer = buffer, - .seek = 0, - .end = 0, - }, - .net_stream = net_stream, - .error_state = null, - }; - } - - fn stream(io_r: *Io.Reader, io_w: *Io.Writer, limit: Io.Limit) Io.Reader.StreamError!usize { - const dest = limit.slice(try io_w.writableSliceGreedy(1)); - var bufs: [1][]u8 = .{dest}; - const n = try readVec(io_r, &bufs); - io_w.advance(n); - return n; - } - - fn readVec(io_r: *std.Io.Reader, data: [][]u8) Io.Reader.Error!usize { - const r: *Reader = @alignCast(@fieldParentPtr("interface_state", io_r)); - var iovecs: [max_buffers_len]windows.ws2_32.WSABUF = undefined; - const bufs_n, const data_size = try io_r.writableVectorWsa(&iovecs, data); - const bufs = iovecs[0..bufs_n]; - assert(bufs[0].len != 0); - const n = streamBufs(r, bufs) catch |err| { - r.error_state = err; - return error.ReadFailed; - }; - if (n == 0) return error.EndOfStream; - if (n > data_size) { - io_r.end += n - data_size; - return data_size; - } - return n; - } - - fn handleRecvError(winsock_error: windows.ws2_32.WinsockError) Error!void { - switch (winsock_error) { - .WSAECONNRESET => return error.ConnectionResetByPeer, - .WSAEFAULT => unreachable, // a pointer is not completely contained in user address space. - .WSAEINPROGRESS, .WSAEINTR => unreachable, // deprecated and removed in WSA 2.2 - .WSAEINVAL => return error.SocketNotBound, - .WSAEMSGSIZE => return error.MessageTooBig, - .WSAENETDOWN => return error.NetworkSubsystemFailed, - .WSAENETRESET => return error.ConnectionResetByPeer, - .WSAENOTCONN => return error.SocketNotConnected, - .WSAEWOULDBLOCK => return error.WouldBlock, - .WSANOTINITIALISED => unreachable, // WSAStartup must be called before this function - .WSA_IO_PENDING => unreachable, - .WSA_OPERATION_ABORTED => unreachable, // not using overlapped I/O - else => |err| return windows.unexpectedWSAError(err), - } - } - - fn streamBufs(r: *Reader, bufs: []windows.ws2_32.WSABUF) Error!u32 { - var flags: u32 = 0; - var overlapped: windows.OVERLAPPED = std.mem.zeroes(windows.OVERLAPPED); - - var n: u32 = undefined; - if (windows.ws2_32.WSARecv( - r.net_stream.handle, - bufs.ptr, - @intCast(bufs.len), - &n, - &flags, - &overlapped, - null, - ) == windows.ws2_32.SOCKET_ERROR) switch (windows.ws2_32.WSAGetLastError()) { - .WSA_IO_PENDING => { - var result_flags: u32 = undefined; - if (windows.ws2_32.WSAGetOverlappedResult( - r.net_stream.handle, - &overlapped, - &n, - windows.TRUE, - &result_flags, - ) == windows.FALSE) try handleRecvError(windows.ws2_32.WSAGetLastError()); - }, - else => |winsock_error| try handleRecvError(winsock_error), - }; - - return n; - } - }, - else => struct { - /// Use `getStream`, `interface`, and `getError` for portable code. - file_reader: File.Reader, - - pub const Error = ReadError; - - pub fn interface(r: *Reader) *Io.Reader { - return &r.file_reader.interface; - } - - pub fn init(net_stream: Stream, buffer: []u8) Reader { - return .{ - .file_reader = .{ - .interface = File.Reader.initInterface(buffer), - .file = .{ .handle = net_stream.handle }, - .mode = .streaming, - .seek_err = error.Unseekable, - .size_err = error.Streaming, - }, - }; - } - - pub fn getStream(r: *const Reader) Stream { - return .{ .handle = r.file_reader.file.handle }; - } - - pub fn getError(r: *const Reader) ?Error { - return r.file_reader.err; - } - }, - }; - - pub const Writer = switch (native_os) { - .windows => struct { - /// This field is present on all systems. - interface: Io.Writer, - /// Use `getStream` for cross-platform support. - stream: Stream, - /// This field is present on all systems. - err: ?Error = null, - - pub const Error = WriteError; - - pub fn init(stream: Stream, buffer: []u8) Writer { - return .{ - .stream = stream, - .interface = .{ - .vtable = &.{ .drain = drain }, - .buffer = buffer, - }, - }; - } - - pub fn getStream(w: *const Writer) Stream { - return w.stream; - } - - fn addWsaBuf(v: []windows.ws2_32.WSABUF, i: *u32, bytes: []const u8) void { - const cap = std.math.maxInt(u32); - var remaining = bytes; - while (remaining.len > cap) { - if (v.len - i.* == 0) return; - v[i.*] = .{ .buf = @constCast(remaining.ptr), .len = cap }; - i.* += 1; - remaining = remaining[cap..]; - } else { - @branchHint(.likely); - if (v.len - i.* == 0) return; - v[i.*] = .{ .buf = @constCast(remaining.ptr), .len = @intCast(remaining.len) }; - i.* += 1; - } - } - - fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize { - const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w)); - const buffered = io_w.buffered(); - comptime assert(native_os == .windows); - var iovecs: [max_buffers_len]windows.ws2_32.WSABUF = undefined; - var len: u32 = 0; - addWsaBuf(&iovecs, &len, buffered); - for (data[0 .. data.len - 1]) |bytes| addWsaBuf(&iovecs, &len, bytes); - const pattern = data[data.len - 1]; - if (iovecs.len - len != 0) switch (splat) { - 0 => {}, - 1 => addWsaBuf(&iovecs, &len, pattern), - else => switch (pattern.len) { - 0 => {}, - 1 => { - const splat_buffer_candidate = io_w.buffer[io_w.end..]; - var backup_buffer: [64]u8 = undefined; - const splat_buffer = if (splat_buffer_candidate.len >= backup_buffer.len) - splat_buffer_candidate - else - &backup_buffer; - const memset_len = @min(splat_buffer.len, splat); - const buf = splat_buffer[0..memset_len]; - @memset(buf, pattern[0]); - addWsaBuf(&iovecs, &len, buf); - var remaining_splat = splat - buf.len; - while (remaining_splat > splat_buffer.len and len < iovecs.len) { - addWsaBuf(&iovecs, &len, splat_buffer); - remaining_splat -= splat_buffer.len; - } - addWsaBuf(&iovecs, &len, splat_buffer[0..remaining_splat]); - }, - else => for (0..@min(splat, iovecs.len - len)) |_| { - addWsaBuf(&iovecs, &len, pattern); - }, - }, - }; - const n = sendBufs(w.stream.handle, iovecs[0..len]) catch |err| { - w.err = err; - return error.WriteFailed; - }; - return io_w.consume(n); - } - - fn handleSendError(winsock_error: windows.ws2_32.WinsockError) Error!void { - switch (winsock_error) { - .WSAECONNABORTED => return error.ConnectionResetByPeer, - .WSAECONNRESET => return error.ConnectionResetByPeer, - .WSAEFAULT => unreachable, // a pointer is not completely contained in user address space. - .WSAEINPROGRESS, .WSAEINTR => unreachable, // deprecated and removed in WSA 2.2 - .WSAEINVAL => return error.SocketNotBound, - .WSAEMSGSIZE => return error.MessageTooBig, - .WSAENETDOWN => return error.NetworkSubsystemFailed, - .WSAENETRESET => return error.ConnectionResetByPeer, - .WSAENOBUFS => return error.SystemResources, - .WSAENOTCONN => return error.SocketNotConnected, - .WSAENOTSOCK => unreachable, // not a socket - .WSAEOPNOTSUPP => unreachable, // only for message-oriented sockets - .WSAESHUTDOWN => unreachable, // cannot send on a socket after write shutdown - .WSAEWOULDBLOCK => return error.WouldBlock, - .WSANOTINITIALISED => unreachable, // WSAStartup must be called before this function - .WSA_IO_PENDING => unreachable, - .WSA_OPERATION_ABORTED => unreachable, // not using overlapped I/O - else => |err| return windows.unexpectedWSAError(err), - } - } - - fn sendBufs(handle: Stream.Handle, bufs: []windows.ws2_32.WSABUF) Error!u32 { - var n: u32 = undefined; - var overlapped: windows.OVERLAPPED = std.mem.zeroes(windows.OVERLAPPED); - if (windows.ws2_32.WSASend( - handle, - bufs.ptr, - @intCast(bufs.len), - &n, - 0, - &overlapped, - null, - ) == windows.ws2_32.SOCKET_ERROR) switch (windows.ws2_32.WSAGetLastError()) { - .WSA_IO_PENDING => { - var result_flags: u32 = undefined; - if (windows.ws2_32.WSAGetOverlappedResult( - handle, - &overlapped, - &n, - windows.TRUE, - &result_flags, - ) == windows.FALSE) try handleSendError(windows.ws2_32.WSAGetLastError()); - }, - else => |winsock_error| try handleSendError(winsock_error), - }; - - return n; - } - }, - else => struct { - /// This field is present on all systems. - interface: Io.Writer, - - err: ?Error = null, - file_writer: File.Writer, - - pub const Error = WriteError; - - pub fn init(stream: Stream, buffer: []u8) Writer { - return .{ - .interface = .{ - .vtable = &.{ - .drain = drain, - .sendFile = sendFile, - }, - .buffer = buffer, - }, - .file_writer = .initStreaming(.{ .handle = stream.handle }, &.{}), - }; - } - - pub fn getStream(w: *const Writer) Stream { - return .{ .handle = w.file_writer.file.handle }; - } - - fn addBuf(v: []posix.iovec_const, i: *@FieldType(posix.msghdr_const, "iovlen"), bytes: []const u8) void { - // OS checks ptr addr before length so zero length vectors must be omitted. - if (bytes.len == 0) return; - if (v.len - i.* == 0) return; - v[i.*] = .{ .base = bytes.ptr, .len = bytes.len }; - i.* += 1; - } - - fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize { - const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w)); - const buffered = io_w.buffered(); - var iovecs: [max_buffers_len]posix.iovec_const = undefined; - var msg: posix.msghdr_const = .{ - .name = null, - .namelen = 0, - .iov = &iovecs, - .iovlen = 0, - .control = null, - .controllen = 0, - .flags = 0, - }; - addBuf(&iovecs, &msg.iovlen, buffered); - for (data[0 .. data.len - 1]) |bytes| addBuf(&iovecs, &msg.iovlen, bytes); - const pattern = data[data.len - 1]; - if (iovecs.len - msg.iovlen != 0) switch (splat) { - 0 => {}, - 1 => addBuf(&iovecs, &msg.iovlen, pattern), - else => switch (pattern.len) { - 0 => {}, - 1 => { - const splat_buffer_candidate = io_w.buffer[io_w.end..]; - var backup_buffer: [64]u8 = undefined; - const splat_buffer = if (splat_buffer_candidate.len >= backup_buffer.len) - splat_buffer_candidate - else - &backup_buffer; - const memset_len = @min(splat_buffer.len, splat); - const buf = splat_buffer[0..memset_len]; - @memset(buf, pattern[0]); - addBuf(&iovecs, &msg.iovlen, buf); - var remaining_splat = splat - buf.len; - while (remaining_splat > splat_buffer.len and iovecs.len - msg.iovlen != 0) { - assert(buf.len == splat_buffer.len); - addBuf(&iovecs, &msg.iovlen, splat_buffer); - remaining_splat -= splat_buffer.len; - } - addBuf(&iovecs, &msg.iovlen, splat_buffer[0..remaining_splat]); - }, - else => for (0..@min(splat, iovecs.len - msg.iovlen)) |_| { - addBuf(&iovecs, &msg.iovlen, pattern); - }, - }, - }; - const flags = posix.MSG.NOSIGNAL; - return io_w.consume(posix.sendmsg(w.file_writer.file.handle, &msg, flags) catch |err| { - w.err = err; - return error.WriteFailed; - }); - } - - fn sendFile(io_w: *Io.Writer, file_reader: *File.Reader, limit: Io.Limit) Io.Writer.FileError!usize { - const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w)); - const n = try w.file_writer.interface.sendFileHeader(io_w.buffered(), file_reader, limit); - return io_w.consume(n); - } - }, - }; - - pub fn reader(stream: Stream, buffer: []u8) Reader { - return .init(stream, buffer); - } - - pub fn writer(stream: Stream, buffer: []u8) Writer { - return .init(stream, buffer); - } - - const max_buffers_len = 8; - - /// Deprecated in favor of `Reader`. - pub fn read(self: Stream, buffer: []u8) ReadError!usize { - if (native_os == .windows) { - return windows.ReadFile(self.handle, buffer, null); - } - - return posix.read(self.handle, buffer); - } - - /// Deprecated in favor of `Reader`. - pub fn readv(s: Stream, iovecs: []const posix.iovec) ReadError!usize { - if (native_os == .windows) { - if (iovecs.len == 0) return 0; - const first = iovecs[0]; - return windows.ReadFile(s.handle, first.base[0..first.len], null); - } - - return posix.readv(s.handle, iovecs); - } - - /// Deprecated in favor of `Reader`. - pub fn readAtLeast(s: Stream, buffer: []u8, len: usize) ReadError!usize { - assert(len <= buffer.len); - var index: usize = 0; - while (index < len) { - const amt = try s.read(buffer[index..]); - if (amt == 0) break; - index += amt; - } - return index; - } - - /// Deprecated in favor of `Writer`. - pub fn write(self: Stream, buffer: []const u8) WriteError!usize { - var stream_writer = self.writer(&.{}); - return stream_writer.interface.writeVec(&.{buffer}) catch return stream_writer.err.?; - } - - /// Deprecated in favor of `Writer`. - pub fn writeAll(self: Stream, bytes: []const u8) WriteError!void { - var index: usize = 0; - while (index < bytes.len) { - index += try self.write(bytes[index..]); - } - } - - /// Deprecated in favor of `Writer`. - pub fn writev(self: Stream, iovecs: []const posix.iovec_const) WriteError!usize { - return @errorCast(posix.writev(self.handle, iovecs)); - } - - /// Deprecated in favor of `Writer`. - pub fn writevAll(self: Stream, iovecs: []posix.iovec_const) WriteError!void { - if (iovecs.len == 0) return; - - var i: usize = 0; - while (true) { - var amt = try self.writev(iovecs[i..]); - while (amt >= iovecs[i].len) { - amt -= iovecs[i].len; - i += 1; - if (i >= iovecs.len) return; - } - iovecs[i].base += amt; - iovecs[i].len -= amt; - } - } -}; - -pub const Server = struct { - listen_address: Address, - stream: Stream, - - pub const Connection = struct { - stream: Stream, - address: Address, - }; - - pub fn deinit(s: *Server) void { - s.stream.close(); - s.* = undefined; - } - - pub const AcceptError = posix.AcceptError; - - /// Blocks until a client connects to the server. The returned `Connection` has - /// an open stream. - pub fn accept(s: *Server) AcceptError!Connection { - var accepted_addr: Address = undefined; - var addr_len: posix.socklen_t = @sizeOf(Address); - const fd = try posix.accept(s.stream.handle, &accepted_addr.any, &addr_len, posix.SOCK.CLOEXEC); - return .{ - .stream = .{ .handle = fd }, - .address = accepted_addr, - }; - } -}; - -test { - if (builtin.os.tag != .wasi) { - _ = Server; - _ = Stream; - _ = Address; - _ = @import("net/test.zig"); - } -} diff --git a/lib/std/net/test.zig b/lib/std/net/test.zig deleted file mode 100644 index bfafbe6044..0000000000 --- a/lib/std/net/test.zig +++ /dev/null @@ -1,373 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const net = std.net; -const mem = std.mem; -const testing = std.testing; - -test "parse and render IP addresses at comptime" { - comptime { - const ipv6addr = net.Address.parseIp("::1", 0) catch unreachable; - try std.testing.expectFmt("[::1]:0", "{f}", .{ipv6addr}); - - const ipv4addr = net.Address.parseIp("127.0.0.1", 0) catch unreachable; - try std.testing.expectFmt("127.0.0.1:0", "{f}", .{ipv4addr}); - - try testing.expectError(error.InvalidIPAddressFormat, net.Address.parseIp("::123.123.123.123", 0)); - try testing.expectError(error.InvalidIPAddressFormat, net.Address.parseIp("127.01.0.1", 0)); - try testing.expectError(error.InvalidIPAddressFormat, net.Address.resolveIp("::123.123.123.123", 0)); - try testing.expectError(error.InvalidIPAddressFormat, net.Address.resolveIp("127.01.0.1", 0)); - } -} - -test "format IPv6 address with no zero runs" { - const addr = try std.net.Address.parseIp6("2001:db8:1:2:3:4:5:6", 0); - try std.testing.expectFmt("[2001:db8:1:2:3:4:5:6]:0", "{f}", .{addr}); -} - -test "parse IPv6 addresses and check compressed form" { - try std.testing.expectFmt("[2001:db8::1:0:0:2]:0", "{f}", .{ - try std.net.Address.parseIp6("2001:0db8:0000:0000:0001:0000:0000:0002", 0), - }); - try std.testing.expectFmt("[2001:db8::1:2]:0", "{f}", .{ - try std.net.Address.parseIp6("2001:0db8:0000:0000:0000:0000:0001:0002", 0), - }); - try std.testing.expectFmt("[2001:db8:1:0:1::2]:0", "{f}", .{ - try std.net.Address.parseIp6("2001:0db8:0001:0000:0001:0000:0000:0002", 0), - }); -} - -test "parse IPv6 address, check raw bytes" { - const expected_raw: [16]u8 = .{ - 0x20, 0x01, 0x0d, 0xb8, // 2001:db8 - 0x00, 0x00, 0x00, 0x00, // :0000:0000 - 0x00, 0x01, 0x00, 0x00, // :0001:0000 - 0x00, 0x00, 0x00, 0x02, // :0000:0002 - }; - - const addr = try std.net.Address.parseIp6("2001:db8:0000:0000:0001:0000:0000:0002", 0); - - const actual_raw = addr.in6.sa.addr[0..]; - try std.testing.expectEqualSlices(u8, expected_raw[0..], actual_raw); -} - -test "parse and render IPv6 addresses" { - var buffer: [100]u8 = undefined; - const ips = [_][]const u8{ - "FF01:0:0:0:0:0:0:FB", - "FF01::Fb", - "::1", - "::", - "1::", - "2001:db8::", - "::1234:5678", - "2001:db8::1234:5678", - "FF01::FB%1234", - "::ffff:123.5.123.5", - }; - const printed = [_][]const u8{ - "ff01::fb", - "ff01::fb", - "::1", - "::", - "1::", - "2001:db8::", - "::1234:5678", - "2001:db8::1234:5678", - "ff01::fb%1234", - "::ffff:123.5.123.5", - }; - for (ips, 0..) |ip, i| { - const addr = net.Address.parseIp6(ip, 0) catch unreachable; - var newIp = std.fmt.bufPrint(buffer[0..], "{f}", .{addr}) catch unreachable; - try std.testing.expect(std.mem.eql(u8, printed[i], newIp[1 .. newIp.len - 3])); - - if (builtin.os.tag == .linux) { - const addr_via_resolve = net.Address.resolveIp6(ip, 0) catch unreachable; - var newResolvedIp = std.fmt.bufPrint(buffer[0..], "{f}", .{addr_via_resolve}) catch unreachable; - try std.testing.expect(std.mem.eql(u8, printed[i], newResolvedIp[1 .. newResolvedIp.len - 3])); - } - } - - try testing.expectError(error.InvalidCharacter, net.Address.parseIp6(":::", 0)); - try testing.expectError(error.Overflow, net.Address.parseIp6("FF001::FB", 0)); - try testing.expectError(error.InvalidCharacter, net.Address.parseIp6("FF01::Fb:zig", 0)); - try testing.expectError(error.InvalidEnd, net.Address.parseIp6("FF01:0:0:0:0:0:0:FB:", 0)); - try testing.expectError(error.Incomplete, net.Address.parseIp6("FF01:", 0)); - try testing.expectError(error.InvalidIpv4Mapping, net.Address.parseIp6("::123.123.123.123", 0)); - try testing.expectError(error.Incomplete, net.Address.parseIp6("1", 0)); - // TODO Make this test pass on other operating systems. - if (builtin.os.tag == .linux or comptime builtin.os.tag.isDarwin() or builtin.os.tag == .windows) { - try testing.expectError(error.Incomplete, net.Address.resolveIp6("ff01::fb%", 0)); - // Assumes IFNAMESIZE will always be a multiple of 2 - try testing.expectError(error.Overflow, net.Address.resolveIp6("ff01::fb%wlp3" ++ "s0" ** @divExact(std.posix.IFNAMESIZE - 4, 2), 0)); - try testing.expectError(error.Overflow, net.Address.resolveIp6("ff01::fb%12345678901234", 0)); - } -} - -test "invalid but parseable IPv6 scope ids" { - if (builtin.os.tag != .linux and comptime !builtin.os.tag.isDarwin() and builtin.os.tag != .windows) { - // Currently, resolveIp6 with alphanumerical scope IDs only works on Linux. - // TODO Make this test pass on other operating systems. - return error.SkipZigTest; - } - - try testing.expectError(error.InterfaceNotFound, net.Address.resolveIp6("ff01::fb%123s45678901234", 0)); -} - -test "parse and render IPv4 addresses" { - var buffer: [18]u8 = undefined; - for ([_][]const u8{ - "0.0.0.0", - "255.255.255.255", - "1.2.3.4", - "123.255.0.91", - "127.0.0.1", - }) |ip| { - const addr = net.Address.parseIp4(ip, 0) catch unreachable; - var newIp = std.fmt.bufPrint(buffer[0..], "{f}", .{addr}) catch unreachable; - try std.testing.expect(std.mem.eql(u8, ip, newIp[0 .. newIp.len - 2])); - } - - try testing.expectError(error.Overflow, net.Address.parseIp4("256.0.0.1", 0)); - try testing.expectError(error.InvalidCharacter, net.Address.parseIp4("x.0.0.1", 0)); - try testing.expectError(error.InvalidEnd, net.Address.parseIp4("127.0.0.1.1", 0)); - try testing.expectError(error.Incomplete, net.Address.parseIp4("127.0.0.", 0)); - try testing.expectError(error.InvalidCharacter, net.Address.parseIp4("100..0.1", 0)); - try testing.expectError(error.NonCanonical, net.Address.parseIp4("127.01.0.1", 0)); -} - -test "parse and render UNIX addresses" { - if (builtin.os.tag == .wasi) return error.SkipZigTest; - if (!net.has_unix_sockets) return error.SkipZigTest; - - const addr = net.Address.initUnix("/tmp/testpath") catch unreachable; - try std.testing.expectFmt("/tmp/testpath", "{f}", .{addr}); - - const too_long = [_]u8{'a'} ** 200; - try testing.expectError(error.NameTooLong, net.Address.initUnix(too_long[0..])); -} - -test "resolve DNS" { - if (builtin.os.tag == .wasi) return error.SkipZigTest; - - if (builtin.os.tag == .windows) { - _ = try std.os.windows.WSAStartup(2, 2); - } - defer { - if (builtin.os.tag == .windows) { - std.os.windows.WSACleanup() catch unreachable; - } - } - - // Resolve localhost, this should not fail. - { - const localhost_v4 = try net.Address.parseIp("127.0.0.1", 80); - const localhost_v6 = try net.Address.parseIp("::2", 80); - - const result = try net.getAddressList(testing.allocator, "localhost", 80); - defer result.deinit(); - for (result.addrs) |addr| { - if (addr.eql(localhost_v4) or addr.eql(localhost_v6)) break; - } else @panic("unexpected address for localhost"); - } - - { - // The tests are required to work even when there is no Internet connection, - // so some of these errors we must accept and skip the test. - const result = net.getAddressList(testing.allocator, "example.com", 80) catch |err| switch (err) { - error.UnknownHostName => return error.SkipZigTest, - error.TemporaryNameServerFailure => return error.SkipZigTest, - else => return err, - }; - result.deinit(); - } -} - -test "listen on a port, send bytes, receive bytes" { - if (builtin.single_threaded) return error.SkipZigTest; - if (builtin.os.tag == .wasi) return error.SkipZigTest; - - if (builtin.os.tag == .windows) { - _ = try std.os.windows.WSAStartup(2, 2); - } - defer { - if (builtin.os.tag == .windows) { - std.os.windows.WSACleanup() catch unreachable; - } - } - - // Try only the IPv4 variant as some CI builders have no IPv6 localhost - // configured. - const localhost = try net.Address.parseIp("127.0.0.1", 0); - - var server = try localhost.listen(.{}); - defer server.deinit(); - - const S = struct { - fn clientFn(server_address: net.Address) !void { - const socket = try net.tcpConnectToAddress(server_address); - defer socket.close(); - - var stream_writer = socket.writer(&.{}); - try stream_writer.interface.writeAll("Hello world!"); - } - }; - - const t = try std.Thread.spawn(.{}, S.clientFn, .{server.listen_address}); - defer t.join(); - - var client = try server.accept(); - defer client.stream.close(); - var buf: [16]u8 = undefined; - var stream_reader = client.stream.reader(&.{}); - const n = try stream_reader.interface().readSliceShort(&buf); - - try testing.expectEqual(@as(usize, 12), n); - try testing.expectEqualSlices(u8, "Hello world!", buf[0..n]); -} - -test "listen on an in use port" { - if (builtin.os.tag != .linux and comptime !builtin.os.tag.isDarwin() and builtin.os.tag != .windows) { - // TODO build abstractions for other operating systems - return error.SkipZigTest; - } - - const localhost = try net.Address.parseIp("127.0.0.1", 0); - - var server1 = try localhost.listen(.{ .reuse_address = true }); - defer server1.deinit(); - - var server2 = try server1.listen_address.listen(.{ .reuse_address = true }); - defer server2.deinit(); -} - -fn testClientToHost(allocator: mem.Allocator, name: []const u8, port: u16) anyerror!void { - if (builtin.os.tag == .wasi) return error.SkipZigTest; - - const connection = try net.tcpConnectToHost(allocator, name, port); - defer connection.close(); - - var buf: [100]u8 = undefined; - const len = try connection.read(&buf); - const msg = buf[0..len]; - try testing.expect(mem.eql(u8, msg, "hello from server\n")); -} - -fn testClient(addr: net.Address) anyerror!void { - if (builtin.os.tag == .wasi) return error.SkipZigTest; - - const socket_file = try net.tcpConnectToAddress(addr); - defer socket_file.close(); - - var buf: [100]u8 = undefined; - const len = try socket_file.read(&buf); - const msg = buf[0..len]; - try testing.expect(mem.eql(u8, msg, "hello from server\n")); -} - -fn testServer(server: *net.Server) anyerror!void { - if (builtin.os.tag == .wasi) return error.SkipZigTest; - - var client = try server.accept(); - - const stream = client.stream.writer(); - try stream.print("hello from server\n", .{}); -} - -test "listen on a unix socket, send bytes, receive bytes" { - if (builtin.single_threaded) return error.SkipZigTest; - if (!net.has_unix_sockets) return error.SkipZigTest; - - if (builtin.os.tag == .windows) { - _ = try std.os.windows.WSAStartup(2, 2); - } - defer { - if (builtin.os.tag == .windows) { - std.os.windows.WSACleanup() catch unreachable; - } - } - - const socket_path = try generateFileName("socket.unix"); - defer testing.allocator.free(socket_path); - - const socket_addr = try net.Address.initUnix(socket_path); - defer std.fs.cwd().deleteFile(socket_path) catch {}; - - var server = try socket_addr.listen(.{}); - defer server.deinit(); - - const S = struct { - fn clientFn(path: []const u8) !void { - const socket = try net.connectUnixSocket(path); - defer socket.close(); - - var stream_writer = socket.writer(&.{}); - try stream_writer.interface.writeAll("Hello world!"); - } - }; - - const t = try std.Thread.spawn(.{}, S.clientFn, .{socket_path}); - defer t.join(); - - var client = try server.accept(); - defer client.stream.close(); - var buf: [16]u8 = undefined; - var stream_reader = client.stream.reader(&.{}); - const n = try stream_reader.interface().readSliceShort(&buf); - - try testing.expectEqual(@as(usize, 12), n); - try testing.expectEqualSlices(u8, "Hello world!", buf[0..n]); -} - -test "listen on a unix socket with reuse_address option" { - if (!net.has_unix_sockets) return error.SkipZigTest; - // Windows doesn't implement reuse port option. - if (builtin.os.tag == .windows) return error.SkipZigTest; - - const socket_path = try generateFileName("socket.unix"); - defer testing.allocator.free(socket_path); - - const socket_addr = try net.Address.initUnix(socket_path); - defer std.fs.cwd().deleteFile(socket_path) catch {}; - - var server = try socket_addr.listen(.{ .reuse_address = true }); - server.deinit(); -} - -fn generateFileName(base_name: []const u8) ![]const u8 { - const random_bytes_count = 12; - const sub_path_len = comptime std.fs.base64_encoder.calcSize(random_bytes_count); - var random_bytes: [12]u8 = undefined; - std.crypto.random.bytes(&random_bytes); - var sub_path: [sub_path_len]u8 = undefined; - _ = std.fs.base64_encoder.encode(&sub_path, &random_bytes); - return std.fmt.allocPrint(testing.allocator, "{s}-{s}", .{ sub_path[0..], base_name }); -} - -test "non-blocking tcp server" { - if (builtin.os.tag == .wasi) return error.SkipZigTest; - if (true) { - // https://github.com/ziglang/zig/issues/18315 - return error.SkipZigTest; - } - - const localhost = try net.Address.parseIp("127.0.0.1", 0); - var server = localhost.listen(.{ .force_nonblocking = true }); - defer server.deinit(); - - const accept_err = server.accept(); - try testing.expectError(error.WouldBlock, accept_err); - - const socket_file = try net.tcpConnectToAddress(server.listen_address); - defer socket_file.close(); - - var client = try server.accept(); - defer client.stream.close(); - const stream = client.stream.writer(); - try stream.print("hello from server\n", .{}); - - var buf: [100]u8 = undefined; - const len = try socket_file.read(&buf); - const msg = buf[0..len]; - try testing.expect(mem.eql(u8, msg, "hello from server\n")); -} diff --git a/lib/std/os.zig b/lib/std/os.zig index a3d659d4ca..02fddb32b4 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -57,7 +57,7 @@ pub var argv: [][*:0]u8 = if (builtin.link_libc) undefined else switch (native_o }; /// Call from Windows-specific code if you already have a WTF-16LE encoded, null terminated string. -/// Otherwise use `access` or `accessZ`. +/// Otherwise use `access`. pub fn accessW(path: [*:0]const u16) windows.GetFileAttributesError!void { const ret = try windows.GetFileAttributesW(path); if (ret != windows.INVALID_FILE_ATTRIBUTES) { @@ -137,8 +137,6 @@ pub fn getFdPath(fd: std.posix.fd_t, out_buffer: *[max_path_bytes]u8) std.posix. switch (err) { error.NotLink => unreachable, error.BadPathName => unreachable, - error.InvalidUtf8 => unreachable, // WASI-only - error.InvalidWtf8 => unreachable, // Windows-only error.UnsupportedReparsePointType => unreachable, // Windows-only error.NetworkNotFound => unreachable, // Windows-only else => |e| return e, @@ -153,7 +151,6 @@ pub fn getFdPath(fd: std.posix.fd_t, out_buffer: *[max_path_bytes]u8) std.posix. const target = posix.readlinkZ(proc_path, out_buffer) catch |err| switch (err) { error.UnsupportedReparsePointType => unreachable, error.NotLink => unreachable, - error.InvalidUtf8 => unreachable, // WASI-only else => |e| return e, }; return target; @@ -201,28 +198,13 @@ pub fn getFdPath(fd: std.posix.fd_t, out_buffer: *[max_path_bytes]u8) std.posix. } } -/// WASI-only. Same as `fstatat` but targeting WASI. -/// `pathname` should be encoded as valid UTF-8. -/// See also `fstatat`. -pub fn fstatat_wasi(dirfd: posix.fd_t, pathname: []const u8, flags: wasi.lookupflags_t) posix.FStatAtError!wasi.filestat_t { - var stat: wasi.filestat_t = undefined; - switch (wasi.path_filestat_get(dirfd, flags, pathname.ptr, pathname.len, &stat)) { - .SUCCESS => return stat, - .INVAL => unreachable, - .BADF => unreachable, // Always a race condition. - .NOMEM => return error.SystemResources, - .ACCES => return error.AccessDenied, - .FAULT => unreachable, - .NAMETOOLONG => return error.NameTooLong, - .NOENT => return error.FileNotFound, - .NOTDIR => return error.FileNotFound, - .NOTCAPABLE => return error.AccessDenied, - .ILSEQ => return error.InvalidUtf8, - else => |err| return posix.unexpectedErrno(err), - } -} +pub const FstatError = error{ + SystemResources, + AccessDenied, + Unexpected, +}; -pub fn fstat_wasi(fd: posix.fd_t) posix.FStatError!wasi.filestat_t { +pub fn fstat_wasi(fd: posix.fd_t) FstatError!wasi.filestat_t { var stat: wasi.filestat_t = undefined; switch (wasi.fd_filestat_get(fd, &stat)) { .SUCCESS => return stat, diff --git a/lib/std/os/emscripten.zig b/lib/std/os/emscripten.zig index 1ecb4f6bb0..cb444b360d 100644 --- a/lib/std/os/emscripten.zig +++ b/lib/std/os/emscripten.zig @@ -479,50 +479,7 @@ pub const SHUT = struct { pub const RDWR = 2; }; -pub const SIG = struct { - pub const BLOCK = 0; - pub const UNBLOCK = 1; - pub const SETMASK = 2; - - pub const HUP = 1; - pub const INT = 2; - pub const QUIT = 3; - pub const ILL = 4; - pub const TRAP = 5; - pub const ABRT = 6; - pub const IOT = ABRT; - pub const BUS = 7; - pub const FPE = 8; - pub const KILL = 9; - pub const USR1 = 10; - pub const SEGV = 11; - pub const USR2 = 12; - pub const PIPE = 13; - pub const ALRM = 14; - pub const TERM = 15; - pub const STKFLT = 16; - pub const CHLD = 17; - pub const CONT = 18; - pub const STOP = 19; - pub const TSTP = 20; - pub const TTIN = 21; - pub const TTOU = 22; - pub const URG = 23; - pub const XCPU = 24; - pub const XFSZ = 25; - pub const VTALRM = 26; - pub const PROF = 27; - pub const WINCH = 28; - pub const IO = 29; - pub const POLL = 29; - pub const PWR = 30; - pub const SYS = 31; - pub const UNUSED = SIG.SYS; - - pub const ERR: ?Sigaction.handler_fn = @ptrFromInt(std.math.maxInt(usize)); - pub const DFL: ?Sigaction.handler_fn = @ptrFromInt(0); - pub const IGN: ?Sigaction.handler_fn = @ptrFromInt(1); -}; +pub const SIG = linux.SIG; pub const Sigaction = extern struct { pub const handler_fn = *align(1) const fn (i32) callconv(.c) void; diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig index 11add50a05..882de1f458 100644 --- a/lib/std/os/linux.zig +++ b/lib/std/os/linux.zig @@ -1,10 +1,8 @@ //! This file provides the system interface functions for Linux matching those //! that are provided by libc, whether or not libc is linked. The following //! abstractions are made: -//! * Work around kernel bugs and limitations. For example, see sendmmsg. //! * Implement all the syscalls in the same way that libc functions will //! provide `rename` when only the `renameat` syscall exists. -//! * Does not support POSIX thread cancellation. const std = @import("../std.zig"); const builtin = @import("builtin"); const assert = std.debug.assert; @@ -624,7 +622,7 @@ pub fn fork() usize { } else if (@hasField(SYS, "fork")) { return syscall0(.fork); } else { - return syscall2(.clone, SIG.CHLD, 0); + return syscall2(.clone, @intFromEnum(SIG.CHLD), 0); } } @@ -1534,16 +1532,16 @@ pub fn getrandom(buf: [*]u8, count: usize, flags: u32) usize { return syscall3(.getrandom, @intFromPtr(buf), count, flags); } -pub fn kill(pid: pid_t, sig: i32) usize { - return syscall2(.kill, @as(usize, @bitCast(@as(isize, pid))), @as(usize, @bitCast(@as(isize, sig)))); +pub fn kill(pid: pid_t, sig: SIG) usize { + return syscall2(.kill, @as(usize, @bitCast(@as(isize, pid))), @intFromEnum(sig)); } -pub fn tkill(tid: pid_t, sig: i32) usize { - return syscall2(.tkill, @as(usize, @bitCast(@as(isize, tid))), @as(usize, @bitCast(@as(isize, sig)))); +pub fn tkill(tid: pid_t, sig: SIG) usize { + return syscall2(.tkill, @as(usize, @bitCast(@as(isize, tid))), @intFromEnum(sig)); } -pub fn tgkill(tgid: pid_t, tid: pid_t, sig: i32) usize { - return syscall3(.tgkill, @as(usize, @bitCast(@as(isize, tgid))), @as(usize, @bitCast(@as(isize, tid))), @as(usize, @bitCast(@as(isize, sig)))); +pub fn tgkill(tgid: pid_t, tid: pid_t, sig: SIG) usize { + return syscall3(.tgkill, @as(usize, @bitCast(@as(isize, tgid))), @as(usize, @bitCast(@as(isize, tid))), @intFromEnum(sig)); } pub fn link(oldpath: [*:0]const u8, newpath: [*:0]const u8) usize { @@ -1836,7 +1834,7 @@ pub fn seteuid(euid: uid_t) usize { // id will not be changed. Since uid_t is unsigned, this wraps around to the // max value in C. comptime assert(@typeInfo(uid_t) == .int and @typeInfo(uid_t).int.signedness == .unsigned); - return setresuid(std.math.maxInt(uid_t), euid, std.math.maxInt(uid_t)); + return setresuid(maxInt(uid_t), euid, maxInt(uid_t)); } pub fn setegid(egid: gid_t) usize { @@ -1847,7 +1845,7 @@ pub fn setegid(egid: gid_t) usize { // id will not be changed. Since gid_t is unsigned, this wraps around to the // max value in C. comptime assert(@typeInfo(uid_t) == .int and @typeInfo(uid_t).int.signedness == .unsigned); - return setresgid(std.math.maxInt(gid_t), egid, std.math.maxInt(gid_t)); + return setresgid(maxInt(gid_t), egid, maxInt(gid_t)); } pub fn getresuid(ruid: *uid_t, euid: *uid_t, suid: *uid_t) usize { @@ -1925,11 +1923,11 @@ pub fn sigprocmask(flags: u32, noalias set: ?*const sigset_t, noalias oldset: ?* return syscall4(.rt_sigprocmask, flags, @intFromPtr(set), @intFromPtr(oldset), NSIG / 8); } -pub fn sigaction(sig: u8, noalias act: ?*const Sigaction, noalias oact: ?*Sigaction) usize { - assert(sig > 0); - assert(sig < NSIG); - assert(sig != SIG.KILL); - assert(sig != SIG.STOP); +pub fn sigaction(sig: SIG, noalias act: ?*const Sigaction, noalias oact: ?*Sigaction) usize { + assert(@intFromEnum(sig) > 0); + assert(@intFromEnum(sig) < NSIG); + assert(sig != .KILL); + assert(sig != .STOP); var ksa: k_sigaction = undefined; var oldksa: k_sigaction = undefined; @@ -1960,8 +1958,8 @@ pub fn sigaction(sig: u8, noalias act: ?*const Sigaction, noalias oact: ?*Sigact const result = switch (native_arch) { // The sparc version of rt_sigaction needs the restorer function to be passed as an argument too. - .sparc, .sparc64 => syscall5(.rt_sigaction, sig, ksa_arg, oldksa_arg, @intFromPtr(ksa.restorer), mask_size), - else => syscall4(.rt_sigaction, sig, ksa_arg, oldksa_arg, mask_size), + .sparc, .sparc64 => syscall5(.rt_sigaction, @intFromEnum(sig), ksa_arg, oldksa_arg, @intFromPtr(ksa.restorer), mask_size), + else => syscall4(.rt_sigaction, @intFromEnum(sig), ksa_arg, oldksa_arg, mask_size), }; if (E.init(result) != .SUCCESS) return result; @@ -2011,27 +2009,27 @@ pub fn sigfillset() sigset_t { return [_]SigsetElement{~@as(SigsetElement, 0)} ** sigset_len; } -fn sigset_bit_index(sig: usize) struct { word: usize, mask: SigsetElement } { - assert(sig > 0); - assert(sig < NSIG); - const bit = sig - 1; +fn sigset_bit_index(sig: SIG) struct { word: usize, mask: SigsetElement } { + assert(@intFromEnum(sig) > 0); + assert(@intFromEnum(sig) < NSIG); + const bit = @intFromEnum(sig) - 1; return .{ .word = bit / @bitSizeOf(SigsetElement), .mask = @as(SigsetElement, 1) << @truncate(bit % @bitSizeOf(SigsetElement)), }; } -pub fn sigaddset(set: *sigset_t, sig: usize) void { +pub fn sigaddset(set: *sigset_t, sig: SIG) void { const index = sigset_bit_index(sig); (set.*)[index.word] |= index.mask; } -pub fn sigdelset(set: *sigset_t, sig: usize) void { +pub fn sigdelset(set: *sigset_t, sig: SIG) void { const index = sigset_bit_index(sig); (set.*)[index.word] ^= index.mask; } -pub fn sigismember(set: *const sigset_t, sig: usize) bool { +pub fn sigismember(set: *const sigset_t, sig: SIG) bool { const index = sigset_bit_index(sig); return ((set.*)[index.word] & index.mask) != 0; } @@ -2081,44 +2079,7 @@ pub fn sendmsg(fd: i32, msg: *const msghdr_const, flags: u32) usize { } } -pub fn sendmmsg(fd: i32, msgvec: [*]mmsghdr_const, vlen: u32, flags: u32) usize { - if (@typeInfo(usize).int.bits > @typeInfo(@typeInfo(mmsghdr).@"struct".fields[1].type).int.bits) { - // workaround kernel brokenness: - // if adding up all iov_len overflows a i32 then split into multiple calls - // see https://www.openwall.com/lists/musl/2014/06/07/5 - const kvlen = if (vlen > IOV_MAX) IOV_MAX else vlen; // matches kernel - var next_unsent: usize = 0; - for (msgvec[0..kvlen], 0..) |*msg, i| { - var size: i32 = 0; - const msg_iovlen = @as(usize, @intCast(msg.hdr.iovlen)); // kernel side this is treated as unsigned - for (msg.hdr.iov[0..msg_iovlen]) |iov| { - if (iov.len > std.math.maxInt(i32) or @addWithOverflow(size, @as(i32, @intCast(iov.len)))[1] != 0) { - // batch-send all messages up to the current message - if (next_unsent < i) { - const batch_size = i - next_unsent; - const r = syscall4(.sendmmsg, @as(usize, @bitCast(@as(isize, fd))), @intFromPtr(&msgvec[next_unsent]), batch_size, flags); - if (E.init(r) != .SUCCESS) return next_unsent; - if (r < batch_size) return next_unsent + r; - } - // send current message as own packet - const r = sendmsg(fd, &msg.hdr, flags); - if (E.init(r) != .SUCCESS) return r; - // Linux limits the total bytes sent by sendmsg to INT_MAX, so this cast is safe. - msg.len = @as(u32, @intCast(r)); - next_unsent = i + 1; - break; - } - size += @intCast(iov.len); - } - } - if (next_unsent < kvlen or next_unsent == 0) { // want to make sure at least one syscall occurs (e.g. to trigger MSG.EOR) - const batch_size = kvlen - next_unsent; - const r = syscall4(.sendmmsg, @as(usize, @bitCast(@as(isize, fd))), @intFromPtr(&msgvec[next_unsent]), batch_size, flags); - if (E.init(r) != .SUCCESS) return r; - return next_unsent + r; - } - return kvlen; - } +pub fn sendmmsg(fd: i32, msgvec: [*]mmsghdr, vlen: u32, flags: u32) usize { return syscall4(.sendmmsg, @as(usize, @bitCast(@as(isize, fd))), @intFromPtr(msgvec), vlen, flags); } @@ -2674,11 +2635,11 @@ pub fn pidfd_getfd(pidfd: fd_t, targetfd: fd_t, flags: u32) usize { ); } -pub fn pidfd_send_signal(pidfd: fd_t, sig: i32, info: ?*siginfo_t, flags: u32) usize { +pub fn pidfd_send_signal(pidfd: fd_t, sig: SIG, info: ?*siginfo_t, flags: u32) usize { return syscall4( .pidfd_send_signal, @as(usize, @bitCast(@as(isize, pidfd))), - @as(usize, @bitCast(@as(isize, sig))), + @intFromEnum(sig), @intFromPtr(info), flags, ); @@ -3775,136 +3736,138 @@ pub const SA = if (is_mips) struct { pub const RESTORER = 0x04000000; }; -pub const SIG = if (is_mips) struct { +pub const SIG = if (is_mips) enum(u32) { pub const BLOCK = 1; pub const UNBLOCK = 2; pub const SETMASK = 3; - // https://github.com/torvalds/linux/blob/ca91b9500108d4cf083a635c2e11c884d5dd20ea/arch/mips/include/uapi/asm/signal.h#L25 - pub const HUP = 1; - pub const INT = 2; - pub const QUIT = 3; - pub const ILL = 4; - pub const TRAP = 5; - pub const ABRT = 6; - pub const IOT = ABRT; - pub const EMT = 7; - pub const FPE = 8; - pub const KILL = 9; - pub const BUS = 10; - pub const SEGV = 11; - pub const SYS = 12; - pub const PIPE = 13; - pub const ALRM = 14; - pub const TERM = 15; - pub const USR1 = 16; - pub const USR2 = 17; - pub const CHLD = 18; - pub const PWR = 19; - pub const WINCH = 20; - pub const URG = 21; - pub const IO = 22; - pub const POLL = IO; - pub const STOP = 23; - pub const TSTP = 24; - pub const CONT = 25; - pub const TTIN = 26; - pub const TTOU = 27; - pub const VTALRM = 28; - pub const PROF = 29; - pub const XCPU = 30; - pub const XFZ = 31; - pub const ERR: ?Sigaction.handler_fn = @ptrFromInt(maxInt(usize)); pub const DFL: ?Sigaction.handler_fn = @ptrFromInt(0); pub const IGN: ?Sigaction.handler_fn = @ptrFromInt(1); -} else if (is_sparc) struct { + + pub const IOT: SIG = .ABRT; + pub const POLL: SIG = .IO; + + // /arch/mips/include/uapi/asm/signal.h#L25 + HUP = 1, + INT = 2, + QUIT = 3, + ILL = 4, + TRAP = 5, + ABRT = 6, + EMT = 7, + FPE = 8, + KILL = 9, + BUS = 10, + SEGV = 11, + SYS = 12, + PIPE = 13, + ALRM = 14, + TERM = 15, + USR1 = 16, + USR2 = 17, + CHLD = 18, + PWR = 19, + WINCH = 20, + URG = 21, + IO = 22, + STOP = 23, + TSTP = 24, + CONT = 25, + TTIN = 26, + TTOU = 27, + VTALRM = 28, + PROF = 29, + XCPU = 30, + XFZ = 31, +} else if (is_sparc) enum(u32) { pub const BLOCK = 1; pub const UNBLOCK = 2; pub const SETMASK = 4; - pub const HUP = 1; - pub const INT = 2; - pub const QUIT = 3; - pub const ILL = 4; - pub const TRAP = 5; - pub const ABRT = 6; - pub const EMT = 7; - pub const FPE = 8; - pub const KILL = 9; - pub const BUS = 10; - pub const SEGV = 11; - pub const SYS = 12; - pub const PIPE = 13; - pub const ALRM = 14; - pub const TERM = 15; - pub const URG = 16; - pub const STOP = 17; - pub const TSTP = 18; - pub const CONT = 19; - pub const CHLD = 20; - pub const TTIN = 21; - pub const TTOU = 22; - pub const POLL = 23; - pub const XCPU = 24; - pub const XFSZ = 25; - pub const VTALRM = 26; - pub const PROF = 27; - pub const WINCH = 28; - pub const LOST = 29; - pub const USR1 = 30; - pub const USR2 = 31; - pub const IOT = ABRT; - pub const CLD = CHLD; - pub const PWR = LOST; - pub const IO = SIG.POLL; - pub const ERR: ?Sigaction.handler_fn = @ptrFromInt(maxInt(usize)); pub const DFL: ?Sigaction.handler_fn = @ptrFromInt(0); pub const IGN: ?Sigaction.handler_fn = @ptrFromInt(1); -} else struct { + + pub const IOT: SIG = .ABRT; + pub const CLD: SIG = .CHLD; + pub const PWR: SIG = .LOST; + pub const POLL: SIG = .IO; + + HUP = 1, + INT = 2, + QUIT = 3, + ILL = 4, + TRAP = 5, + ABRT = 6, + EMT = 7, + FPE = 8, + KILL = 9, + BUS = 10, + SEGV = 11, + SYS = 12, + PIPE = 13, + ALRM = 14, + TERM = 15, + URG = 16, + STOP = 17, + TSTP = 18, + CONT = 19, + CHLD = 20, + TTIN = 21, + TTOU = 22, + IO = 23, + XCPU = 24, + XFSZ = 25, + VTALRM = 26, + PROF = 27, + WINCH = 28, + LOST = 29, + USR1 = 30, + USR2 = 31, +} else enum(u32) { pub const BLOCK = 0; pub const UNBLOCK = 1; pub const SETMASK = 2; - pub const HUP = 1; - pub const INT = 2; - pub const QUIT = 3; - pub const ILL = 4; - pub const TRAP = 5; - pub const ABRT = 6; - pub const IOT = ABRT; - pub const BUS = 7; - pub const FPE = 8; - pub const KILL = 9; - pub const USR1 = 10; - pub const SEGV = 11; - pub const USR2 = 12; - pub const PIPE = 13; - pub const ALRM = 14; - pub const TERM = 15; - pub const STKFLT = 16; - pub const CHLD = 17; - pub const CONT = 18; - pub const STOP = 19; - pub const TSTP = 20; - pub const TTIN = 21; - pub const TTOU = 22; - pub const URG = 23; - pub const XCPU = 24; - pub const XFSZ = 25; - pub const VTALRM = 26; - pub const PROF = 27; - pub const WINCH = 28; - pub const IO = 29; - pub const POLL = 29; - pub const PWR = 30; - pub const SYS = 31; - pub const UNUSED = SIG.SYS; - pub const ERR: ?Sigaction.handler_fn = @ptrFromInt(maxInt(usize)); pub const DFL: ?Sigaction.handler_fn = @ptrFromInt(0); pub const IGN: ?Sigaction.handler_fn = @ptrFromInt(1); + + pub const POLL: SIG = .IO; + pub const IOT: SIG = .ABRT; + + HUP = 1, + INT = 2, + QUIT = 3, + ILL = 4, + TRAP = 5, + ABRT = 6, + BUS = 7, + FPE = 8, + KILL = 9, + USR1 = 10, + SEGV = 11, + USR2 = 12, + PIPE = 13, + ALRM = 14, + TERM = 15, + STKFLT = 16, + CHLD = 17, + CONT = 18, + STOP = 19, + TSTP = 20, + TTIN = 21, + TTOU = 22, + URG = 23, + XCPU = 24, + XFSZ = 25, + VTALRM = 26, + PROF = 27, + WINCH = 28, + IO = 29, + PWR = 30, + SYS = 31, }; pub const kernel_rwf = u32; @@ -5825,7 +5788,7 @@ pub const TFD = switch (native_arch) { }; const k_sigaction_funcs = struct { - const handler = ?*align(1) const fn (i32) callconv(.c) void; + const handler = ?*align(1) const fn (SIG) callconv(.c) void; const restorer = *const fn () callconv(.c) void; }; @@ -5856,8 +5819,8 @@ pub const k_sigaction = switch (native_arch) { /// /// Renamed from `sigaction` to `Sigaction` to avoid conflict with the syscall. pub const Sigaction = struct { - pub const handler_fn = *align(1) const fn (i32) callconv(.c) void; - pub const sigaction_fn = *const fn (i32, *const siginfo_t, ?*anyopaque) callconv(.c) void; + pub const handler_fn = *align(1) const fn (SIG) callconv(.c) void; + pub const sigaction_fn = *const fn (SIG, *const siginfo_t, ?*anyopaque) callconv(.c) void; handler: extern union { handler: ?handler_fn, @@ -5994,11 +5957,6 @@ pub const mmsghdr = extern struct { len: u32, }; -pub const mmsghdr_const = extern struct { - hdr: msghdr_const, - len: u32, -}; - pub const epoll_data = extern union { ptr: usize, fd: i32, @@ -6304,14 +6262,14 @@ const siginfo_fields_union = extern union { pub const siginfo_t = if (is_mips) extern struct { - signo: i32, + signo: SIG, code: i32, errno: i32, fields: siginfo_fields_union, } else extern struct { - signo: i32, + signo: SIG, errno: i32, code: i32, fields: siginfo_fields_union, @@ -7140,12 +7098,6 @@ pub const IPPROTO = struct { pub const MAX = 256; }; -pub const RR = struct { - pub const A = 1; - pub const CNAME = 5; - pub const AAAA = 28; -}; - pub const tcp_repair_opt = extern struct { opt_code: u32, opt_val: u32, @@ -8700,7 +8652,7 @@ pub const PR = enum(i32) { pub const SET_MM_MAP = 14; pub const SET_MM_MAP_SIZE = 15; - pub const SET_PTRACER_ANY = std.math.maxInt(c_ulong); + pub const SET_PTRACER_ANY = maxInt(c_ulong); pub const FP_MODE_FR = 1 << 0; pub const FP_MODE_FRE = 1 << 1; @@ -9884,8 +9836,10 @@ pub const msghdr = extern struct { name: ?*sockaddr, namelen: socklen_t, iov: [*]iovec, + /// The kernel and glibc use `usize` for this field; POSIX and musl use `c_int`. iovlen: usize, control: ?*anyopaque, + /// The kernel and glibc use `usize` for this field; POSIX and musl use `socklen_t`. controllen: usize, flags: u32, }; @@ -9902,6 +9856,7 @@ pub const msghdr_const = extern struct { // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/socket.h?id=b320789d6883cc00ac78ce83bccbfe7ed58afcf0#n105 pub const cmsghdr = extern struct { + /// The kernel and glibc use `usize` for this field; musl uses `socklen_t`. len: usize, level: i32, type: i32, diff --git a/lib/std/os/linux/IoUring.zig b/lib/std/os/linux/IoUring.zig index 25d4d88fd0..60c24be227 100644 --- a/lib/std/os/linux/IoUring.zig +++ b/lib/std/os/linux/IoUring.zig @@ -3,14 +3,14 @@ const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; const mem = std.mem; -const net = std.net; +const net = std.Io.net; const posix = std.posix; const linux = std.os.linux; const testing = std.testing; const is_linux = builtin.os.tag == .linux; const page_size_min = std.heap.page_size_min; -fd: posix.fd_t = -1, +fd: linux.fd_t = -1, sq: SubmissionQueue, cq: CompletionQueue, flags: u32, @@ -62,7 +62,7 @@ pub fn init_params(entries: u16, p: *linux.io_uring_params) !IoUring { .NOSYS => return error.SystemOutdated, else => |errno| return posix.unexpectedErrno(errno), } - const fd = @as(posix.fd_t, @intCast(res)); + const fd = @as(linux.fd_t, @intCast(res)); assert(fd >= 0); errdefer posix.close(fd); @@ -341,7 +341,7 @@ pub fn cq_advance(self: *IoUring, count: u32) void { /// apply to the write, since the fsync may complete before the write is issued to the disk. /// You should preferably use `link_with_next_sqe()` on a write's SQE to link it with an fsync, /// or else insert a full write barrier using `drain_previous_sqes()` when queueing an fsync. -pub fn fsync(self: *IoUring, user_data: u64, fd: posix.fd_t, flags: u32) !*linux.io_uring_sqe { +pub fn fsync(self: *IoUring, user_data: u64, fd: linux.fd_t, flags: u32) !*linux.io_uring_sqe { const sqe = try self.get_sqe(); sqe.prep_fsync(fd, flags); sqe.user_data = user_data; @@ -386,7 +386,7 @@ pub const ReadBuffer = union(enum) { pub fn read( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, buffer: ReadBuffer, offset: u64, ) !*linux.io_uring_sqe { @@ -409,7 +409,7 @@ pub fn read( pub fn write( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, buffer: []const u8, offset: u64, ) !*linux.io_uring_sqe { @@ -433,7 +433,7 @@ pub fn write( /// See https://github.com/axboe/liburing/issues/291 /// /// Returns a pointer to the SQE so that you can further modify the SQE for advanced use cases. -pub fn splice(self: *IoUring, user_data: u64, fd_in: posix.fd_t, off_in: u64, fd_out: posix.fd_t, off_out: u64, len: usize) !*linux.io_uring_sqe { +pub fn splice(self: *IoUring, user_data: u64, fd_in: linux.fd_t, off_in: u64, fd_out: linux.fd_t, off_out: u64, len: usize) !*linux.io_uring_sqe { const sqe = try self.get_sqe(); sqe.prep_splice(fd_in, off_in, fd_out, off_out, len); sqe.user_data = user_data; @@ -448,7 +448,7 @@ pub fn splice(self: *IoUring, user_data: u64, fd_in: posix.fd_t, off_in: u64, fd pub fn read_fixed( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, buffer: *posix.iovec, offset: u64, buffer_index: u16, @@ -466,7 +466,7 @@ pub fn read_fixed( pub fn writev( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, iovecs: []const posix.iovec_const, offset: u64, ) !*linux.io_uring_sqe { @@ -484,7 +484,7 @@ pub fn writev( pub fn write_fixed( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, buffer: *posix.iovec, offset: u64, buffer_index: u16, @@ -501,7 +501,7 @@ pub fn write_fixed( pub fn accept( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, addr: ?*posix.sockaddr, addrlen: ?*posix.socklen_t, flags: u32, @@ -523,7 +523,7 @@ pub fn accept( pub fn accept_multishot( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, addr: ?*posix.sockaddr, addrlen: ?*posix.socklen_t, flags: u32, @@ -548,7 +548,7 @@ pub fn accept_multishot( pub fn accept_direct( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, addr: ?*posix.sockaddr, addrlen: ?*posix.socklen_t, flags: u32, @@ -564,7 +564,7 @@ pub fn accept_direct( pub fn accept_multishot_direct( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, addr: ?*posix.sockaddr, addrlen: ?*posix.socklen_t, flags: u32, @@ -580,7 +580,7 @@ pub fn accept_multishot_direct( pub fn connect( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, addr: *const posix.sockaddr, addrlen: posix.socklen_t, ) !*linux.io_uring_sqe { @@ -595,8 +595,8 @@ pub fn connect( pub fn epoll_ctl( self: *IoUring, user_data: u64, - epfd: posix.fd_t, - fd: posix.fd_t, + epfd: linux.fd_t, + fd: linux.fd_t, op: u32, ev: ?*linux.epoll_event, ) !*linux.io_uring_sqe { @@ -626,7 +626,7 @@ pub const RecvBuffer = union(enum) { pub fn recv( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, buffer: RecvBuffer, flags: u32, ) !*linux.io_uring_sqe { @@ -650,7 +650,7 @@ pub fn recv( pub fn send( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, buffer: []const u8, flags: u32, ) !*linux.io_uring_sqe { @@ -678,7 +678,7 @@ pub fn send( pub fn send_zc( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, buffer: []const u8, send_flags: u32, zc_flags: u16, @@ -695,7 +695,7 @@ pub fn send_zc( pub fn send_zc_fixed( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, buffer: []const u8, send_flags: u32, zc_flags: u16, @@ -713,8 +713,8 @@ pub fn send_zc_fixed( pub fn recvmsg( self: *IoUring, user_data: u64, - fd: posix.fd_t, - msg: *posix.msghdr, + fd: linux.fd_t, + msg: *linux.msghdr, flags: u32, ) !*linux.io_uring_sqe { const sqe = try self.get_sqe(); @@ -729,8 +729,8 @@ pub fn recvmsg( pub fn sendmsg( self: *IoUring, user_data: u64, - fd: posix.fd_t, - msg: *const posix.msghdr_const, + fd: linux.fd_t, + msg: *const linux.msghdr_const, flags: u32, ) !*linux.io_uring_sqe { const sqe = try self.get_sqe(); @@ -745,8 +745,8 @@ pub fn sendmsg( pub fn sendmsg_zc( self: *IoUring, user_data: u64, - fd: posix.fd_t, - msg: *const posix.msghdr_const, + fd: linux.fd_t, + msg: *const linux.msghdr_const, flags: u32, ) !*linux.io_uring_sqe { const sqe = try self.get_sqe(); @@ -761,7 +761,7 @@ pub fn sendmsg_zc( pub fn openat( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, path: [*:0]const u8, flags: linux.O, mode: posix.mode_t, @@ -786,7 +786,7 @@ pub fn openat( pub fn openat_direct( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, path: [*:0]const u8, flags: linux.O, mode: posix.mode_t, @@ -801,7 +801,7 @@ pub fn openat_direct( /// Queues (but does not submit) an SQE to perform a `close(2)`. /// Returns a pointer to the SQE. /// Available since 5.6. -pub fn close(self: *IoUring, user_data: u64, fd: posix.fd_t) !*linux.io_uring_sqe { +pub fn close(self: *IoUring, user_data: u64, fd: linux.fd_t) !*linux.io_uring_sqe { const sqe = try self.get_sqe(); sqe.prep_close(fd); sqe.user_data = user_data; @@ -896,7 +896,7 @@ pub fn link_timeout( pub fn poll_add( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, poll_mask: u32, ) !*linux.io_uring_sqe { const sqe = try self.get_sqe(); @@ -939,7 +939,7 @@ pub fn poll_update( pub fn fallocate( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, mode: i32, offset: u64, len: u64, @@ -955,7 +955,7 @@ pub fn fallocate( pub fn statx( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, path: [:0]const u8, flags: u32, mask: u32, @@ -1008,9 +1008,9 @@ pub fn shutdown( pub fn renameat( self: *IoUring, user_data: u64, - old_dir_fd: posix.fd_t, + old_dir_fd: linux.fd_t, old_path: [*:0]const u8, - new_dir_fd: posix.fd_t, + new_dir_fd: linux.fd_t, new_path: [*:0]const u8, flags: u32, ) !*linux.io_uring_sqe { @@ -1025,7 +1025,7 @@ pub fn renameat( pub fn unlinkat( self: *IoUring, user_data: u64, - dir_fd: posix.fd_t, + dir_fd: linux.fd_t, path: [*:0]const u8, flags: u32, ) !*linux.io_uring_sqe { @@ -1040,7 +1040,7 @@ pub fn unlinkat( pub fn mkdirat( self: *IoUring, user_data: u64, - dir_fd: posix.fd_t, + dir_fd: linux.fd_t, path: [*:0]const u8, mode: posix.mode_t, ) !*linux.io_uring_sqe { @@ -1056,7 +1056,7 @@ pub fn symlinkat( self: *IoUring, user_data: u64, target: [*:0]const u8, - new_dir_fd: posix.fd_t, + new_dir_fd: linux.fd_t, link_path: [*:0]const u8, ) !*linux.io_uring_sqe { const sqe = try self.get_sqe(); @@ -1070,9 +1070,9 @@ pub fn symlinkat( pub fn linkat( self: *IoUring, user_data: u64, - old_dir_fd: posix.fd_t, + old_dir_fd: linux.fd_t, old_path: [*:0]const u8, - new_dir_fd: posix.fd_t, + new_dir_fd: linux.fd_t, new_path: [*:0]const u8, flags: u32, ) !*linux.io_uring_sqe { @@ -1144,7 +1144,7 @@ pub fn waitid( /// Registering file descriptors will wait for the ring to idle. /// Files are automatically unregistered by the kernel when the ring is torn down. /// An application need unregister only if it wants to register a new array of file descriptors. -pub fn register_files(self: *IoUring, fds: []const posix.fd_t) !void { +pub fn register_files(self: *IoUring, fds: []const linux.fd_t) !void { assert(self.fd >= 0); const res = linux.io_uring_register( self.fd, @@ -1163,7 +1163,7 @@ pub fn register_files(self: *IoUring, fds: []const posix.fd_t) !void { /// * removing an existing entry (set the fd to -1) /// * replacing an existing entry with a new fd /// Adding new file descriptors must be done with `register_files`. -pub fn register_files_update(self: *IoUring, offset: u32, fds: []const posix.fd_t) !void { +pub fn register_files_update(self: *IoUring, offset: u32, fds: []const linux.fd_t) !void { assert(self.fd >= 0); const FilesUpdate = extern struct { @@ -1232,7 +1232,7 @@ pub fn register_file_alloc_range(self: *IoUring, offset: u32, len: u32) !void { /// Registers the file descriptor for an eventfd that will be notified of completion events on /// an io_uring instance. /// Only a single a eventfd can be registered at any given point in time. -pub fn register_eventfd(self: *IoUring, fd: posix.fd_t) !void { +pub fn register_eventfd(self: *IoUring, fd: linux.fd_t) !void { assert(self.fd >= 0); const res = linux.io_uring_register( self.fd, @@ -1247,7 +1247,7 @@ pub fn register_eventfd(self: *IoUring, fd: posix.fd_t) !void { /// an io_uring instance. Notifications are only posted for events that complete in an async manner. /// This means that events that complete inline while being submitted do not trigger a notification event. /// Only a single eventfd can be registered at any given point in time. -pub fn register_eventfd_async(self: *IoUring, fd: posix.fd_t) !void { +pub fn register_eventfd_async(self: *IoUring, fd: linux.fd_t) !void { assert(self.fd >= 0); const res = linux.io_uring_register( self.fd, @@ -1405,7 +1405,7 @@ pub fn socket_direct_alloc( pub fn bind( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, addr: *const posix.sockaddr, addrlen: posix.socklen_t, flags: u32, @@ -1422,7 +1422,7 @@ pub fn bind( pub fn listen( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, backlog: usize, flags: u32, ) !*linux.io_uring_sqe { @@ -1513,7 +1513,7 @@ pub const SubmissionQueue = struct { sqe_head: u32 = 0, sqe_tail: u32 = 0, - pub fn init(fd: posix.fd_t, p: linux.io_uring_params) !SubmissionQueue { + pub fn init(fd: linux.fd_t, p: linux.io_uring_params) !SubmissionQueue { assert(fd >= 0); assert((p.features & linux.IORING_FEAT_SINGLE_MMAP) != 0); const size = @max( @@ -1576,7 +1576,7 @@ pub const CompletionQueue = struct { overflow: *u32, cqes: []linux.io_uring_cqe, - pub fn init(fd: posix.fd_t, p: linux.io_uring_params, sq: SubmissionQueue) !CompletionQueue { + pub fn init(fd: linux.fd_t, p: linux.io_uring_params, sq: SubmissionQueue) !CompletionQueue { assert(fd >= 0); assert((p.features & linux.IORING_FEAT_SINGLE_MMAP) != 0); const mmap = sq.mmap; @@ -1677,7 +1677,7 @@ pub const BufferGroup = struct { } // Prepare recv operation which will select buffer from this group. - pub fn recv(self: *BufferGroup, user_data: u64, fd: posix.fd_t, flags: u32) !*linux.io_uring_sqe { + pub fn recv(self: *BufferGroup, user_data: u64, fd: linux.fd_t, flags: u32) !*linux.io_uring_sqe { var sqe = try self.ring.get_sqe(); sqe.prep_rw(.RECV, fd, 0, 0, 0); sqe.rw_flags = flags; @@ -1688,7 +1688,7 @@ pub const BufferGroup = struct { } // Prepare multishot recv operation which will select buffer from this group. - pub fn recv_multishot(self: *BufferGroup, user_data: u64, fd: posix.fd_t, flags: u32) !*linux.io_uring_sqe { + pub fn recv_multishot(self: *BufferGroup, user_data: u64, fd: linux.fd_t, flags: u32) !*linux.io_uring_sqe { var sqe = try self.recv(user_data, fd, flags); sqe.ioprio |= linux.IORING_RECV_MULTISHOT; return sqe; @@ -1732,7 +1732,7 @@ pub const BufferGroup = struct { /// `entries` is the number of entries requested in the buffer ring, must be power of 2. /// `group_id` is the chosen buffer group ID, unique in IO_Uring. pub fn setup_buf_ring( - fd: posix.fd_t, + fd: linux.fd_t, entries: u16, group_id: u16, flags: linux.io_uring_buf_reg.Flags, @@ -1758,7 +1758,7 @@ pub fn setup_buf_ring( } fn register_buf_ring( - fd: posix.fd_t, + fd: linux.fd_t, addr: u64, entries: u32, group_id: u16, @@ -1780,7 +1780,7 @@ fn register_buf_ring( try handle_register_buf_ring_result(res); } -fn unregister_buf_ring(fd: posix.fd_t, group_id: u16) !void { +fn unregister_buf_ring(fd: linux.fd_t, group_id: u16) !void { var reg = mem.zeroInit(linux.io_uring_buf_reg, .{ .bgid = group_id, }); @@ -1802,7 +1802,7 @@ fn handle_register_buf_ring_result(res: usize) !void { } // Unregisters a previously registered shared buffer ring, returned from io_uring_setup_buf_ring. -pub fn free_buf_ring(fd: posix.fd_t, br: *align(page_size_min) linux.io_uring_buf_ring, entries: u32, group_id: u16) void { +pub fn free_buf_ring(fd: linux.fd_t, br: *align(page_size_min) linux.io_uring_buf_ring, entries: u32, group_id: u16) void { unregister_buf_ring(fd, group_id) catch {}; var mmap: []align(page_size_min) u8 = undefined; mmap.ptr = @ptrCast(br); @@ -1873,7 +1873,7 @@ test "nop" { }; defer { ring.deinit(); - testing.expectEqual(@as(posix.fd_t, -1), ring.fd) catch @panic("test failed"); + testing.expectEqual(@as(linux.fd_t, -1), ring.fd) catch @panic("test failed"); } const sqe = try ring.nop(0xaaaaaaaa); @@ -1949,7 +1949,7 @@ test "readv" { // https://github.com/torvalds/linux/blob/v5.4/fs/io_uring.c#L3119-L3124 vs // https://github.com/torvalds/linux/blob/v5.8/fs/io_uring.c#L6687-L6691 // We therefore avoid stressing sparse fd sets here: - var registered_fds = [_]posix.fd_t{0} ** 1; + var registered_fds = [_]linux.fd_t{0} ** 1; const fd_index = 0; registered_fds[fd_index] = fd; try ring.register_files(registered_fds[0..]); @@ -2361,28 +2361,31 @@ test "sendmsg/recvmsg" { }; defer ring.deinit(); - var address_server = try net.Address.parseIp4("127.0.0.1", 0); + var address_server: linux.sockaddr.in = .{ + .port = 0, + .addr = @bitCast([4]u8{ 127, 0, 0, 1 }), + }; - const server = try posix.socket(address_server.any.family, posix.SOCK.DGRAM, 0); + const server = try posix.socket(address_server.family, posix.SOCK.DGRAM, 0); defer posix.close(server); try posix.setsockopt(server, posix.SOL.SOCKET, posix.SO.REUSEPORT, &mem.toBytes(@as(c_int, 1))); try posix.setsockopt(server, posix.SOL.SOCKET, posix.SO.REUSEADDR, &mem.toBytes(@as(c_int, 1))); - try posix.bind(server, &address_server.any, address_server.getOsSockLen()); + try posix.bind(server, addrAny(&address_server), @sizeOf(linux.sockaddr.in)); // set address_server to the OS-chosen IP/port. - var slen: posix.socklen_t = address_server.getOsSockLen(); - try posix.getsockname(server, &address_server.any, &slen); + var slen: posix.socklen_t = @sizeOf(linux.sockaddr.in); + try posix.getsockname(server, addrAny(&address_server), &slen); - const client = try posix.socket(address_server.any.family, posix.SOCK.DGRAM, 0); + const client = try posix.socket(address_server.family, posix.SOCK.DGRAM, 0); defer posix.close(client); const buffer_send = [_]u8{42} ** 128; const iovecs_send = [_]posix.iovec_const{ posix.iovec_const{ .base = &buffer_send, .len = buffer_send.len }, }; - const msg_send: posix.msghdr_const = .{ - .name = &address_server.any, - .namelen = address_server.getOsSockLen(), + const msg_send: linux.msghdr_const = .{ + .name = addrAny(&address_server), + .namelen = @sizeOf(linux.sockaddr.in), .iov = &iovecs_send, .iovlen = 1, .control = null, @@ -2398,11 +2401,13 @@ test "sendmsg/recvmsg" { var iovecs_recv = [_]posix.iovec{ posix.iovec{ .base = &buffer_recv, .len = buffer_recv.len }, }; - const addr = [_]u8{0} ** 4; - var address_recv = net.Address.initIp4(addr, 0); - var msg_recv: posix.msghdr = .{ - .name = &address_recv.any, - .namelen = address_recv.getOsSockLen(), + var address_recv: linux.sockaddr.in = .{ + .port = 0, + .addr = 0, + }; + var msg_recv: linux.msghdr = .{ + .name = addrAny(&address_recv), + .namelen = @sizeOf(linux.sockaddr.in), .iov = &iovecs_recv, .iovlen = 1, .control = null, @@ -2441,6 +2446,8 @@ test "sendmsg/recvmsg" { test "timeout (after a relative time)" { if (!is_linux) return error.SkipZigTest; + const io = testing.io; + var ring = IoUring.init(1, 0) catch |err| switch (err) { error.SystemOutdated => return error.SkipZigTest, error.PermissionDenied => return error.SkipZigTest, @@ -2452,12 +2459,12 @@ test "timeout (after a relative time)" { const margin = 5; const ts: linux.kernel_timespec = .{ .sec = 0, .nsec = ms * 1000000 }; - const started = std.time.milliTimestamp(); + const started = try std.Io.Clock.awake.now(io); const sqe = try ring.timeout(0x55555555, &ts, 0, 0); try testing.expectEqual(linux.IORING_OP.TIMEOUT, sqe.opcode); try testing.expectEqual(@as(u32, 1), try ring.submit()); const cqe = try ring.copy_cqe(); - const stopped = std.time.milliTimestamp(); + const stopped = try std.Io.Clock.awake.now(io); try testing.expectEqual(linux.io_uring_cqe{ .user_data = 0x55555555, @@ -2466,7 +2473,8 @@ test "timeout (after a relative time)" { }, cqe); // Tests should not depend on timings: skip test if outside margin. - if (!std.math.approxEqAbs(f64, ms, @as(f64, @floatFromInt(stopped - started)), margin)) return error.SkipZigTest; + const ms_elapsed = started.durationTo(stopped).toMilliseconds(); + if (ms_elapsed > margin) return error.SkipZigTest; } test "timeout (after a number of completions)" { @@ -2777,7 +2785,7 @@ test "register_files_update" { const fd = try posix.openZ("/dev/zero", .{ .ACCMODE = .RDONLY, .CLOEXEC = true }, 0); defer posix.close(fd); - var registered_fds = [_]posix.fd_t{0} ** 2; + var registered_fds = [_]linux.fd_t{0} ** 2; const fd_index = 0; const fd_index2 = 1; registered_fds[fd_index] = fd; @@ -2861,19 +2869,22 @@ test "shutdown" { }; defer ring.deinit(); - var address = try net.Address.parseIp4("127.0.0.1", 0); + var address: linux.sockaddr.in = .{ + .port = 0, + .addr = @bitCast([4]u8{ 127, 0, 0, 1 }), + }; // Socket bound, expect shutdown to work { - const server = try posix.socket(address.any.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); + const server = try posix.socket(address.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); defer posix.close(server); try posix.setsockopt(server, posix.SOL.SOCKET, posix.SO.REUSEADDR, &mem.toBytes(@as(c_int, 1))); - try posix.bind(server, &address.any, address.getOsSockLen()); + try posix.bind(server, addrAny(&address), @sizeOf(linux.sockaddr.in)); try posix.listen(server, 1); // set address to the OS-chosen IP/port. - var slen: posix.socklen_t = address.getOsSockLen(); - try posix.getsockname(server, &address.any, &slen); + var slen: posix.socklen_t = @sizeOf(linux.sockaddr.in); + try posix.getsockname(server, addrAny(&address), &slen); const shutdown_sqe = try ring.shutdown(0x445445445, server, linux.SHUT.RD); try testing.expectEqual(linux.IORING_OP.SHUTDOWN, shutdown_sqe.opcode); @@ -2898,7 +2909,7 @@ test "shutdown" { // Socket not bound, expect to fail with ENOTCONN { - const server = try posix.socket(address.any.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); + const server = try posix.socket(address.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); defer posix.close(server); const shutdown_sqe = ring.shutdown(0x445445445, server, linux.SHUT.RD) catch |err| switch (err) { @@ -2966,22 +2977,11 @@ test "renameat" { }, cqe); // Validate that the old file doesn't exist anymore - { - _ = tmp.dir.openFile(old_path, .{}) catch |err| switch (err) { - error.FileNotFound => {}, - else => std.debug.panic("unexpected error: {}", .{err}), - }; - } + try testing.expectError(error.FileNotFound, tmp.dir.openFile(old_path, .{})); // Validate that the new file exists with the proper content - { - const new_file = try tmp.dir.openFile(new_path, .{}); - defer new_file.close(); - - var new_file_data: [16]u8 = undefined; - const bytes_read = try new_file.readAll(&new_file_data); - try testing.expectEqualStrings("hello", new_file_data[0..bytes_read]); - } + var new_file_data: [16]u8 = undefined; + try testing.expectEqualStrings("hello", try tmp.dir.readFile(new_path, &new_file_data)); } test "unlinkat" { @@ -3179,12 +3179,8 @@ test "linkat" { }, cqe); // Validate the second file - const second_file = try tmp.dir.openFile(second_path, .{}); - defer second_file.close(); - var second_file_data: [16]u8 = undefined; - const bytes_read = try second_file.readAll(&second_file_data); - try testing.expectEqualStrings("hello", second_file_data[0..bytes_read]); + try testing.expectEqualStrings("hello", try tmp.dir.readFile(second_path, &second_file_data)); } test "provide_buffers: read" { @@ -3588,7 +3584,10 @@ const SocketTestHarness = struct { fn createSocketTestHarness(ring: *IoUring) !SocketTestHarness { // Create a TCP server socket - var address = try net.Address.parseIp4("127.0.0.1", 0); + var address: linux.sockaddr.in = .{ + .port = 0, + .addr = @bitCast([4]u8{ 127, 0, 0, 1 }), + }; const listener_socket = try createListenerSocket(&address); errdefer posix.close(listener_socket); @@ -3598,9 +3597,9 @@ fn createSocketTestHarness(ring: *IoUring) !SocketTestHarness { _ = try ring.accept(0xaaaaaaaa, listener_socket, &accept_addr, &accept_addr_len, 0); // Create a TCP client socket - const client = try posix.socket(address.any.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); + const client = try posix.socket(address.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); errdefer posix.close(client); - _ = try ring.connect(0xcccccccc, client, &address.any, address.getOsSockLen()); + _ = try ring.connect(0xcccccccc, client, addrAny(&address), @sizeOf(linux.sockaddr.in)); try testing.expectEqual(@as(u32, 2), try ring.submit()); @@ -3636,18 +3635,18 @@ fn createSocketTestHarness(ring: *IoUring) !SocketTestHarness { }; } -fn createListenerSocket(address: *net.Address) !posix.socket_t { +fn createListenerSocket(address: *linux.sockaddr.in) !posix.socket_t { const kernel_backlog = 1; - const listener_socket = try posix.socket(address.any.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); + const listener_socket = try posix.socket(address.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); errdefer posix.close(listener_socket); try posix.setsockopt(listener_socket, posix.SOL.SOCKET, posix.SO.REUSEADDR, &mem.toBytes(@as(c_int, 1))); - try posix.bind(listener_socket, &address.any, address.getOsSockLen()); + try posix.bind(listener_socket, addrAny(address), @sizeOf(linux.sockaddr.in)); try posix.listen(listener_socket, kernel_backlog); // set address to the OS-chosen IP/port. - var slen: posix.socklen_t = address.getOsSockLen(); - try posix.getsockname(listener_socket, &address.any, &slen); + var slen: posix.socklen_t = @sizeOf(linux.sockaddr.in); + try posix.getsockname(listener_socket, addrAny(address), &slen); return listener_socket; } @@ -3662,7 +3661,10 @@ test "accept multishot" { }; defer ring.deinit(); - var address = try net.Address.parseIp4("127.0.0.1", 0); + var address: linux.sockaddr.in = .{ + .port = 0, + .addr = @bitCast([4]u8{ 127, 0, 0, 1 }), + }; const listener_socket = try createListenerSocket(&address); defer posix.close(listener_socket); @@ -3676,9 +3678,9 @@ test "accept multishot" { var nr: usize = 4; // number of clients to connect while (nr > 0) : (nr -= 1) { // connect client - const client = try posix.socket(address.any.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); + const client = try posix.socket(address.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); errdefer posix.close(client); - try posix.connect(client, &address.any, address.getOsSockLen()); + try posix.connect(client, addrAny(&address), @sizeOf(linux.sockaddr.in)); // test accept completion var cqe = try ring.copy_cqe(); @@ -3756,10 +3758,13 @@ test "accept_direct" { else => return err, }; defer ring.deinit(); - var address = try net.Address.parseIp4("127.0.0.1", 0); + var address: linux.sockaddr.in = .{ + .port = 0, + .addr = @bitCast([4]u8{ 127, 0, 0, 1 }), + }; // register direct file descriptors - var registered_fds = [_]posix.fd_t{-1} ** 2; + var registered_fds = [_]linux.fd_t{-1} ** 2; try ring.register_files(registered_fds[0..]); const listener_socket = try createListenerSocket(&address); @@ -3779,8 +3784,8 @@ test "accept_direct" { try testing.expectEqual(@as(u32, 1), try ring.submit()); // connect - const client = try posix.socket(address.any.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); - try posix.connect(client, &address.any, address.getOsSockLen()); + const client = try posix.socket(address.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); + try posix.connect(client, addrAny(&address), @sizeOf(linux.sockaddr.in)); defer posix.close(client); // accept completion @@ -3813,8 +3818,8 @@ test "accept_direct" { _ = try ring.accept_direct(accept_userdata, listener_socket, null, null, 0); try testing.expectEqual(@as(u32, 1), try ring.submit()); // connect - const client = try posix.socket(address.any.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); - try posix.connect(client, &address.any, address.getOsSockLen()); + const client = try posix.socket(address.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); + try posix.connect(client, addrAny(&address), @sizeOf(linux.sockaddr.in)); defer posix.close(client); // completion with error const cqe_accept = try ring.copy_cqe(); @@ -3830,6 +3835,11 @@ test "accept_direct" { test "accept_multishot_direct" { try skipKernelLessThan(.{ .major = 5, .minor = 19, .patch = 0 }); + if (builtin.cpu.arch == .riscv64) { + // https://github.com/ziglang/zig/issues/25734 + return error.SkipZigTest; + } + var ring = IoUring.init(1, 0) catch |err| switch (err) { error.SystemOutdated => return error.SkipZigTest, error.PermissionDenied => return error.SkipZigTest, @@ -3837,9 +3847,12 @@ test "accept_multishot_direct" { }; defer ring.deinit(); - var address = try net.Address.parseIp4("127.0.0.1", 0); + var address: linux.sockaddr.in = .{ + .port = 0, + .addr = @bitCast([4]u8{ 127, 0, 0, 1 }), + }; - var registered_fds = [_]posix.fd_t{-1} ** 2; + var registered_fds = [_]linux.fd_t{-1} ** 2; try ring.register_files(registered_fds[0..]); const listener_socket = try createListenerSocket(&address); @@ -3855,8 +3868,8 @@ test "accept_multishot_direct" { for (registered_fds) |_| { // connect - const client = try posix.socket(address.any.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); - try posix.connect(client, &address.any, address.getOsSockLen()); + const client = try posix.socket(address.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); + try posix.connect(client, addrAny(&address), @sizeOf(linux.sockaddr.in)); defer posix.close(client); // accept completion @@ -3870,8 +3883,8 @@ test "accept_multishot_direct" { // Multishot is terminated (more flag is not set). { // connect - const client = try posix.socket(address.any.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); - try posix.connect(client, &address.any, address.getOsSockLen()); + const client = try posix.socket(address.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); + try posix.connect(client, addrAny(&address), @sizeOf(linux.sockaddr.in)); defer posix.close(client); // completion with error const cqe_accept = try ring.copy_cqe(); @@ -3902,7 +3915,7 @@ test "socket" { // test completion var cqe = try ring.copy_cqe(); try testing.expectEqual(posix.E.SUCCESS, cqe.err()); - const fd: posix.fd_t = @intCast(cqe.res); + const fd: linux.fd_t = @intCast(cqe.res); try testing.expect(fd > 2); posix.close(fd); @@ -3918,7 +3931,7 @@ test "socket_direct/socket_direct_alloc/close_direct" { }; defer ring.deinit(); - var registered_fds = [_]posix.fd_t{-1} ** 3; + var registered_fds = [_]linux.fd_t{-1} ** 3; try ring.register_files(registered_fds[0..]); // create socket in registered file descriptor at index 0 (last param) @@ -3944,7 +3957,10 @@ test "socket_direct/socket_direct_alloc/close_direct" { try testing.expect(cqe_socket.res == 2); // returns registered file index // use sockets from registered_fds in connect operation - var address = try net.Address.parseIp4("127.0.0.1", 0); + var address: linux.sockaddr.in = .{ + .port = 0, + .addr = @bitCast([4]u8{ 127, 0, 0, 1 }), + }; const listener_socket = try createListenerSocket(&address); defer posix.close(listener_socket); const accept_userdata: u64 = 0xaaaaaaaa; @@ -3954,7 +3970,7 @@ test "socket_direct/socket_direct_alloc/close_direct" { // prepare accept _ = try ring.accept(accept_userdata, listener_socket, null, null, 0); // prepare connect with fixed socket - const connect_sqe = try ring.connect(connect_userdata, @intCast(fd_index), &address.any, address.getOsSockLen()); + const connect_sqe = try ring.connect(connect_userdata, @intCast(fd_index), addrAny(&address), @sizeOf(linux.sockaddr.in)); connect_sqe.flags |= linux.IOSQE_FIXED_FILE; // fd is fixed file index // submit both try testing.expectEqual(@as(u32, 2), try ring.submit()); @@ -3996,7 +4012,7 @@ test "openat_direct/close_direct" { }; defer ring.deinit(); - var registered_fds = [_]posix.fd_t{-1} ** 3; + var registered_fds = [_]linux.fd_t{-1} ** 3; try ring.register_files(registered_fds[0..]); var tmp = std.testing.tmpDir(.{}); @@ -4383,7 +4399,7 @@ test "ring mapped buffers multishot recv" { fn buf_grp_recv_submit_get_cqe( ring: *IoUring, buf_grp: *BufferGroup, - fd: posix.fd_t, + fd: linux.fd_t, user_data: u64, ) !linux.io_uring_cqe { // prepare and submit recv @@ -4483,24 +4499,27 @@ test "bind/listen/connect" { // LISTEN is higher required operation if (!probe.is_supported(.LISTEN)) return error.SkipZigTest; - var addr = net.Address.initIp4([4]u8{ 127, 0, 0, 1 }, 0); - const proto: u32 = if (addr.any.family == linux.AF.UNIX) 0 else linux.IPPROTO.TCP; + var addr: linux.sockaddr.in = .{ + .port = 0, + .addr = @bitCast([4]u8{ 127, 0, 0, 1 }), + }; + const proto: u32 = if (addr.family == linux.AF.UNIX) 0 else linux.IPPROTO.TCP; const listen_fd = brk: { // Create socket - _ = try ring.socket(1, addr.any.family, linux.SOCK.STREAM | linux.SOCK.CLOEXEC, proto, 0); + _ = try ring.socket(1, addr.family, linux.SOCK.STREAM | linux.SOCK.CLOEXEC, proto, 0); try testing.expectEqual(1, try ring.submit()); var cqe = try ring.copy_cqe(); try testing.expectEqual(1, cqe.user_data); try testing.expectEqual(posix.E.SUCCESS, cqe.err()); - const listen_fd: posix.fd_t = @intCast(cqe.res); + const listen_fd: linux.fd_t = @intCast(cqe.res); try testing.expect(listen_fd > 2); // Prepare: set socket option * 2, bind, listen var optval: u32 = 1; (try ring.setsockopt(2, listen_fd, linux.SOL.SOCKET, linux.SO.REUSEADDR, mem.asBytes(&optval))).link_next(); (try ring.setsockopt(3, listen_fd, linux.SOL.SOCKET, linux.SO.REUSEPORT, mem.asBytes(&optval))).link_next(); - (try ring.bind(4, listen_fd, &addr.any, addr.getOsSockLen(), 0)).link_next(); + (try ring.bind(4, listen_fd, addrAny(&addr), @sizeOf(linux.sockaddr.in), 0)).link_next(); _ = try ring.listen(5, listen_fd, 1, 0); // Submit 4 operations try testing.expectEqual(4, try ring.submit()); @@ -4521,28 +4540,28 @@ test "bind/listen/connect" { try testing.expectEqual(1, optval); // Read system assigned port into addr - var addr_len: posix.socklen_t = addr.getOsSockLen(); - try posix.getsockname(listen_fd, &addr.any, &addr_len); + var addr_len: posix.socklen_t = @sizeOf(linux.sockaddr.in); + try posix.getsockname(listen_fd, addrAny(&addr), &addr_len); break :brk listen_fd; }; const connect_fd = brk: { // Create connect socket - _ = try ring.socket(6, addr.any.family, linux.SOCK.STREAM | linux.SOCK.CLOEXEC, proto, 0); + _ = try ring.socket(6, addr.family, linux.SOCK.STREAM | linux.SOCK.CLOEXEC, proto, 0); try testing.expectEqual(1, try ring.submit()); const cqe = try ring.copy_cqe(); try testing.expectEqual(6, cqe.user_data); try testing.expectEqual(posix.E.SUCCESS, cqe.err()); // Get connect socket fd - const connect_fd: posix.fd_t = @intCast(cqe.res); + const connect_fd: linux.fd_t = @intCast(cqe.res); try testing.expect(connect_fd > 2 and connect_fd != listen_fd); break :brk connect_fd; }; // Prepare accept/connect operations _ = try ring.accept(7, listen_fd, null, null, 0); - _ = try ring.connect(8, connect_fd, &addr.any, addr.getOsSockLen()); + _ = try ring.connect(8, connect_fd, addrAny(&addr), @sizeOf(linux.sockaddr.in)); try testing.expectEqual(2, try ring.submit()); // Get listener accepted socket var accept_fd: posix.socket_t = 0; @@ -4604,3 +4623,7 @@ fn testSendRecv(ring: *IoUring, send_fd: posix.socket_t, recv_fd: posix.socket_t try testing.expectEqualSlices(u8, buffer_send, buffer_recv[0..buffer_send.len]); try testing.expectEqualSlices(u8, buffer_send, buffer_recv[buffer_send.len..]); } + +fn addrAny(addr: *linux.sockaddr.in) *linux.sockaddr { + return @ptrCast(addr); +} diff --git a/lib/std/os/linux/s390x.zig b/lib/std/os/linux/s390x.zig index 13b6bfd512..0a09982f2a 100644 --- a/lib/std/os/linux/s390x.zig +++ b/lib/std/os/linux/s390x.zig @@ -136,7 +136,13 @@ pub fn clone() callconv(.naked) u64 { ); } -pub const restore = restore_rt; +pub fn restore() callconv(.naked) noreturn { + asm volatile ( + \\svc 0 + : + : [number] "{r1}" (@intFromEnum(SYS.sigreturn)), + ); +} pub fn restore_rt() callconv(.naked) noreturn { asm volatile ( diff --git a/lib/std/os/linux/test.zig b/lib/std/os/linux/test.zig index e38687dbde..6b658b87c0 100644 --- a/lib/std/os/linux/test.zig +++ b/lib/std/os/linux/test.zig @@ -1,5 +1,7 @@ -const std = @import("../../std.zig"); const builtin = @import("builtin"); + +const std = @import("../../std.zig"); +const assert = std.debug.assert; const linux = std.os.linux; const mem = std.mem; const elf = std.elf; @@ -128,58 +130,32 @@ test "fadvise" { } test "sigset_t" { - std.debug.assert(@sizeOf(linux.sigset_t) == (linux.NSIG / 8)); + const SIG = linux.SIG; + assert(@sizeOf(linux.sigset_t) == (linux.NSIG / 8)); var sigset = linux.sigemptyset(); // See that none are set, then set each one, see that they're all set, then // remove them all, and then see that none are set. for (1..linux.NSIG) |i| { - try expectEqual(linux.sigismember(&sigset, @truncate(i)), false); + const sig = std.meta.intToEnum(SIG, i) catch continue; + try expectEqual(false, linux.sigismember(&sigset, sig)); } for (1..linux.NSIG) |i| { - linux.sigaddset(&sigset, @truncate(i)); + const sig = std.meta.intToEnum(SIG, i) catch continue; + linux.sigaddset(&sigset, sig); } for (1..linux.NSIG) |i| { - try expectEqual(linux.sigismember(&sigset, @truncate(i)), true); + const sig = std.meta.intToEnum(SIG, i) catch continue; + try expectEqual(true, linux.sigismember(&sigset, sig)); } for (1..linux.NSIG) |i| { - linux.sigdelset(&sigset, @truncate(i)); + const sig = std.meta.intToEnum(SIG, i) catch continue; + linux.sigdelset(&sigset, sig); } for (1..linux.NSIG) |i| { - try expectEqual(linux.sigismember(&sigset, @truncate(i)), false); - } - - // Kernel sigset_t is either 2+ 32-bit values or 1+ 64-bit value(s). - const sigset_len = @typeInfo(linux.sigset_t).array.len; - const sigset_elemis64 = 64 == @bitSizeOf(@typeInfo(linux.sigset_t).array.child); - - linux.sigaddset(&sigset, 1); - try expectEqual(sigset[0], 1); - if (sigset_len > 1) { - try expectEqual(sigset[1], 0); - } - - linux.sigaddset(&sigset, 31); - try expectEqual(sigset[0], 0x4000_0001); - if (sigset_len > 1) { - try expectEqual(sigset[1], 0); - } - - linux.sigaddset(&sigset, 36); - if (sigset_elemis64) { - try expectEqual(sigset[0], 0x8_4000_0001); - } else { - try expectEqual(sigset[0], 0x4000_0001); - try expectEqual(sigset[1], 0x8); - } - - linux.sigaddset(&sigset, 64); - if (sigset_elemis64) { - try expectEqual(sigset[0], 0x8000_0008_4000_0001); - } else { - try expectEqual(sigset[0], 0x4000_0001); - try expectEqual(sigset[1], 0x8000_0008); + const sig = std.meta.intToEnum(SIG, i) catch continue; + try expectEqual(false, linux.sigismember(&sigset, sig)); } } @@ -187,14 +163,16 @@ test "sigfillset" { // unlike the C library, all the signals are set in the kernel-level fillset const sigset = linux.sigfillset(); for (1..linux.NSIG) |i| { - try expectEqual(linux.sigismember(&sigset, @truncate(i)), true); + const sig = std.meta.intToEnum(linux.SIG, i) catch continue; + try expectEqual(true, linux.sigismember(&sigset, sig)); } } test "sigemptyset" { const sigset = linux.sigemptyset(); for (1..linux.NSIG) |i| { - try expectEqual(linux.sigismember(&sigset, @truncate(i)), false); + const sig = std.meta.intToEnum(linux.SIG, i) catch continue; + try expectEqual(false, linux.sigismember(&sigset, sig)); } } @@ -208,14 +186,14 @@ test "sysinfo" { } comptime { - std.debug.assert(128 == @as(u32, @bitCast(linux.FUTEX_OP{ .cmd = @enumFromInt(0), .private = true, .realtime = false }))); - std.debug.assert(256 == @as(u32, @bitCast(linux.FUTEX_OP{ .cmd = @enumFromInt(0), .private = false, .realtime = true }))); + assert(128 == @as(u32, @bitCast(linux.FUTEX_OP{ .cmd = @enumFromInt(0), .private = true, .realtime = false }))); + assert(256 == @as(u32, @bitCast(linux.FUTEX_OP{ .cmd = @enumFromInt(0), .private = false, .realtime = true }))); // Check futex_param4 union is packed correctly const param_union = linux.futex_param4{ .val2 = 0xaabbcc, }; - std.debug.assert(@intFromPtr(param_union.timeout) == 0xaabbcc); + assert(@intFromPtr(param_union.timeout) == 0xaabbcc); } test "futex v1" { @@ -298,8 +276,8 @@ test "futex v1" { } comptime { - std.debug.assert(2 == @as(u32, @bitCast(linux.FUTEX2_FLAGS{ .size = .U32, .private = false }))); - std.debug.assert(128 == @as(u32, @bitCast(linux.FUTEX2_FLAGS{ .size = @enumFromInt(0), .private = true }))); + assert(2 == @as(u32, @bitCast(linux.FUTEX2_FLAGS{ .size = .U32, .private = false }))); + assert(128 == @as(u32, @bitCast(linux.FUTEX2_FLAGS{ .size = @enumFromInt(0), .private = true }))); } test "futex2_waitv" { diff --git a/lib/std/os/linux/x86.zig b/lib/std/os/linux/x86.zig index 3770607f55..a68a4af317 100644 --- a/lib/std/os/linux/x86.zig +++ b/lib/std/os/linux/x86.zig @@ -159,12 +159,14 @@ pub fn clone() callconv(.naked) u32 { pub fn restore() callconv(.naked) noreturn { switch (builtin.zig_backend) { .stage2_c => asm volatile ( + \\ addl $4, %%esp \\ movl %[number], %%eax \\ int $0x80 : : [number] "i" (@intFromEnum(SYS.sigreturn)), ), else => asm volatile ( + \\ addl $4, %%esp \\ int $0x80 : : [number] "{eax}" (@intFromEnum(SYS.sigreturn)), diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 27b40fcd60..19b6f8169d 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -5,12 +5,14 @@ //! slices as well as APIs which accept null-terminated WTF16LE byte buffers. const builtin = @import("builtin"); +const native_arch = builtin.cpu.arch; + const std = @import("../std.zig"); +const Io = std.Io; const mem = std.mem; const assert = std.debug.assert; const math = std.math; const maxInt = std.math.maxInt; -const native_arch = builtin.cpu.arch; const UnexpectedError = std.posix.UnexpectedError; test { @@ -87,7 +89,7 @@ pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HAN }; var attr = OBJECT_ATTRIBUTES{ .Length = @sizeOf(OBJECT_ATTRIBUTES), - .RootDirectory = if (std.fs.path.isAbsoluteWindowsWTF16(sub_path_w)) null else options.dir, + .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else options.dir, .Attributes = if (options.sa) |ptr| blk: { // Note we do not use OBJ_CASE_INSENSITIVE here. const inherit: ULONG = if (ptr.bInheritHandle == TRUE) OBJ_INHERIT else 0; break :blk inherit; @@ -146,7 +148,7 @@ pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HAN // call has failed. There is not really a sane way to handle // this other than retrying the creation after the OS finishes // the deletion. - std.Thread.sleep(std.time.ns_per_ms); + _ = kernel32.SleepEx(1, TRUE); continue; }, .VIRUS_INFECTED, .VIRUS_DELETED => return error.AntivirusInterference, @@ -604,7 +606,7 @@ pub const ReadFileError = error{ BrokenPipe, /// The specified network name is no longer available. ConnectionResetByPeer, - OperationAborted, + Canceled, /// Unable to read file due to lock. LockViolation, /// Known to be possible when: @@ -654,7 +656,7 @@ pub fn ReadFile(in_hFile: HANDLE, buffer: []u8, offset: ?u64) ReadFileError!usiz pub const WriteFileError = error{ SystemResources, - OperationAborted, + Canceled, BrokenPipe, NotOpenForWriting, /// The process cannot access the file because another process has locked @@ -694,7 +696,7 @@ pub fn WriteFile( switch (GetLastError()) { .INVALID_USER_BUFFER => return error.SystemResources, .NOT_ENOUGH_MEMORY => return error.SystemResources, - .OPERATION_ABORTED => return error.OperationAborted, + .OPERATION_ABORTED => return error.Canceled, .NOT_ENOUGH_QUOTA => return error.SystemResources, .IO_PENDING => unreachable, .NO_DATA => return error.BrokenPipe, @@ -845,7 +847,7 @@ pub fn CreateSymbolicLink( // the C:\ drive. .rooted => break :target_path target_path, // Keep relative paths relative, but anything else needs to get NT-prefixed. - else => if (!std.fs.path.isAbsoluteWindowsWTF16(target_path)) + else => if (!std.fs.path.isAbsoluteWindowsWtf16(target_path)) break :target_path target_path, }, // Already an NT path, no need to do anything to it @@ -854,7 +856,7 @@ pub fn CreateSymbolicLink( } var prefixed_target_path = try wToPrefixedFileW(dir, target_path); // We do this after prefixing to ensure that drive-relative paths are treated as absolute - is_target_absolute = std.fs.path.isAbsoluteWindowsWTF16(prefixed_target_path.span()); + is_target_absolute = std.fs.path.isAbsoluteWindowsWtf16(prefixed_target_path.span()); break :target_path prefixed_target_path.span(); }; @@ -862,7 +864,7 @@ pub fn CreateSymbolicLink( var buffer: [MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined; const buf_len = @sizeOf(SYMLINK_DATA) + final_target_path.len * 4; const header_len = @sizeOf(ULONG) + @sizeOf(USHORT) * 2; - const target_is_absolute = std.fs.path.isAbsoluteWindowsWTF16(final_target_path); + const target_is_absolute = std.fs.path.isAbsoluteWindowsWtf16(final_target_path); const symlink_data = SYMLINK_DATA{ .ReparseTag = IO_REPARSE_TAG_SYMLINK, .ReparseDataLength = @intCast(buf_len - header_len), @@ -903,7 +905,7 @@ pub fn ReadLink(dir: ?HANDLE, sub_path_w: []const u16, out_buffer: []u8) ReadLin }; var attr = OBJECT_ATTRIBUTES{ .Length = @sizeOf(OBJECT_ATTRIBUTES), - .RootDirectory = if (std.fs.path.isAbsoluteWindowsWTF16(sub_path_w)) null else dir, + .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir, .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. .ObjectName = &nt_name, .SecurityDescriptor = null, @@ -1033,7 +1035,7 @@ pub fn DeleteFile(sub_path_w: []const u16, options: DeleteFileOptions) DeleteFil var attr = OBJECT_ATTRIBUTES{ .Length = @sizeOf(OBJECT_ATTRIBUTES), - .RootDirectory = if (std.fs.path.isAbsoluteWindowsWTF16(sub_path_w)) null else options.dir, + .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else options.dir, .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. .ObjectName = &nt_name, .SecurityDescriptor = null, @@ -1572,131 +1574,6 @@ pub fn GetFileAttributesW(lpFileName: [*:0]const u16) GetFileAttributesError!DWO return rc; } -pub fn WSAStartup(majorVersion: u8, minorVersion: u8) !ws2_32.WSADATA { - var wsadata: ws2_32.WSADATA = undefined; - return switch (ws2_32.WSAStartup((@as(WORD, minorVersion) << 8) | majorVersion, &wsadata)) { - 0 => wsadata, - else => |err_int| switch (@as(ws2_32.WinsockError, @enumFromInt(@as(u16, @intCast(err_int))))) { - .WSASYSNOTREADY => return error.SystemNotAvailable, - .WSAVERNOTSUPPORTED => return error.VersionNotSupported, - .WSAEINPROGRESS => return error.BlockingOperationInProgress, - .WSAEPROCLIM => return error.ProcessFdQuotaExceeded, - else => |err| return unexpectedWSAError(err), - }, - }; -} - -pub fn WSACleanup() !void { - return switch (ws2_32.WSACleanup()) { - 0 => {}, - ws2_32.SOCKET_ERROR => switch (ws2_32.WSAGetLastError()) { - .WSANOTINITIALISED => return error.NotInitialized, - .WSAENETDOWN => return error.NetworkNotAvailable, - .WSAEINPROGRESS => return error.BlockingOperationInProgress, - else => |err| return unexpectedWSAError(err), - }, - else => unreachable, - }; -} - -var wsa_startup_mutex: std.Thread.Mutex = .{}; - -pub fn callWSAStartup() !void { - wsa_startup_mutex.lock(); - defer wsa_startup_mutex.unlock(); - - // Here we could use a flag to prevent multiple threads to prevent - // multiple calls to WSAStartup, but it doesn't matter. We're globally - // leaking the resource intentionally, and the mutex already prevents - // data races within the WSAStartup function. - _ = WSAStartup(2, 2) catch |err| switch (err) { - error.SystemNotAvailable => return error.SystemResources, - error.VersionNotSupported => return error.Unexpected, - error.BlockingOperationInProgress => return error.Unexpected, - error.ProcessFdQuotaExceeded => return error.ProcessFdQuotaExceeded, - error.Unexpected => return error.Unexpected, - }; -} - -/// Microsoft requires WSAStartup to be called to initialize, or else -/// WSASocketW will return WSANOTINITIALISED. -/// Since this is a standard library, we do not have the luxury of -/// putting initialization code anywhere, because we would not want -/// to pay the cost of calling WSAStartup if there ended up being no -/// networking. Also, if Zig code is used as a library, Zig is not in -/// charge of the start code, and we couldn't put in any initialization -/// code even if we wanted to. -/// The documentation for WSAStartup mentions that there must be a -/// matching WSACleanup call. It is not possible for the Zig Standard -/// Library to honor this for the same reason - there is nowhere to put -/// deinitialization code. -/// So, API users of the zig std lib have two options: -/// * (recommended) The simple, cross-platform way: just call `WSASocketW` -/// and don't worry about it. Zig will call WSAStartup() in a thread-safe -/// manner and never deinitialize networking. This is ideal for an -/// application which has the capability to do networking. -/// * The getting-your-hands-dirty way: call `WSAStartup()` before doing -/// networking, so that the error handling code for WSANOTINITIALISED never -/// gets run, which then allows the application or library to call `WSACleanup()`. -/// This could make sense for a library, which has init and deinit -/// functions for the whole library's lifetime. -pub fn WSASocketW( - af: i32, - socket_type: i32, - protocol: i32, - protocolInfo: ?*ws2_32.WSAPROTOCOL_INFOW, - g: ws2_32.GROUP, - dwFlags: DWORD, -) !ws2_32.SOCKET { - var first = true; - while (true) { - const rc = ws2_32.WSASocketW(af, socket_type, protocol, protocolInfo, g, dwFlags); - if (rc == ws2_32.INVALID_SOCKET) { - switch (ws2_32.WSAGetLastError()) { - .WSAEAFNOSUPPORT => return error.AddressFamilyNotSupported, - .WSAEMFILE => return error.ProcessFdQuotaExceeded, - .WSAENOBUFS => return error.SystemResources, - .WSAEPROTONOSUPPORT => return error.ProtocolNotSupported, - .WSANOTINITIALISED => { - if (!first) return error.Unexpected; - first = false; - try callWSAStartup(); - continue; - }, - else => |err| return unexpectedWSAError(err), - } - } - return rc; - } -} - -pub fn bind(s: ws2_32.SOCKET, name: *const ws2_32.sockaddr, namelen: ws2_32.socklen_t) i32 { - return ws2_32.bind(s, name, @as(i32, @intCast(namelen))); -} - -pub fn listen(s: ws2_32.SOCKET, backlog: u31) i32 { - return ws2_32.listen(s, backlog); -} - -pub fn closesocket(s: ws2_32.SOCKET) !void { - switch (ws2_32.closesocket(s)) { - 0 => {}, - ws2_32.SOCKET_ERROR => switch (ws2_32.WSAGetLastError()) { - else => |err| return unexpectedWSAError(err), - }, - else => unreachable, - } -} - -pub fn accept(s: ws2_32.SOCKET, name: ?*ws2_32.sockaddr, namelen: ?*ws2_32.socklen_t) ws2_32.SOCKET { - assert((name == null) == (namelen == null)); - return ws2_32.accept(s, name, @as(?*i32, @ptrCast(namelen))); -} - -pub fn getsockname(s: ws2_32.SOCKET, name: *ws2_32.sockaddr, namelen: *ws2_32.socklen_t) i32 { - return ws2_32.getsockname(s, name, @as(*i32, @ptrCast(namelen))); -} - pub fn getpeername(s: ws2_32.SOCKET, name: *ws2_32.sockaddr, namelen: *ws2_32.socklen_t) i32 { return ws2_32.getpeername(s, name, @as(*i32, @ptrCast(namelen))); } @@ -2219,25 +2096,25 @@ pub fn peb() *PEB { /// Universal Time (UTC). /// This function returns the number of nanoseconds since the canonical epoch, /// which is the POSIX one (Jan 01, 1970 AD). -pub fn fromSysTime(hns: i64) i128 { +pub fn fromSysTime(hns: i64) Io.Timestamp { const adjusted_epoch: i128 = hns + std.time.epoch.windows * (std.time.ns_per_s / 100); - return adjusted_epoch * 100; + return .fromNanoseconds(@intCast(adjusted_epoch * 100)); } -pub fn toSysTime(ns: i128) i64 { - const hns = @divFloor(ns, 100); +pub fn toSysTime(ns: Io.Timestamp) i64 { + const hns = @divFloor(ns.nanoseconds, 100); return @as(i64, @intCast(hns)) - std.time.epoch.windows * (std.time.ns_per_s / 100); } -pub fn fileTimeToNanoSeconds(ft: FILETIME) i128 { +pub fn fileTimeToNanoSeconds(ft: FILETIME) Io.Timestamp { const hns = (@as(i64, ft.dwHighDateTime) << 32) | ft.dwLowDateTime; return fromSysTime(hns); } /// Converts a number of nanoseconds since the POSIX epoch to a Windows FILETIME. -pub fn nanoSecondsToFileTime(ns: i128) FILETIME { +pub fn nanoSecondsToFileTime(ns: Io.Timestamp) FILETIME { const adjusted: u64 = @bitCast(toSysTime(ns)); - return FILETIME{ + return .{ .dwHighDateTime = @as(u32, @truncate(adjusted >> 32)), .dwLowDateTime = @as(u32, @truncate(adjusted)), }; @@ -2425,7 +2302,7 @@ pub fn normalizePath(comptime T: type, path: []T) RemoveDotDirsError!usize { return prefix_len + try removeDotDirsSanitized(T, path[prefix_len..new_len]); } -pub const Wtf8ToPrefixedFileWError = error{InvalidWtf8} || Wtf16ToPrefixedFileWError; +pub const Wtf8ToPrefixedFileWError = Wtf16ToPrefixedFileWError; /// Same as `sliceToPrefixedFileW` but accepts a pointer /// to a null-terminated WTF-8 encoded path. @@ -2438,7 +2315,9 @@ pub fn cStrToPrefixedFileW(dir: ?HANDLE, s: [*:0]const u8) Wtf8ToPrefixedFileWEr /// https://wtf-8.codeberg.page/ pub fn sliceToPrefixedFileW(dir: ?HANDLE, path: []const u8) Wtf8ToPrefixedFileWError!PathSpace { var temp_path: PathSpace = undefined; - temp_path.len = try std.unicode.wtf8ToWtf16Le(&temp_path.data, path); + temp_path.len = std.unicode.wtf8ToWtf16Le(&temp_path.data, path) catch |err| switch (err) { + error.InvalidWtf8 => return error.BadPathName, + }; temp_path.data[temp_path.len] = 0; return wToPrefixedFileW(dir, temp_path.span()); } @@ -2812,38 +2691,6 @@ inline fn MAKELANGID(p: c_ushort, s: c_ushort) LANGID { return (s << 10) | p; } -/// Loads a Winsock extension function in runtime specified by a GUID. -pub fn loadWinsockExtensionFunction(comptime T: type, sock: ws2_32.SOCKET, guid: GUID) !T { - var function: T = undefined; - var num_bytes: DWORD = undefined; - - const rc = ws2_32.WSAIoctl( - sock, - ws2_32.SIO_GET_EXTENSION_FUNCTION_POINTER, - &guid, - @sizeOf(GUID), - @as(?*anyopaque, @ptrFromInt(@intFromPtr(&function))), - @sizeOf(T), - &num_bytes, - null, - null, - ); - - if (rc == ws2_32.SOCKET_ERROR) { - return switch (ws2_32.WSAGetLastError()) { - .WSAEOPNOTSUPP => error.OperationNotSupported, - .WSAENOTSOCK => error.FileDescriptorNotASocket, - else => |err| unexpectedWSAError(err), - }; - } - - if (num_bytes != @sizeOf(T)) { - return error.ShortRead; - } - - return function; -} - /// Call this when you made a windows DLL call or something that does SetLastError /// and you get an unexpected error. pub fn unexpectedError(err: Win32Error) UnexpectedError { @@ -2881,6 +2728,20 @@ pub fn unexpectedStatus(status: NTSTATUS) UnexpectedError { return error.Unexpected; } +pub fn statusBug(status: NTSTATUS) UnexpectedError { + switch (builtin.mode) { + .Debug => std.debug.panic("programmer bug caused syscall status: {t}", .{status}), + else => return error.Unexpected, + } +} + +pub fn errorBug(err: Win32Error) UnexpectedError { + switch (builtin.mode) { + .Debug => std.debug.panic("programmer bug caused syscall status: {t}", .{err}), + else => return error.Unexpected, + } +} + pub const Win32Error = @import("windows/win32error.zig").Win32Error; pub const NTSTATUS = @import("windows/ntstatus.zig").NTSTATUS; pub const LANG = @import("windows/lang.zig"); @@ -5737,3 +5598,16 @@ pub fn ProcessBaseAddress(handle: HANDLE) ProcessBaseAddressError!HMODULE { const ppeb: *const PEB = @ptrCast(@alignCast(peb_out.ptr)); return ppeb.ImageBaseAddress; } + +pub fn wtf8ToWtf16Le(wtf16le: []u16, wtf8: []const u8) error{ BadPathName, NameTooLong }!usize { + // Each u8 in UTF-8/WTF-8 correlates to at most one u16 in UTF-16LE/WTF-16LE. + if (wtf16le.len < wtf8.len) { + const utf16_len = std.unicode.calcUtf16LeLenImpl(wtf8, .can_encode_surrogate_half) catch + return error.BadPathName; + if (utf16_len > wtf16le.len) + return error.NameTooLong; + } + return std.unicode.wtf8ToWtf16Le(wtf16le, wtf8) catch |err| switch (err) { + error.InvalidWtf8 => return error.BadPathName, + }; +} diff --git a/lib/std/os/windows/kernel32.zig b/lib/std/os/windows/kernel32.zig index 2ef08dd97a..c93304d82b 100644 --- a/lib/std/os/windows/kernel32.zig +++ b/lib/std/os/windows/kernel32.zig @@ -326,10 +326,11 @@ pub extern "kernel32" fn ExitProcess( exit_code: UINT, ) callconv(.winapi) noreturn; -// TODO: SleepEx with bAlertable=false. -pub extern "kernel32" fn Sleep( +// TODO: implement via ntdll instead +pub extern "kernel32" fn SleepEx( dwMilliseconds: DWORD, -) callconv(.winapi) void; + bAlertable: BOOL, +) callconv(.winapi) DWORD; // TODO: Wrapper around NtQueryInformationProcess with `PROCESS_BASIC_INFORMATION`. pub extern "kernel32" fn GetExitCodeProcess( diff --git a/lib/std/os/windows/test.zig b/lib/std/os/windows/test.zig index b78e4c323a..2c2c113d18 100644 --- a/lib/std/os/windows/test.zig +++ b/lib/std/os/windows/test.zig @@ -237,28 +237,3 @@ test "removeDotDirs" { try testRemoveDotDirs("a\\b\\..\\", "a\\"); try testRemoveDotDirs("a\\b\\..\\c", "a\\c"); } - -test "loadWinsockExtensionFunction" { - _ = try windows.WSAStartup(2, 2); - defer windows.WSACleanup() catch unreachable; - - const LPFN_CONNECTEX = *const fn ( - Socket: windows.ws2_32.SOCKET, - SockAddr: *const windows.ws2_32.sockaddr, - SockLen: std.posix.socklen_t, - SendBuf: ?*const anyopaque, - SendBufLen: windows.DWORD, - BytesSent: *windows.DWORD, - Overlapped: *windows.OVERLAPPED, - ) callconv(.winapi) windows.BOOL; - - _ = windows.loadWinsockExtensionFunction( - LPFN_CONNECTEX, - try std.posix.socket(std.posix.AF.INET, std.posix.SOCK.DGRAM, 0), - windows.ws2_32.WSAID_CONNECTEX, - ) catch |err| switch (err) { - error.OperationNotSupported => unreachable, - error.ShortRead => unreachable, - else => |e| return e, - }; -} diff --git a/lib/std/os/windows/ws2_32.zig b/lib/std/os/windows/ws2_32.zig index 83194425fa..d5c74e2212 100644 --- a/lib/std/os/windows/ws2_32.zig +++ b/lib/std/os/windows/ws2_32.zig @@ -702,28 +702,32 @@ pub const FIONBIO = -2147195266; pub const ADDRINFOEX_VERSION_2 = 2; pub const ADDRINFOEX_VERSION_3 = 3; pub const ADDRINFOEX_VERSION_4 = 4; -pub const NS_ALL = 0; -pub const NS_SAP = 1; -pub const NS_NDS = 2; -pub const NS_PEER_BROWSE = 3; -pub const NS_SLP = 5; -pub const NS_DHCP = 6; -pub const NS_TCPIP_LOCAL = 10; -pub const NS_TCPIP_HOSTS = 11; -pub const NS_DNS = 12; -pub const NS_NETBT = 13; -pub const NS_WINS = 14; -pub const NS_NLA = 15; -pub const NS_NBP = 20; -pub const NS_MS = 30; -pub const NS_STDA = 31; -pub const NS_NTDS = 32; -pub const NS_EMAIL = 37; -pub const NS_X500 = 40; -pub const NS_NIS = 41; -pub const NS_NISPLUS = 42; -pub const NS_WRQ = 50; -pub const NS_NETDES = 60; + +pub const NS = enum(u32) { + ALL = 0, + SAP = 1, + NDS = 2, + PEER_BROWSE = 3, + SLP = 5, + DHCP = 6, + TCPIP_LOCAL = 10, + TCPIP_HOSTS = 11, + DNS = 12, + NETBT = 13, + WINS = 14, + NLA = 15, + NBP = 20, + MS = 30, + STDA = 31, + NTDS = 32, + EMAIL = 37, + X500 = 40, + NIS = 41, + NISPLUS = 42, + WRQ = 50, + NETDES = 60, +}; + pub const NI_NOFQDN = 1; pub const NI_NUMERICHOST = 2; pub const NI_NAMEREQD = 4; @@ -1080,31 +1084,18 @@ pub const WSANETWORKEVENTS = extern struct { iErrorCode: [10]i32, }; -pub const addrinfo = addrinfoa; - -pub const addrinfoa = extern struct { +pub const ADDRINFOEXW = extern struct { flags: AI, family: i32, socktype: i32, protocol: i32, addrlen: usize, - canonname: ?[*:0]u8, + canonname: ?[*:0]u16, addr: ?*sockaddr, - next: ?*addrinfo, -}; - -pub const addrinfoexA = extern struct { - flags: AI, - family: i32, - socktype: i32, - protocol: i32, - addrlen: usize, - canonname: [*:0]u8, - addr: *sockaddr, - blob: *anyopaque, + blob: ?*anyopaque, bloblen: usize, - provider: *GUID, - next: *addrinfoexA, + provider: ?*GUID, + next: ?*ADDRINFOEXW, }; pub const sockaddr = extern struct { @@ -1271,130 +1262,105 @@ pub const timeval = extern struct { usec: LONG, }; -// https://docs.microsoft.com/en-au/windows/win32/winsock/windows-sockets-error-codes-2 +/// https://docs.microsoft.com/en-au/windows/win32/winsock/windows-sockets-error-codes-2 pub const WinsockError = enum(u16) { /// Specified event object handle is invalid. /// An application attempts to use an event object, but the specified handle is not valid. - WSA_INVALID_HANDLE = 6, - + INVALID_HANDLE = 6, /// Insufficient memory available. /// An application used a Windows Sockets function that directly maps to a Windows function. /// The Windows function is indicating a lack of required memory resources. - WSA_NOT_ENOUGH_MEMORY = 8, - + NOT_ENOUGH_MEMORY = 8, /// One or more parameters are invalid. /// An application used a Windows Sockets function which directly maps to a Windows function. /// The Windows function is indicating a problem with one or more parameters. - WSA_INVALID_PARAMETER = 87, - + INVALID_PARAMETER = 87, /// Overlapped operation aborted. /// An overlapped operation was canceled due to the closure of the socket, or the execution of the SIO_FLUSH command in WSAIoctl. - WSA_OPERATION_ABORTED = 995, - + OPERATION_ABORTED = 995, /// Overlapped I/O event object not in signaled state. /// The application has tried to determine the status of an overlapped operation which is not yet completed. /// Applications that use WSAGetOverlappedResult (with the fWait flag set to FALSE) in a polling mode to determine when an overlapped operation has completed, get this error code until the operation is complete. - WSA_IO_INCOMPLETE = 996, - + IO_INCOMPLETE = 996, /// The application has initiated an overlapped operation that cannot be completed immediately. /// A completion indication will be given later when the operation has been completed. - WSA_IO_PENDING = 997, - + IO_PENDING = 997, /// Interrupted function call. /// A blocking operation was interrupted by a call to WSACancelBlockingCall. - WSAEINTR = 10004, - + EINTR = 10004, /// File handle is not valid. /// The file handle supplied is not valid. - WSAEBADF = 10009, - + EBADF = 10009, /// Permission denied. /// An attempt was made to access a socket in a way forbidden by its access permissions. /// An example is using a broadcast address for sendto without broadcast permission being set using setsockopt(SO.BROADCAST). /// Another possible reason for the WSAEACCES error is that when the bind function is called (on Windows NT 4.0 with SP4 and later), another application, service, or kernel mode driver is bound to the same address with exclusive access. /// Such exclusive access is a new feature of Windows NT 4.0 with SP4 and later, and is implemented by using the SO.EXCLUSIVEADDRUSE option. - WSAEACCES = 10013, - + EACCES = 10013, /// Bad address. /// The system detected an invalid pointer address in attempting to use a pointer argument of a call. /// This error occurs if an application passes an invalid pointer value, or if the length of the buffer is too small. /// For instance, if the length of an argument, which is a sockaddr structure, is smaller than the sizeof(sockaddr). - WSAEFAULT = 10014, - + EFAULT = 10014, /// Invalid argument. /// Some invalid argument was supplied (for example, specifying an invalid level to the setsockopt function). /// In some instances, it also refers to the current state of the socket—for instance, calling accept on a socket that is not listening. - WSAEINVAL = 10022, - + EINVAL = 10022, /// Too many open files. /// Too many open sockets. Each implementation may have a maximum number of socket handles available, either globally, per process, or per thread. - WSAEMFILE = 10024, - + EMFILE = 10024, /// Resource temporarily unavailable. /// This error is returned from operations on nonblocking sockets that cannot be completed immediately, for example recv when no data is queued to be read from the socket. /// It is a nonfatal error, and the operation should be retried later. /// It is normal for WSAEWOULDBLOCK to be reported as the result from calling connect on a nonblocking SOCK.STREAM socket, since some time must elapse for the connection to be established. - WSAEWOULDBLOCK = 10035, - + EWOULDBLOCK = 10035, /// Operation now in progress. /// A blocking operation is currently executing. /// Windows Sockets only allows a single blocking operation—per- task or thread—to be outstanding, and if any other function call is made (whether or not it references that or any other socket) the function fails with the WSAEINPROGRESS error. - WSAEINPROGRESS = 10036, - + EINPROGRESS = 10036, /// Operation already in progress. /// An operation was attempted on a nonblocking socket with an operation already in progress—that is, calling connect a second time on a nonblocking socket that is already connecting, or canceling an asynchronous request (WSAAsyncGetXbyY) that has already been canceled or completed. - WSAEALREADY = 10037, - + EALREADY = 10037, /// Socket operation on nonsocket. /// An operation was attempted on something that is not a socket. /// Either the socket handle parameter did not reference a valid socket, or for select, a member of an fd_set was not valid. - WSAENOTSOCK = 10038, - + ENOTSOCK = 10038, /// Destination address required. /// A required address was omitted from an operation on a socket. /// For example, this error is returned if sendto is called with the remote address of ADDR_ANY. - WSAEDESTADDRREQ = 10039, - + EDESTADDRREQ = 10039, /// Message too long. /// A message sent on a datagram socket was larger than the internal message buffer or some other network limit, or the buffer used to receive a datagram was smaller than the datagram itself. - WSAEMSGSIZE = 10040, - + EMSGSIZE = 10040, /// Protocol wrong type for socket. /// A protocol was specified in the socket function call that does not support the semantics of the socket type requested. /// For example, the ARPA Internet UDP protocol cannot be specified with a socket type of SOCK.STREAM. - WSAEPROTOTYPE = 10041, - + EPROTOTYPE = 10041, /// Bad protocol option. /// An unknown, invalid or unsupported option or level was specified in a getsockopt or setsockopt call. - WSAENOPROTOOPT = 10042, - + ENOPROTOOPT = 10042, /// Protocol not supported. /// The requested protocol has not been configured into the system, or no implementation for it exists. /// For example, a socket call requests a SOCK.DGRAM socket, but specifies a stream protocol. - WSAEPROTONOSUPPORT = 10043, - + EPROTONOSUPPORT = 10043, /// Socket type not supported. /// The support for the specified socket type does not exist in this address family. /// For example, the optional type SOCK.RAW might be selected in a socket call, and the implementation does not support SOCK.RAW sockets at all. - WSAESOCKTNOSUPPORT = 10044, - + ESOCKTNOSUPPORT = 10044, /// Operation not supported. /// The attempted operation is not supported for the type of object referenced. /// Usually this occurs when a socket descriptor to a socket that cannot support this operation is trying to accept a connection on a datagram socket. - WSAEOPNOTSUPP = 10045, - + EOPNOTSUPP = 10045, /// Protocol family not supported. /// The protocol family has not been configured into the system or no implementation for it exists. /// This message has a slightly different meaning from WSAEAFNOSUPPORT. /// However, it is interchangeable in most cases, and all Windows Sockets functions that return one of these messages also specify WSAEAFNOSUPPORT. - WSAEPFNOSUPPORT = 10046, - + EPFNOSUPPORT = 10046, /// Address family not supported by protocol family. /// An address incompatible with the requested protocol was used. /// All sockets are created with an associated address family (that is, AF.INET for Internet Protocols) and a generic protocol type (that is, SOCK.STREAM). /// This error is returned if an incorrect protocol is explicitly requested in the socket call, or if an address of the wrong family is used for a socket, for example, in sendto. - WSAEAFNOSUPPORT = 10047, - + EAFNOSUPPORT = 10047, /// Address already in use. /// Typically, only one usage of each socket address (protocol/IP address/port) is permitted. /// This error occurs if an application attempts to bind a socket to an IP address/port that has already been used for an existing socket, or a socket that was not closed properly, or one that is still in the process of closing. @@ -1402,115 +1368,91 @@ pub const WinsockError = enum(u16) { /// Client applications usually need not call bind at all—connect chooses an unused port automatically. /// When bind is called with a wildcard address (involving ADDR_ANY), a WSAEADDRINUSE error could be delayed until the specific address is committed. /// This could happen with a call to another function later, including connect, listen, WSAConnect, or WSAJoinLeaf. - WSAEADDRINUSE = 10048, - + EADDRINUSE = 10048, /// Cannot assign requested address. /// The requested address is not valid in its context. /// This normally results from an attempt to bind to an address that is not valid for the local computer. /// This can also result from connect, sendto, WSAConnect, WSAJoinLeaf, or WSASendTo when the remote address or port is not valid for a remote computer (for example, address or port 0). - WSAEADDRNOTAVAIL = 10049, - + EADDRNOTAVAIL = 10049, /// Network is down. /// A socket operation encountered a dead network. /// This could indicate a serious failure of the network system (that is, the protocol stack that the Windows Sockets DLL runs over), the network interface, or the local network itself. - WSAENETDOWN = 10050, - + ENETDOWN = 10050, /// Network is unreachable. /// A socket operation was attempted to an unreachable network. /// This usually means the local software knows no route to reach the remote host. - WSAENETUNREACH = 10051, - + ENETUNREACH = 10051, /// Network dropped connection on reset. /// The connection has been broken due to keep-alive activity detecting a failure while the operation was in progress. /// It can also be returned by setsockopt if an attempt is made to set SO.KEEPALIVE on a connection that has already failed. - WSAENETRESET = 10052, - + ENETRESET = 10052, /// Software caused connection abort. /// An established connection was aborted by the software in your host computer, possibly due to a data transmission time-out or protocol error. - WSAECONNABORTED = 10053, - + ECONNABORTED = 10053, /// Connection reset by peer. /// An existing connection was forcibly closed by the remote host. /// This normally results if the peer application on the remote host is suddenly stopped, the host is rebooted, the host or remote network interface is disabled, or the remote host uses a hard close (see setsockopt for more information on the SO.LINGER option on the remote socket). /// This error may also result if a connection was broken due to keep-alive activity detecting a failure while one or more operations are in progress. /// Operations that were in progress fail with WSAENETRESET. Subsequent operations fail with WSAECONNRESET. - WSAECONNRESET = 10054, - + ECONNRESET = 10054, /// No buffer space available. /// An operation on a socket could not be performed because the system lacked sufficient buffer space or because a queue was full. - WSAENOBUFS = 10055, - + ENOBUFS = 10055, /// Socket is already connected. /// A connect request was made on an already-connected socket. /// Some implementations also return this error if sendto is called on a connected SOCK.DGRAM socket (for SOCK.STREAM sockets, the to parameter in sendto is ignored) although other implementations treat this as a legal occurrence. - WSAEISCONN = 10056, - + EISCONN = 10056, /// Socket is not connected. /// A request to send or receive data was disallowed because the socket is not connected and (when sending on a datagram socket using sendto) no address was supplied. /// Any other type of operation might also return this error—for example, setsockopt setting SO.KEEPALIVE if the connection has been reset. - WSAENOTCONN = 10057, - + ENOTCONN = 10057, /// Cannot send after socket shutdown. /// A request to send or receive data was disallowed because the socket had already been shut down in that direction with a previous shutdown call. /// By calling shutdown a partial close of a socket is requested, which is a signal that sending or receiving, or both have been discontinued. - WSAESHUTDOWN = 10058, - + ESHUTDOWN = 10058, /// Too many references. /// Too many references to some kernel object. - WSAETOOMANYREFS = 10059, - + ETOOMANYREFS = 10059, /// Connection timed out. /// A connection attempt failed because the connected party did not properly respond after a period of time, or the established connection failed because the connected host has failed to respond. - WSAETIMEDOUT = 10060, - + ETIMEDOUT = 10060, /// Connection refused. /// No connection could be made because the target computer actively refused it. /// This usually results from trying to connect to a service that is inactive on the foreign host—that is, one with no server application running. - WSAECONNREFUSED = 10061, - + ECONNREFUSED = 10061, /// Cannot translate name. /// Cannot translate a name. - WSAELOOP = 10062, - + ELOOP = 10062, /// Name too long. /// A name component or a name was too long. - WSAENAMETOOLONG = 10063, - + ENAMETOOLONG = 10063, /// Host is down. /// A socket operation failed because the destination host is down. A socket operation encountered a dead host. /// Networking activity on the local host has not been initiated. /// These conditions are more likely to be indicated by the error WSAETIMEDOUT. - WSAEHOSTDOWN = 10064, - + EHOSTDOWN = 10064, /// No route to host. /// A socket operation was attempted to an unreachable host. See WSAENETUNREACH. - WSAEHOSTUNREACH = 10065, - + EHOSTUNREACH = 10065, /// Directory not empty. /// Cannot remove a directory that is not empty. - WSAENOTEMPTY = 10066, - + ENOTEMPTY = 10066, /// Too many processes. /// A Windows Sockets implementation may have a limit on the number of applications that can use it simultaneously. /// WSAStartup may fail with this error if the limit has been reached. - WSAEPROCLIM = 10067, - + EPROCLIM = 10067, /// User quota exceeded. /// Ran out of user quota. - WSAEUSERS = 10068, - + EUSERS = 10068, /// Disk quota exceeded. /// Ran out of disk quota. - WSAEDQUOT = 10069, - + EDQUOT = 10069, /// Stale file handle reference. /// The file handle reference is no longer available. - WSAESTALE = 10070, - + ESTALE = 10070, /// Item is remote. /// The item is not available locally. - WSAEREMOTE = 10071, - + EREMOTE = 10071, /// Network subsystem is unavailable. /// This error is returned by WSAStartup if the Windows Sockets implementation cannot function at this time because the underlying system it uses to provide network services is currently unavailable. /// Users should check: @@ -1518,47 +1460,38 @@ pub const WinsockError = enum(u16) { /// - That they are not trying to use more than one Windows Sockets implementation simultaneously. /// - If there is more than one Winsock DLL on your system, be sure the first one in the path is appropriate for the network subsystem currently loaded. /// - The Windows Sockets implementation documentation to be sure all necessary components are currently installed and configured correctly. - WSASYSNOTREADY = 10091, - + SYSNOTREADY = 10091, /// Winsock.dll version out of range. /// The current Windows Sockets implementation does not support the Windows Sockets specification version requested by the application. /// Check that no old Windows Sockets DLL files are being accessed. - WSAVERNOTSUPPORTED = 10092, - + VERNOTSUPPORTED = 10092, /// Successful WSAStartup not yet performed. /// Either the application has not called WSAStartup or WSAStartup failed. /// The application may be accessing a socket that the current active task does not own (that is, trying to share a socket between tasks), or WSACleanup has been called too many times. - WSANOTINITIALISED = 10093, - + NOTINITIALISED = 10093, /// Graceful shutdown in progress. /// Returned by WSARecv and WSARecvFrom to indicate that the remote party has initiated a graceful shutdown sequence. - WSAEDISCON = 10101, - + EDISCON = 10101, /// No more results. /// No more results can be returned by the WSALookupServiceNext function. - WSAENOMORE = 10102, - + ENOMORE = 10102, /// Call has been canceled. /// A call to the WSALookupServiceEnd function was made while this call was still processing. The call has been canceled. - WSAECANCELLED = 10103, - + ECANCELLED = 10103, /// Procedure call table is invalid. /// The service provider procedure call table is invalid. /// A service provider returned a bogus procedure table to Ws2_32.dll. /// This is usually caused by one or more of the function pointers being NULL. - WSAEINVALIDPROCTABLE = 10104, - + EINVALIDPROCTABLE = 10104, /// Service provider is invalid. /// The requested service provider is invalid. /// This error is returned by the WSCGetProviderInfo and WSCGetProviderInfo32 functions if the protocol entry specified could not be found. /// This error is also returned if the service provider returned a version number other than 2.0. - WSAEINVALIDPROVIDER = 10105, - + EINVALIDPROVIDER = 10105, /// Service provider failed to initialize. /// The requested service provider could not be loaded or initialized. /// This error is returned if either a service provider's DLL could not be loaded (LoadLibrary failed) or the provider's WSPStartup or NSPStartup function failed. - WSAEPROVIDERFAILEDINIT = 10106, - + EPROVIDERFAILEDINIT = 10106, /// System call failure. /// A system call that should never fail has failed. /// This is a generic error code, returned under various conditions. @@ -1566,157 +1499,120 @@ pub const WinsockError = enum(u16) { /// For example, if a call to WaitForMultipleEvents fails or one of the registry functions fails trying to manipulate the protocol/namespace catalogs. /// Returned when a provider does not return SUCCESS and does not provide an extended error code. /// Can indicate a service provider implementation error. - WSASYSCALLFAILURE = 10107, - + SYSCALLFAILURE = 10107, /// Service not found. /// No such service is known. The service cannot be found in the specified name space. - WSASERVICE_NOT_FOUND = 10108, - + SERVICE_NOT_FOUND = 10108, /// Class type not found. /// The specified class was not found. - WSATYPE_NOT_FOUND = 10109, - + TYPE_NOT_FOUND = 10109, /// No more results. /// No more results can be returned by the WSALookupServiceNext function. - WSA_E_NO_MORE = 10110, - + E_NO_MORE = 10110, /// Call was canceled. /// A call to the WSALookupServiceEnd function was made while this call was still processing. The call has been canceled. - WSA_E_CANCELLED = 10111, - + E_CANCELLED = 10111, /// Database query was refused. /// A database query failed because it was actively refused. - WSAEREFUSED = 10112, - + EREFUSED = 10112, /// Host not found. /// No such host is known. The name is not an official host name or alias, or it cannot be found in the database(s) being queried. /// This error may also be returned for protocol and service queries, and means that the specified name could not be found in the relevant database. - WSAHOST_NOT_FOUND = 11001, - + HOST_NOT_FOUND = 11001, /// Nonauthoritative host not found. /// This is usually a temporary error during host name resolution and means that the local server did not receive a response from an authoritative server. A retry at some time later may be successful. - WSATRY_AGAIN = 11002, - + TRY_AGAIN = 11002, /// This is a nonrecoverable error. /// This indicates that some sort of nonrecoverable error occurred during a database lookup. /// This may be because the database files (for example, BSD-compatible HOSTS, SERVICES, or PROTOCOLS files) could not be found, or a DNS request was returned by the server with a severe error. - WSANO_RECOVERY = 11003, - + NO_RECOVERY = 11003, /// Valid name, no data record of requested type. /// The requested name is valid and was found in the database, but it does not have the correct associated data being resolved for. /// The usual example for this is a host name-to-address translation attempt (using gethostbyname or WSAAsyncGetHostByName) which uses the DNS (Domain Name Server). /// An MX record is returned but no A record—indicating the host itself exists, but is not directly reachable. - WSANO_DATA = 11004, - + NO_DATA = 11004, /// QoS receivers. /// At least one QoS reserve has arrived. - WSA_QOS_RECEIVERS = 11005, - + QOS_RECEIVERS = 11005, /// QoS senders. /// At least one QoS send path has arrived. - WSA_QOS_SENDERS = 11006, - + QOS_SENDERS = 11006, /// No QoS senders. /// There are no QoS senders. - WSA_QOS_NO_SENDERS = 11007, - + QOS_NO_SENDERS = 11007, /// QoS no receivers. /// There are no QoS receivers. - WSA_QOS_NO_RECEIVERS = 11008, - + QOS_NO_RECEIVERS = 11008, /// QoS request confirmed. /// The QoS reserve request has been confirmed. - WSA_QOS_REQUEST_CONFIRMED = 11009, - + QOS_REQUEST_CONFIRMED = 11009, /// QoS admission error. /// A QoS error occurred due to lack of resources. - WSA_QOS_ADMISSION_FAILURE = 11010, - + QOS_ADMISSION_FAILURE = 11010, /// QoS policy failure. /// The QoS request was rejected because the policy system couldn't allocate the requested resource within the existing policy. - WSA_QOS_POLICY_FAILURE = 11011, - + QOS_POLICY_FAILURE = 11011, /// QoS bad style. /// An unknown or conflicting QoS style was encountered. - WSA_QOS_BAD_STYLE = 11012, - + QOS_BAD_STYLE = 11012, /// QoS bad object. /// A problem was encountered with some part of the filterspec or the provider-specific buffer in general. - WSA_QOS_BAD_OBJECT = 11013, - + QOS_BAD_OBJECT = 11013, /// QoS traffic control error. /// An error with the underlying traffic control (TC) API as the generic QoS request was converted for local enforcement by the TC API. /// This could be due to an out of memory error or to an internal QoS provider error. - WSA_QOS_TRAFFIC_CTRL_ERROR = 11014, - + QOS_TRAFFIC_CTRL_ERROR = 11014, /// QoS generic error. /// A general QoS error. - WSA_QOS_GENERIC_ERROR = 11015, - + QOS_GENERIC_ERROR = 11015, /// QoS service type error. /// An invalid or unrecognized service type was found in the QoS flowspec. - WSA_QOS_ESERVICETYPE = 11016, - + QOS_ESERVICETYPE = 11016, /// QoS flowspec error. /// An invalid or inconsistent flowspec was found in the QOS structure. - WSA_QOS_EFLOWSPEC = 11017, - + QOS_EFLOWSPEC = 11017, /// Invalid QoS provider buffer. /// An invalid QoS provider-specific buffer. - WSA_QOS_EPROVSPECBUF = 11018, - + QOS_EPROVSPECBUF = 11018, /// Invalid QoS filter style. /// An invalid QoS filter style was used. - WSA_QOS_EFILTERSTYLE = 11019, - + QOS_EFILTERSTYLE = 11019, /// Invalid QoS filter type. /// An invalid QoS filter type was used. - WSA_QOS_EFILTERTYPE = 11020, - + QOS_EFILTERTYPE = 11020, /// Incorrect QoS filter count. /// An incorrect number of QoS FILTERSPECs were specified in the FLOWDESCRIPTOR. - WSA_QOS_EFILTERCOUNT = 11021, - + QOS_EFILTERCOUNT = 11021, /// Invalid QoS object length. /// An object with an invalid ObjectLength field was specified in the QoS provider-specific buffer. - WSA_QOS_EOBJLENGTH = 11022, - + QOS_EOBJLENGTH = 11022, /// Incorrect QoS flow count. /// An incorrect number of flow descriptors was specified in the QoS structure. - WSA_QOS_EFLOWCOUNT = 11023, - + QOS_EFLOWCOUNT = 11023, /// Unrecognized QoS object. /// An unrecognized object was found in the QoS provider-specific buffer. - WSA_QOS_EUNKOWNPSOBJ = 11024, - + QOS_EUNKOWNPSOBJ = 11024, /// Invalid QoS policy object. /// An invalid policy object was found in the QoS provider-specific buffer. - WSA_QOS_EPOLICYOBJ = 11025, - + QOS_EPOLICYOBJ = 11025, /// Invalid QoS flow descriptor. /// An invalid QoS flow descriptor was found in the flow descriptor list. - WSA_QOS_EFLOWDESC = 11026, - + QOS_EFLOWDESC = 11026, /// Invalid QoS provider-specific flowspec. /// An invalid or inconsistent flowspec was found in the QoS provider-specific buffer. - WSA_QOS_EPSFLOWSPEC = 11027, - + QOS_EPSFLOWSPEC = 11027, /// Invalid QoS provider-specific filterspec. /// An invalid FILTERSPEC was found in the QoS provider-specific buffer. - WSA_QOS_EPSFILTERSPEC = 11028, - + QOS_EPSFILTERSPEC = 11028, /// Invalid QoS shape discard mode object. /// An invalid shape discard mode object was found in the QoS provider-specific buffer. - WSA_QOS_ESDMODEOBJ = 11029, - + QOS_ESDMODEOBJ = 11029, /// Invalid QoS shaping rate object. /// An invalid shaping rate object was found in the QoS provider-specific buffer. - WSA_QOS_ESHAPERATEOBJ = 11030, - + QOS_ESHAPERATEOBJ = 11030, /// Reserved policy QoS element type. /// A reserved policy element was found in the QoS provider-specific buffer. - WSA_QOS_RESERVED_PETYPE = 11031, - + QOS_RESERVED_PETYPE = 11031, _, }; @@ -1946,18 +1842,6 @@ pub extern "ws2_32" fn WSAConnectByNameW( Reserved: *OVERLAPPED, ) callconv(.winapi) BOOL; -pub extern "ws2_32" fn WSAConnectByNameA( - s: SOCKET, - nodename: [*:0]const u8, - servicename: [*:0]const u8, - LocalAddressLength: ?*u32, - LocalAddress: ?*sockaddr, - RemoteAddressLength: ?*u32, - RemoteAddress: ?*sockaddr, - timeout: ?*const timeval, - Reserved: *OVERLAPPED, -) callconv(.winapi) BOOL; - pub extern "ws2_32" fn WSAConnectByList( s: SOCKET, SocketAddress: *SOCKET_ADDRESS_LIST, @@ -1971,12 +1855,6 @@ pub extern "ws2_32" fn WSAConnectByList( pub extern "ws2_32" fn WSACreateEvent() callconv(.winapi) HANDLE; -pub extern "ws2_32" fn WSADuplicateSocketA( - s: SOCKET, - dwProcessId: u32, - lpProtocolInfo: *WSAPROTOCOL_INFOA, -) callconv(.winapi) i32; - pub extern "ws2_32" fn WSADuplicateSocketW( s: SOCKET, dwProcessId: u32, @@ -1989,12 +1867,6 @@ pub extern "ws2_32" fn WSAEnumNetworkEvents( lpNetworkEvents: *WSANETWORKEVENTS, ) callconv(.winapi) i32; -pub extern "ws2_32" fn WSAEnumProtocolsA( - lpiProtocols: ?*i32, - lpProtocolBuffer: ?*WSAPROTOCOL_INFOA, - lpdwBufferLength: *u32, -) callconv(.winapi) i32; - pub extern "ws2_32" fn WSAEnumProtocolsW( lpiProtocols: ?*i32, lpProtocolBuffer: ?*WSAPROTOCOL_INFOW, @@ -2137,15 +2009,6 @@ pub extern "ws2_32" fn WSASetEvent( hEvent: HANDLE, ) callconv(.winapi) BOOL; -pub extern "ws2_32" fn WSASocketA( - af: i32, - @"type": i32, - protocol: i32, - lpProtocolInfo: ?*WSAPROTOCOL_INFOA, - g: u32, - dwFlags: u32, -) callconv(.winapi) SOCKET; - pub extern "ws2_32" fn WSASocketW( af: i32, @"type": i32, @@ -2163,14 +2026,6 @@ pub extern "ws2_32" fn WSAWaitForMultipleEvents( fAlertable: BOOL, ) callconv(.winapi) u32; -pub extern "ws2_32" fn WSAAddressToStringA( - lpsaAddress: *sockaddr, - dwAddressLength: u32, - lpProtocolInfo: ?*WSAPROTOCOL_INFOA, - lpszAddressString: [*]u8, - lpdwAddressStringLength: *u32, -) callconv(.winapi) i32; - pub extern "ws2_32" fn WSAAddressToStringW( lpsaAddress: *sockaddr, dwAddressLength: u32, @@ -2179,14 +2034,6 @@ pub extern "ws2_32" fn WSAAddressToStringW( lpdwAddressStringLength: *u32, ) callconv(.winapi) i32; -pub extern "ws2_32" fn WSAStringToAddressA( - AddressString: [*:0]const u8, - AddressFamily: i32, - lpProtocolInfo: ?*WSAPROTOCOL_INFOA, - lpAddress: *sockaddr, - lpAddressLength: *i32, -) callconv(.winapi) i32; - pub extern "ws2_32" fn WSAStringToAddressW( AddressString: [*:0]const u16, AddressFamily: i32, @@ -2251,32 +2098,14 @@ pub extern "ws2_32" fn WSAProviderCompleteAsyncCall( iRetCode: i32, ) callconv(.winapi) i32; -pub extern "mswsock" fn EnumProtocolsA( - lpiProtocols: ?*i32, - lpProtocolBuffer: *anyopaque, - lpdwBufferLength: *u32, -) callconv(.winapi) i32; - pub extern "mswsock" fn EnumProtocolsW( lpiProtocols: ?*i32, lpProtocolBuffer: *anyopaque, lpdwBufferLength: *u32, ) callconv(.winapi) i32; -pub extern "mswsock" fn GetAddressByNameA( - dwNameSpace: u32, - lpServiceType: *GUID, - lpServiceName: ?[*:0]u8, - lpiProtocols: ?*i32, - dwResolution: u32, - lpServiceAsyncInfo: ?*SERVICE_ASYNC_INFO, - lpCsaddrBuffer: *anyopaque, - lpAliasBuffer: ?[*:0]const u8, - lpdwAliasBufferLength: *u32, -) callconv(.winapi) i32; - pub extern "mswsock" fn GetAddressByNameW( - dwNameSpace: u32, + dwNameSpace: NS, lpServiceType: *GUID, lpServiceName: ?[*:0]u16, lpiProtocols: ?*i32, @@ -2288,45 +2117,28 @@ pub extern "mswsock" fn GetAddressByNameW( lpdwAliasBufferLength: *u32, ) callconv(.winapi) i32; -pub extern "mswsock" fn GetTypeByNameA( - lpServiceName: [*:0]u8, - lpServiceType: *GUID, -) callconv(.winapi) i32; - pub extern "mswsock" fn GetTypeByNameW( lpServiceName: [*:0]u16, lpServiceType: *GUID, ) callconv(.winapi) i32; -pub extern "mswsock" fn GetNameByTypeA( - lpServiceType: *GUID, - lpServiceName: [*:0]u8, - dwNameLength: u32, -) callconv(.winapi) i32; - pub extern "mswsock" fn GetNameByTypeW( lpServiceType: *GUID, lpServiceName: [*:0]u16, dwNameLength: u32, ) callconv(.winapi) i32; -pub extern "ws2_32" fn getaddrinfo( - pNodeName: ?[*:0]const u8, - pServiceName: ?[*:0]const u8, - pHints: ?*const addrinfoa, - ppResult: *?*addrinfoa, -) callconv(.winapi) i32; - -pub extern "ws2_32" fn GetAddrInfoExA( - pName: ?[*:0]const u8, - pServiceName: ?[*:0]const u8, - dwNameSapce: u32, +pub extern "ws2_32" fn GetAddrInfoExW( + pName: ?[*:0]const u16, + pServiceName: ?[*:0]const u16, + dwNameSpace: NS, lpNspId: ?*GUID, - hints: ?*const addrinfoexA, - ppResult: **addrinfoexA, + hints: ?*const ADDRINFOEXW, + ppResult: **ADDRINFOEXW, timeout: ?*timeval, lpOverlapped: ?*OVERLAPPED, lpCompletionRoutine: ?LPLOOKUPSERVICE_COMPLETION_ROUTINE, + lpNameHandle: ?*HANDLE, ) callconv(.winapi) i32; pub extern "ws2_32" fn GetAddrInfoExCancel( @@ -2337,12 +2149,8 @@ pub extern "ws2_32" fn GetAddrInfoExOverlappedResult( lpOverlapped: *OVERLAPPED, ) callconv(.winapi) i32; -pub extern "ws2_32" fn freeaddrinfo( - pAddrInfo: ?*addrinfoa, -) callconv(.winapi) void; - -pub extern "ws2_32" fn FreeAddrInfoEx( - pAddrInfoEx: ?*addrinfoexA, +pub extern "ws2_32" fn FreeAddrInfoExW( + pAddrInfoEx: ?*ADDRINFOEXW, ) callconv(.winapi) void; pub extern "ws2_32" fn getnameinfo( @@ -2354,7 +2162,3 @@ pub extern "ws2_32" fn getnameinfo( ServiceBufferName: u32, Flags: i32, ) callconv(.winapi) i32; - -pub extern "iphlpapi" fn if_nametoindex( - InterfaceName: [*:0]const u8, -) callconv(.winapi) u32; diff --git a/lib/std/posix.zig b/lib/std/posix.zig index e602b47c7b..6faf6bbe72 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -52,6 +52,10 @@ else switch (native_os) { pub const fd_t = void; pub const uid_t = void; pub const gid_t = void; + pub const mode_t = u0; + pub const ino_t = void; + pub const IFNAMESIZE = {}; + pub const SIG = void; }, }; @@ -98,7 +102,6 @@ pub const POSIX_FADV = system.POSIX_FADV; pub const PR = system.PR; pub const PROT = system.PROT; pub const RLIM = system.RLIM; -pub const RR = system.RR; pub const S = system.S; pub const SA = system.SA; pub const SC = system.SC; @@ -357,6 +360,7 @@ pub const FChmodAtError = FChmodError || error{ ProcessFdQuotaExceeded, /// The procfs fallback was used but the system exceeded it open file limit. SystemFdQuotaExceeded, + Canceled, }; /// Changes the `mode` of `path` relative to the directory referred to by @@ -486,7 +490,9 @@ fn fchmodat2(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodAtEr const stat = fstatatZ(pathfd, "", AT.EMPTY_PATH) catch |err| switch (err) { error.NameTooLong => unreachable, error.FileNotFound => unreachable, - error.InvalidUtf8 => unreachable, + error.Streaming => unreachable, + error.BadPathName => return error.Unexpected, + error.Canceled => return error.Canceled, else => |e| return e, }; if ((stat.mode & S.IFMT) == S.IFLNK) @@ -664,18 +670,22 @@ pub fn getrandom(buffer: []u8) GetRandomError!void { return getRandomBytesDevURandom(buffer); } -fn getRandomBytesDevURandom(buf: []u8) !void { +fn getRandomBytesDevURandom(buf: []u8) GetRandomError!void { const fd = try openZ("/dev/urandom", .{ .ACCMODE = .RDONLY, .CLOEXEC = true }, 0); defer close(fd); - const st = try fstat(fd); + const st = fstat(fd) catch |err| switch (err) { + error.Streaming => return error.NoDevice, + else => |e| return e, + }; if (!S.ISCHR(st.mode)) { return error.NoDevice; } - const file: fs.File = .{ .handle = fd }; - var file_reader = file.readerStreaming(&.{}); - file_reader.interface.readSliceAll(buf) catch return error.Unexpected; + var i: usize = 0; + while (i < buf.len) { + i += read(fd, buf[i..]) catch return error.Unexpected; + } } /// Causes abnormal process termination. @@ -699,7 +709,7 @@ pub fn abort() noreturn { // for user-defined signal handlers that want to restore some state in // some program sections and crash in others. // So, the user-installed SIGABRT handler is run, if present. - raise(SIG.ABRT) catch {}; + raise(.ABRT) catch {}; // Disable all signal handlers. const filledset = linux.sigfillset(); @@ -719,17 +729,17 @@ pub fn abort() noreturn { .mask = sigemptyset(), .flags = 0, }; - sigaction(SIG.ABRT, &sigact, null); + sigaction(.ABRT, &sigact, null); - _ = linux.tkill(linux.gettid(), SIG.ABRT); + _ = linux.tkill(linux.gettid(), .ABRT); var sigabrtmask = sigemptyset(); - sigaddset(&sigabrtmask, SIG.ABRT); + sigaddset(&sigabrtmask, .ABRT); sigprocmask(SIG.UNBLOCK, &sigabrtmask, null); // Beyond this point should be unreachable. @as(*allowzero volatile u8, @ptrFromInt(0)).* = 0; - raise(SIG.KILL) catch {}; + raise(.KILL) catch {}; exit(127); // Pid 1 might not be signalled in some containers. } switch (native_os) { @@ -740,7 +750,7 @@ pub fn abort() noreturn { pub const RaiseError = UnexpectedError; -pub fn raise(sig: u8) RaiseError!void { +pub fn raise(sig: SIG) RaiseError!void { if (builtin.link_libc) { switch (errno(system.raise(sig))) { .SUCCESS => return, @@ -768,7 +778,7 @@ pub fn raise(sig: u8) RaiseError!void { pub const KillError = error{ ProcessNotFound, PermissionDenied } || UnexpectedError; -pub fn kill(pid: pid_t, sig: u8) KillError!void { +pub fn kill(pid: pid_t, sig: SIG) KillError!void { switch (errno(system.kill(pid, sig))) { .SUCCESS => return, .INVAL => unreachable, // invalid signal @@ -805,36 +815,7 @@ pub fn exit(status: u8) noreturn { system.exit(status); } -pub const ReadError = error{ - InputOutput, - SystemResources, - IsDir, - OperationAborted, - BrokenPipe, - ConnectionResetByPeer, - ConnectionTimedOut, - NotOpenForReading, - SocketNotConnected, - - /// This error occurs when no global event loop is configured, - /// and reading from the file descriptor would block. - WouldBlock, - - /// reading a timerfd with CANCEL_ON_SET will lead to this error - /// when the clock goes through a discontinuous change - Canceled, - - /// In WASI, this error occurs when the file descriptor does - /// not hold the required rights to read from it. - AccessDenied, - - /// This error occurs in Linux if the process to be read from - /// no longer exists. - ProcessNotFound, - - /// Unable to read file due to lock. - LockViolation, -} || UnexpectedError; +pub const ReadError = std.Io.File.Reader.Error; /// Returns the number of bytes that were read, which can be less than /// buf.len. If 0 bytes were read, that means EOF. @@ -869,9 +850,9 @@ pub fn read(fd: fd_t, buf: []u8) ReadError!usize { .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, .NOTCAPABLE => return error.AccessDenied, else => |err| return unexpectedErrno(err), } @@ -898,9 +879,9 @@ pub fn read(fd: fd_t, buf: []u8) ReadError!usize { .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, else => |err| return unexpectedErrno(err), } } @@ -921,7 +902,6 @@ pub fn read(fd: fd_t, buf: []u8) ReadError!usize { /// a pointer within the address space of the application. pub fn readv(fd: fd_t, iov: []const iovec) ReadError!usize { if (native_os == .windows) { - // TODO improve this to use ReadFileScatter if (iov.len == 0) return 0; const first = iov[0]; return read(fd, first.base[0..first.len]); @@ -939,9 +919,9 @@ pub fn readv(fd: fd_t, iov: []const iovec) ReadError!usize { .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, .NOTCAPABLE => return error.AccessDenied, else => |err| return unexpectedErrno(err), } @@ -961,15 +941,15 @@ pub fn readv(fd: fd_t, iov: []const iovec) ReadError!usize { .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, else => |err| return unexpectedErrno(err), } } } -pub const PReadError = ReadError || error{Unseekable}; +pub const PReadError = std.Io.File.ReadPositionalError; /// Number of bytes read is returned. Upon reading end-of-file, zero is returned. /// @@ -1008,9 +988,9 @@ pub fn pread(fd: fd_t, buf: []u8, offset: u64) PReadError!usize { .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, .NXIO => return error.Unseekable, .SPIPE => return error.Unseekable, .OVERFLOW => return error.Unseekable, @@ -1041,9 +1021,9 @@ pub fn pread(fd: fd_t, buf: []u8, offset: u64) PReadError!usize { .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, .NXIO => return error.Unseekable, .SPIPE => return error.Unseekable, .OVERFLOW => return error.Unseekable, @@ -1159,9 +1139,9 @@ pub fn preadv(fd: fd_t, iov: []const iovec, offset: u64) PReadError!usize { .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, .NXIO => return error.Unseekable, .SPIPE => return error.Unseekable, .OVERFLOW => return error.Unseekable, @@ -1185,9 +1165,9 @@ pub fn preadv(fd: fd_t, iov: []const iovec, offset: u64) PReadError!usize { .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, .NXIO => return error.Unseekable, .SPIPE => return error.Unseekable, .OVERFLOW => return error.Unseekable, @@ -1209,7 +1189,7 @@ pub const WriteError = error{ PermissionDenied, BrokenPipe, SystemResources, - OperationAborted, + Canceled, NotOpenForWriting, /// The process cannot access the file because another process has locked @@ -1232,7 +1212,7 @@ pub const WriteError = error{ /// The socket type requires that message be sent atomically, and the size of the message /// to be sent made this impossible. The message is not transmitted. - MessageTooBig, + MessageOversize, } || UnexpectedError; /// Write to a file descriptor. @@ -1314,7 +1294,7 @@ pub fn write(fd: fd_t, bytes: []const u8) WriteError!usize { .CONNRESET => return error.ConnectionResetByPeer, .BUSY => return error.DeviceBusy, .NXIO => return error.NoDevice, - .MSGSIZE => return error.MessageTooBig, + .MSGSIZE => return error.MessageOversize, else => |err| return unexpectedErrno(err), } } @@ -1570,81 +1550,7 @@ pub fn pwritev(fd: fd_t, iov: []const iovec_const, offset: u64) PWriteError!usiz } } -pub const OpenError = error{ - /// In WASI, this error may occur when the file descriptor does - /// not hold the required rights to open a new resource relative to it. - AccessDenied, - PermissionDenied, - SymLinkLoop, - ProcessFdQuotaExceeded, - SystemFdQuotaExceeded, - NoDevice, - /// Either: - /// * One of the path components does not exist. - /// * Cwd was used, but cwd has been deleted. - /// * The path associated with the open directory handle has been deleted. - /// * On macOS, multiple processes or threads raced to create the same file - /// with `O.EXCL` set to `false`. - FileNotFound, - - /// The path exceeded `max_path_bytes` bytes. - NameTooLong, - - /// Insufficient kernel memory was available, or - /// the named file is a FIFO and per-user hard limit on - /// memory allocation for pipes has been reached. - SystemResources, - - /// The file is too large to be opened. This error is unreachable - /// for 64-bit targets, as well as when opening directories. - FileTooBig, - - /// The path refers to directory but the `DIRECTORY` flag was not provided. - IsDir, - - /// A new path cannot be created because the device has no room for the new file. - /// This error is only reachable when the `CREAT` flag is provided. - NoSpaceLeft, - - /// A component used as a directory in the path was not, in fact, a directory, or - /// `DIRECTORY` was specified and the path was not a directory. - NotDir, - - /// The path already exists and the `CREAT` and `EXCL` flags were provided. - PathAlreadyExists, - DeviceBusy, - - /// The underlying filesystem does not support file locks - FileLocksNotSupported, - - /// Path contains characters that are disallowed by the underlying filesystem. - BadPathName, - - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - - /// Windows-only; file paths provided by the user must be valid WTF-8. - /// https://wtf-8.codeberg.page/ - InvalidWtf8, - - /// On Windows, `\\server` or `\\server\share` was not found. - NetworkNotFound, - - /// This error occurs in Linux if the process to be open was not found. - ProcessNotFound, - - /// One of these three things: - /// * pathname refers to an executable image which is currently being - /// executed and write access was requested. - /// * pathname refers to a file that is currently in use as a swap - /// file, and the O_TRUNC flag was specified. - /// * pathname refers to a file that is currently being read by the - /// kernel (e.g., for module/firmware loading), and write access was - /// requested. - FileBusy, - - WouldBlock, -} || UnexpectedError; +pub const OpenError = std.Io.File.OpenError || error{WouldBlock}; /// Open and possibly create a file. Keeps trying if it gets interrupted. /// On Windows, `file_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). @@ -1699,10 +1605,7 @@ pub fn openZ(file_path: [*:0]const u8, flags: O, perm: mode_t) OpenError!fd_t { .PERM => return error.PermissionDenied, .EXIST => return error.PathAlreadyExists, .BUSY => return error.DeviceBusy, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -1718,119 +1621,12 @@ pub fn openat(dir_fd: fd_t, file_path: []const u8, flags: O, mode: mode_t) OpenE if (native_os == .windows) { @compileError("Windows does not support POSIX; use Windows-specific API or cross-platform std.fs API"); } else if (native_os == .wasi and !builtin.link_libc) { - // `mode` is ignored on WASI, which does not support unix-style file permissions - const opts = try openOptionsFromFlagsWasi(flags); - const fd = try openatWasi( - dir_fd, - file_path, - opts.lookup_flags, - opts.oflags, - opts.fs_flags, - opts.fs_rights_base, - opts.fs_rights_inheriting, - ); - errdefer close(fd); - - if (flags.write) { - const info = try std.os.fstat_wasi(fd); - if (info.filetype == .DIRECTORY) - return error.IsDir; - } - - return fd; + @compileError("use std.Io instead"); } const file_path_c = try toPosixPath(file_path); return openatZ(dir_fd, &file_path_c, flags, mode); } -/// Open and possibly create a file in WASI. -pub fn openatWasi( - dir_fd: fd_t, - file_path: []const u8, - lookup_flags: wasi.lookupflags_t, - oflags: wasi.oflags_t, - fdflags: wasi.fdflags_t, - base: wasi.rights_t, - inheriting: wasi.rights_t, -) OpenError!fd_t { - while (true) { - var fd: fd_t = undefined; - switch (wasi.path_open(dir_fd, lookup_flags, file_path.ptr, file_path.len, oflags, base, inheriting, fdflags, &fd)) { - .SUCCESS => return fd, - .INTR => continue, - - .FAULT => unreachable, - // Provides INVAL with a linux host on a bad path name, but NOENT on Windows - .INVAL => return error.BadPathName, - .BADF => unreachable, - .ACCES => return error.AccessDenied, - .FBIG => return error.FileTooBig, - .OVERFLOW => return error.FileTooBig, - .ISDIR => return error.IsDir, - .LOOP => return error.SymLinkLoop, - .MFILE => return error.ProcessFdQuotaExceeded, - .NAMETOOLONG => return error.NameTooLong, - .NFILE => return error.SystemFdQuotaExceeded, - .NODEV => return error.NoDevice, - .NOENT => return error.FileNotFound, - .NOMEM => return error.SystemResources, - .NOSPC => return error.NoSpaceLeft, - .NOTDIR => return error.NotDir, - .PERM => return error.PermissionDenied, - .EXIST => return error.PathAlreadyExists, - .BUSY => return error.DeviceBusy, - .NOTCAPABLE => return error.AccessDenied, - .ILSEQ => return error.InvalidUtf8, - else => |err| return unexpectedErrno(err), - } - } -} - -/// A struct to contain all lookup/rights flags accepted by `wasi.path_open` -const WasiOpenOptions = struct { - oflags: wasi.oflags_t, - lookup_flags: wasi.lookupflags_t, - fs_rights_base: wasi.rights_t, - fs_rights_inheriting: wasi.rights_t, - fs_flags: wasi.fdflags_t, -}; - -/// Compute rights + flags corresponding to the provided POSIX access mode. -fn openOptionsFromFlagsWasi(oflag: O) OpenError!WasiOpenOptions { - const w = std.os.wasi; - - // Next, calculate the read/write rights to request, depending on the - // provided POSIX access mode - var rights: w.rights_t = .{}; - if (oflag.read) { - rights.FD_READ = true; - rights.FD_READDIR = true; - } - if (oflag.write) { - rights.FD_DATASYNC = true; - rights.FD_WRITE = true; - rights.FD_ALLOCATE = true; - rights.FD_FILESTAT_SET_SIZE = true; - } - - // https://github.com/ziglang/zig/issues/18882 - const flag_bits: u32 = @bitCast(oflag); - const oflags_int: u16 = @as(u12, @truncate(flag_bits >> 12)); - const fs_flags_int: u16 = @as(u12, @truncate(flag_bits)); - - return .{ - // https://github.com/ziglang/zig/issues/18882 - .oflags = @bitCast(oflags_int), - .lookup_flags = .{ - .SYMLINK_FOLLOW = !oflag.NOFOLLOW, - }, - .fs_rights_base = rights, - .fs_rights_inheriting = rights, - // https://github.com/ziglang/zig/issues/18882 - .fs_flags = @bitCast(fs_flags_int), - }; -} - /// Open and possibly create a file. Keeps trying if it gets interrupted. /// `file_path` is relative to the open directory handle `dir_fd`. /// On Windows, `file_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). @@ -1875,10 +1671,7 @@ pub fn openatZ(dir_fd: fd_t, file_path: [*:0]const u8, flags: O, mode: mode_t) O .AGAIN => return error.WouldBlock, .TXTBSY => return error.FileBusy, .NXIO => return error.NoDevice, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -2132,14 +1925,9 @@ pub const SymLinkError = error{ ReadOnlyFileSystem, NotDir, NameTooLong, - - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - - /// Windows-only; file paths provided by the user must be valid WTF-8. + /// WASI: file paths must be valid UTF-8. + /// Windows: file paths provided by the user must be valid WTF-8. /// https://wtf-8.codeberg.page/ - InvalidWtf8, - BadPathName, } || UnexpectedError; @@ -2186,10 +1974,7 @@ pub fn symlinkZ(target_path: [*:0]const u8, sym_link_path: [*:0]const u8) SymLin .NOMEM => return error.SystemResources, .NOSPC => return error.NoSpaceLeft, .ROFS => return error.ReadOnlyFileSystem, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -2235,7 +2020,7 @@ pub fn symlinkatWasi(target_path: []const u8, newdirfd: fd_t, sym_link_path: []c .NOSPC => return error.NoSpaceLeft, .ROFS => return error.ReadOnlyFileSystem, .NOTCAPABLE => return error.AccessDenied, - .ILSEQ => return error.InvalidUtf8, + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -2264,10 +2049,7 @@ pub fn symlinkatZ(target_path: [*:0]const u8, newdirfd: fd_t, sym_link_path: [*: .NOMEM => return error.SystemResources, .NOSPC => return error.NoSpaceLeft, .ROFS => return error.ReadOnlyFileSystem, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -2286,9 +2068,7 @@ pub const LinkError = UnexpectedError || error{ NoSpaceLeft, ReadOnlyFileSystem, NotSameFileSystem, - - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, + BadPathName, }; /// On WASI, both paths should be encoded as valid UTF-8. @@ -2314,10 +2094,7 @@ pub fn linkZ(oldpath: [*:0]const u8, newpath: [*:0]const u8) LinkError!void { .ROFS => return error.ReadOnlyFileSystem, .XDEV => return error.NotSameFileSystem, .INVAL => unreachable, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -2368,10 +2145,7 @@ pub fn linkatZ( .ROFS => return error.ReadOnlyFileSystem, .XDEV => return error.NotSameFileSystem, .INVAL => unreachable, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -2417,7 +2191,7 @@ pub fn linkat( .ROFS => return error.ReadOnlyFileSystem, .XDEV => return error.NotSameFileSystem, .INVAL => unreachable, - .ILSEQ => return error.InvalidUtf8, + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -2442,14 +2216,10 @@ pub const UnlinkError = error{ SystemResources, ReadOnlyFileSystem, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - - /// Windows-only; file paths provided by the user must be valid WTF-8. + /// WASI: file paths must be valid UTF-8. + /// Windows: file paths provided by the user must be valid WTF-8. /// https://wtf-8.codeberg.page/ - InvalidWtf8, - - /// On Windows, file paths cannot contain these characters: + /// Windows: file paths cannot contain these characters: /// '/', '*', '?', '"', '<', '>', '|' BadPathName, @@ -2500,10 +2270,7 @@ pub fn unlinkZ(file_path: [*:0]const u8) UnlinkError!void { .NOTDIR => return error.NotDir, .NOMEM => return error.SystemResources, .ROFS => return error.ReadOnlyFileSystem, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -2562,7 +2329,7 @@ pub fn unlinkatWasi(dirfd: fd_t, file_path: []const u8, flags: u32) UnlinkatErro .ROFS => return error.ReadOnlyFileSystem, .NOTEMPTY => return error.DirNotEmpty, .NOTCAPABLE => return error.AccessDenied, - .ILSEQ => return error.InvalidUtf8, + .ILSEQ => return error.BadPathName, .INVAL => unreachable, // invalid flags, or pathname has . as last component .BADF => unreachable, // always a race condition @@ -2595,10 +2362,7 @@ pub fn unlinkatZ(dirfd: fd_t, file_path_c: [*:0]const u8, flags: u32) UnlinkatEr .ROFS => return error.ReadOnlyFileSystem, .EXIST => return error.DirNotEmpty, .NOTEMPTY => return error.DirNotEmpty, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, .INVAL => unreachable, // invalid flags, or pathname has . as last component .BADF => unreachable, // always a race condition @@ -2634,11 +2398,9 @@ pub const RenameError = error{ PathAlreadyExists, ReadOnlyFileSystem, RenameAcrossMountPoints, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - /// Windows-only; file paths provided by the user must be valid WTF-8. + /// WASI: file paths must be valid UTF-8. + /// Windows: file paths provided by the user must be valid WTF-8. /// https://wtf-8.codeberg.page/ - InvalidWtf8, BadPathName, NoDevice, SharingViolation, @@ -2700,10 +2462,7 @@ pub fn renameZ(old_path: [*:0]const u8, new_path: [*:0]const u8) RenameError!voi .NOTEMPTY => return error.PathAlreadyExists, .ROFS => return error.ReadOnlyFileSystem, .XDEV => return error.RenameAcrossMountPoints, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -2764,7 +2523,7 @@ fn renameatWasi(old: RelativePathWasi, new: RelativePathWasi) RenameError!void { .ROFS => return error.ReadOnlyFileSystem, .XDEV => return error.RenameAcrossMountPoints, .NOTCAPABLE => return error.AccessDenied, - .ILSEQ => return error.InvalidUtf8, + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -2815,10 +2574,7 @@ pub fn renameatZ( .NOTEMPTY => return error.PathAlreadyExists, .ROFS => return error.ReadOnlyFileSystem, .XDEV => return error.RenameAcrossMountPoints, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -2869,7 +2625,7 @@ pub fn renameatW( if (ReplaceIfExists == windows.TRUE) flags |= windows.FILE_RENAME_REPLACE_IF_EXISTS; rename_info.* = .{ .Flags = flags, - .RootDirectory = if (fs.path.isAbsoluteWindowsWTF16(new_path_w)) null else new_dir_fd, + .RootDirectory = if (fs.path.isAbsoluteWindowsWtf16(new_path_w)) null else new_dir_fd, .FileNameLength = @intCast(new_path_w.len * 2), // already checked error.NameTooLong .FileName = undefined, }; @@ -2906,7 +2662,7 @@ pub fn renameatW( rename_info.* = .{ .Flags = ReplaceIfExists, - .RootDirectory = if (fs.path.isAbsoluteWindowsWTF16(new_path_w)) null else new_dir_fd, + .RootDirectory = if (fs.path.isAbsoluteWindowsWtf16(new_path_w)) null else new_dir_fd, .FileNameLength = @intCast(new_path_w.len * 2), // already checked error.NameTooLong .FileName = undefined, }; @@ -2943,47 +2699,21 @@ pub fn renameatW( /// On other platforms, `sub_dir_path` is an opaque sequence of bytes with no particular encoding. pub fn mkdirat(dir_fd: fd_t, sub_dir_path: []const u8, mode: mode_t) MakeDirError!void { if (native_os == .windows) { - const sub_dir_path_w = try windows.sliceToPrefixedFileW(dir_fd, sub_dir_path); - return mkdiratW(dir_fd, sub_dir_path_w.span(), mode); + @compileError("use std.Io instead"); } else if (native_os == .wasi and !builtin.link_libc) { - return mkdiratWasi(dir_fd, sub_dir_path, mode); + @compileError("use std.Io instead"); } else { const sub_dir_path_c = try toPosixPath(sub_dir_path); return mkdiratZ(dir_fd, &sub_dir_path_c, mode); } } -pub fn mkdiratWasi(dir_fd: fd_t, sub_dir_path: []const u8, mode: mode_t) MakeDirError!void { - _ = mode; - switch (wasi.path_create_directory(dir_fd, sub_dir_path.ptr, sub_dir_path.len)) { - .SUCCESS => return, - .ACCES => return error.AccessDenied, - .BADF => unreachable, - .PERM => return error.PermissionDenied, - .DQUOT => return error.DiskQuota, - .EXIST => return error.PathAlreadyExists, - .FAULT => unreachable, - .LOOP => return error.SymLinkLoop, - .MLINK => return error.LinkQuotaExceeded, - .NAMETOOLONG => return error.NameTooLong, - .NOENT => return error.FileNotFound, - .NOMEM => return error.SystemResources, - .NOSPC => return error.NoSpaceLeft, - .NOTDIR => return error.NotDir, - .ROFS => return error.ReadOnlyFileSystem, - .NOTCAPABLE => return error.AccessDenied, - .ILSEQ => return error.InvalidUtf8, - else => |err| return unexpectedErrno(err), - } -} - /// Same as `mkdirat` except the parameters are null-terminated. pub fn mkdiratZ(dir_fd: fd_t, sub_dir_path: [*:0]const u8, mode: mode_t) MakeDirError!void { if (native_os == .windows) { - const sub_dir_path_w = try windows.cStrToPrefixedFileW(dir_fd, sub_dir_path); - return mkdiratW(dir_fd, sub_dir_path_w.span(), mode); + @compileError("use std.Io instead"); } else if (native_os == .wasi and !builtin.link_libc) { - return mkdirat(dir_fd, mem.sliceTo(sub_dir_path, 0), mode); + @compileError("use std.Io instead"); } switch (errno(system.mkdirat(dir_fd, sub_dir_path, mode))) { .SUCCESS => return, @@ -3003,58 +2733,12 @@ pub fn mkdiratZ(dir_fd: fd_t, sub_dir_path: [*:0]const u8, mode: mode_t) MakeDir .ROFS => return error.ReadOnlyFileSystem, // dragonfly: when dir_fd is unlinked from filesystem .NOTCONN => return error.FileNotFound, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } -/// Windows-only. Same as `mkdirat` except the parameter WTF16 LE encoded. -pub fn mkdiratW(dir_fd: fd_t, sub_path_w: []const u16, mode: mode_t) MakeDirError!void { - _ = mode; - const sub_dir_handle = windows.OpenFile(sub_path_w, .{ - .dir = dir_fd, - .access_mask = windows.GENERIC_READ | windows.SYNCHRONIZE, - .creation = windows.FILE_CREATE, - .filter = .dir_only, - }) catch |err| switch (err) { - error.IsDir => return error.Unexpected, - error.PipeBusy => return error.Unexpected, - error.NoDevice => return error.Unexpected, - error.WouldBlock => return error.Unexpected, - error.AntivirusInterference => return error.Unexpected, - else => |e| return e, - }; - windows.CloseHandle(sub_dir_handle); -} - -pub const MakeDirError = error{ - /// In WASI, this error may occur when the file descriptor does - /// not hold the required rights to create a new directory relative to it. - AccessDenied, - PermissionDenied, - DiskQuota, - PathAlreadyExists, - SymLinkLoop, - LinkQuotaExceeded, - NameTooLong, - FileNotFound, - SystemResources, - NoSpaceLeft, - NotDir, - ReadOnlyFileSystem, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - /// Windows-only; file paths provided by the user must be valid WTF-8. - /// https://wtf-8.codeberg.page/ - InvalidWtf8, - BadPathName, - NoDevice, - /// On Windows, `\\server` or `\\server\share` was not found. - NetworkNotFound, -} || UnexpectedError; +pub const MakeDirError = std.Io.Dir.MakeError; /// Create a directory. /// `mode` is ignored on Windows and WASI. @@ -3099,10 +2783,7 @@ pub fn mkdirZ(dir_path: [*:0]const u8, mode: mode_t) MakeDirError!void { .NOSPC => return error.NoSpaceLeft, .NOTDIR => return error.NotDir, .ROFS => return error.ReadOnlyFileSystem, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -3137,11 +2818,9 @@ pub const DeleteDirError = error{ NotDir, DirNotEmpty, ReadOnlyFileSystem, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - /// Windows-only; file paths provided by the user must be valid WTF-8. + /// WASI: file paths must be valid UTF-8. + /// Windows: file paths provided by the user must be valid WTF-8. /// https://wtf-8.codeberg.page/ - InvalidWtf8, BadPathName, /// On Windows, `\\server` or `\\server\share` was not found. NetworkNotFound, @@ -3193,10 +2872,7 @@ pub fn rmdirZ(dir_path: [*:0]const u8) DeleteDirError!void { .EXIST => return error.DirNotEmpty, .NOTEMPTY => return error.DirNotEmpty, .ROFS => return error.ReadOnlyFileSystem, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -3217,12 +2893,10 @@ pub const ChangeCurDirError = error{ FileNotFound, SystemResources, NotDir, - BadPathName, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - /// Windows-only; file paths provided by the user must be valid WTF-8. + /// WASI: file paths must be valid UTF-8. + /// Windows: file paths provided by the user must be valid WTF-8. /// https://wtf-8.codeberg.page/ - InvalidWtf8, + BadPathName, } || UnexpectedError; /// Changes the current working directory of the calling process. @@ -3234,10 +2908,7 @@ pub fn chdir(dir_path: []const u8) ChangeCurDirError!void { @compileError("WASI does not support os.chdir"); } else if (native_os == .windows) { var wtf16_dir_path: [windows.PATH_MAX_WIDE]u16 = undefined; - if (try std.unicode.checkWtf8ToWtf16LeOverflow(dir_path, &wtf16_dir_path)) { - return error.NameTooLong; - } - const len = try std.unicode.wtf8ToWtf16Le(&wtf16_dir_path, dir_path); + const len = try windows.wtf8ToWtf16Le(&wtf16_dir_path, dir_path); return chdirW(wtf16_dir_path[0..len]); } else { const dir_path_c = try toPosixPath(dir_path); @@ -3253,10 +2924,7 @@ pub fn chdirZ(dir_path: [*:0]const u8) ChangeCurDirError!void { if (native_os == .windows) { const dir_path_span = mem.span(dir_path); var wtf16_dir_path: [windows.PATH_MAX_WIDE]u16 = undefined; - if (try std.unicode.checkWtf8ToWtf16LeOverflow(dir_path_span, &wtf16_dir_path)) { - return error.NameTooLong; - } - const len = try std.unicode.wtf8ToWtf16Le(&wtf16_dir_path, dir_path_span); + const len = try windows.wtf8ToWtf16Le(&wtf16_dir_path, dir_path_span); return chdirW(wtf16_dir_path[0..len]); } else if (native_os == .wasi and !builtin.link_libc) { return chdir(mem.span(dir_path)); @@ -3271,10 +2939,7 @@ pub fn chdirZ(dir_path: [*:0]const u8) ChangeCurDirError!void { .NOENT => return error.FileNotFound, .NOMEM => return error.SystemResources, .NOTDIR => return error.NotDir, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -3320,11 +2985,9 @@ pub const ReadLinkError = error{ SystemResources, NotLink, NotDir, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - /// Windows-only; file paths provided by the user must be valid WTF-8. + /// WASI: file paths must be valid UTF-8. + /// Windows: file paths provided by the user must be valid WTF-8. /// https://wtf-8.codeberg.page/ - InvalidWtf8, BadPathName, /// Windows-only. This error may occur if the opened reparse point is /// of unsupported type. @@ -3380,10 +3043,7 @@ pub fn readlinkZ(file_path: [*:0]const u8, out_buffer: []u8) ReadLinkError![]u8 .NOENT => return error.FileNotFound, .NOMEM => return error.SystemResources, .NOTDIR => return error.NotDir, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -3425,7 +3085,7 @@ pub fn readlinkatWasi(dirfd: fd_t, file_path: []const u8, out_buffer: []u8) Read .NOMEM => return error.SystemResources, .NOTDIR => return error.NotDir, .NOTCAPABLE => return error.AccessDenied, - .ILSEQ => return error.InvalidUtf8, + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -3458,10 +3118,7 @@ pub fn readlinkatZ(dirfd: fd_t, file_path: [*:0]const u8, out_buffer: []u8) Read .NOENT => return error.FileNotFound, .NOMEM => return error.SystemResources, .NOTDIR => return error.NotDir, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -3612,7 +3269,7 @@ pub const SocketError = error{ AccessDenied, /// The implementation does not support the specified address family. - AddressFamilyNotSupported, + AddressFamilyUnsupported, /// Unknown protocol, or protocol family not available. ProtocolFamilyNotAvailable, @@ -3635,33 +3292,6 @@ pub const SocketError = error{ } || UnexpectedError; pub fn socket(domain: u32, socket_type: u32, protocol: u32) SocketError!socket_t { - if (native_os == .windows) { - // These flags are not actually part of the Windows API, instead they are converted here for compatibility - const filtered_sock_type = socket_type & ~@as(u32, SOCK.NONBLOCK | SOCK.CLOEXEC); - var flags: u32 = windows.ws2_32.WSA_FLAG_OVERLAPPED; - if ((socket_type & SOCK.CLOEXEC) != 0) flags |= windows.ws2_32.WSA_FLAG_NO_HANDLE_INHERIT; - - const rc = try windows.WSASocketW( - @bitCast(domain), - @bitCast(filtered_sock_type), - @bitCast(protocol), - null, - 0, - flags, - ); - errdefer windows.closesocket(rc) catch unreachable; - if ((socket_type & SOCK.NONBLOCK) != 0) { - var mode: c_ulong = 1; // nonblocking - if (windows.ws2_32.SOCKET_ERROR == windows.ws2_32.ioctlsocket(rc, windows.ws2_32.FIONBIO, &mode)) { - switch (windows.ws2_32.WSAGetLastError()) { - // have not identified any error codes that should be handled yet - else => unreachable, - } - } - } - return rc; - } - const have_sock_flags = !builtin.target.os.tag.isDarwin() and native_os != .haiku; const filtered_sock_type = if (!have_sock_flags) socket_type & ~@as(u32, SOCK.NONBLOCK | SOCK.CLOEXEC) @@ -3678,7 +3308,7 @@ pub fn socket(domain: u32, socket_type: u32, protocol: u32) SocketError!socket_t return fd; }, .ACCES => return error.AccessDenied, - .AFNOSUPPORT => return error.AddressFamilyNotSupported, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, .INVAL => return error.ProtocolFamilyNotAvailable, .MFILE => return error.ProcessFdQuotaExceeded, .NFILE => return error.SystemFdQuotaExceeded, @@ -3718,7 +3348,7 @@ pub fn socketpair(domain: u32, socket_type: u32, protocol: u32) SocketError![2]s return socks; }, .ACCES => return error.AccessDenied, - .AFNOSUPPORT => return error.AddressFamilyNotSupported, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, .INVAL => return error.ProtocolFamilyNotAvailable, .MFILE => return error.ProcessFdQuotaExceeded, .NFILE => return error.SystemFdQuotaExceeded, @@ -3738,10 +3368,10 @@ pub const ShutdownError = error{ BlockingOperationInProgress, /// The network subsystem has failed. - NetworkSubsystemFailed, + NetworkDown, /// The socket is not connected (connection-oriented sockets only). - SocketNotConnected, + SocketUnconnected, SystemResources, } || UnexpectedError; @@ -3756,14 +3386,14 @@ pub fn shutdown(sock: socket_t, how: ShutdownHow) ShutdownError!void { .both => windows.ws2_32.SD_BOTH, }); if (0 != result) switch (windows.ws2_32.WSAGetLastError()) { - .WSAECONNABORTED => return error.ConnectionAborted, - .WSAECONNRESET => return error.ConnectionResetByPeer, - .WSAEINPROGRESS => return error.BlockingOperationInProgress, - .WSAEINVAL => unreachable, - .WSAENETDOWN => return error.NetworkSubsystemFailed, - .WSAENOTCONN => return error.SocketNotConnected, - .WSAENOTSOCK => unreachable, - .WSANOTINITIALISED => unreachable, + .ECONNABORTED => return error.ConnectionAborted, + .ECONNRESET => return error.ConnectionResetByPeer, + .EINPROGRESS => return error.BlockingOperationInProgress, + .EINVAL => unreachable, + .ENETDOWN => return error.NetworkDown, + .ENOTCONN => return error.SocketUnconnected, + .ENOTSOCK => unreachable, + .NOTINITIALISED => unreachable, else => |err| return windows.unexpectedWSAError(err), }; } else { @@ -3776,7 +3406,7 @@ pub fn shutdown(sock: socket_t, how: ShutdownHow) ShutdownError!void { .SUCCESS => return, .BADF => unreachable, .INVAL => unreachable, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .NOTSOCK => unreachable, .NOBUFS => return error.SystemResources, else => |err| return unexpectedErrno(err), @@ -3785,70 +3415,17 @@ pub fn shutdown(sock: socket_t, how: ShutdownHow) ShutdownError!void { } pub const BindError = error{ - /// The address is protected, and the user is not the superuser. - /// For UNIX domain sockets: Search permission is denied on a component - /// of the path prefix. - AccessDenied, - - /// The given address is already in use, or in the case of Internet domain sockets, - /// The port number was specified as zero in the socket - /// address structure, but, upon attempting to bind to an ephemeral port, it was - /// determined that all port numbers in the ephemeral port range are currently in - /// use. See the discussion of /proc/sys/net/ipv4/ip_local_port_range ip(7). - AddressInUse, - - /// A nonexistent interface was requested or the requested address was not local. - AddressNotAvailable, - - /// The address is not valid for the address family of socket. - AddressFamilyNotSupported, - - /// Too many symbolic links were encountered in resolving addr. SymLinkLoop, - - /// addr is too long. NameTooLong, - - /// A component in the directory prefix of the socket pathname does not exist. FileNotFound, - - /// Insufficient kernel memory was available. - SystemResources, - - /// A component of the path prefix is not a directory. NotDir, - - /// The socket inode would reside on a read-only filesystem. ReadOnlyFileSystem, + AccessDenied, +} || std.Io.net.IpAddress.BindError; - /// The network subsystem has failed. - NetworkSubsystemFailed, - - FileDescriptorNotASocket, - - AlreadyBound, -} || UnexpectedError; - -/// addr is `*const T` where T is one of the sockaddr pub fn bind(sock: socket_t, addr: *const sockaddr, len: socklen_t) BindError!void { if (native_os == .windows) { - const rc = windows.bind(sock, addr, len); - if (rc == windows.ws2_32.SOCKET_ERROR) { - switch (windows.ws2_32.WSAGetLastError()) { - .WSANOTINITIALISED => unreachable, // not initialized WSA - .WSAEACCES => return error.AccessDenied, - .WSAEADDRINUSE => return error.AddressInUse, - .WSAEADDRNOTAVAIL => return error.AddressNotAvailable, - .WSAENOTSOCK => return error.FileDescriptorNotASocket, - .WSAEFAULT => unreachable, // invalid pointers - .WSAEINVAL => return error.AlreadyBound, - .WSAENOBUFS => return error.SystemResources, - .WSAENETDOWN => return error.NetworkSubsystemFailed, - else => |err| return windows.unexpectedWSAError(err), - } - unreachable; - } - return; + @compileError("use std.Io instead"); } else { const rc = system.bind(sock, addr, len); switch (errno(rc)) { @@ -3858,8 +3435,8 @@ pub fn bind(sock: socket_t, addr: *const sockaddr, len: socklen_t) BindError!voi .BADF => unreachable, // always a race condition if this error is returned .INVAL => unreachable, // invalid parameters .NOTSOCK => unreachable, // invalid `sockfd` - .AFNOSUPPORT => return error.AddressFamilyNotSupported, - .ADDRNOTAVAIL => return error.AddressNotAvailable, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .ADDRNOTAVAIL => return error.AddressUnavailable, .FAULT => unreachable, // invalid `addr` pointer .LOOP => return error.SymLinkLoop, .NAMETOOLONG => return error.NameTooLong, @@ -3874,51 +3451,13 @@ pub fn bind(sock: socket_t, addr: *const sockaddr, len: socklen_t) BindError!voi } pub const ListenError = error{ - /// Another socket is already listening on the same port. - /// For Internet domain sockets, the socket referred to by sockfd had not previously - /// been bound to an address and, upon attempting to bind it to an ephemeral port, it - /// was determined that all port numbers in the ephemeral port range are currently in - /// use. See the discussion of /proc/sys/net/ipv4/ip_local_port_range in ip(7). - AddressInUse, - - /// The file descriptor sockfd does not refer to a socket. FileDescriptorNotASocket, - - /// The socket is not of a type that supports the listen() operation. OperationNotSupported, - - /// The network subsystem has failed. - NetworkSubsystemFailed, - - /// Ran out of system resources - /// On Windows it can either run out of socket descriptors or buffer space - SystemResources, - - /// Already connected - AlreadyConnected, - - /// Socket has not been bound yet - SocketNotBound, -} || UnexpectedError; +} || std.Io.net.IpAddress.ListenError || std.Io.net.UnixAddress.ListenError; pub fn listen(sock: socket_t, backlog: u31) ListenError!void { if (native_os == .windows) { - const rc = windows.listen(sock, backlog); - if (rc == windows.ws2_32.SOCKET_ERROR) { - switch (windows.ws2_32.WSAGetLastError()) { - .WSANOTINITIALISED => unreachable, // not initialized WSA - .WSAENETDOWN => return error.NetworkSubsystemFailed, - .WSAEADDRINUSE => return error.AddressInUse, - .WSAEISCONN => return error.AlreadyConnected, - .WSAEINVAL => return error.SocketNotBound, - .WSAEMFILE, .WSAENOBUFS => return error.SystemResources, - .WSAENOTSOCK => return error.FileDescriptorNotASocket, - .WSAEOPNOTSUPP => return error.OperationNotSupported, - .WSAEINPROGRESS => unreachable, - else => |err| return windows.unexpectedWSAError(err), - } - } - return; + @compileError("use std.Io instead"); } else { const rc = system.listen(sock, backlog); switch (errno(rc)) { @@ -3932,70 +3471,12 @@ pub fn listen(sock: socket_t, backlog: u31) ListenError!void { } } -pub const AcceptError = error{ - ConnectionAborted, +pub const AcceptError = std.Io.net.Server.AcceptError; - /// The file descriptor sockfd does not refer to a socket. - FileDescriptorNotASocket, - - /// The per-process limit on the number of open file descriptors has been reached. - ProcessFdQuotaExceeded, - - /// The system-wide limit on the total number of open files has been reached. - SystemFdQuotaExceeded, - - /// Not enough free memory. This often means that the memory allocation is limited - /// by the socket buffer limits, not by the system memory. - SystemResources, - - /// Socket is not listening for new connections. - SocketNotListening, - - ProtocolFailure, - - /// Firewall rules forbid connection. - BlockedByFirewall, - - /// This error occurs when no global event loop is configured, - /// and accepting from the socket would block. - WouldBlock, - - /// An incoming connection was indicated, but was subsequently terminated by the - /// remote peer prior to accepting the call. - ConnectionResetByPeer, - - /// The network subsystem has failed. - NetworkSubsystemFailed, - - /// The referenced socket is not a type that supports connection-oriented service. - OperationNotSupported, -} || UnexpectedError; - -/// Accept a connection on a socket. -/// If `sockfd` is opened in non blocking mode, the function will -/// return error.WouldBlock when EAGAIN is received. pub fn accept( - /// This argument is a socket that has been created with `socket`, bound to a local address - /// with `bind`, and is listening for connections after a `listen`. sock: socket_t, - /// This argument is a pointer to a sockaddr structure. This structure is filled in with the - /// address of the peer socket, as known to the communications layer. The exact format of the - /// address returned addr is determined by the socket's address family (see `socket` and the - /// respective protocol man pages). addr: ?*sockaddr, - /// This argument is a value-result argument: the caller must initialize it to contain the - /// size (in bytes) of the structure pointed to by addr; on return it will contain the actual size - /// of the peer address. - /// - /// The returned address is truncated if the buffer provided is too small; in this case, `addr_size` - /// will return a value greater than was supplied to the call. addr_size: ?*socklen_t, - /// The following values can be bitwise ORed in flags to obtain different behavior: - /// * `SOCK.NONBLOCK` - Set the `NONBLOCK` file status flag on the open file description (see `open`) - /// referred to by the new file descriptor. Using this flag saves extra calls to `fcntl` to achieve - /// the same result. - /// * `SOCK.CLOEXEC` - Set the close-on-exec (`FD_CLOEXEC`) flag on the new file descriptor. See the - /// description of the `CLOEXEC` flag in `open` for reasons why this may be useful. flags: u32, ) AcceptError!socket_t { const have_accept4 = !(builtin.target.os.tag.isDarwin() or native_os == .windows or native_os == .haiku); @@ -4004,29 +3485,11 @@ pub fn accept( const accepted_sock: socket_t = while (true) { const rc = if (have_accept4) system.accept4(sock, addr, addr_size, flags) - else if (native_os == .windows) - windows.accept(sock, addr, addr_size) else system.accept(sock, addr, addr_size); if (native_os == .windows) { - if (rc == windows.ws2_32.INVALID_SOCKET) { - switch (windows.ws2_32.WSAGetLastError()) { - .WSANOTINITIALISED => unreachable, // not initialized WSA - .WSAECONNRESET => return error.ConnectionResetByPeer, - .WSAEFAULT => unreachable, - .WSAENOTSOCK => return error.FileDescriptorNotASocket, - .WSAEINVAL => return error.SocketNotListening, - .WSAEMFILE => return error.ProcessFdQuotaExceeded, - .WSAENETDOWN => return error.NetworkSubsystemFailed, - .WSAENOBUFS => return error.FileDescriptorNotASocket, - .WSAEOPNOTSUPP => return error.OperationNotSupported, - .WSAEWOULDBLOCK => return error.WouldBlock, - else => |err| return windows.unexpectedWSAError(err), - } - } else { - break rc; - } + @compileError("use std.Io instead"); } else { switch (errno(rc)) { .SUCCESS => break @intCast(rc), @@ -4088,9 +3551,9 @@ fn setSockFlags(sock: socket_t, flags: u32) !void { var mode: c_ulong = 1; if (windows.ws2_32.ioctlsocket(sock, windows.ws2_32.FIONBIO, &mode) == windows.ws2_32.SOCKET_ERROR) { switch (windows.ws2_32.WSAGetLastError()) { - .WSANOTINITIALISED => unreachable, - .WSAENETDOWN => return error.NetworkSubsystemFailed, - .WSAENOTSOCK => return error.FileDescriptorNotASocket, + .NOTINITIALISED => unreachable, + .ENETDOWN => return error.NetworkDown, + .ENOTSOCK => return error.FileDescriptorNotASocket, // TODO: handle more errors else => |err| return windows.unexpectedWSAError(err), } @@ -4230,7 +3693,7 @@ pub const GetSockNameError = error{ SystemResources, /// The network subsystem has failed. - NetworkSubsystemFailed, + NetworkDown, /// Socket hasn't been bound yet SocketNotBound, @@ -4243,11 +3706,11 @@ pub fn getsockname(sock: socket_t, addr: *sockaddr, addrlen: *socklen_t) GetSock const rc = windows.getsockname(sock, addr, addrlen); if (rc == windows.ws2_32.SOCKET_ERROR) { switch (windows.ws2_32.WSAGetLastError()) { - .WSANOTINITIALISED => unreachable, - .WSAENETDOWN => return error.NetworkSubsystemFailed, - .WSAEFAULT => unreachable, // addr or addrlen have invalid pointers or addrlen points to an incorrect value - .WSAENOTSOCK => return error.FileDescriptorNotASocket, - .WSAEINVAL => return error.SocketNotBound, + .NOTINITIALISED => unreachable, + .ENETDOWN => return error.NetworkDown, + .EFAULT => unreachable, // addr or addrlen have invalid pointers or addrlen points to an incorrect value + .ENOTSOCK => return error.FileDescriptorNotASocket, + .EINVAL => return error.SocketNotBound, else => |err| return windows.unexpectedWSAError(err), } } @@ -4272,11 +3735,11 @@ pub fn getpeername(sock: socket_t, addr: *sockaddr, addrlen: *socklen_t) GetSock const rc = windows.getpeername(sock, addr, addrlen); if (rc == windows.ws2_32.SOCKET_ERROR) { switch (windows.ws2_32.WSAGetLastError()) { - .WSANOTINITIALISED => unreachable, - .WSAENETDOWN => return error.NetworkSubsystemFailed, - .WSAEFAULT => unreachable, // addr or addrlen have invalid pointers or addrlen points to an incorrect value - .WSAENOTSOCK => return error.FileDescriptorNotASocket, - .WSAEINVAL => return error.SocketNotBound, + .NOTINITIALISED => unreachable, + .ENETDOWN => return error.NetworkDown, + .EFAULT => unreachable, // addr or addrlen have invalid pointers or addrlen points to an incorrect value + .ENOTSOCK => return error.FileDescriptorNotASocket, + .EINVAL => return error.SocketNotBound, else => |err| return windows.unexpectedWSAError(err), } } @@ -4296,86 +3759,11 @@ pub fn getpeername(sock: socket_t, addr: *sockaddr, addrlen: *socklen_t) GetSock } } -pub const ConnectError = error{ - /// For UNIX domain sockets, which are identified by pathname: Write permission is denied on the socket - /// file, or search permission is denied for one of the directories in the path prefix. - /// or - /// The user tried to connect to a broadcast address without having the socket broadcast flag enabled or - /// the connection request failed because of a local firewall rule. - AccessDenied, +pub const ConnectError = std.Io.net.IpAddress.ConnectError || std.Io.net.UnixAddress.ConnectError; - /// See AccessDenied - PermissionDenied, - - /// Local address is already in use. - AddressInUse, - - /// (Internet domain sockets) The socket referred to by sockfd had not previously been bound to an - /// address and, upon attempting to bind it to an ephemeral port, it was determined that all port numbers - /// in the ephemeral port range are currently in use. See the discussion of - /// /proc/sys/net/ipv4/ip_local_port_range in ip(7). - AddressNotAvailable, - - /// The passed address didn't have the correct address family in its sa_family field. - AddressFamilyNotSupported, - - /// Insufficient entries in the routing cache. - SystemResources, - - /// A connect() on a stream socket found no one listening on the remote address. - ConnectionRefused, - - /// Network is unreachable. - NetworkUnreachable, - - /// Timeout while attempting connection. The server may be too busy to accept new connections. Note - /// that for IP sockets the timeout may be very long when syncookies are enabled on the server. - ConnectionTimedOut, - - /// This error occurs when no global event loop is configured, - /// and connecting to the socket would block. - WouldBlock, - - /// The given path for the unix socket does not exist. - FileNotFound, - - /// Connection was reset by peer before connect could complete. - ConnectionResetByPeer, - - /// Socket is non-blocking and already has a pending connection in progress. - ConnectionPending, - - /// Socket was already connected - AlreadyConnected, -} || UnexpectedError; - -/// Initiate a connection on a socket. -/// If `sockfd` is opened in non blocking mode, the function will -/// return error.WouldBlock when EAGAIN or EINPROGRESS is received. pub fn connect(sock: socket_t, sock_addr: *const sockaddr, len: socklen_t) ConnectError!void { if (native_os == .windows) { - const rc = windows.ws2_32.connect(sock, sock_addr, @intCast(len)); - if (rc == 0) return; - switch (windows.ws2_32.WSAGetLastError()) { - .WSAEADDRINUSE => return error.AddressInUse, - .WSAEADDRNOTAVAIL => return error.AddressNotAvailable, - .WSAECONNREFUSED => return error.ConnectionRefused, - .WSAECONNRESET => return error.ConnectionResetByPeer, - .WSAETIMEDOUT => return error.ConnectionTimedOut, - .WSAEHOSTUNREACH, // TODO: should we return NetworkUnreachable in this case as well? - .WSAENETUNREACH, - => return error.NetworkUnreachable, - .WSAEFAULT => unreachable, - .WSAEINVAL => unreachable, - .WSAEISCONN => return error.AlreadyConnected, - .WSAENOTSOCK => unreachable, - .WSAEWOULDBLOCK => return error.WouldBlock, - .WSAEACCES => unreachable, - .WSAENOBUFS => return error.SystemResources, - .WSAEAFNOSUPPORT => return error.AddressFamilyNotSupported, - else => |err| return windows.unexpectedWSAError(err), - } - return; + @compileError("use std.Io instead"); } while (true) { @@ -4383,9 +3771,8 @@ pub fn connect(sock: socket_t, sock_addr: *const sockaddr, len: socklen_t) Conne .SUCCESS => return, .ACCES => return error.AccessDenied, .PERM => return error.PermissionDenied, - .ADDRINUSE => return error.AddressInUse, - .ADDRNOTAVAIL => return error.AddressNotAvailable, - .AFNOSUPPORT => return error.AddressFamilyNotSupported, + .ADDRNOTAVAIL => return error.AddressUnavailable, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, .AGAIN, .INPROGRESS => return error.WouldBlock, .ALREADY => return error.ConnectionPending, .BADF => unreachable, // sockfd is not a valid open file descriptor. @@ -4393,12 +3780,12 @@ pub fn connect(sock: socket_t, sock_addr: *const sockaddr, len: socklen_t) Conne .CONNRESET => return error.ConnectionResetByPeer, .FAULT => unreachable, // The socket structure address is outside the user's address space. .INTR => continue, - .ISCONN => return error.AlreadyConnected, // The socket is already connected. + .ISCONN => @panic("AlreadyConnected"), // The socket is already connected. .HOSTUNREACH => return error.NetworkUnreachable, .NETUNREACH => return error.NetworkUnreachable, .NOTSOCK => unreachable, // The file descriptor sockfd does not refer to a socket. .PROTOTYPE => unreachable, // The socket type does not support the requested communications protocol. - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, .NOENT => return error.FileNotFound, // Returned when socket is AF.UNIX and the given path does not exist. .CONNABORTED => unreachable, // Tried to reuse socket that previously received error.ConnectionRefused. else => |err| return unexpectedErrno(err), @@ -4446,8 +3833,8 @@ pub fn getsockoptError(sockfd: fd_t) ConnectError!void { .ACCES => return error.AccessDenied, .PERM => return error.PermissionDenied, .ADDRINUSE => return error.AddressInUse, - .ADDRNOTAVAIL => return error.AddressNotAvailable, - .AFNOSUPPORT => return error.AddressFamilyNotSupported, + .ADDRNOTAVAIL => return error.AddressUnavailable, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, .AGAIN => return error.SystemResources, .ALREADY => return error.ConnectionPending, .BADF => unreachable, // sockfd is not a valid open file descriptor. @@ -4458,7 +3845,7 @@ pub fn getsockoptError(sockfd: fd_t) ConnectError!void { .NETUNREACH => return error.NetworkUnreachable, .NOTSOCK => unreachable, // The file descriptor sockfd does not refer to a socket. .PROTOTYPE => unreachable, // The socket type does not support the requested communications protocol. - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, .CONNRESET => return error.ConnectionResetByPeer, else => |err| return unexpectedErrno(err), }, @@ -4512,14 +3899,7 @@ pub fn wait4(pid: pid_t, flags: u32, ru: ?*rusage) WaitPidResult { } } -pub const FStatError = error{ - SystemResources, - - /// In WASI, this error may occur when the file descriptor does - /// not hold the required rights to get its filestat information. - AccessDenied, - PermissionDenied, -} || UnexpectedError; +pub const FStatError = std.Io.File.StatError; /// Return information about a file descriptor. pub fn fstat(fd: fd_t) FStatError!Stat { @@ -4546,21 +3926,17 @@ pub const FStatAtError = FStatError || error{ NameTooLong, FileNotFound, SymLinkLoop, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, + BadPathName, }; /// Similar to `fstat`, but returns stat of a resource pointed to by `pathname` /// which is relative to `dirfd` handle. /// On WASI, `pathname` should be encoded as valid UTF-8. /// On other platforms, `pathname` is an opaque sequence of bytes with no particular encoding. -/// See also `fstatatZ` and `std.os.fstatat_wasi`. +/// See also `fstatatZ`. pub fn fstatat(dirfd: fd_t, pathname: []const u8, flags: u32) FStatAtError!Stat { if (native_os == .wasi and !builtin.link_libc) { - const filestat = try std.os.fstatat_wasi(dirfd, pathname, .{ - .SYMLINK_FOLLOW = (flags & AT.SYMLINK_NOFOLLOW) == 0, - }); - return Stat.fromFilestat(filestat); + @compileError("use std.Io instead"); } else if (native_os == .windows) { @compileError("fstatat is not yet implemented on Windows"); } else { @@ -4573,10 +3949,7 @@ pub fn fstatat(dirfd: fd_t, pathname: []const u8, flags: u32) FStatAtError!Stat /// See also `fstatat`. pub fn fstatatZ(dirfd: fd_t, pathname: [*:0]const u8, flags: u32) FStatAtError!Stat { if (native_os == .wasi and !builtin.link_libc) { - const filestat = try std.os.fstatat_wasi(dirfd, mem.sliceTo(pathname, 0), .{ - .SYMLINK_FOLLOW = (flags & AT.SYMLINK_NOFOLLOW) == 0, - }); - return Stat.fromFilestat(filestat); + @compileError("use std.Io instead"); } const fstatat_sym = if (lfs64_abi) system.fstatat64 else system.fstatat; @@ -4593,10 +3966,7 @@ pub fn fstatatZ(dirfd: fd_t, pathname: [*:0]const u8, flags: u32) FStatAtError!S .LOOP => return error.SymLinkLoop, .NOENT => return error.FileNotFound, .NOTDIR => return error.FileNotFound, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -5069,32 +4439,29 @@ pub const AccessError = error{ NameTooLong, InputOutput, SystemResources, - BadPathName, FileBusy, SymLinkLoop, ReadOnlyFileSystem, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - /// Windows-only; file paths provided by the user must be valid WTF-8. + /// WASI: file paths must be valid UTF-8. + /// Windows: file paths provided by the user must be valid WTF-8. /// https://wtf-8.codeberg.page/ - InvalidWtf8, + BadPathName, + Canceled, } || UnexpectedError; /// check user's permissions for a file /// /// * On Windows, asserts `path` is valid [WTF-8](https://wtf-8.codeberg.page/). -/// * On WASI, invalid UTF-8 passed to `path` causes `error.InvalidUtf8`. +/// * On WASI, invalid UTF-8 passed to `path` causes `error.BadPathName`. /// * On other platforms, `path` is an opaque sequence of bytes with no particular encoding. /// /// On Windows, `mode` is ignored. This is a POSIX API that is only partially supported by /// Windows. See `fs` for the cross-platform file system API. pub fn access(path: []const u8, mode: u32) AccessError!void { if (native_os == .windows) { - const path_w = try windows.sliceToPrefixedFileW(null, path); - _ = try windows.GetFileAttributesW(path_w.span().ptr); - return; + @compileError("use std.Io instead"); } else if (native_os == .wasi and !builtin.link_libc) { - return faccessat(AT.FDCWD, path, mode, 0); + @compileError("wasi doesn't support absolute paths"); } const path_c = try toPosixPath(path); return accessZ(&path_c, mode); @@ -5103,9 +4470,7 @@ pub fn access(path: []const u8, mode: u32) AccessError!void { /// Same as `access` except `path` is null-terminated. pub fn accessZ(path: [*:0]const u8, mode: u32) AccessError!void { if (native_os == .windows) { - const path_w = try windows.cStrToPrefixedFileW(null, path); - _ = try windows.GetFileAttributesW(path_w.span().ptr); - return; + @compileError("use std.Io instead"); } else if (native_os == .wasi and !builtin.link_libc) { return access(mem.sliceTo(path, 0), mode); } @@ -5123,132 +4488,11 @@ pub fn accessZ(path: [*:0]const u8, mode: u32) AccessError!void { .FAULT => unreachable, .IO => return error.InputOutput, .NOMEM => return error.SystemResources, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } -/// Check user's permissions for a file, based on an open directory handle. -/// -/// * On Windows, asserts `path` is valid [WTF-8](https://wtf-8.codeberg.page/). -/// * On WASI, invalid UTF-8 passed to `path` causes `error.InvalidUtf8`. -/// * On other platforms, `path` is an opaque sequence of bytes with no particular encoding. -/// -/// On Windows, `mode` is ignored. This is a POSIX API that is only partially supported by -/// Windows. See `fs` for the cross-platform file system API. -pub fn faccessat(dirfd: fd_t, path: []const u8, mode: u32, flags: u32) AccessError!void { - if (native_os == .windows) { - const path_w = try windows.sliceToPrefixedFileW(dirfd, path); - return faccessatW(dirfd, path_w.span().ptr); - } else if (native_os == .wasi and !builtin.link_libc) { - const resolved: RelativePathWasi = .{ .dir_fd = dirfd, .relative_path = path }; - - const st = try std.os.fstatat_wasi(dirfd, path, .{ - .SYMLINK_FOLLOW = (flags & AT.SYMLINK_NOFOLLOW) == 0, - }); - - if (mode != F_OK) { - var directory: wasi.fdstat_t = undefined; - if (wasi.fd_fdstat_get(resolved.dir_fd, &directory) != .SUCCESS) { - return error.AccessDenied; - } - - var rights: wasi.rights_t = .{}; - if (mode & R_OK != 0) { - if (st.filetype == .DIRECTORY) { - rights.FD_READDIR = true; - } else { - rights.FD_READ = true; - } - } - if (mode & W_OK != 0) { - rights.FD_WRITE = true; - } - // No validation for X_OK - - // https://github.com/ziglang/zig/issues/18882 - const rights_int: u64 = @bitCast(rights); - const inheriting_int: u64 = @bitCast(directory.fs_rights_inheriting); - if ((rights_int & inheriting_int) != rights_int) { - return error.AccessDenied; - } - } - return; - } - const path_c = try toPosixPath(path); - return faccessatZ(dirfd, &path_c, mode, flags); -} - -/// Same as `faccessat` except the path parameter is null-terminated. -pub fn faccessatZ(dirfd: fd_t, path: [*:0]const u8, mode: u32, flags: u32) AccessError!void { - if (native_os == .windows) { - const path_w = try windows.cStrToPrefixedFileW(dirfd, path); - return faccessatW(dirfd, path_w.span().ptr); - } else if (native_os == .wasi and !builtin.link_libc) { - return faccessat(dirfd, mem.sliceTo(path, 0), mode, flags); - } - switch (errno(system.faccessat(dirfd, path, mode, flags))) { - .SUCCESS => return, - .ACCES => return error.AccessDenied, - .PERM => return error.PermissionDenied, - .ROFS => return error.ReadOnlyFileSystem, - .LOOP => return error.SymLinkLoop, - .TXTBSY => return error.FileBusy, - .NOTDIR => return error.FileNotFound, - .NOENT => return error.FileNotFound, - .NAMETOOLONG => return error.NameTooLong, - .INVAL => unreachable, - .FAULT => unreachable, - .IO => return error.InputOutput, - .NOMEM => return error.SystemResources, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), - else => |err| return unexpectedErrno(err), - } -} - -/// Same as `faccessat` except asserts the target is Windows and the path parameter -/// is NtDll-prefixed, null-terminated, WTF-16 encoded. -pub fn faccessatW(dirfd: fd_t, sub_path_w: [*:0]const u16) AccessError!void { - if (sub_path_w[0] == '.' and sub_path_w[1] == 0) { - return; - } - if (sub_path_w[0] == '.' and sub_path_w[1] == '.' and sub_path_w[2] == 0) { - return; - } - - const path_len_bytes = cast(u16, mem.sliceTo(sub_path_w, 0).len * 2) orelse return error.NameTooLong; - var nt_name = windows.UNICODE_STRING{ - .Length = path_len_bytes, - .MaximumLength = path_len_bytes, - .Buffer = @constCast(sub_path_w), - }; - var attr = windows.OBJECT_ATTRIBUTES{ - .Length = @sizeOf(windows.OBJECT_ATTRIBUTES), - .RootDirectory = if (fs.path.isAbsoluteWindowsW(sub_path_w)) null else dirfd, - .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. - .ObjectName = &nt_name, - .SecurityDescriptor = null, - .SecurityQualityOfService = null, - }; - var basic_info: windows.FILE_BASIC_INFORMATION = undefined; - switch (windows.ntdll.NtQueryAttributesFile(&attr, &basic_info)) { - .SUCCESS => return, - .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, - .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, - .OBJECT_NAME_INVALID => unreachable, - .INVALID_PARAMETER => unreachable, - .ACCESS_DENIED => return error.AccessDenied, - .OBJECT_PATH_SYNTAX_BAD => unreachable, - else => |rc| return windows.unexpectedStatus(rc), - } -} - pub const PipeError = error{ SystemFdQuotaExceeded, ProcessFdQuotaExceeded, @@ -5393,15 +4637,8 @@ pub fn gettimeofday(tv: ?*timeval, tz: ?*timezone) void { } } -pub const SeekError = error{ - Unseekable, +pub const SeekError = std.Io.File.SeekError; - /// In WASI, this error may occur when the file descriptor does - /// not hold the required rights to seek on it. - AccessDenied, -} || UnexpectedError; - -/// Repositions read/write file offset relative to the beginning. pub fn lseek_SET(fd: fd_t, offset: u64) SeekError!void { if (native_os == .linux and !builtin.link_libc and @sizeOf(usize) == 4) { var result: u64 = undefined; @@ -5645,16 +4882,15 @@ pub const RealPathError = error{ SystemResources, NoSpaceLeft, FileSystem, - BadPathName, DeviceBusy, ProcessNotFound, SharingViolation, PipeBusy, - /// Windows-only; file paths provided by the user must be valid WTF-8. + /// Windows: file paths provided by the user must be valid WTF-8. /// https://wtf-8.codeberg.page/ - InvalidWtf8, + BadPathName, /// On Windows, `\\server` or `\\server\share` was not found. NetworkNotFound, @@ -5671,6 +4907,8 @@ pub const RealPathError = error{ /// On Windows, the volume does not contain a recognized file system. File /// system drivers might not be loaded, or the volume may be corrupt. UnrecognizedVolume, + + Canceled, } || UnexpectedError; /// Return the canonicalized absolute pathname. @@ -5735,7 +4973,6 @@ pub fn realpathZ(pathname: [*:0]const u8, out_buffer: *[max_path_bytes]u8) RealP error.FileLocksNotSupported => unreachable, error.WouldBlock => unreachable, error.FileBusy => unreachable, // not asking for write permissions - error.InvalidUtf8 => unreachable, // WASI-only else => |e| return e, }; defer close(fd); @@ -5999,7 +5236,7 @@ pub fn sigemptyset() sigset_t { return system.sigemptyset(); } -pub fn sigaddset(set: *sigset_t, sig: u8) void { +pub fn sigaddset(set: *sigset_t, sig: SIG) void { if (builtin.link_libc) { switch (errno(system.sigaddset(set, sig))) { .SUCCESS => return, @@ -6009,7 +5246,7 @@ pub fn sigaddset(set: *sigset_t, sig: u8) void { system.sigaddset(set, sig); } -pub fn sigdelset(set: *sigset_t, sig: u8) void { +pub fn sigdelset(set: *sigset_t, sig: SIG) void { if (builtin.link_libc) { switch (errno(system.sigdelset(set, sig))) { .SUCCESS => return, @@ -6019,7 +5256,7 @@ pub fn sigdelset(set: *sigset_t, sig: u8) void { system.sigdelset(set, sig); } -pub fn sigismember(set: *const sigset_t, sig: u8) bool { +pub fn sigismember(set: *const sigset_t, sig: SIG) bool { if (builtin.link_libc) { const rc = system.sigismember(set, sig); switch (errno(rc)) { @@ -6031,7 +5268,7 @@ pub fn sigismember(set: *const sigset_t, sig: u8) bool { } /// Examine and change a signal action. -pub fn sigaction(sig: u8, noalias act: ?*const Sigaction, noalias oact: ?*Sigaction) void { +pub fn sigaction(sig: SIG, noalias act: ?*const Sigaction, noalias oact: ?*Sigaction) void { switch (errno(system.sigaction(sig, act, oact))) { .SUCCESS => return, // EINVAL means the signal is either invalid or some signal that cannot have its action @@ -6152,55 +5389,6 @@ pub fn uname() utsname { } } -pub fn res_mkquery( - op: u4, - dname: []const u8, - class: u8, - ty: u8, - data: []const u8, - newrr: ?[*]const u8, - buf: []u8, -) usize { - _ = data; - _ = newrr; - // This implementation is ported from musl libc. - // A more idiomatic "ziggy" implementation would be welcome. - var name = dname; - if (mem.endsWith(u8, name, ".")) name.len -= 1; - assert(name.len <= 253); - const n = 17 + name.len + @intFromBool(name.len != 0); - - // Construct query template - ID will be filled later - var q: [280]u8 = undefined; - @memset(q[0..n], 0); - q[2] = @as(u8, op) * 8 + 1; - q[5] = 1; - @memcpy(q[13..][0..name.len], name); - var i: usize = 13; - var j: usize = undefined; - while (q[i] != 0) : (i = j + 1) { - j = i; - while (q[j] != 0 and q[j] != '.') : (j += 1) {} - // TODO determine the circumstances for this and whether or - // not this should be an error. - if (j - i - 1 > 62) unreachable; - q[i - 1] = @intCast(j - i); - } - q[i + 1] = ty; - q[i + 3] = class; - - // Make a reasonably unpredictable id - const ts = clock_gettime(.REALTIME) catch unreachable; - const UInt = std.meta.Int(.unsigned, @bitSizeOf(@TypeOf(ts.nsec))); - const unsec: UInt = @bitCast(ts.nsec); - const id: u32 = @truncate(unsec + unsec / 65536); - q[0] = @truncate(id / 256); - q[1] = @truncate(id); - - @memcpy(buf[0..n], q[0..n]); - return n; -} - pub const SendError = error{ /// (For UNIX domain sockets, which are identified by pathname) Write permission is denied /// on the destination socket file, or search permission is denied for one of the @@ -6226,7 +5414,7 @@ pub const SendError = error{ /// The socket type requires that message be sent atomically, and the size of the message /// to be sent made this impossible. The message is not transmitted. - MessageTooBig, + MessageOversize, /// The output queue for a network interface was full. This generally indicates that the /// interface has stopped sending, but may be caused by transient congestion. (Normally, @@ -6245,7 +5433,7 @@ pub const SendError = error{ NetworkUnreachable, /// The local network interface used to reach the destination is down. - NetworkSubsystemFailed, + NetworkDown, /// The destination address is not listening. ConnectionRefused, @@ -6253,7 +5441,7 @@ pub const SendError = error{ pub const SendMsgError = SendError || error{ /// The passed address didn't have the correct address family in its sa_family field. - AddressFamilyNotSupported, + AddressFamilyUnsupported, /// Returned when socket is AF.UNIX and the given path has a symlink loop. SymLinkLoop, @@ -6266,8 +5454,8 @@ pub const SendMsgError = SendError || error{ NotDir, /// The socket is not connected (connection-oriented sockets only). - SocketNotConnected, - AddressNotAvailable, + SocketUnconnected, + AddressUnavailable, }; pub fn sendmsg( @@ -6282,25 +5470,25 @@ pub fn sendmsg( if (native_os == .windows) { if (rc == windows.ws2_32.SOCKET_ERROR) { switch (windows.ws2_32.WSAGetLastError()) { - .WSAEACCES => return error.AccessDenied, - .WSAEADDRNOTAVAIL => return error.AddressNotAvailable, - .WSAECONNRESET => return error.ConnectionResetByPeer, - .WSAEMSGSIZE => return error.MessageTooBig, - .WSAENOBUFS => return error.SystemResources, - .WSAENOTSOCK => return error.FileDescriptorNotASocket, - .WSAEAFNOSUPPORT => return error.AddressFamilyNotSupported, - .WSAEDESTADDRREQ => unreachable, // A destination address is required. - .WSAEFAULT => unreachable, // The lpBuffers, lpTo, lpOverlapped, lpNumberOfBytesSent, or lpCompletionRoutine parameters are not part of the user address space, or the lpTo parameter is too small. - .WSAEHOSTUNREACH => return error.NetworkUnreachable, - // TODO: WSAEINPROGRESS, WSAEINTR - .WSAEINVAL => unreachable, - .WSAENETDOWN => return error.NetworkSubsystemFailed, - .WSAENETRESET => return error.ConnectionResetByPeer, - .WSAENETUNREACH => return error.NetworkUnreachable, - .WSAENOTCONN => return error.SocketNotConnected, - .WSAESHUTDOWN => unreachable, // The socket has been shut down; it is not possible to WSASendTo on a socket after shutdown has been invoked with how set to SD_SEND or SD_BOTH. - .WSAEWOULDBLOCK => return error.WouldBlock, - .WSANOTINITIALISED => unreachable, // A successful WSAStartup call must occur before using this function. + .EACCES => return error.AccessDenied, + .EADDRNOTAVAIL => return error.AddressUnavailable, + .ECONNRESET => return error.ConnectionResetByPeer, + .EMSGSIZE => return error.MessageOversize, + .ENOBUFS => return error.SystemResources, + .ENOTSOCK => return error.FileDescriptorNotASocket, + .EAFNOSUPPORT => return error.AddressFamilyUnsupported, + .EDESTADDRREQ => unreachable, // A destination address is required. + .EFAULT => unreachable, // The lpBuffers, lpTo, lpOverlapped, lpNumberOfBytesSent, or lpCompletionRoutine parameters are not part of the user address space, or the lpTo parameter is too small. + .EHOSTUNREACH => return error.NetworkUnreachable, + // TODO: EINPROGRESS, EINTR + .EINVAL => unreachable, + .ENETDOWN => return error.NetworkDown, + .ENETRESET => return error.ConnectionResetByPeer, + .ENETUNREACH => return error.NetworkUnreachable, + .ENOTCONN => return error.SocketUnconnected, + .ESHUTDOWN => unreachable, // The socket has been shut down; it is not possible to WSASendTo on a socket after shutdown has been invoked with how set to SD_SEND or SD_BOTH. + .EWOULDBLOCK => return error.WouldBlock, + .NOTINITIALISED => unreachable, // A successful WSAStartup call must occur before using this function. else => |err| return windows.unexpectedWSAError(err), } } else { @@ -6320,21 +5508,21 @@ pub fn sendmsg( .INTR => continue, .INVAL => unreachable, // Invalid argument passed. .ISCONN => unreachable, // connection-mode socket was connected already but a recipient was specified - .MSGSIZE => return error.MessageTooBig, + .MSGSIZE => return error.MessageOversize, .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, .NOTSOCK => unreachable, // The file descriptor sockfd does not refer to a socket. .OPNOTSUPP => unreachable, // Some bit in the flags argument is inappropriate for the socket type. .PIPE => return error.BrokenPipe, - .AFNOSUPPORT => return error.AddressFamilyNotSupported, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, .LOOP => return error.SymLinkLoop, .NAMETOOLONG => return error.NameTooLong, .NOENT => return error.FileNotFound, .NOTDIR => return error.NotDir, .HOSTUNREACH => return error.NetworkUnreachable, .NETUNREACH => return error.NetworkUnreachable, - .NOTCONN => return error.SocketNotConnected, - .NETDOWN => return error.NetworkSubsystemFailed, + .NOTCONN => return error.SocketUnconnected, + .NETDOWN => return error.NetworkDown, else => |err| return unexpectedErrno(err), } } @@ -6365,7 +5553,7 @@ pub const SendToError = SendMsgError || error{ /// Otherwise, the address of the target is given by `dest_addr` with `addrlen` specifying its size. /// /// If the message is too long to pass atomically through the underlying protocol, -/// `SendError.MessageTooBig` is returned, and the message is not transmitted. +/// `SendError.MessageOversize` is returned, and the message is not transmitted. /// /// There is no indication of failure to deliver. /// @@ -6385,25 +5573,25 @@ pub fn sendto( if (native_os == .windows) { switch (windows.sendto(sockfd, buf.ptr, buf.len, flags, dest_addr, addrlen)) { windows.ws2_32.SOCKET_ERROR => switch (windows.ws2_32.WSAGetLastError()) { - .WSAEACCES => return error.AccessDenied, - .WSAEADDRNOTAVAIL => return error.AddressNotAvailable, - .WSAECONNRESET => return error.ConnectionResetByPeer, - .WSAEMSGSIZE => return error.MessageTooBig, - .WSAENOBUFS => return error.SystemResources, - .WSAENOTSOCK => return error.FileDescriptorNotASocket, - .WSAEAFNOSUPPORT => return error.AddressFamilyNotSupported, - .WSAEDESTADDRREQ => unreachable, // A destination address is required. - .WSAEFAULT => unreachable, // The lpBuffers, lpTo, lpOverlapped, lpNumberOfBytesSent, or lpCompletionRoutine parameters are not part of the user address space, or the lpTo parameter is too small. - .WSAEHOSTUNREACH => return error.NetworkUnreachable, - // TODO: WSAEINPROGRESS, WSAEINTR - .WSAEINVAL => unreachable, - .WSAENETDOWN => return error.NetworkSubsystemFailed, - .WSAENETRESET => return error.ConnectionResetByPeer, - .WSAENETUNREACH => return error.NetworkUnreachable, - .WSAENOTCONN => return error.SocketNotConnected, - .WSAESHUTDOWN => unreachable, // The socket has been shut down; it is not possible to WSASendTo on a socket after shutdown has been invoked with how set to SD_SEND or SD_BOTH. - .WSAEWOULDBLOCK => return error.WouldBlock, - .WSANOTINITIALISED => unreachable, // A successful WSAStartup call must occur before using this function. + .EACCES => return error.AccessDenied, + .EADDRNOTAVAIL => return error.AddressUnavailable, + .ECONNRESET => return error.ConnectionResetByPeer, + .EMSGSIZE => return error.MessageOversize, + .ENOBUFS => return error.SystemResources, + .ENOTSOCK => return error.FileDescriptorNotASocket, + .EAFNOSUPPORT => return error.AddressFamilyUnsupported, + .EDESTADDRREQ => unreachable, // A destination address is required. + .EFAULT => unreachable, // The lpBuffers, lpTo, lpOverlapped, lpNumberOfBytesSent, or lpCompletionRoutine parameters are not part of the user address space, or the lpTo parameter is too small. + .EHOSTUNREACH => return error.NetworkUnreachable, + // TODO: EINPROGRESS, EINTR + .EINVAL => unreachable, + .ENETDOWN => return error.NetworkDown, + .ENETRESET => return error.ConnectionResetByPeer, + .ENETUNREACH => return error.NetworkUnreachable, + .ENOTCONN => return error.SocketUnconnected, + .ESHUTDOWN => unreachable, // The socket has been shut down; it is not possible to WSASendTo on a socket after shutdown has been invoked with how set to SD_SEND or SD_BOTH. + .EWOULDBLOCK => return error.WouldBlock, + .NOTINITIALISED => unreachable, // A successful WSAStartup call must occur before using this function. else => |err| return windows.unexpectedWSAError(err), }, else => |rc| return @intCast(rc), @@ -6425,21 +5613,21 @@ pub fn sendto( .INTR => continue, .INVAL => return error.UnreachableAddress, .ISCONN => unreachable, // connection-mode socket was connected already but a recipient was specified - .MSGSIZE => return error.MessageTooBig, + .MSGSIZE => return error.MessageOversize, .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, .NOTSOCK => unreachable, // The file descriptor sockfd does not refer to a socket. .OPNOTSUPP => unreachable, // Some bit in the flags argument is inappropriate for the socket type. .PIPE => return error.BrokenPipe, - .AFNOSUPPORT => return error.AddressFamilyNotSupported, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, .LOOP => return error.SymLinkLoop, .NAMETOOLONG => return error.NameTooLong, .NOENT => return error.FileNotFound, .NOTDIR => return error.NotDir, .HOSTUNREACH => return error.NetworkUnreachable, .NETUNREACH => return error.NetworkUnreachable, - .NOTCONN => return error.SocketNotConnected, - .NETDOWN => return error.NetworkSubsystemFailed, + .NOTCONN => return error.SocketUnconnected, + .NETDOWN => return error.NetworkDown, else => |err| return unexpectedErrno(err), } } @@ -6471,14 +5659,14 @@ pub fn send( flags: u32, ) SendError!usize { return sendto(sockfd, buf, flags, null, 0) catch |err| switch (err) { - error.AddressFamilyNotSupported => unreachable, + error.AddressFamilyUnsupported => unreachable, error.SymLinkLoop => unreachable, error.NameTooLong => unreachable, error.FileNotFound => unreachable, error.NotDir => unreachable, error.NetworkUnreachable => unreachable, - error.AddressNotAvailable => unreachable, - error.SocketNotConnected => unreachable, + error.AddressUnavailable => unreachable, + error.SocketUnconnected => unreachable, error.UnreachableAddress => unreachable, else => |e| return e, }; @@ -6578,7 +5766,7 @@ pub fn copy_file_range(fd_in: fd_t, off_in: u64, fd_out: fd_t, off_out: u64, len pub const PollError = error{ /// The network subsystem has failed. - NetworkSubsystemFailed, + NetworkDown, /// The kernel had no space to allocate file descriptor tables. SystemResources, @@ -6588,9 +5776,9 @@ pub fn poll(fds: []pollfd, timeout: i32) PollError!usize { if (native_os == .windows) { switch (windows.poll(fds.ptr, @intCast(fds.len), timeout)) { windows.ws2_32.SOCKET_ERROR => switch (windows.ws2_32.WSAGetLastError()) { - .WSANOTINITIALISED => unreachable, - .WSAENETDOWN => return error.NetworkSubsystemFailed, - .WSAENOBUFS => return error.SystemResources, + .NOTINITIALISED => unreachable, + .ENETDOWN => return error.NetworkDown, + .ENOBUFS => return error.SystemResources, // TODO: handle more errors else => |err| return windows.unexpectedWSAError(err), }, @@ -6652,19 +5840,19 @@ pub const RecvFromError = error{ SystemResources, ConnectionResetByPeer, - ConnectionTimedOut, + Timeout, /// The socket has not been bound. SocketNotBound, /// The UDP message was too big for the buffer and part of it has been discarded - MessageTooBig, + MessageOversize, /// The network subsystem has failed. - NetworkSubsystemFailed, + NetworkDown, /// The socket is not connected (connection-oriented sockets only). - SocketNotConnected, + SocketUnconnected, /// The other end closed the socket unexpectedly or a read is executed on a shut down socket BrokenPipe, @@ -6688,14 +5876,14 @@ pub fn recvfrom( if (native_os == .windows) { if (rc == windows.ws2_32.SOCKET_ERROR) { switch (windows.ws2_32.WSAGetLastError()) { - .WSANOTINITIALISED => unreachable, - .WSAECONNRESET => return error.ConnectionResetByPeer, - .WSAEINVAL => return error.SocketNotBound, - .WSAEMSGSIZE => return error.MessageTooBig, - .WSAENETDOWN => return error.NetworkSubsystemFailed, - .WSAENOTCONN => return error.SocketNotConnected, - .WSAEWOULDBLOCK => return error.WouldBlock, - .WSAETIMEDOUT => return error.ConnectionTimedOut, + .NOTINITIALISED => unreachable, + .ECONNRESET => return error.ConnectionResetByPeer, + .EINVAL => return error.SocketNotBound, + .EMSGSIZE => return error.MessageOversize, + .ENETDOWN => return error.NetworkDown, + .ENOTCONN => return error.SocketUnconnected, + .EWOULDBLOCK => return error.WouldBlock, + .ETIMEDOUT => return error.Timeout, // TODO: handle more errors else => |err| return windows.unexpectedWSAError(err), } @@ -6708,14 +5896,14 @@ pub fn recvfrom( .BADF => unreachable, // always a race condition .FAULT => unreachable, .INVAL => unreachable, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .NOTSOCK => unreachable, .INTR => continue, .AGAIN => return error.WouldBlock, .NOMEM => return error.SystemResources, .CONNREFUSED => return error.ConnectionRefused, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, .PIPE => return error.BrokenPipe, else => |err| return unexpectedErrno(err), } @@ -6760,68 +5948,18 @@ pub fn recvmsg( .ISCONN => unreachable, // connection-mode socket was connected already but a recipient was specified .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .NOTSOCK => unreachable, // The file descriptor sockfd does not refer to a socket. - .MSGSIZE => return error.MessageTooBig, + .MSGSIZE => return error.MessageOversize, .PIPE => return error.BrokenPipe, .OPNOTSUPP => unreachable, // Some bit in the flags argument is inappropriate for the socket type. .CONNRESET => return error.ConnectionResetByPeer, - .NETDOWN => return error.NetworkSubsystemFailed, + .NETDOWN => return error.NetworkDown, else => |err| return unexpectedErrno(err), } } } -pub const DnExpandError = error{InvalidDnsPacket}; - -pub fn dn_expand( - msg: []const u8, - comp_dn: []const u8, - exp_dn: []u8, -) DnExpandError!usize { - // This implementation is ported from musl libc. - // A more idiomatic "ziggy" implementation would be welcome. - var p = comp_dn.ptr; - var len: usize = maxInt(usize); - const end = msg.ptr + msg.len; - if (p == end or exp_dn.len == 0) return error.InvalidDnsPacket; - var dest = exp_dn.ptr; - const dend = dest + @min(exp_dn.len, 254); - // detect reference loop using an iteration counter - var i: usize = 0; - while (i < msg.len) : (i += 2) { - // loop invariants: p= msg.len) return error.InvalidDnsPacket; - p = msg.ptr + j; - } else if (p[0] != 0) { - if (dest != exp_dn.ptr) { - dest[0] = '.'; - dest += 1; - } - var j = p[0]; - p += 1; - if (j >= @intFromPtr(end) - @intFromPtr(p) or j >= @intFromPtr(dend) - @intFromPtr(dest)) { - return error.InvalidDnsPacket; - } - while (j != 0) { - j -= 1; - dest[0] = p[0]; - dest += 1; - p += 1; - } - } else { - dest[0] = 0; - if (len == maxInt(usize)) len = @intFromPtr(p) + 1 - @intFromPtr(comp_dn.ptr); - return len; - } - } - return error.InvalidDnsPacket; -} - pub const SetSockOptError = error{ /// The socket is already connected, and a specified option cannot be set while the socket is connected. AlreadyConnected, @@ -6839,7 +5977,7 @@ pub const SetSockOptError = error{ PermissionDenied, OperationNotSupported, - NetworkSubsystemFailed, + NetworkDown, FileDescriptorNotASocket, SocketNotBound, NoDevice, @@ -6851,11 +5989,11 @@ pub fn setsockopt(fd: socket_t, level: i32, optname: u32, opt: []const u8) SetSo const rc = windows.ws2_32.setsockopt(fd, level, @intCast(optname), opt.ptr, @intCast(opt.len)); if (rc == windows.ws2_32.SOCKET_ERROR) { switch (windows.ws2_32.WSAGetLastError()) { - .WSANOTINITIALISED => unreachable, - .WSAENETDOWN => return error.NetworkSubsystemFailed, - .WSAEFAULT => unreachable, - .WSAENOTSOCK => return error.FileDescriptorNotASocket, - .WSAEINVAL => return error.SocketNotBound, + .NOTINITIALISED => unreachable, + .ENETDOWN => return error.NetworkDown, + .EFAULT => unreachable, + .ENOTSOCK => return error.FileDescriptorNotASocket, + .EINVAL => return error.SocketNotBound, else => |err| return windows.unexpectedWSAError(err), } } @@ -7572,7 +6710,7 @@ pub fn ioctl_SIOCGIFINDEX(fd: fd_t, ifr: *ifreq) IoCtl_SIOCGIFINDEX_Error!void { } } -const lfs64_abi = native_os == .linux and builtin.link_libc and (builtin.abi.isGnu() or builtin.abi.isAndroid()); +pub const lfs64_abi = native_os == .linux and builtin.link_libc and (builtin.abi.isGnu() or builtin.abi.isAndroid()); /// Whether or not `error.Unexpected` will print its value and a stack trace. /// @@ -7584,17 +6722,7 @@ pub const unexpected_error_tracing = builtin.mode == .Debug and switch (builtin. else => false, }; -pub const UnexpectedError = error{ - /// The Operating System returned an undocumented error code. - /// - /// This error is in theory not possible, but it would be better - /// to handle this error than to invoke undefined behavior. - /// - /// When this error code is observed, it usually means the Zig Standard - /// Library needs a small patch to add the error code to the error set for - /// the respective function. - Unexpected, -}; +pub const UnexpectedError = std.Io.UnexpectedError; /// Call this when you made a syscall or something that sets errno /// and you get an unexpected error. diff --git a/lib/std/posix/test.zig b/lib/std/posix/test.zig index 50ffd7998f..946dd90027 100644 --- a/lib/std/posix/test.zig +++ b/lib/std/posix/test.zig @@ -109,64 +109,6 @@ test "open smoke test" { } } -test "openat smoke test" { - if (native_os == .windows) return error.SkipZigTest; - - // TODO verify file attributes using `fstatat` - - var tmp = tmpDir(.{}); - defer tmp.cleanup(); - - var fd: posix.fd_t = undefined; - const mode: posix.mode_t = if (native_os == .windows) 0 else 0o666; - - // Create some file using `openat`. - fd = try posix.openat(tmp.dir.fd, "some_file", CommonOpenFlags.lower(.{ - .ACCMODE = .RDWR, - .CREAT = true, - .EXCL = true, - }), mode); - posix.close(fd); - - // Try this again with the same flags. This op should fail with error.PathAlreadyExists. - try expectError(error.PathAlreadyExists, posix.openat(tmp.dir.fd, "some_file", CommonOpenFlags.lower(.{ - .ACCMODE = .RDWR, - .CREAT = true, - .EXCL = true, - }), mode)); - - // Try opening without `EXCL` flag. - fd = try posix.openat(tmp.dir.fd, "some_file", CommonOpenFlags.lower(.{ - .ACCMODE = .RDWR, - .CREAT = true, - }), mode); - posix.close(fd); - - // Try opening as a directory which should fail. - try expectError(error.NotDir, posix.openat(tmp.dir.fd, "some_file", CommonOpenFlags.lower(.{ - .ACCMODE = .RDWR, - .DIRECTORY = true, - }), mode)); - - // Create some directory - try posix.mkdirat(tmp.dir.fd, "some_dir", mode); - - // Open dir using `open` - fd = try posix.openat(tmp.dir.fd, "some_dir", CommonOpenFlags.lower(.{ - .ACCMODE = .RDONLY, - .DIRECTORY = true, - }), mode); - posix.close(fd); - - // Try opening as file which should fail (skip on wasi+libc due to - // https://github.com/bytecodealliance/wasmtime/issues/9054) - if (native_os != .wasi or !builtin.link_libc) { - try expectError(error.IsDir, posix.openat(tmp.dir.fd, "some_dir", CommonOpenFlags.lower(.{ - .ACCMODE = .RDWR, - }), mode)); - } -} - test "readlink on Windows" { if (native_os != .windows) return error.SkipZigTest; @@ -226,49 +168,6 @@ test "linkat with different directories" { } } -test "fstatat" { - if ((builtin.cpu.arch == .riscv32 or builtin.cpu.arch.isLoongArch()) and builtin.os.tag == .linux and !builtin.link_libc) return error.SkipZigTest; // No `fstatat()`. - // enable when `fstat` and `fstatat` are implemented on Windows - if (native_os == .windows) return error.SkipZigTest; - - var tmp = tmpDir(.{}); - defer tmp.cleanup(); - - // create dummy file - const contents = "nonsense"; - try tmp.dir.writeFile(.{ .sub_path = "file.txt", .data = contents }); - - // fetch file's info on the opened fd directly - const file = try tmp.dir.openFile("file.txt", .{}); - const stat = try posix.fstat(file.handle); - defer file.close(); - - // now repeat but using `fstatat` instead - const statat = try posix.fstatat(tmp.dir.fd, "file.txt", posix.AT.SYMLINK_NOFOLLOW); - - try expectEqual(stat.dev, statat.dev); - try expectEqual(stat.ino, statat.ino); - try expectEqual(stat.nlink, statat.nlink); - try expectEqual(stat.mode, statat.mode); - try expectEqual(stat.uid, statat.uid); - try expectEqual(stat.gid, statat.gid); - try expectEqual(stat.rdev, statat.rdev); - try expectEqual(stat.size, statat.size); - try expectEqual(stat.blksize, statat.blksize); - - // The stat.blocks/statat.blocks count is managed by the filesystem and may - // change if the file is stored in a journal or "inline". - // try expectEqual(stat.blocks, statat.blocks); - - // s390x-linux does not have nanosecond precision for fstat(), but it does for - // fstatat(). As a result, comparing the timestamps isn't worth the effort - if (!(builtin.cpu.arch == .s390x and builtin.os.tag == .linux)) { - try expectEqual(stat.atime(), statat.atime()); - try expectEqual(stat.mtime(), statat.mtime()); - try expectEqual(stat.ctime(), statat.ctime()); - } -} - test "readlinkat" { var tmp = tmpDir(.{}); defer tmp.cleanup(); @@ -621,25 +520,6 @@ test "getrlimit and setrlimit" { } } -test "shutdown socket" { - if (native_os == .wasi) - return error.SkipZigTest; - if (native_os == .windows) { - _ = try std.os.windows.WSAStartup(2, 2); - } - defer { - if (native_os == .windows) { - std.os.windows.WSACleanup() catch unreachable; - } - } - const sock = try posix.socket(posix.AF.INET, posix.SOCK.STREAM, 0); - posix.shutdown(sock, .both) catch |err| switch (err) { - error.SocketNotConnected => {}, - else => |e| return e, - }; - std.net.Stream.close(.{ .handle = sock }); -} - test "sigrtmin/max" { if (native_os == .wasi or native_os == .windows or native_os == .macos) { return error.SkipZigTest; @@ -656,14 +536,15 @@ test "sigset empty/full" { var set: posix.sigset_t = posix.sigemptyset(); for (1..posix.NSIG) |i| { - try expectEqual(false, posix.sigismember(&set, @truncate(i))); + const sig = std.meta.intToEnum(posix.SIG, i) catch continue; + try expectEqual(false, posix.sigismember(&set, sig)); } // The C library can reserve some (unnamed) signals, so can't check the full // NSIG set is defined, but just test a couple: set = posix.sigfillset(); - try expectEqual(true, posix.sigismember(&set, @truncate(posix.SIG.CHLD))); - try expectEqual(true, posix.sigismember(&set, @truncate(posix.SIG.INT))); + try expectEqual(true, posix.sigismember(&set, .CHLD)); + try expectEqual(true, posix.sigismember(&set, .INT)); } // Some signals (i.e., 32 - 34 on glibc/musl) are not allowed to be added to a @@ -684,25 +565,30 @@ test "sigset add/del" { // See that none are set, then set each one, see that they're all set, then // remove them all, and then see that none are set. for (1..posix.NSIG) |i| { - try expectEqual(false, posix.sigismember(&sigset, @truncate(i))); + const sig = std.meta.intToEnum(posix.SIG, i) catch continue; + try expectEqual(false, posix.sigismember(&sigset, sig)); } for (1..posix.NSIG) |i| { if (!reserved_signo(i)) { - posix.sigaddset(&sigset, @truncate(i)); + const sig = std.meta.intToEnum(posix.SIG, i) catch continue; + posix.sigaddset(&sigset, sig); } } for (1..posix.NSIG) |i| { if (!reserved_signo(i)) { - try expectEqual(true, posix.sigismember(&sigset, @truncate(i))); + const sig = std.meta.intToEnum(posix.SIG, i) catch continue; + try expectEqual(true, posix.sigismember(&sigset, sig)); } } for (1..posix.NSIG) |i| { if (!reserved_signo(i)) { - posix.sigdelset(&sigset, @truncate(i)); + const sig = std.meta.intToEnum(posix.SIG, i) catch continue; + posix.sigdelset(&sigset, sig); } } for (1..posix.NSIG) |i| { - try expectEqual(false, posix.sigismember(&sigset, @truncate(i))); + const sig = std.meta.intToEnum(posix.SIG, i) catch continue; + try expectEqual(false, posix.sigismember(&sigset, sig)); } } @@ -731,11 +617,8 @@ test "dup & dup2" { try dup2ed.writeAll("dup2"); } - var file = try tmp.dir.openFile("os_dup_test", .{}); - defer file.close(); - - var buf: [7]u8 = undefined; - try testing.expectEqualStrings("dupdup2", buf[0..try file.readAll(&buf)]); + var buffer: [8]u8 = undefined; + try testing.expectEqualStrings("dupdup2", try tmp.dir.readFile("os_dup_test", &buffer)); } test "writev longer than IOV_MAX" { @@ -966,20 +849,6 @@ test "isatty" { try expectEqual(posix.isatty(file.handle), false); } -test "read with empty buffer" { - var tmp = tmpDir(.{}); - defer tmp.cleanup(); - - var file = try tmp.dir.createFile("read_empty", .{ .read = true }); - defer file.close(); - - const bytes = try a.alloc(u8, 0); - defer a.free(bytes); - - const rc = try posix.read(file.handle, bytes); - try expectEqual(rc, 0); -} - test "pread with empty buffer" { var tmp = tmpDir(.{}); defer tmp.cleanup(); diff --git a/lib/std/process/Child.zig b/lib/std/process/Child.zig index 50157d52d9..c84c878972 100644 --- a/lib/std/process/Child.zig +++ b/lib/std/process/Child.zig @@ -1,5 +1,9 @@ -const std = @import("../std.zig"); +const ChildProcess = @This(); + const builtin = @import("builtin"); +const native_os = builtin.os.tag; + +const std = @import("../std.zig"); const unicode = std.unicode; const fs = std.fs; const process = std.process; @@ -11,9 +15,7 @@ const mem = std.mem; const EnvMap = std.process.EnvMap; const maxInt = std.math.maxInt; const assert = std.debug.assert; -const native_os = builtin.os.tag; const Allocator = std.mem.Allocator; -const ChildProcess = @This(); const ArrayList = std.ArrayList; pub const Id = switch (native_os) { @@ -317,16 +319,23 @@ pub fn waitForSpawn(self: *ChildProcess) SpawnError!void { const err_pipe = self.err_pipe orelse return; self.err_pipe = null; - // Wait for the child to report any errors in or before `execvpe`. - if (readIntFd(err_pipe)) |child_err_int| { - posix.close(err_pipe); + const report = readIntFd(err_pipe); + posix.close(err_pipe); + if (report) |child_err_int| { const child_err: SpawnError = @errorCast(@errorFromInt(child_err_int)); self.term = child_err; return child_err; - } else |_| { - // Write end closed by CLOEXEC at the time of the `execvpe` call, indicating success! - posix.close(err_pipe); + } else |read_err| switch (read_err) { + error.EndOfStream => { + // Write end closed by CLOEXEC at the time of the `execvpe` call, + // indicating success. + }, + else => { + // Problem reading the error from the error reporting pipe. We + // don't know if the child is alive or dead. Better to assume it is + // alive so the resource does not risk being leaked. + }, } } @@ -563,6 +572,10 @@ fn spawnPosix(self: *ChildProcess) SpawnError!void { error.BadPathName => unreachable, // Windows-only error.WouldBlock => unreachable, error.NetworkNotFound => unreachable, // Windows-only + error.Canceled => unreachable, // temporarily in the posix error set + error.SharingViolation => unreachable, // Windows-only + error.PipeBusy => unreachable, // not a pipe + error.AntivirusInterference => unreachable, // Windows-only else => |e| return e, } else @@ -1014,8 +1027,14 @@ fn writeIntFd(fd: i32, value: ErrInt) !void { fn readIntFd(fd: i32) !ErrInt { var buffer: [8]u8 = undefined; - var fr: std.fs.File.Reader = .initStreaming(.{ .handle = fd }, &buffer); - return @intCast(fr.interface.takeInt(u64, .little) catch return error.SystemResources); + var i: usize = 0; + while (i < buffer.len) { + const n = try std.posix.read(fd, buffer[i..]); + if (n == 0) return error.EndOfStream; + i += n; + } + const int = mem.readInt(u64, &buffer, .little); + return @intCast(int); } const ErrInt = std.meta.Int(.unsigned, @sizeOf(anyerror) * 8); @@ -1065,16 +1084,24 @@ fn windowsCreateProcessPathExt( // or a version with a supported PATHEXT appended. We then try calling CreateProcessW // with the found versions in the appropriate order. + // In the future, child process execution needs to move to Io implementation. + // Under those conditions, here we will have access to lower level directory + // opening function knowing which implementation we are in. Here, we imitate + // that scenario. + var threaded: std.Io.Threaded = .init_single_threaded; + const io = threaded.ioBasic(); + var dir = dir: { // needs to be null-terminated try dir_buf.append(allocator, 0); defer dir_buf.shrinkRetainingCapacity(dir_path_len); const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; const prefixed_path = try windows.wToPrefixedFileW(null, dir_path_z); - break :dir fs.cwd().openDirW(prefixed_path.span().ptr, .{ .iterate = true }) catch - return error.FileNotFound; + break :dir threaded.dirOpenDirWindows(.cwd(), prefixed_path.span(), .{ + .iterate = true, + }) catch return error.FileNotFound; }; - defer dir.close(); + defer dir.close(io); // Add wildcard and null-terminator try app_buf.append(allocator, '*'); @@ -1108,7 +1135,7 @@ fn windowsCreateProcessPathExt( .Buffer = @constCast(app_name_wildcard.ptr), }; const rc = windows.ntdll.NtQueryDirectoryFile( - dir.fd, + dir.handle, null, null, null, diff --git a/lib/std/start.zig b/lib/std/start.zig index 69207e690d..a563912bbc 100644 --- a/lib/std/start.zig +++ b/lib/std/start.zig @@ -652,7 +652,6 @@ inline fn callMainWithArgs(argc: usize, argv: [*][*:0]u8, envp: [][*:0]u8) u8 { std.os.environ = envp; std.debug.maybeEnableSegfaultHandler(); - maybeIgnoreSigpipe(); return callMain(); } @@ -756,39 +755,3 @@ pub fn call_wWinMain() std.os.windows.INT { // second parameter hPrevInstance, MSDN: "This parameter is always NULL" return root.wWinMain(hInstance, null, lpCmdLine, nCmdShow); } - -fn maybeIgnoreSigpipe() void { - const have_sigpipe_support = switch (builtin.os.tag) { - .linux, - .plan9, - .illumos, - .netbsd, - .openbsd, - .haiku, - .macos, - .ios, - .watchos, - .tvos, - .visionos, - .dragonfly, - .freebsd, - .serenity, - => true, - - else => false, - }; - - if (have_sigpipe_support and !std.options.keep_sigpipe) { - const posix = std.posix; - const act: posix.Sigaction = .{ - // Set handler to a noop function instead of `SIG.IGN` to prevent - // leaking signal disposition to a child process. - .handler = .{ .handler = noopSigHandler }, - .mask = posix.sigemptyset(), - .flags = 0, - }; - posix.sigaction(posix.SIG.PIPE, &act, null); - } -} - -fn noopSigHandler(_: i32) callconv(.c) void {} diff --git a/lib/std/std.zig b/lib/std/std.zig index 4e68d1d611..1b8142ce4c 100644 --- a/lib/std/std.zig +++ b/lib/std/std.zig @@ -85,7 +85,6 @@ pub const macho = @import("macho.zig"); pub const math = @import("math.zig"); pub const mem = @import("mem.zig"); pub const meta = @import("meta.zig"); -pub const net = @import("net.zig"); pub const os = @import("os.zig"); pub const once = @import("once.zig").once; pub const pdb = @import("pdb.zig"); @@ -145,19 +144,6 @@ pub const Options = struct { crypto_fork_safety: bool = true, - /// By default Zig disables SIGPIPE by setting a "no-op" handler for it. Set this option - /// to `true` to prevent that. - /// - /// Note that we use a "no-op" handler instead of SIG_IGN because it will not be inherited by - /// any child process. - /// - /// SIGPIPE is triggered when a process attempts to write to a broken pipe. By default, SIGPIPE - /// will terminate the process instead of exiting. It doesn't trigger the panic handler so in many - /// cases it's unclear why the process was terminated. By capturing SIGPIPE instead, functions that - /// write to broken pipes will return the EPIPE error (error.BrokenPipe) and the program can handle - /// it like any other error. - keep_sigpipe: bool = false, - /// By default, std.http.Client will support HTTPS connections. Set this option to `true` to /// disable TLS support. /// diff --git a/lib/std/tar.zig b/lib/std/tar.zig index e397677cf3..12f9c837a2 100644 --- a/lib/std/tar.zig +++ b/lib/std/tar.zig @@ -977,7 +977,7 @@ test pipeToFileSystem { const data = @embedFile("tar/testdata/example.tar"); var reader: std.Io.Reader = .fixed(data); - var tmp = testing.tmpDir(.{ .no_follow = true }); + var tmp = testing.tmpDir(.{ .follow_symlinks = false }); defer tmp.cleanup(); const dir = tmp.dir; @@ -1010,7 +1010,7 @@ test "pipeToFileSystem root_dir" { // with strip_components = 1 { - var tmp = testing.tmpDir(.{ .no_follow = true }); + var tmp = testing.tmpDir(.{ .follow_symlinks = false }); defer tmp.cleanup(); var diagnostics: Diagnostics = .{ .allocator = testing.allocator }; defer diagnostics.deinit(); @@ -1032,7 +1032,7 @@ test "pipeToFileSystem root_dir" { // with strip_components = 0 { reader = .fixed(data); - var tmp = testing.tmpDir(.{ .no_follow = true }); + var tmp = testing.tmpDir(.{ .follow_symlinks = false }); defer tmp.cleanup(); var diagnostics: Diagnostics = .{ .allocator = testing.allocator }; defer diagnostics.deinit(); @@ -1084,7 +1084,7 @@ test "pipeToFileSystem strip_components" { const data = @embedFile("tar/testdata/example.tar"); var reader: std.Io.Reader = .fixed(data); - var tmp = testing.tmpDir(.{ .no_follow = true }); + var tmp = testing.tmpDir(.{ .follow_symlinks = false }); defer tmp.cleanup(); var diagnostics: Diagnostics = .{ .allocator = testing.allocator }; defer diagnostics.deinit(); @@ -1145,7 +1145,7 @@ test "executable bit" { for ([_]PipeOptions.ModeMode{ .ignore, .executable_bit_only }) |opt| { var reader: std.Io.Reader = .fixed(data); - var tmp = testing.tmpDir(.{ .no_follow = true }); + var tmp = testing.tmpDir(.{ .follow_symlinks = false }); //defer tmp.cleanup(); pipeToFileSystem(tmp.dir, &reader, .{ diff --git a/lib/std/tar/Writer.zig b/lib/std/tar/Writer.zig index bffdb8ee7c..583bc8cfc1 100644 --- a/lib/std/tar/Writer.zig +++ b/lib/std/tar/Writer.zig @@ -1,7 +1,9 @@ +const Writer = @This(); + const std = @import("std"); +const Io = std.Io; const assert = std.debug.assert; const testing = std.testing; -const Writer = @This(); const block_size = @sizeOf(Header); @@ -14,9 +16,8 @@ pub const Options = struct { mtime: u64 = 0, }; -underlying_writer: *std.Io.Writer, +underlying_writer: *Io.Writer, prefix: []const u8 = "", -mtime_now: u64 = 0, const Error = error{ WriteFailed, @@ -36,16 +37,27 @@ pub fn writeDir(w: *Writer, sub_path: []const u8, options: Options) Error!void { try w.writeHeader(.directory, sub_path, "", 0, options); } -pub const WriteFileError = std.Io.Writer.FileError || Error || std.fs.File.Reader.SizeError; +pub const WriteFileError = Io.Writer.FileError || Error || Io.File.Reader.SizeError; + +pub fn writeFileTimestamp( + w: *Writer, + sub_path: []const u8, + file_reader: *Io.File.Reader, + mtime: Io.Timestamp, +) WriteFileError!void { + return writeFile(w, sub_path, file_reader, @intCast(mtime.toSeconds())); +} pub fn writeFile( w: *Writer, sub_path: []const u8, - file_reader: *std.fs.File.Reader, - stat_mtime: i128, + file_reader: *Io.File.Reader, + /// If you want to match the file format's expectations, it wants number of + /// seconds since POSIX epoch. Zero is also a great option here to make + /// generated tarballs more reproducible. + mtime: u64, ) WriteFileError!void { const size = try file_reader.getSize(); - const mtime: u64 = @intCast(@divFloor(stat_mtime, std.time.ns_per_s)); var header: Header = .{}; try w.setPath(&header, sub_path); @@ -58,7 +70,7 @@ pub fn writeFile( try w.writePadding64(size); } -pub const WriteFileStreamError = Error || std.Io.Reader.StreamError; +pub const WriteFileStreamError = Error || Io.Reader.StreamError; /// Writes file reading file content from `reader`. Reads exactly `size` bytes /// from `reader`, or returns `error.EndOfStream`. @@ -66,7 +78,7 @@ pub fn writeFileStream( w: *Writer, sub_path: []const u8, size: u64, - reader: *std.Io.Reader, + reader: *Io.Reader, options: Options, ) WriteFileStreamError!void { try w.writeHeader(.regular, sub_path, "", size, options); @@ -136,15 +148,15 @@ fn writeExtendedHeader(w: *Writer, typeflag: Header.FileType, buffers: []const [ try w.writePadding(len); } -fn writePadding(w: *Writer, bytes: usize) std.Io.Writer.Error!void { +fn writePadding(w: *Writer, bytes: usize) Io.Writer.Error!void { return writePaddingPos(w, bytes % block_size); } -fn writePadding64(w: *Writer, bytes: u64) std.Io.Writer.Error!void { +fn writePadding64(w: *Writer, bytes: u64) Io.Writer.Error!void { return writePaddingPos(w, @intCast(bytes % block_size)); } -fn writePaddingPos(w: *Writer, pos: usize) std.Io.Writer.Error!void { +fn writePaddingPos(w: *Writer, pos: usize) Io.Writer.Error!void { if (pos == 0) return; try w.underlying_writer.splatByteAll(0, block_size - pos); } @@ -153,7 +165,7 @@ fn writePaddingPos(w: *Writer, pos: usize) std.Io.Writer.Error!void { /// "reasonable system must not assume that such a block exists when reading an /// archive". Therefore, the Zig standard library recommends to not call this /// function. -pub fn finishPedantically(w: *Writer) std.Io.Writer.Error!void { +pub fn finishPedantically(w: *Writer) Io.Writer.Error!void { try w.underlying_writer.splatByteAll(0, block_size * 2); } @@ -236,7 +248,6 @@ pub const Header = extern struct { } // Integer number of seconds since January 1, 1970, 00:00 Coordinated Universal Time. - // mtime == 0 will use current time pub fn setMtime(w: *Header, mtime: u64) error{OctalOverflow}!void { try octal(&w.mtime, mtime); } @@ -248,7 +259,7 @@ pub const Header = extern struct { try octal(&w.checksum, checksum); } - pub fn write(h: *Header, bw: *std.Io.Writer) error{ OctalOverflow, WriteFailed }!void { + pub fn write(h: *Header, bw: *Io.Writer) error{ OctalOverflow, WriteFailed }!void { try h.updateChecksum(); try bw.writeAll(std.mem.asBytes(h)); } @@ -396,14 +407,14 @@ test "write files" { { const root = "root"; - var output: std.Io.Writer.Allocating = .init(testing.allocator); + var output: Io.Writer.Allocating = .init(testing.allocator); var w: Writer = .{ .underlying_writer = &output.writer }; defer output.deinit(); try w.setRoot(root); for (files) |file| try w.writeFileBytes(file.path, file.content, .{}); - var input: std.Io.Reader = .fixed(output.written()); + var input: Io.Reader = .fixed(output.written()); var it: std.tar.Iterator = .init(&input, .{ .file_name_buffer = &file_name_buffer, .link_name_buffer = &link_name_buffer, @@ -424,7 +435,7 @@ test "write files" { try testing.expectEqual('/', actual.name[root.len..][0]); try testing.expectEqualStrings(expected.path, actual.name[root.len + 1 ..]); - var content: std.Io.Writer.Allocating = .init(testing.allocator); + var content: Io.Writer.Allocating = .init(testing.allocator); defer content.deinit(); try it.streamRemaining(actual, &content.writer); try testing.expectEqualSlices(u8, expected.content, content.written()); @@ -432,15 +443,15 @@ test "write files" { } // without root { - var output: std.Io.Writer.Allocating = .init(testing.allocator); + var output: Io.Writer.Allocating = .init(testing.allocator); var w: Writer = .{ .underlying_writer = &output.writer }; defer output.deinit(); for (files) |file| { - var content: std.Io.Reader = .fixed(file.content); + var content: Io.Reader = .fixed(file.content); try w.writeFileStream(file.path, file.content.len, &content, .{}); } - var input: std.Io.Reader = .fixed(output.written()); + var input: Io.Reader = .fixed(output.written()); var it: std.tar.Iterator = .init(&input, .{ .file_name_buffer = &file_name_buffer, .link_name_buffer = &link_name_buffer, @@ -452,7 +463,7 @@ test "write files" { const expected = files[i]; try testing.expectEqualStrings(expected.path, actual.name); - var content: std.Io.Writer.Allocating = .init(testing.allocator); + var content: Io.Writer.Allocating = .init(testing.allocator); defer content.deinit(); try it.streamRemaining(actual, &content.writer); try testing.expectEqualSlices(u8, expected.content, content.written()); diff --git a/lib/std/testing.zig b/lib/std/testing.zig index e1e78c3bec..7cef6f9c58 100644 --- a/lib/std/testing.zig +++ b/lib/std/testing.zig @@ -28,6 +28,9 @@ pub var allocator_instance: std.heap.GeneralPurposeAllocator(.{ break :b .init; }; +pub var io_instance: std.Io.Threaded = undefined; +pub const io = io_instance.io(); + /// TODO https://github.com/ziglang/zig/issues/5738 pub var log_level = std.log.Level.warn; @@ -1145,6 +1148,7 @@ pub fn checkAllAllocationFailures(backing_allocator: std.mem.Allocator, comptime } else |err| switch (err) { error.OutOfMemory => { if (failing_allocator_inst.allocated_bytes != failing_allocator_inst.freed_bytes) { + const tty_config = std.Io.tty.detectConfig(.stderr()); print( "\nfail_index: {d}/{d}\nallocated bytes: {d}\nfreed bytes: {d}\nallocations: {d}\ndeallocations: {d}\nallocation that was made to fail: {f}", .{ @@ -1154,7 +1158,10 @@ pub fn checkAllAllocationFailures(backing_allocator: std.mem.Allocator, comptime failing_allocator_inst.freed_bytes, failing_allocator_inst.allocations, failing_allocator_inst.deallocations, - failing_allocator_inst.getStackTrace(), + std.debug.FormatStackTrace{ + .stack_trace = failing_allocator_inst.getStackTrace(), + .tty_config = tty_config, + }, }, ); return error.MemoryLeakDetected; diff --git a/lib/std/time.zig b/lib/std/time.zig index 504257a852..66a38051ba 100644 --- a/lib/std/time.zig +++ b/lib/std/time.zig @@ -8,74 +8,6 @@ const posix = std.posix; pub const epoch = @import("time/epoch.zig"); -/// Get a calendar timestamp, in seconds, relative to UTC 1970-01-01. -/// Precision of timing depends on the hardware and operating system. -/// The return value is signed because it is possible to have a date that is -/// before the epoch. -/// See `posix.clock_gettime` for a POSIX timestamp. -pub fn timestamp() i64 { - return @divFloor(milliTimestamp(), ms_per_s); -} - -/// Get a calendar timestamp, in milliseconds, relative to UTC 1970-01-01. -/// Precision of timing depends on the hardware and operating system. -/// The return value is signed because it is possible to have a date that is -/// before the epoch. -/// See `posix.clock_gettime` for a POSIX timestamp. -pub fn milliTimestamp() i64 { - return @as(i64, @intCast(@divFloor(nanoTimestamp(), ns_per_ms))); -} - -/// Get a calendar timestamp, in microseconds, relative to UTC 1970-01-01. -/// Precision of timing depends on the hardware and operating system. -/// The return value is signed because it is possible to have a date that is -/// before the epoch. -/// See `posix.clock_gettime` for a POSIX timestamp. -pub fn microTimestamp() i64 { - return @as(i64, @intCast(@divFloor(nanoTimestamp(), ns_per_us))); -} - -/// Get a calendar timestamp, in nanoseconds, relative to UTC 1970-01-01. -/// Precision of timing depends on the hardware and operating system. -/// On Windows this has a maximum granularity of 100 nanoseconds. -/// The return value is signed because it is possible to have a date that is -/// before the epoch. -/// See `posix.clock_gettime` for a POSIX timestamp. -pub fn nanoTimestamp() i128 { - switch (builtin.os.tag) { - .windows => { - // RtlGetSystemTimePrecise() has a granularity of 100 nanoseconds and uses the NTFS/Windows epoch, - // which is 1601-01-01. - const epoch_adj = epoch.windows * (ns_per_s / 100); - return @as(i128, windows.ntdll.RtlGetSystemTimePrecise() + epoch_adj) * 100; - }, - .wasi => { - var ns: std.os.wasi.timestamp_t = undefined; - const err = std.os.wasi.clock_time_get(.REALTIME, 1, &ns); - assert(err == .SUCCESS); - return ns; - }, - .uefi => { - const value, _ = std.os.uefi.system_table.runtime_services.getTime() catch return 0; - return value.toEpoch(); - }, - else => { - const ts = posix.clock_gettime(.REALTIME) catch |err| switch (err) { - error.UnsupportedClock, error.Unexpected => return 0, // "Precision of timing depends on hardware and OS". - }; - return (@as(i128, ts.sec) * ns_per_s) + ts.nsec; - }, - } -} - -test milliTimestamp { - const time_0 = milliTimestamp(); - std.Thread.sleep(ns_per_ms); - const time_1 = milliTimestamp(); - const interval = time_1 - time_0; - try testing.expect(interval > 0); -} - // Divisions of a nanosecond. pub const ns_per_us = 1000; pub const ns_per_ms = 1000 * ns_per_us; @@ -268,9 +200,11 @@ pub const Timer = struct { }; test Timer { + const io = std.testing.io; + var timer = try Timer.start(); - std.Thread.sleep(10 * ns_per_ms); + try std.Io.Clock.Duration.sleep(.{ .clock = .awake, .raw = .fromMilliseconds(10) }, io); const time_0 = timer.read(); try testing.expect(time_0 > 0); diff --git a/lib/std/unicode.zig b/lib/std/unicode.zig index 7fbf1094ba..1aae6d488f 100644 --- a/lib/std/unicode.zig +++ b/lib/std/unicode.zig @@ -1809,30 +1809,6 @@ pub fn wtf8ToWtf16Le(wtf16le: []u16, wtf8: []const u8) error{InvalidWtf8}!usize return utf8ToUtf16LeImpl(wtf16le, wtf8, .can_encode_surrogate_half); } -fn checkUtf8ToUtf16LeOverflowImpl(utf8: []const u8, utf16le: []const u16, comptime surrogates: Surrogates) !bool { - // Each u8 in UTF-8/WTF-8 correlates to at most one u16 in UTF-16LE/WTF-16LE. - if (utf16le.len >= utf8.len) return false; - const utf16_len = calcUtf16LeLenImpl(utf8, surrogates) catch { - return switch (surrogates) { - .cannot_encode_surrogate_half => error.InvalidUtf8, - .can_encode_surrogate_half => error.InvalidWtf8, - }; - }; - return utf16_len > utf16le.len; -} - -/// Checks if calling `utf8ToUtf16Le` would overflow. Might fail if utf8 is not -/// valid UTF-8. -pub fn checkUtf8ToUtf16LeOverflow(utf8: []const u8, utf16le: []const u16) error{InvalidUtf8}!bool { - return checkUtf8ToUtf16LeOverflowImpl(utf8, utf16le, .cannot_encode_surrogate_half); -} - -/// Checks if calling `utf8ToUtf16Le` would overflow. Might fail if wtf8 is not -/// valid WTF-8. -pub fn checkWtf8ToWtf16LeOverflow(wtf8: []const u8, wtf16le: []const u16) error{InvalidWtf8}!bool { - return checkUtf8ToUtf16LeOverflowImpl(wtf8, wtf16le, .can_encode_surrogate_half); -} - /// Surrogate codepoints (U+D800 to U+DFFF) are replaced by the Unicode replacement /// character (U+FFFD). /// All surrogate codepoints and the replacement character are encoded as three @@ -2039,7 +2015,6 @@ fn testRoundtripWtf8(wtf8: []const u8) !void { var wtf16_buf: [32]u16 = undefined; const wtf16_len = try wtf8ToWtf16Le(&wtf16_buf, wtf8); try testing.expectEqual(wtf16_len, calcWtf16LeLen(wtf8)); - try testing.expectEqual(false, checkWtf8ToWtf16LeOverflow(wtf8, &wtf16_buf)); const wtf16 = wtf16_buf[0..wtf16_len]; var roundtripped_buf: [32]u8 = undefined; diff --git a/lib/std/zig.zig b/lib/std/zig.zig index 9dc3818241..34851709ab 100644 --- a/lib/std/zig.zig +++ b/lib/std/zig.zig @@ -6,6 +6,7 @@ const std = @import("std.zig"); const tokenizer = @import("zig/tokenizer.zig"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; +const Io = std.Io; const Writer = std.Io.Writer; pub const ErrorBundle = @import("zig/ErrorBundle.zig"); @@ -52,9 +53,9 @@ pub const Color = enum { /// Assume stderr is a terminal. on, - pub fn get_tty_conf(color: Color) std.Io.tty.Config { + pub fn get_tty_conf(color: Color) Io.tty.Config { return switch (color) { - .auto => std.Io.tty.detectConfig(std.fs.File.stderr()), + .auto => Io.tty.detectConfig(std.fs.File.stderr()), .on => .escape_codes, .off => .no_color, }; @@ -323,7 +324,7 @@ pub const BuildId = union(enum) { try std.testing.expectError(error.InvalidBuildIdStyle, parse("yaddaxxx")); } - pub fn format(id: BuildId, writer: *std.Io.Writer) std.Io.Writer.Error!void { + pub fn format(id: BuildId, writer: *Writer) Writer.Error!void { switch (id) { .none, .fast, .uuid, .sha1, .md5 => { try writer.writeAll(@tagName(id)); @@ -558,7 +559,7 @@ test isUnderscore { /// If the source can be UTF-16LE encoded, this function asserts that `gpa` /// will align a byte-sized allocation to at least 2. Allocators that don't do /// this are rare. -pub fn readSourceFileToEndAlloc(gpa: Allocator, file_reader: *std.fs.File.Reader) ![:0]u8 { +pub fn readSourceFileToEndAlloc(gpa: Allocator, file_reader: *Io.File.Reader) ![:0]u8 { var buffer: std.ArrayList(u8) = .empty; defer buffer.deinit(gpa); @@ -620,8 +621,8 @@ pub fn putAstErrorsIntoBundle( try wip_errors.addZirErrorMessages(zir, tree, tree.source, path); } -pub fn resolveTargetQueryOrFatal(target_query: std.Target.Query) std.Target { - return std.zig.system.resolveTargetQuery(target_query) catch |err| +pub fn resolveTargetQueryOrFatal(io: Io, target_query: std.Target.Query) std.Target { + return std.zig.system.resolveTargetQuery(io, target_query) catch |err| std.process.fatal("unable to resolve target: {s}", .{@errorName(err)}); } diff --git a/lib/std/zig/ErrorBundle.zig b/lib/std/zig/ErrorBundle.zig index 8ec3ae18b6..2b2ad396de 100644 --- a/lib/std/zig/ErrorBundle.zig +++ b/lib/std/zig/ErrorBundle.zig @@ -6,12 +6,13 @@ //! There is one special encoding for this data structure. If both arrays are //! empty, it means there are no errors. This special encoding exists so that //! heap allocation is not needed in the common case of no errors. +const ErrorBundle = @This(); const std = @import("std"); -const ErrorBundle = @This(); +const Io = std.Io; +const Writer = std.Io.Writer; const Allocator = std.mem.Allocator; const assert = std.debug.assert; -const Writer = std.Io.Writer; string_bytes: []const u8, /// The first thing in this array is an `ErrorMessageList`. @@ -156,7 +157,7 @@ pub fn nullTerminatedString(eb: ErrorBundle, index: String) [:0]const u8 { } pub const RenderOptions = struct { - ttyconf: std.Io.tty.Config, + ttyconf: Io.tty.Config, include_reference_trace: bool = true, include_source_line: bool = true, include_log_text: bool = true, @@ -190,7 +191,7 @@ fn renderErrorMessageToWriter( err_msg_index: MessageIndex, w: *Writer, kind: []const u8, - color: std.Io.tty.Color, + color: Io.tty.Color, indent: usize, ) (Writer.Error || std.posix.UnexpectedError)!void { const ttyconf = options.ttyconf; @@ -806,7 +807,7 @@ pub const Wip = struct { }; defer bundle.deinit(std.testing.allocator); - const ttyconf: std.Io.tty.Config = .no_color; + const ttyconf: Io.tty.Config = .no_color; var bundle_buf: Writer.Allocating = .init(std.testing.allocator); const bundle_bw = &bundle_buf.interface; diff --git a/lib/std/zig/LibCInstallation.zig b/lib/std/zig/LibCInstallation.zig index f6d381be82..2ab4e48570 100644 --- a/lib/std/zig/LibCInstallation.zig +++ b/lib/std/zig/LibCInstallation.zig @@ -329,7 +329,7 @@ fn findNativeIncludeDirPosix(self: *LibCInstallation, args: FindNativeOptions) F defer search_dir.close(); if (self.include_dir == null) { - if (search_dir.accessZ(include_dir_example_file, .{})) |_| { + if (search_dir.access(include_dir_example_file, .{})) |_| { self.include_dir = try allocator.dupeZ(u8, search_path); } else |err| switch (err) { error.FileNotFound => {}, @@ -338,7 +338,7 @@ fn findNativeIncludeDirPosix(self: *LibCInstallation, args: FindNativeOptions) F } if (self.sys_include_dir == null) { - if (search_dir.accessZ(sys_include_dir_example_file, .{})) |_| { + if (search_dir.access(sys_include_dir_example_file, .{})) |_| { self.sys_include_dir = try allocator.dupeZ(u8, search_path); } else |err| switch (err) { error.FileNotFound => {}, @@ -382,7 +382,7 @@ fn findNativeIncludeDirWindows( }; defer dir.close(); - dir.accessZ("stdlib.h", .{}) catch |err| switch (err) { + dir.access("stdlib.h", .{}) catch |err| switch (err) { error.FileNotFound => continue, else => return error.FileSystem, }; @@ -429,7 +429,7 @@ fn findNativeCrtDirWindows( }; defer dir.close(); - dir.accessZ("ucrt.lib", .{}) catch |err| switch (err) { + dir.access("ucrt.lib", .{}) catch |err| switch (err) { error.FileNotFound => continue, else => return error.FileSystem, }; @@ -496,7 +496,7 @@ fn findNativeKernel32LibDir( }; defer dir.close(); - dir.accessZ("kernel32.lib", .{}) catch |err| switch (err) { + dir.access("kernel32.lib", .{}) catch |err| switch (err) { error.FileNotFound => continue, else => return error.FileSystem, }; @@ -531,7 +531,7 @@ fn findNativeMsvcIncludeDir( }; defer dir.close(); - dir.accessZ("vcruntime.h", .{}) catch |err| switch (err) { + dir.access("vcruntime.h", .{}) catch |err| switch (err) { error.FileNotFound => return error.LibCStdLibHeaderNotFound, else => return error.FileSystem, }; diff --git a/lib/std/zig/system.zig b/lib/std/zig/system.zig index e90c4ae023..fc433d9936 100644 --- a/lib/std/zig/system.zig +++ b/lib/std/zig/system.zig @@ -1,3 +1,14 @@ +const builtin = @import("builtin"); +const std = @import("../std.zig"); +const mem = std.mem; +const elf = std.elf; +const fs = std.fs; +const assert = std.debug.assert; +const Target = std.Target; +const native_endian = builtin.cpu.arch.endian(); +const posix = std.posix; +const Io = std.Io; + pub const NativePaths = @import("system/NativePaths.zig"); pub const windows = @import("system/windows.zig"); @@ -199,14 +210,14 @@ pub const DetectError = error{ OSVersionDetectionFail, Unexpected, ProcessNotFound, -}; +} || Io.Cancelable; /// Given a `Target.Query`, which specifies in detail which parts of the /// target should be detected natively, which should be standard or default, /// and which are provided explicitly, this function resolves the native /// components by detecting the native system, and then resolves /// standard/default parts relative to that. -pub fn resolveTargetQuery(query: Target.Query) DetectError!Target { +pub fn resolveTargetQuery(io: Io, query: Target.Query) DetectError!Target { // Until https://github.com/ziglang/zig/issues/4592 is implemented (support detecting the // native CPU architecture as being different than the current target), we use this: const query_cpu_arch = query.cpu_arch orelse builtin.cpu.arch; @@ -356,10 +367,10 @@ pub fn resolveTargetQuery(query: Target.Query) DetectError!Target { } var cpu = switch (query.cpu_model) { - .native => detectNativeCpuAndFeatures(query_cpu_arch, os, query), + .native => detectNativeCpuAndFeatures(io, query_cpu_arch, os, query), .baseline => Target.Cpu.baseline(query_cpu_arch, os), .determined_by_arch_os => if (query.cpu_arch == null) - detectNativeCpuAndFeatures(query_cpu_arch, os, query) + detectNativeCpuAndFeatures(io, query_cpu_arch, os, query) else Target.Cpu.baseline(query_cpu_arch, os), .explicit => |model| model.toCpu(query_cpu_arch), @@ -411,7 +422,34 @@ pub fn resolveTargetQuery(query: Target.Query) DetectError!Target { query.cpu_features_sub, ); - var result = try detectAbiAndDynamicLinker(cpu, os, query); + var result = detectAbiAndDynamicLinker(io, cpu, os, query) catch |err| switch (err) { + error.Canceled => |e| return e, + error.Unexpected => |e| return e, + error.WouldBlock => return error.Unexpected, + error.BrokenPipe => return error.Unexpected, + error.ConnectionResetByPeer => return error.Unexpected, + error.Timeout => return error.Unexpected, + error.NotOpenForReading => return error.Unexpected, + error.SocketUnconnected => return error.Unexpected, + + error.AccessDenied, + error.ProcessNotFound, + error.SymLinkLoop, + error.ProcessFdQuotaExceeded, + error.SystemFdQuotaExceeded, + error.SystemResources, + error.IsDir, + error.DeviceBusy, + error.InputOutput, + error.LockViolation, + error.FileSystem, + + error.UnableToOpenElfFile, + error.UnhelpfulFile, + error.InvalidElfFile, + error.RelativeShebang, + => return defaultAbiAndDynamicLinker(cpu, os, query), + }; // These CPU feature hacks have to come after ABI detection. { @@ -483,7 +521,7 @@ fn updateCpuFeatures( set.removeFeatureSet(sub_set); } -fn detectNativeCpuAndFeatures(cpu_arch: Target.Cpu.Arch, os: Target.Os, query: Target.Query) ?Target.Cpu { +fn detectNativeCpuAndFeatures(io: Io, cpu_arch: Target.Cpu.Arch, os: Target.Os, query: Target.Query) ?Target.Cpu { // Here we switch on a comptime value rather than `cpu_arch`. This is valid because `cpu_arch`, // although it is a runtime value, is guaranteed to be one of the architectures in the set // of the respective switch prong. @@ -494,7 +532,7 @@ fn detectNativeCpuAndFeatures(cpu_arch: Target.Cpu.Arch, os: Target.Os, query: T } switch (builtin.os.tag) { - .linux => return linux.detectNativeCpuAndFeatures(), + .linux => return linux.detectNativeCpuAndFeatures(io), .macos => return darwin.macos.detectNativeCpuAndFeatures(), .windows => return windows.detectNativeCpuAndFeatures(), else => {}, @@ -506,53 +544,42 @@ fn detectNativeCpuAndFeatures(cpu_arch: Target.Cpu.Arch, os: Target.Os, query: T } pub const AbiAndDynamicLinkerFromFileError = error{ - FileSystem, - SystemResources, + Canceled, + AccessDenied, + Unexpected, + Unseekable, + ReadFailed, + EndOfStream, + NameTooLong, + StaticElfFile, + InvalidElfFile, + StreamTooLong, + Timeout, SymLinkLoop, + SystemResources, ProcessFdQuotaExceeded, SystemFdQuotaExceeded, - UnableToReadElfFile, - InvalidElfClass, - InvalidElfVersion, - InvalidElfEndian, - InvalidElfFile, - InvalidElfMagic, - Unexpected, - UnexpectedEndOfFile, - NameTooLong, ProcessNotFound, - StaticElfFile, + IsDir, + WouldBlock, + InputOutput, + BrokenPipe, + ConnectionResetByPeer, + NotOpenForReading, + SocketUnconnected, + LockViolation, + FileSystem, }; -pub fn abiAndDynamicLinkerFromFile( - file: fs.File, +fn abiAndDynamicLinkerFromFile( + file_reader: *Io.File.Reader, + header: *const elf.Header, cpu: Target.Cpu, os: Target.Os, ld_info_list: []const LdInfo, query: Target.Query, ) AbiAndDynamicLinkerFromFileError!Target { - var hdr_buf: [@sizeOf(elf.Elf64_Ehdr)]u8 align(@alignOf(elf.Elf64_Ehdr)) = undefined; - _ = try preadAtLeast(file, &hdr_buf, 0, hdr_buf.len); - const hdr32: *elf.Elf32_Ehdr = @ptrCast(&hdr_buf); - const hdr64: *elf.Elf64_Ehdr = @ptrCast(&hdr_buf); - if (!mem.eql(u8, hdr32.e_ident[0..4], elf.MAGIC)) return error.InvalidElfMagic; - const elf_endian: std.builtin.Endian = switch (hdr32.e_ident[elf.EI.DATA]) { - elf.ELFDATA2LSB => .little, - elf.ELFDATA2MSB => .big, - else => return error.InvalidElfEndian, - }; - const need_bswap = elf_endian != native_endian; - if (hdr32.e_ident[elf.EI.VERSION] != 1) return error.InvalidElfVersion; - - const is_64 = switch (hdr32.e_ident[elf.EI.CLASS]) { - elf.ELFCLASS32 => false, - elf.ELFCLASS64 => true, - else => return error.InvalidElfClass, - }; - var phoff = elfInt(is_64, need_bswap, hdr32.e_phoff, hdr64.e_phoff); - const phentsize = elfInt(is_64, need_bswap, hdr32.e_phentsize, hdr64.e_phentsize); - const phnum = elfInt(is_64, need_bswap, hdr32.e_phnum, hdr64.e_phnum); - + const io = file_reader.io; var result: Target = .{ .cpu = cpu, .os = os, @@ -563,170 +590,90 @@ pub fn abiAndDynamicLinkerFromFile( var rpath_offset: ?u64 = null; // Found inside PT_DYNAMIC const look_for_ld = query.dynamic_linker.get() == null; - var ph_buf: [16 * @sizeOf(elf.Elf64_Phdr)]u8 align(@alignOf(elf.Elf64_Phdr)) = undefined; - if (phentsize > @sizeOf(elf.Elf64_Phdr)) return error.InvalidElfFile; - - var ph_i: u16 = 0; var got_dyn_section: bool = false; + { + var it = header.iterateProgramHeaders(file_reader); + while (try it.next()) |phdr| switch (phdr.p_type) { + elf.PT_INTERP => { + got_dyn_section = true; - while (ph_i < phnum) { - // Reserve some bytes so that we can deref the 64-bit struct fields - // even when the ELF file is 32-bits. - const ph_reserve: usize = @sizeOf(elf.Elf64_Phdr) - @sizeOf(elf.Elf32_Phdr); - const ph_read_byte_len = try preadAtLeast(file, ph_buf[0 .. ph_buf.len - ph_reserve], phoff, phentsize); - var ph_buf_i: usize = 0; - while (ph_buf_i < ph_read_byte_len and ph_i < phnum) : ({ - ph_i += 1; - phoff += phentsize; - ph_buf_i += phentsize; - }) { - const ph32: *elf.Elf32_Phdr = @ptrCast(@alignCast(&ph_buf[ph_buf_i])); - const ph64: *elf.Elf64_Phdr = @ptrCast(@alignCast(&ph_buf[ph_buf_i])); - const p_type = elfInt(is_64, need_bswap, ph32.p_type, ph64.p_type); - switch (p_type) { - elf.PT_INTERP => { - got_dyn_section = true; + if (look_for_ld) { + const p_filesz = phdr.p_filesz; + if (p_filesz > result.dynamic_linker.buffer.len) return error.NameTooLong; + const filesz: usize = @intCast(p_filesz); + try file_reader.seekTo(phdr.p_offset); + try file_reader.interface.readSliceAll(result.dynamic_linker.buffer[0..filesz]); + // PT_INTERP includes a null byte in filesz. + const len = filesz - 1; + // dynamic_linker.max_byte is "max", not "len". + // We know it will fit in u8 because we check against dynamic_linker.buffer.len above. + result.dynamic_linker.len = @intCast(len); - if (look_for_ld) { - const p_offset = elfInt(is_64, need_bswap, ph32.p_offset, ph64.p_offset); - const p_filesz = elfInt(is_64, need_bswap, ph32.p_filesz, ph64.p_filesz); - if (p_filesz > result.dynamic_linker.buffer.len) return error.NameTooLong; - const filesz: usize = @intCast(p_filesz); - _ = try preadAtLeast(file, result.dynamic_linker.buffer[0..filesz], p_offset, filesz); - // PT_INTERP includes a null byte in filesz. - const len = filesz - 1; - // dynamic_linker.max_byte is "max", not "len". - // We know it will fit in u8 because we check against dynamic_linker.buffer.len above. - result.dynamic_linker.len = @intCast(len); - - // Use it to determine ABI. - const full_ld_path = result.dynamic_linker.buffer[0..len]; - for (ld_info_list) |ld_info| { - const standard_ld_basename = fs.path.basename(ld_info.ld.get().?); - if (std.mem.endsWith(u8, full_ld_path, standard_ld_basename)) { - result.abi = ld_info.abi; - break; - } + // Use it to determine ABI. + const full_ld_path = result.dynamic_linker.buffer[0..len]; + for (ld_info_list) |ld_info| { + const standard_ld_basename = fs.path.basename(ld_info.ld.get().?); + if (std.mem.endsWith(u8, full_ld_path, standard_ld_basename)) { + result.abi = ld_info.abi; + break; } } - }, - // We only need this for detecting glibc version. - elf.PT_DYNAMIC => { - got_dyn_section = true; + } + }, + // We only need this for detecting glibc version. + elf.PT_DYNAMIC => { + got_dyn_section = true; - if (builtin.target.os.tag == .linux and result.isGnuLibC() and - query.glibc_version == null) - { - var dyn_off = elfInt(is_64, need_bswap, ph32.p_offset, ph64.p_offset); - const p_filesz = elfInt(is_64, need_bswap, ph32.p_filesz, ph64.p_filesz); - const dyn_size: usize = if (is_64) @sizeOf(elf.Elf64_Dyn) else @sizeOf(elf.Elf32_Dyn); - const dyn_num = p_filesz / dyn_size; - var dyn_buf: [16 * @sizeOf(elf.Elf64_Dyn)]u8 align(@alignOf(elf.Elf64_Dyn)) = undefined; - var dyn_i: usize = 0; - dyn: while (dyn_i < dyn_num) { - // Reserve some bytes so that we can deref the 64-bit struct fields - // even when the ELF file is 32-bits. - const dyn_reserve: usize = @sizeOf(elf.Elf64_Dyn) - @sizeOf(elf.Elf32_Dyn); - const dyn_read_byte_len = try preadAtLeast( - file, - dyn_buf[0 .. dyn_buf.len - dyn_reserve], - dyn_off, - dyn_size, - ); - var dyn_buf_i: usize = 0; - while (dyn_buf_i < dyn_read_byte_len and dyn_i < dyn_num) : ({ - dyn_i += 1; - dyn_off += dyn_size; - dyn_buf_i += dyn_size; - }) { - const dyn32: *elf.Elf32_Dyn = @ptrCast(@alignCast(&dyn_buf[dyn_buf_i])); - const dyn64: *elf.Elf64_Dyn = @ptrCast(@alignCast(&dyn_buf[dyn_buf_i])); - const tag = elfInt(is_64, need_bswap, dyn32.d_tag, dyn64.d_tag); - const val = elfInt(is_64, need_bswap, dyn32.d_val, dyn64.d_val); - if (tag == elf.DT_RUNPATH) { - rpath_offset = val; - break :dyn; - } - } + if (builtin.target.os.tag == .linux and result.isGnuLibC() and query.glibc_version == null) { + var dyn_it = header.iterateDynamicSection(file_reader, phdr.p_offset, phdr.p_filesz); + while (try dyn_it.next()) |dyn| { + if (dyn.d_tag == elf.DT_RUNPATH) { + rpath_offset = dyn.d_val; + break; } } - }, - else => continue, - } - } + } + }, + else => continue, + }; } if (!got_dyn_section) { return error.StaticElfFile; } - if (builtin.target.os.tag == .linux and result.isGnuLibC() and - query.glibc_version == null) - { - const shstrndx = elfInt(is_64, need_bswap, hdr32.e_shstrndx, hdr64.e_shstrndx); - - var shoff = elfInt(is_64, need_bswap, hdr32.e_shoff, hdr64.e_shoff); - const shentsize = elfInt(is_64, need_bswap, hdr32.e_shentsize, hdr64.e_shentsize); - const str_section_off = shoff + @as(u64, shentsize) * @as(u64, shstrndx); - - var sh_buf: [16 * @sizeOf(elf.Elf64_Shdr)]u8 align(@alignOf(elf.Elf64_Shdr)) = undefined; - if (sh_buf.len < shentsize) return error.InvalidElfFile; - - _ = try preadAtLeast(file, &sh_buf, str_section_off, shentsize); - const shstr32: *elf.Elf32_Shdr = @ptrCast(@alignCast(&sh_buf)); - const shstr64: *elf.Elf64_Shdr = @ptrCast(@alignCast(&sh_buf)); - const shstrtab_off = elfInt(is_64, need_bswap, shstr32.sh_offset, shstr64.sh_offset); - const shstrtab_size = elfInt(is_64, need_bswap, shstr32.sh_size, shstr64.sh_size); - var strtab_buf: [4096:0]u8 = undefined; - const shstrtab_len = @min(shstrtab_size, strtab_buf.len); - const shstrtab_read_len = try preadAtLeast(file, &strtab_buf, shstrtab_off, shstrtab_len); - const shstrtab = strtab_buf[0..shstrtab_read_len]; - - const shnum = elfInt(is_64, need_bswap, hdr32.e_shnum, hdr64.e_shnum); - var sh_i: u16 = 0; - const dynstr: ?struct { offset: u64, size: u64 } = find_dyn_str: while (sh_i < shnum) { - // Reserve some bytes so that we can deref the 64-bit struct fields - // even when the ELF file is 32-bits. - const sh_reserve: usize = @sizeOf(elf.Elf64_Shdr) - @sizeOf(elf.Elf32_Shdr); - const sh_read_byte_len = try preadAtLeast( - file, - sh_buf[0 .. sh_buf.len - sh_reserve], - shoff, - shentsize, - ); - var sh_buf_i: usize = 0; - while (sh_buf_i < sh_read_byte_len and sh_i < shnum) : ({ - sh_i += 1; - shoff += shentsize; - sh_buf_i += shentsize; - }) { - const sh32: *elf.Elf32_Shdr = @ptrCast(@alignCast(&sh_buf[sh_buf_i])); - const sh64: *elf.Elf64_Shdr = @ptrCast(@alignCast(&sh_buf[sh_buf_i])); - const sh_name_off = elfInt(is_64, need_bswap, sh32.sh_name, sh64.sh_name); - const sh_name = mem.sliceTo(shstrtab[sh_name_off..], 0); - if (mem.eql(u8, sh_name, ".dynstr")) { - break :find_dyn_str .{ - .offset = elfInt(is_64, need_bswap, sh32.sh_offset, sh64.sh_offset), - .size = elfInt(is_64, need_bswap, sh32.sh_size, sh64.sh_size), - }; - } - } - } else null; - + if (builtin.target.os.tag == .linux and result.isGnuLibC() and query.glibc_version == null) { + const str_section_off = header.shoff + @as(u64, header.shentsize) * @as(u64, header.shstrndx); + try file_reader.seekTo(str_section_off); + const shstr = try elf.takeSectionHeader(&file_reader.interface, header.is_64, header.endian); + var strtab_buf: [4096]u8 = undefined; + const shstrtab = strtab_buf[0..@min(shstr.sh_size, strtab_buf.len)]; + try file_reader.seekTo(shstr.sh_offset); + try file_reader.interface.readSliceAll(shstrtab); + const dynstr: ?struct { offset: u64, size: u64 } = find_dyn_str: { + var it = header.iterateSectionHeaders(file_reader); + while (try it.next()) |shdr| { + const end = mem.findScalarPos(u8, shstrtab, shdr.sh_name, 0) orelse continue; + const sh_name = shstrtab[shdr.sh_name..end :0]; + if (mem.eql(u8, sh_name, ".dynstr")) break :find_dyn_str .{ + .offset = shdr.sh_offset, + .size = shdr.sh_size, + }; + } else break :find_dyn_str null; + }; if (dynstr) |ds| { if (rpath_offset) |rpoff| { if (rpoff > ds.size) return error.InvalidElfFile; const rpoff_file = ds.offset + rpoff; const rp_max_size = ds.size - rpoff; - const strtab_len = @min(rp_max_size, strtab_buf.len); - const strtab_read_len = try preadAtLeast(file, &strtab_buf, rpoff_file, strtab_len); - const strtab = strtab_buf[0..strtab_read_len]; + try file_reader.seekTo(rpoff_file); + const rpath_list = try file_reader.interface.takeSentinel(0); + if (rpath_list.len > rp_max_size) return error.StreamTooLong; - const rpath_list = mem.sliceTo(strtab, 0); var it = mem.tokenizeScalar(u8, rpath_list, ':'); while (it.next()) |rpath| { - if (glibcVerFromRPath(rpath)) |ver| { + if (glibcVerFromRPath(io, rpath)) |ver| { result.os.version_range.linux.glibc = ver; return result; } else |err| switch (err) { @@ -741,7 +688,7 @@ pub fn abiAndDynamicLinkerFromFile( // There is no DT_RUNPATH so we try to find libc.so.6 inside the same // directory as the dynamic linker. if (fs.path.dirname(dl_path)) |rpath| { - if (glibcVerFromRPath(rpath)) |ver| { + if (glibcVerFromRPath(io, rpath)) |ver| { result.os.version_range.linux.glibc = ver; return result; } else |err| switch (err) { @@ -755,8 +702,6 @@ pub fn abiAndDynamicLinkerFromFile( var link_buf: [posix.PATH_MAX]u8 = undefined; const link_name = posix.readlink(dl_path, &link_buf) catch |err| switch (err) { error.NameTooLong => unreachable, - error.InvalidUtf8 => unreachable, // WASI only - error.InvalidWtf8 => unreachable, // Windows only error.BadPathName => unreachable, // Windows only error.UnsupportedReparsePointType => unreachable, // Windows only error.NetworkNotFound => unreachable, // Windows only @@ -806,7 +751,7 @@ pub fn abiAndDynamicLinkerFromFile( @memcpy(path_buf[index..][0..abi.len], abi); index += abi.len; const rpath = path_buf[0..index]; - if (glibcVerFromRPath(rpath)) |ver| { + if (glibcVerFromRPath(io, rpath)) |ver| { result.os.version_range.linux.glibc = ver; return result; } else |err| switch (err) { @@ -845,29 +790,25 @@ test glibcVerFromLinkName { try std.testing.expectError(error.InvalidGnuLibCVersion, glibcVerFromLinkName("ld-2.37.4.5.so", "ld-")); } -fn glibcVerFromRPath(rpath: []const u8) !std.SemanticVersion { +fn glibcVerFromRPath(io: Io, rpath: []const u8) !std.SemanticVersion { var dir = fs.cwd().openDir(rpath, .{}) catch |err| switch (err) { - error.NameTooLong => unreachable, - error.InvalidUtf8 => unreachable, // WASI only - error.InvalidWtf8 => unreachable, // Windows-only - error.BadPathName => unreachable, - error.DeviceBusy => unreachable, - error.NetworkNotFound => unreachable, // Windows-only + error.NameTooLong => return error.Unexpected, + error.BadPathName => return error.Unexpected, + error.DeviceBusy => return error.Unexpected, + error.NetworkNotFound => return error.Unexpected, // Windows-only - error.FileNotFound, - error.NotDir, - error.AccessDenied, - error.PermissionDenied, - error.NoDevice, - => return error.GLibCNotFound, + error.FileNotFound => return error.GLibCNotFound, + error.NotDir => return error.GLibCNotFound, + error.AccessDenied => return error.GLibCNotFound, + error.PermissionDenied => return error.GLibCNotFound, + error.NoDevice => return error.GLibCNotFound, - error.ProcessNotFound, - error.ProcessFdQuotaExceeded, - error.SystemFdQuotaExceeded, - error.SystemResources, - error.SymLinkLoop, - error.Unexpected, - => |e| return e, + error.ProcessFdQuotaExceeded => |e| return e, + error.SystemFdQuotaExceeded => |e| return e, + error.SystemResources => |e| return e, + error.SymLinkLoop => |e| return e, + error.Unexpected => |e| return e, + error.Canceled => |e| return e, }; defer dir.close(); @@ -879,155 +820,103 @@ fn glibcVerFromRPath(rpath: []const u8) !std.SemanticVersion { // .dynstr section, and finding the max version number of symbols // that start with "GLIBC_2.". const glibc_so_basename = "libc.so.6"; - var f = dir.openFile(glibc_so_basename, .{}) catch |err| switch (err) { - error.NameTooLong => unreachable, - error.InvalidUtf8 => unreachable, // WASI only - error.InvalidWtf8 => unreachable, // Windows only - error.BadPathName => unreachable, // Windows only - error.PipeBusy => unreachable, // Windows-only - error.SharingViolation => unreachable, // Windows-only - error.NetworkNotFound => unreachable, // Windows-only - error.AntivirusInterference => unreachable, // Windows-only - error.FileLocksNotSupported => unreachable, // No lock requested. - error.NoSpaceLeft => unreachable, // read-only - error.PathAlreadyExists => unreachable, // read-only - error.DeviceBusy => unreachable, // read-only - error.FileBusy => unreachable, // read-only - error.WouldBlock => unreachable, // not using O_NONBLOCK - error.NoDevice => unreachable, // not asking for a special device - - error.AccessDenied, - error.PermissionDenied, - error.FileNotFound, - error.NotDir, - error.IsDir, - => return error.GLibCNotFound, - + var file = dir.openFile(glibc_so_basename, .{}) catch |err| switch (err) { + error.NameTooLong => return error.Unexpected, + error.BadPathName => return error.Unexpected, + error.PipeBusy => return error.Unexpected, // Windows-only + error.SharingViolation => return error.Unexpected, // Windows-only + error.NetworkNotFound => return error.Unexpected, // Windows-only + error.AntivirusInterference => return error.Unexpected, // Windows-only + error.FileLocksNotSupported => return error.Unexpected, // No lock requested. + error.NoSpaceLeft => return error.Unexpected, // read-only + error.PathAlreadyExists => return error.Unexpected, // read-only + error.DeviceBusy => return error.Unexpected, // read-only + error.FileBusy => return error.Unexpected, // read-only + error.NoDevice => return error.Unexpected, // not asking for a special device error.FileTooBig => return error.Unexpected, + error.WouldBlock => return error.Unexpected, // not opened in non-blocking - error.ProcessNotFound, - error.ProcessFdQuotaExceeded, - error.SystemFdQuotaExceeded, - error.SystemResources, - error.SymLinkLoop, - error.Unexpected, - => |e| return e, + error.AccessDenied => return error.GLibCNotFound, + error.PermissionDenied => return error.GLibCNotFound, + error.FileNotFound => return error.GLibCNotFound, + error.NotDir => return error.GLibCNotFound, + error.IsDir => return error.GLibCNotFound, + + error.ProcessNotFound => |e| return e, + error.ProcessFdQuotaExceeded => |e| return e, + error.SystemFdQuotaExceeded => |e| return e, + error.SystemResources => |e| return e, + error.SymLinkLoop => |e| return e, + error.Unexpected => |e| return e, + error.Canceled => |e| return e, }; - defer f.close(); + defer file.close(); - return glibcVerFromSoFile(f) catch |err| switch (err) { + // Empirically, glibc 2.34 libc.so .dynstr section is 32441 bytes on my system. + var buffer: [8000]u8 = undefined; + var file_reader: Io.File.Reader = .initAdapted(file, io, &buffer); + + return glibcVerFromSoFile(&file_reader) catch |err| switch (err) { error.InvalidElfMagic, error.InvalidElfEndian, error.InvalidElfClass, - error.InvalidElfFile, error.InvalidElfVersion, error.InvalidGnuLibCVersion, - error.UnexpectedEndOfFile, + error.EndOfStream, => return error.GLibCNotFound, - error.SystemResources, - error.UnableToReadElfFile, - error.Unexpected, - error.FileSystem, - error.ProcessNotFound, - => |e| return e, + error.ReadFailed => return file_reader.err.?, + else => |e| return e, }; } -fn glibcVerFromSoFile(file: fs.File) !std.SemanticVersion { - var hdr_buf: [@sizeOf(elf.Elf64_Ehdr)]u8 align(@alignOf(elf.Elf64_Ehdr)) = undefined; - _ = try preadAtLeast(file, &hdr_buf, 0, hdr_buf.len); - const hdr32: *elf.Elf32_Ehdr = @ptrCast(&hdr_buf); - const hdr64: *elf.Elf64_Ehdr = @ptrCast(&hdr_buf); - if (!mem.eql(u8, hdr32.e_ident[0..4], elf.MAGIC)) return error.InvalidElfMagic; - const elf_endian: std.builtin.Endian = switch (hdr32.e_ident[elf.EI.DATA]) { - elf.ELFDATA2LSB => .little, - elf.ELFDATA2MSB => .big, - else => return error.InvalidElfEndian, +fn glibcVerFromSoFile(file_reader: *Io.File.Reader) !std.SemanticVersion { + const header = try elf.Header.read(&file_reader.interface); + const str_section_off = header.shoff + @as(u64, header.shentsize) * @as(u64, header.shstrndx); + try file_reader.seekTo(str_section_off); + const shstr = try elf.takeSectionHeader(&file_reader.interface, header.is_64, header.endian); + var strtab_buf: [4096]u8 = undefined; + const shstrtab = strtab_buf[0..@min(shstr.sh_size, strtab_buf.len)]; + try file_reader.seekTo(shstr.sh_offset); + try file_reader.interface.readSliceAll(shstrtab); + const dynstr: struct { offset: u64, size: u64 } = find_dyn_str: { + var it = header.iterateSectionHeaders(file_reader); + while (try it.next()) |shdr| { + const end = mem.findScalarPos(u8, shstrtab, shdr.sh_name, 0) orelse continue; + const sh_name = shstrtab[shdr.sh_name..end :0]; + if (mem.eql(u8, sh_name, ".dynstr")) break :find_dyn_str .{ + .offset = shdr.sh_offset, + .size = shdr.sh_size, + }; + } else return error.InvalidGnuLibCVersion; }; - const need_bswap = elf_endian != native_endian; - if (hdr32.e_ident[elf.EI.VERSION] != 1) return error.InvalidElfVersion; - - const is_64 = switch (hdr32.e_ident[elf.EI.CLASS]) { - elf.ELFCLASS32 => false, - elf.ELFCLASS64 => true, - else => return error.InvalidElfClass, - }; - const shstrndx = elfInt(is_64, need_bswap, hdr32.e_shstrndx, hdr64.e_shstrndx); - var shoff = elfInt(is_64, need_bswap, hdr32.e_shoff, hdr64.e_shoff); - const shentsize = elfInt(is_64, need_bswap, hdr32.e_shentsize, hdr64.e_shentsize); - const str_section_off = shoff + @as(u64, shentsize) * @as(u64, shstrndx); - var sh_buf: [16 * @sizeOf(elf.Elf64_Shdr)]u8 align(@alignOf(elf.Elf64_Shdr)) = undefined; - if (sh_buf.len < shentsize) return error.InvalidElfFile; - - _ = try preadAtLeast(file, &sh_buf, str_section_off, shentsize); - const shstr32: *elf.Elf32_Shdr = @ptrCast(@alignCast(&sh_buf)); - const shstr64: *elf.Elf64_Shdr = @ptrCast(@alignCast(&sh_buf)); - const shstrtab_off = elfInt(is_64, need_bswap, shstr32.sh_offset, shstr64.sh_offset); - const shstrtab_size = elfInt(is_64, need_bswap, shstr32.sh_size, shstr64.sh_size); - var strtab_buf: [4096:0]u8 = undefined; - const shstrtab_len = @min(shstrtab_size, strtab_buf.len); - const shstrtab_read_len = try preadAtLeast(file, &strtab_buf, shstrtab_off, shstrtab_len); - const shstrtab = strtab_buf[0..shstrtab_read_len]; - const shnum = elfInt(is_64, need_bswap, hdr32.e_shnum, hdr64.e_shnum); - var sh_i: u16 = 0; - const dynstr: struct { offset: u64, size: u64 } = find_dyn_str: while (sh_i < shnum) { - // Reserve some bytes so that we can deref the 64-bit struct fields - // even when the ELF file is 32-bits. - const sh_reserve: usize = @sizeOf(elf.Elf64_Shdr) - @sizeOf(elf.Elf32_Shdr); - const sh_read_byte_len = try preadAtLeast( - file, - sh_buf[0 .. sh_buf.len - sh_reserve], - shoff, - shentsize, - ); - var sh_buf_i: usize = 0; - while (sh_buf_i < sh_read_byte_len and sh_i < shnum) : ({ - sh_i += 1; - shoff += shentsize; - sh_buf_i += shentsize; - }) { - const sh32: *elf.Elf32_Shdr = @ptrCast(@alignCast(&sh_buf[sh_buf_i])); - const sh64: *elf.Elf64_Shdr = @ptrCast(@alignCast(&sh_buf[sh_buf_i])); - const sh_name_off = elfInt(is_64, need_bswap, sh32.sh_name, sh64.sh_name); - const sh_name = mem.sliceTo(shstrtab[sh_name_off..], 0); - if (mem.eql(u8, sh_name, ".dynstr")) { - break :find_dyn_str .{ - .offset = elfInt(is_64, need_bswap, sh32.sh_offset, sh64.sh_offset), - .size = elfInt(is_64, need_bswap, sh32.sh_size, sh64.sh_size), - }; - } - } - } else return error.InvalidGnuLibCVersion; // Here we loop over all the strings in the dynstr string table, assuming that any // strings that start with "GLIBC_2." indicate the existence of such a glibc version, // and furthermore, that the system-installed glibc is at minimum that version. - - // Empirically, glibc 2.34 libc.so .dynstr section is 32441 bytes on my system. - // Here I use double this value plus some headroom. This makes it only need - // a single read syscall here. - var buf: [80000]u8 = undefined; - if (buf.len < dynstr.size) return error.InvalidGnuLibCVersion; - - const dynstr_size: usize = @intCast(dynstr.size); - const dynstr_bytes = buf[0..dynstr_size]; - _ = try preadAtLeast(file, dynstr_bytes, dynstr.offset, dynstr_bytes.len); - var it = mem.splitScalar(u8, dynstr_bytes, 0); var max_ver: std.SemanticVersion = .{ .major = 2, .minor = 2, .patch = 5 }; - while (it.next()) |s| { - if (mem.startsWith(u8, s, "GLIBC_2.")) { - const chopped = s["GLIBC_".len..]; - const ver = Target.Query.parseVersion(chopped) catch |err| switch (err) { - error.Overflow => return error.InvalidGnuLibCVersion, - error.InvalidVersion => return error.InvalidGnuLibCVersion, - }; - switch (ver.order(max_ver)) { - .gt => max_ver = ver, - .lt, .eq => continue, + var offset: u64 = 0; + try file_reader.seekTo(dynstr.offset); + while (offset < dynstr.size) { + if (file_reader.interface.takeSentinel(0)) |s| { + if (mem.startsWith(u8, s, "GLIBC_2.")) { + const chopped = s["GLIBC_".len..]; + const ver = Target.Query.parseVersion(chopped) catch |err| switch (err) { + error.Overflow => return error.InvalidGnuLibCVersion, + error.InvalidVersion => return error.InvalidGnuLibCVersion, + }; + switch (ver.order(max_ver)) { + .gt => max_ver = ver, + .lt, .eq => continue, + } } + offset += s.len + 1; + } else |err| switch (err) { + error.EndOfStream, error.StreamTooLong => break, + error.ReadFailed => |e| return e, } } + return max_ver; } @@ -1044,11 +933,7 @@ fn glibcVerFromSoFile(file: fs.File) !std.SemanticVersion { /// answer to these questions, or if there is a shebang line, then it chases the referenced /// file recursively. If that does not provide the answer, then the function falls back to /// defaults. -fn detectAbiAndDynamicLinker( - cpu: Target.Cpu, - os: Target.Os, - query: Target.Query, -) DetectError!Target { +fn detectAbiAndDynamicLinker(io: Io, cpu: Target.Cpu, os: Target.Os, query: Target.Query) !Target { const native_target_has_ld = comptime Target.DynamicLinker.kind(builtin.os.tag) != .none; const is_linux = builtin.target.os.tag == .linux; const is_illumos = builtin.target.os.tag == .illumos; @@ -1111,49 +996,49 @@ fn detectAbiAndDynamicLinker( const ld_info_list = ld_info_list_buffer[0..ld_info_list_len]; + var file_reader: Io.File.Reader = undefined; + // According to `man 2 execve`: + // + // The kernel imposes a maximum length on the text + // that follows the "#!" characters at the start of a script; + // characters beyond the limit are ignored. + // Before Linux 5.1, the limit is 127 characters. + // Since Linux 5.1, the limit is 255 characters. + // + // Tests show that bash and zsh consider 255 as total limit, + // *including* "#!" characters and ignoring newline. + // For safety, we set max length as 255 + \n (1). + const max_shebang_line_size = 256; + var file_reader_buffer: [4096]u8 = undefined; + comptime assert(file_reader_buffer.len >= max_shebang_line_size); + // Best case scenario: the executable is dynamically linked, and we can iterate // over our own shared objects and find a dynamic linker. - const elf_file = elf_file: { - // This block looks for a shebang line in /usr/bin/env, - // if it finds one, then instead of using /usr/bin/env as the ELF file to examine, it uses the file it references instead, - // doing the same logic recursively in case it finds another shebang line. + const header = elf_file: { + // This block looks for a shebang line in "/usr/bin/env". If it finds + // one, then instead of using "/usr/bin/env" as the ELF file to examine, + // it uses the file it references instead, doing the same logic + // recursively in case it finds another shebang line. var file_name: []const u8 = switch (os.tag) { - // Since /usr/bin/env is hard-coded into the shebang line of many portable scripts, it's a - // reasonably reliable path to start with. + // Since /usr/bin/env is hard-coded into the shebang line of many + // portable scripts, it's a reasonably reliable path to start with. else => "/usr/bin/env", // Haiku does not have a /usr root directory. .haiku => "/bin/env", }; - // According to `man 2 execve`: - // - // The kernel imposes a maximum length on the text - // that follows the "#!" characters at the start of a script; - // characters beyond the limit are ignored. - // Before Linux 5.1, the limit is 127 characters. - // Since Linux 5.1, the limit is 255 characters. - // - // Tests show that bash and zsh consider 255 as total limit, - // *including* "#!" characters and ignoring newline. - // For safety, we set max length as 255 + \n (1). - var buffer: [255 + 1]u8 = undefined; while (true) { - // Interpreter path can be relative on Linux, but - // for simplicity we are asserting it is an absolute path. const file = fs.openFileAbsolute(file_name, .{}) catch |err| switch (err) { - error.NoSpaceLeft => unreachable, - error.NameTooLong => unreachable, - error.PathAlreadyExists => unreachable, - error.SharingViolation => unreachable, - error.InvalidUtf8 => unreachable, // WASI only - error.InvalidWtf8 => unreachable, // Windows only - error.BadPathName => unreachable, - error.PipeBusy => unreachable, - error.FileLocksNotSupported => unreachable, - error.WouldBlock => unreachable, - error.FileBusy => unreachable, // opened without write permissions - error.AntivirusInterference => unreachable, // Windows-only error + error.NoSpaceLeft => return error.Unexpected, + error.NameTooLong => return error.Unexpected, + error.PathAlreadyExists => return error.Unexpected, + error.SharingViolation => return error.Unexpected, + error.BadPathName => return error.Unexpected, + error.PipeBusy => return error.Unexpected, + error.FileLocksNotSupported => return error.Unexpected, + error.FileBusy => return error.Unexpected, // opened without write permissions + error.AntivirusInterference => return error.Unexpected, // Windows-only error error.IsDir, error.NotDir, @@ -1164,87 +1049,71 @@ fn detectAbiAndDynamicLinker( error.NetworkNotFound, error.FileTooBig, error.Unexpected, - => |e| { - std.log.warn("Encountered error: {s}, falling back to default ABI and dynamic linker.", .{@errorName(e)}); - return defaultAbiAndDynamicLinker(cpu, os, query); - }, + => return error.UnableToOpenElfFile, else => |e| return e, }; var is_elf_file = false; - defer if (is_elf_file == false) file.close(); + defer if (!is_elf_file) file.close(); - // Shortest working interpreter path is "#!/i" (4) - // (interpreter is "/i", assuming all paths are absolute, like in above comment). - // ELF magic number length is also 4. - // - // If file is shorter than that, it is definitely not ELF file - // nor file with "shebang" line. - const min_len: usize = 4; + file_reader = .initAdapted(file, io, &file_reader_buffer); + file_name = undefined; // it aliases file_reader_buffer - const len = preadAtLeast(file, &buffer, 0, min_len) catch |err| switch (err) { - error.UnexpectedEndOfFile, - error.UnableToReadElfFile, - error.ProcessNotFound, - => return defaultAbiAndDynamicLinker(cpu, os, query), + const header = elf.Header.read(&file_reader.interface) catch |hdr_err| switch (hdr_err) { + error.EndOfStream, + error.InvalidElfMagic, + => { + const shebang_line = file_reader.interface.takeSentinel('\n') catch |err| switch (err) { + error.ReadFailed => return file_reader.err.?, + // It's neither an ELF file nor file with shebang line. + error.EndOfStream, error.StreamTooLong => return error.UnhelpfulFile, + }; + if (!mem.startsWith(u8, shebang_line, "#!")) return error.UnhelpfulFile; + // We detected shebang, now parse entire line. - else => |e| return e, + // Trim leading "#!", spaces and tabs. + const trimmed_line = mem.trimStart(u8, shebang_line[2..], &.{ ' ', '\t' }); + + // This line can have: + // * Interpreter path only, + // * Interpreter path and arguments, all separated by space, tab or NUL character. + // And optionally newline at the end. + const path_maybe_args = mem.trimEnd(u8, trimmed_line, "\n"); + + // Separate path and args. + const path_end = mem.indexOfAny(u8, path_maybe_args, &.{ ' ', '\t', 0 }) orelse path_maybe_args.len; + const unvalidated_path = path_maybe_args[0..path_end]; + file_name = if (fs.path.isAbsolute(unvalidated_path)) unvalidated_path else return error.RelativeShebang; + continue; + }, + + error.InvalidElfVersion, + error.InvalidElfClass, + error.InvalidElfEndian, + => return error.InvalidElfFile, + + error.ReadFailed => return file_reader.err.?, }; - const content = buffer[0..len]; - - if (mem.eql(u8, content[0..4], std.elf.MAGIC)) { - // It is very likely ELF file! - is_elf_file = true; - break :elf_file file; - } else if (mem.eql(u8, content[0..2], "#!")) { - // We detected shebang, now parse entire line. - - // Trim leading "#!", spaces and tabs. - const trimmed_line = mem.trimStart(u8, content[2..], &.{ ' ', '\t' }); - - // This line can have: - // * Interpreter path only, - // * Interpreter path and arguments, all separated by space, tab or NUL character. - // And optionally newline at the end. - const path_maybe_args = mem.trimEnd(u8, trimmed_line, "\n"); - - // Separate path and args. - const path_end = mem.indexOfAny(u8, path_maybe_args, &.{ ' ', '\t', 0 }) orelse path_maybe_args.len; - - file_name = path_maybe_args[0..path_end]; - continue; - } else { - // Not a ELF file, not a shell script with "shebang line", invalid duck. - return defaultAbiAndDynamicLinker(cpu, os, query); - } + is_elf_file = true; + break :elf_file header; } }; - defer elf_file.close(); + defer file_reader.file.close(io); - // TODO: inline this function and combine the buffer we already read above to find - // the possible shebang line with the buffer we use for the ELF header. - return abiAndDynamicLinkerFromFile(elf_file, cpu, os, ld_info_list, query) catch |err| switch (err) { + return abiAndDynamicLinkerFromFile(&file_reader, &header, cpu, os, ld_info_list, query) catch |err| switch (err) { error.FileSystem, error.SystemResources, error.SymLinkLoop, error.ProcessFdQuotaExceeded, error.SystemFdQuotaExceeded, error.ProcessNotFound, + error.Canceled, => |e| return e, - error.UnableToReadElfFile, - error.InvalidElfClass, - error.InvalidElfVersion, - error.InvalidElfEndian, - error.InvalidElfFile, - error.InvalidElfMagic, - error.Unexpected, - error.UnexpectedEndOfFile, - error.NameTooLong, - error.StaticElfFile, - // Finally, we fall back on the standard path. - => |e| { - std.log.warn("Encountered error: {s}, falling back to default ABI and dynamic linker.", .{@errorName(e)}); + error.ReadFailed => return file_reader.err.?, + + else => |e| { + std.log.warn("encountered {t}; falling back to default ABI and dynamic linker", .{e}); return defaultAbiAndDynamicLinker(cpu, os, query); }, }; @@ -1269,59 +1138,6 @@ const LdInfo = struct { abi: Target.Abi, }; -fn preadAtLeast(file: fs.File, buf: []u8, offset: u64, min_read_len: usize) !usize { - var i: usize = 0; - while (i < min_read_len) { - const len = file.pread(buf[i..], offset + i) catch |err| switch (err) { - error.OperationAborted => unreachable, // Windows-only - error.WouldBlock => unreachable, // Did not request blocking mode - error.Canceled => unreachable, // timerfd is unseekable - error.NotOpenForReading => unreachable, - error.SystemResources => return error.SystemResources, - error.IsDir => return error.UnableToReadElfFile, - error.BrokenPipe => return error.UnableToReadElfFile, - error.Unseekable => return error.UnableToReadElfFile, - error.ConnectionResetByPeer => return error.UnableToReadElfFile, - error.ConnectionTimedOut => return error.UnableToReadElfFile, - error.SocketNotConnected => return error.UnableToReadElfFile, - error.Unexpected => return error.Unexpected, - error.InputOutput => return error.FileSystem, - error.AccessDenied => return error.Unexpected, - error.ProcessNotFound => return error.ProcessNotFound, - error.LockViolation => return error.UnableToReadElfFile, - }; - if (len == 0) return error.UnexpectedEndOfFile; - i += len; - } - return i; -} - -fn elfInt(is_64: bool, need_bswap: bool, int_32: anytype, int_64: anytype) @TypeOf(int_64) { - if (is_64) { - if (need_bswap) { - return @byteSwap(int_64); - } else { - return int_64; - } - } else { - if (need_bswap) { - return @byteSwap(int_32); - } else { - return int_32; - } - } -} - -const builtin = @import("builtin"); -const std = @import("../std.zig"); -const mem = std.mem; -const elf = std.elf; -const fs = std.fs; -const assert = std.debug.assert; -const Target = std.Target; -const native_endian = builtin.cpu.arch.endian(); -const posix = std.posix; - test { _ = NativePaths; diff --git a/lib/std/zig/system/linux.zig b/lib/std/zig/system/linux.zig index bbe7f8eaf9..8bdfae1de4 100644 --- a/lib/std/zig/system/linux.zig +++ b/lib/std/zig/system/linux.zig @@ -1,5 +1,7 @@ -const std = @import("std"); const builtin = @import("builtin"); + +const std = @import("std"); +const Io = std.Io; const mem = std.mem; const fs = std.fs; const fmt = std.fmt; @@ -344,7 +346,7 @@ fn testParser( expected_model: *const Target.Cpu.Model, input: []const u8, ) !void { - var r: std.Io.Reader = .fixed(input); + var r: Io.Reader = .fixed(input); const result = try parser.parse(arch, &r); try testing.expectEqual(expected_model, result.?.model); try testing.expect(expected_model.features.eql(result.?.features)); @@ -357,7 +359,7 @@ fn testParser( // When all the lines have been analyzed the finalize method is called. fn CpuinfoParser(comptime impl: anytype) type { return struct { - fn parse(arch: Target.Cpu.Arch, reader: *std.Io.Reader) !?Target.Cpu { + fn parse(arch: Target.Cpu.Arch, reader: *Io.Reader) !?Target.Cpu { var obj: impl = .{}; while (try reader.takeDelimiter('\n')) |line| { const colon_pos = mem.indexOfScalar(u8, line, ':') orelse continue; @@ -376,14 +378,14 @@ inline fn getAArch64CpuFeature(comptime feat_reg: []const u8) u64 { ); } -pub fn detectNativeCpuAndFeatures() ?Target.Cpu { +pub fn detectNativeCpuAndFeatures(io: Io) ?Target.Cpu { var file = fs.openFileAbsolute("/proc/cpuinfo", .{}) catch |err| switch (err) { else => return null, }; defer file.close(); var buffer: [4096]u8 = undefined; // "flags" lines can get pretty long. - var file_reader = file.reader(&buffer); + var file_reader = file.reader(io, &buffer); const current_arch = builtin.cpu.arch; switch (current_arch) { diff --git a/src/Builtin.zig b/src/Builtin.zig index 7680d495d7..b0077f2276 100644 --- a/src/Builtin.zig +++ b/src/Builtin.zig @@ -360,7 +360,7 @@ pub fn updateFileOnDisk(file: *File, comp: *Compilation) !void { file.stat = .{ .size = file.source.?.len, .inode = 0, // dummy value - .mtime = 0, // dummy value + .mtime = .zero, // dummy value }; } diff --git a/src/Compilation.zig b/src/Compilation.zig index 760c8f8b11..3670bc51b5 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -1,7 +1,9 @@ const Compilation = @This(); +const builtin = @import("builtin"); const std = @import("std"); -const builtin = @import("builtin"); +const Io = std.Io; +const Writer = std.Io.Writer; const fs = std.fs; const mem = std.mem; const Allocator = std.mem.Allocator; @@ -12,7 +14,6 @@ const ThreadPool = std.Thread.Pool; const WaitGroup = std.Thread.WaitGroup; const ErrorBundle = std.zig.ErrorBundle; const fatal = std.process.fatal; -const Writer = std.Io.Writer; const Value = @import("Value.zig"); const Type = @import("Type.zig"); @@ -54,6 +55,7 @@ gpa: Allocator, /// Not thread-safe - lock `mutex` if potentially accessing from multiple /// threads at once. arena: Allocator, +io: Io, /// Not every Compilation compiles .zig code! For example you could do `zig build-exe foo.o`. zcu: ?*Zcu, /// Contains different state depending on the `CacheMode` used by this `Compilation`. @@ -1076,21 +1078,22 @@ pub const CObject = struct { diag.* = undefined; } - pub fn count(diag: Diag) u32 { + pub fn count(diag: *const Diag) u32 { var total: u32 = 1; for (diag.sub_diags) |sub_diag| total += sub_diag.count(); return total; } - pub fn addToErrorBundle(diag: Diag, eb: *ErrorBundle.Wip, bundle: Bundle, note: *u32) !void { - const err_msg = try eb.addErrorMessage(try diag.toErrorMessage(eb, bundle, 0)); + pub fn addToErrorBundle(diag: *const Diag, io: Io, eb: *ErrorBundle.Wip, bundle: Bundle, note: *u32) !void { + const err_msg = try eb.addErrorMessage(try diag.toErrorMessage(io, eb, bundle, 0)); eb.extra.items[note.*] = @intFromEnum(err_msg); note.* += 1; - for (diag.sub_diags) |sub_diag| try sub_diag.addToErrorBundle(eb, bundle, note); + for (diag.sub_diags) |sub_diag| try sub_diag.addToErrorBundle(io, eb, bundle, note); } pub fn toErrorMessage( - diag: Diag, + diag: *const Diag, + io: Io, eb: *ErrorBundle.Wip, bundle: Bundle, notes_len: u32, @@ -1117,7 +1120,7 @@ pub const CObject = struct { const file = fs.cwd().openFile(file_name, .{}) catch break :source_line 0; defer file.close(); var buffer: [1024]u8 = undefined; - var file_reader = file.reader(&buffer); + var file_reader = file.reader(io, &buffer); file_reader.seekTo(diag.src_loc.offset + 1 - diag.src_loc.column) catch break :source_line 0; var aw: Writer.Allocating = .init(eb.gpa); defer aw.deinit(); @@ -1155,7 +1158,7 @@ pub const CObject = struct { gpa.destroy(bundle); } - pub fn parse(gpa: Allocator, path: []const u8) !*Bundle { + pub fn parse(gpa: Allocator, io: Io, path: []const u8) !*Bundle { const BlockId = enum(u32) { Meta = 8, Diag, @@ -1191,7 +1194,7 @@ pub const CObject = struct { var buffer: [1024]u8 = undefined; const file = try fs.cwd().openFile(path, .{}); defer file.close(); - var file_reader = file.reader(&buffer); + var file_reader = file.reader(io, &buffer); var bc = std.zig.llvm.BitcodeReader.init(gpa, .{ .reader = &file_reader.interface }); defer bc.deinit(); @@ -1305,14 +1308,14 @@ pub const CObject = struct { return bundle; } - pub fn addToErrorBundle(bundle: Bundle, eb: *ErrorBundle.Wip) !void { + pub fn addToErrorBundle(bundle: Bundle, io: Io, eb: *ErrorBundle.Wip) !void { for (bundle.diags) |diag| { const notes_len = diag.count() - 1; - try eb.addRootErrorMessage(try diag.toErrorMessage(eb, bundle, notes_len)); + try eb.addRootErrorMessage(try diag.toErrorMessage(io, eb, bundle, notes_len)); if (notes_len > 0) { var note = try eb.reserveNotes(notes_len); for (diag.sub_diags) |sub_diag| - try sub_diag.addToErrorBundle(eb, bundle, ¬e); + try sub_diag.addToErrorBundle(io, eb, bundle, ¬e); } } } @@ -1904,7 +1907,7 @@ pub const CreateDiagnostic = union(enum) { return error.CreateFail; } }; -pub fn create(gpa: Allocator, arena: Allocator, diag: *CreateDiagnostic, options: CreateOptions) error{ +pub fn create(gpa: Allocator, arena: Allocator, io: Io, diag: *CreateDiagnostic, options: CreateOptions) error{ OutOfMemory, Unexpected, CurrentWorkingDirectoryUnlinked, @@ -2112,6 +2115,7 @@ pub fn create(gpa: Allocator, arena: Allocator, diag: *CreateDiagnostic, options const cache = try arena.create(Cache); cache.* = .{ .gpa = gpa, + .io = io, .manifest_dir = options.dirs.local_cache.handle.makeOpenPath("h", .{}) catch |err| { return diag.fail(.{ .create_cache_path = .{ .which = .local, .sub = "h", .err = err } }); }, @@ -2230,6 +2234,7 @@ pub fn create(gpa: Allocator, arena: Allocator, diag: *CreateDiagnostic, options comp.* = .{ .gpa = gpa, .arena = arena, + .io = io, .zcu = opt_zcu, .cache_use = undefined, // populated below .bin_file = null, // populated below if necessary @@ -3917,13 +3922,14 @@ fn addBuf(list: *std.array_list.Managed([]const u8), buf: []const u8) void { /// This function is temporally single-threaded. pub fn getAllErrorsAlloc(comp: *Compilation) error{OutOfMemory}!ErrorBundle { const gpa = comp.gpa; + const io = comp.io; var bundle: ErrorBundle.Wip = undefined; try bundle.init(gpa); defer bundle.deinit(); for (comp.failed_c_objects.values()) |diag_bundle| { - try diag_bundle.addToErrorBundle(&bundle); + try diag_bundle.addToErrorBundle(io, &bundle); } for (comp.failed_win32_resources.values()) |error_bundle| { @@ -5308,6 +5314,7 @@ fn docsCopyModule( name: []const u8, tar_file_writer: *fs.File.Writer, ) !void { + const io = comp.io; const root = module.root; var mod_dir = d: { const root_dir, const sub_path = root.openInfo(comp.dirs); @@ -5341,9 +5348,9 @@ fn docsCopyModule( }; defer file.close(); const stat = try file.stat(); - var file_reader: fs.File.Reader = .initSize(file, &buffer, stat.size); + var file_reader: fs.File.Reader = .initSize(file.adaptToNewApi(), io, &buffer, stat.size); - archiver.writeFile(entry.path, &file_reader, stat.mtime) catch |err| { + archiver.writeFileTimestamp(entry.path, &file_reader, stat.mtime) catch |err| { return comp.lockAndSetMiscFailure(.docs_copy, "unable to archive {f}{s}: {t}", .{ root.fmt(comp), entry.path, err, }); @@ -5363,6 +5370,7 @@ fn workerDocsWasm(comp: *Compilation, parent_prog_node: std.Progress.Node) void fn workerDocsWasmFallible(comp: *Compilation, prog_node: std.Progress.Node) SubUpdateError!void { const gpa = comp.gpa; + const io = comp.io; var arena_allocator = std.heap.ArenaAllocator.init(gpa); defer arena_allocator.deinit(); @@ -5371,7 +5379,7 @@ fn workerDocsWasmFallible(comp: *Compilation, prog_node: std.Progress.Node) SubU const optimize_mode = std.builtin.OptimizeMode.ReleaseSmall; const output_mode = std.builtin.OutputMode.Exe; const resolved_target: Package.Module.ResolvedTarget = .{ - .result = std.zig.system.resolveTargetQuery(.{ + .result = std.zig.system.resolveTargetQuery(io, .{ .cpu_arch = .wasm32, .os_tag = .freestanding, .cpu_features_add = std.Target.wasm.featureSet(&.{ @@ -5447,7 +5455,7 @@ fn workerDocsWasmFallible(comp: *Compilation, prog_node: std.Progress.Node) SubU try root_mod.deps.put(arena, "Walk", walk_mod); var sub_create_diag: CreateDiagnostic = undefined; - const sub_compilation = Compilation.create(gpa, arena, &sub_create_diag, .{ + const sub_compilation = Compilation.create(gpa, arena, io, &sub_create_diag, .{ .dirs = dirs, .self_exe_path = comp.self_exe_path, .config = config, @@ -5665,6 +5673,8 @@ pub fn translateC( ) !CImportResult { dev.check(.translate_c_command); + const gpa = comp.gpa; + const io = comp.io; const tmp_basename = std.fmt.hex(std.crypto.random.int(u64)); const tmp_sub_path = "tmp" ++ fs.path.sep_str ++ tmp_basename; const cache_dir = comp.dirs.local_cache.handle; @@ -5704,9 +5714,9 @@ pub fn translateC( const mcpu = mcpu: { var buf: std.ArrayListUnmanaged(u8) = .empty; - defer buf.deinit(comp.gpa); + defer buf.deinit(gpa); - try buf.print(comp.gpa, "-mcpu={s}", .{target.cpu.model.name}); + try buf.print(gpa, "-mcpu={s}", .{target.cpu.model.name}); // TODO better serialization https://github.com/ziglang/zig/issues/4584 const all_features_list = target.cpu.arch.allFeaturesList(); @@ -5716,7 +5726,7 @@ pub fn translateC( const is_enabled = target.cpu.features.isEnabled(index); const plus_or_minus = "-+"[@intFromBool(is_enabled)]; - try buf.print(comp.gpa, "{c}{s}", .{ plus_or_minus, feature.name }); + try buf.print(gpa, "{c}{s}", .{ plus_or_minus, feature.name }); } break :mcpu try buf.toOwnedSlice(arena); }; @@ -5729,7 +5739,7 @@ pub fn translateC( } var stdout: []u8 = undefined; - try @import("main.zig").translateC(comp.gpa, arena, argv.items, prog_node, &stdout); + try @import("main.zig").translateC(gpa, arena, io, argv.items, prog_node, &stdout); if (out_dep_path) |dep_file_path| add_deps: { if (comp.verbose_cimport) log.info("processing dep file at {s}", .{dep_file_path}); @@ -5765,7 +5775,7 @@ pub fn translateC( fatal("unable to read {}-byte translate-c message body: {s}", .{ header.bytes_len, @errorName(err) }); switch (header.tag) { .error_bundle => { - const error_bundle = try std.zig.Server.allocErrorBundle(comp.gpa, body); + const error_bundle = try std.zig.Server.allocErrorBundle(gpa, body); return .{ .digest = undefined, .cache_hit = false, @@ -6152,6 +6162,7 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr log.debug("updating C object: {s}", .{c_object.src.src_path}); const gpa = comp.gpa; + const io = comp.io; if (c_object.clearStatus(gpa)) { // There was previous failure. @@ -6351,7 +6362,7 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr try child.spawn(); - var stderr_reader = child.stderr.?.readerStreaming(&.{}); + var stderr_reader = child.stderr.?.readerStreaming(io, &.{}); const stderr = try stderr_reader.interface.allocRemaining(arena, .limited(std.math.maxInt(u32))); const term = child.wait() catch |err| { @@ -6360,7 +6371,7 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr switch (term) { .Exited => |code| if (code != 0) if (out_diag_path) |diag_file_path| { - const bundle = CObject.Diag.Bundle.parse(gpa, diag_file_path) catch |err| { + const bundle = CObject.Diag.Bundle.parse(gpa, io, diag_file_path) catch |err| { log.err("{}: failed to parse clang diagnostics: {s}", .{ err, stderr }); return comp.failCObj(c_object, "clang exited with code {d}", .{code}); }; @@ -7805,6 +7816,7 @@ fn buildOutputFromZig( defer tracy_trace.end(); const gpa = comp.gpa; + const io = comp.io; var arena_allocator = std.heap.ArenaAllocator.init(gpa); defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); @@ -7878,7 +7890,7 @@ fn buildOutputFromZig( }; var sub_create_diag: CreateDiagnostic = undefined; - const sub_compilation = Compilation.create(gpa, arena, &sub_create_diag, .{ + const sub_compilation = Compilation.create(gpa, arena, io, &sub_create_diag, .{ .dirs = comp.dirs.withoutLocalCache(), .cache_mode = .whole, .parent_whole_cache = parent_whole_cache, @@ -7946,6 +7958,7 @@ pub fn build_crt_file( defer tracy_trace.end(); const gpa = comp.gpa; + const io = comp.io; var arena_allocator = std.heap.ArenaAllocator.init(gpa); defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); @@ -8014,7 +8027,7 @@ pub fn build_crt_file( } var sub_create_diag: CreateDiagnostic = undefined; - const sub_compilation = Compilation.create(gpa, arena, &sub_create_diag, .{ + const sub_compilation = Compilation.create(gpa, arena, io, &sub_create_diag, .{ .dirs = comp.dirs.withoutLocalCache(), .self_exe_path = comp.self_exe_path, .cache_mode = .whole, diff --git a/src/IncrementalDebugServer.zig b/src/IncrementalDebugServer.zig index 358b1a4327..0ac1b360b8 100644 --- a/src/IncrementalDebugServer.zig +++ b/src/IncrementalDebugServer.zig @@ -44,22 +44,24 @@ pub fn spawn(ids: *IncrementalDebugServer) void { } fn runThread(ids: *IncrementalDebugServer) void { const gpa = ids.zcu.gpa; + const io = ids.zcu.comp.io; var cmd_buf: [1024]u8 = undefined; var text_out: std.ArrayListUnmanaged(u8) = .empty; defer text_out.deinit(gpa); - const addr = std.net.Address.parseIp6("::", port) catch unreachable; - var server = addr.listen(.{}) catch @panic("IncrementalDebugServer: failed to listen"); - defer server.deinit(); - const conn = server.accept() catch @panic("IncrementalDebugServer: failed to accept"); - defer conn.stream.close(); + const addr: std.Io.net.IpAddress = .{ .ip6 = .loopback(port) }; + var server = addr.listen(io, .{}) catch @panic("IncrementalDebugServer: failed to listen"); + defer server.deinit(io); + var stream = server.accept(io) catch @panic("IncrementalDebugServer: failed to accept"); + defer stream.close(io); - var stream_reader = conn.stream.reader(&cmd_buf); + var stream_reader = stream.reader(io, &cmd_buf); + var stream_writer = stream.writer(io, &.{}); while (ids.running.load(.monotonic)) { - conn.stream.writeAll("zig> ") catch @panic("IncrementalDebugServer: failed to write"); - const untrimmed = stream_reader.interface().takeSentinel('\n') catch |err| switch (err) { + stream_writer.interface.writeAll("zig> ") catch @panic("IncrementalDebugServer: failed to write"); + const untrimmed = stream_reader.interface.takeSentinel('\n') catch |err| switch (err) { error.EndOfStream => break, else => @panic("IncrementalDebugServer: failed to read command"), }; @@ -72,7 +74,7 @@ fn runThread(ids: *IncrementalDebugServer) void { text_out.clearRetainingCapacity(); { if (!ids.mutex.tryLock()) { - conn.stream.writeAll("waiting for in-progress update to finish...\n") catch @panic("IncrementalDebugServer: failed to write"); + stream_writer.interface.writeAll("waiting for in-progress update to finish...\n") catch @panic("IncrementalDebugServer: failed to write"); ids.mutex.lock(); } defer ids.mutex.unlock(); @@ -81,7 +83,7 @@ fn runThread(ids: *IncrementalDebugServer) void { handleCommand(ids.zcu, &allocating.writer, cmd, arg) catch @panic("IncrementalDebugServer: out of memory"); } text_out.append(gpa, '\n') catch @panic("IncrementalDebugServer: out of memory"); - conn.stream.writeAll(text_out.items) catch @panic("IncrementalDebugServer: failed to write"); + stream_writer.interface.writeAll(text_out.items) catch @panic("IncrementalDebugServer: failed to write"); } std.debug.print("closing incremental debug server\n", .{}); } @@ -373,6 +375,7 @@ fn printType(ty: Type, zcu: *const Zcu, w: anytype) !void { } const std = @import("std"); +const Io = std.Io; const Allocator = std.mem.Allocator; const Compilation = @import("Compilation.zig"); diff --git a/src/Package/Fetch.zig b/src/Package/Fetch.zig index 9b1d1f18cf..46be6fa069 100644 --- a/src/Package/Fetch.zig +++ b/src/Package/Fetch.zig @@ -26,9 +26,13 @@ //! //! All of this must be done with only referring to the state inside this struct //! because this work will be done in a dedicated thread. +const Fetch = @This(); const builtin = @import("builtin"); +const native_os = builtin.os.tag; + const std = @import("std"); +const Io = std.Io; const fs = std.fs; const assert = std.debug.assert; const ascii = std.ascii; @@ -36,14 +40,13 @@ const Allocator = std.mem.Allocator; const Cache = std.Build.Cache; const ThreadPool = std.Thread.Pool; const WaitGroup = std.Thread.WaitGroup; -const Fetch = @This(); const git = @import("Fetch/git.zig"); const Package = @import("../Package.zig"); const Manifest = Package.Manifest; const ErrorBundle = std.zig.ErrorBundle; -const native_os = builtin.os.tag; arena: std.heap.ArenaAllocator, +io: Io, location: Location, location_tok: std.zig.Ast.TokenIndex, hash_tok: std.zig.Ast.OptionalTokenIndex, @@ -323,6 +326,7 @@ pub const RunError = error{ }; pub fn run(f: *Fetch) RunError!void { + const io = f.io; const eb = &f.error_bundle; const arena = f.arena.allocator(); const gpa = f.arena.child_allocator; @@ -389,7 +393,7 @@ pub fn run(f: *Fetch) RunError!void { const file_err = if (dir_err == error.NotDir) e: { if (fs.cwd().openFile(path_or_url, .{})) |file| { - var resource: Resource = .{ .file = file.reader(&server_header_buffer) }; + var resource: Resource = .{ .file = file.reader(io, &server_header_buffer) }; return f.runResource(path_or_url, &resource, null); } else |err| break :e err; } else dir_err; @@ -484,7 +488,8 @@ fn runResource( resource: *Resource, remote_hash: ?Package.Hash, ) RunError!void { - defer resource.deinit(); + const io = f.io; + defer resource.deinit(io); const arena = f.arena.allocator(); const eb = &f.error_bundle; const s = fs.path.sep_str; @@ -697,6 +702,7 @@ fn loadManifest(f: *Fetch, pkg_root: Cache.Path) RunError!void { } fn queueJobsForDeps(f: *Fetch) RunError!void { + const io = f.io; assert(f.job_queue.recursive); // If the package does not have a build.zig.zon file then there are no dependencies. @@ -786,6 +792,7 @@ fn queueJobsForDeps(f: *Fetch) RunError!void { f.job_queue.all_fetches.appendAssumeCapacity(new_fetch); } new_fetch.* = .{ + .io = io, .arena = std.heap.ArenaAllocator.init(gpa), .location = location, .location_tok = dep.location_tok, @@ -897,9 +904,9 @@ const Resource = union(enum) { decompress_buffer: []u8, }; - fn deinit(resource: *Resource) void { + fn deinit(resource: *Resource, io: Io) void { switch (resource.*) { - .file => |*file_reader| file_reader.file.close(), + .file => |*file_reader| file_reader.file.close(io), .http_request => |*http_request| http_request.request.deinit(), .git => |*git_resource| { git_resource.fetch_stream.deinit(); @@ -909,7 +916,7 @@ const Resource = union(enum) { resource.* = undefined; } - fn reader(resource: *Resource) *std.Io.Reader { + fn reader(resource: *Resource) *Io.Reader { return switch (resource.*) { .file => |*file_reader| return &file_reader.interface, .http_request => |*http_request| return http_request.response.readerDecompressing( @@ -985,6 +992,7 @@ const FileType = enum { const init_resource_buffer_size = git.Packet.max_data_length; fn initResource(f: *Fetch, uri: std.Uri, resource: *Resource, reader_buffer: []u8) RunError!void { + const io = f.io; const arena = f.arena.allocator(); const eb = &f.error_bundle; @@ -995,7 +1003,7 @@ fn initResource(f: *Fetch, uri: std.Uri, resource: *Resource, reader_buffer: []u f.parent_package_root, path, err, })); }; - resource.* = .{ .file = file.reader(reader_buffer) }; + resource.* = .{ .file = file.reader(io, reader_buffer) }; return; } @@ -1242,7 +1250,7 @@ fn unpackResource( } } -fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: *std.Io.Reader) RunError!UnpackResult { +fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: *Io.Reader) RunError!UnpackResult { const eb = &f.error_bundle; const arena = f.arena.allocator(); @@ -1273,11 +1281,12 @@ fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: *std.Io.Reader) RunError!Un return res; } -fn unzip(f: *Fetch, out_dir: fs.Dir, reader: *std.Io.Reader) error{ ReadFailed, OutOfMemory, FetchFailed }!UnpackResult { +fn unzip(f: *Fetch, out_dir: fs.Dir, reader: *Io.Reader) error{ ReadFailed, OutOfMemory, FetchFailed }!UnpackResult { // We write the entire contents to a file first because zip files // must be processed back to front and they could be too large to // load into memory. + const io = f.io; const cache_root = f.job_queue.global_cache; const prefix = "tmp/"; const suffix = ".zip"; @@ -1319,7 +1328,7 @@ fn unzip(f: *Fetch, out_dir: fs.Dir, reader: *std.Io.Reader) error{ ReadFailed, f.location_tok, try eb.printString("failed writing temporary zip file: {t}", .{err}), ); - break :b zip_file_writer.moveToReader(); + break :b zip_file_writer.moveToReader(io); }; var diagnostics: std.zip.Diagnostics = .{ .allocator = f.arena.allocator() }; @@ -1339,7 +1348,10 @@ fn unzip(f: *Fetch, out_dir: fs.Dir, reader: *std.Io.Reader) error{ ReadFailed, } fn unpackGitPack(f: *Fetch, out_dir: fs.Dir, resource: *Resource.Git) anyerror!UnpackResult { + const io = f.io; const arena = f.arena.allocator(); + // TODO don't try to get a gpa from an arena. expose this dependency higher up + // because the backing of arena could be page allocator const gpa = f.arena.child_allocator; const object_format: git.Oid.Format = resource.want_oid; @@ -1358,7 +1370,7 @@ fn unpackGitPack(f: *Fetch, out_dir: fs.Dir, resource: *Resource.Git) anyerror!U const fetch_reader = &resource.fetch_stream.reader; _ = try fetch_reader.streamRemaining(&pack_file_writer.interface); try pack_file_writer.interface.flush(); - break :b pack_file_writer.moveToReader(); + break :b pack_file_writer.moveToReader(io); }; var index_file = try pack_dir.createFile("pkg.idx", .{ .read = true }); @@ -1372,7 +1384,7 @@ fn unpackGitPack(f: *Fetch, out_dir: fs.Dir, resource: *Resource.Git) anyerror!U } { - var index_file_reader = index_file.reader(&index_file_buffer); + var index_file_reader = index_file.reader(io, &index_file_buffer); const checkout_prog_node = f.prog_node.start("Checkout", 0); defer checkout_prog_node.end(); var repository: git.Repository = undefined; @@ -2029,7 +2041,7 @@ const UnpackResult = struct { // output errors to string var errors = try fetch.error_bundle.toOwnedBundle(""); defer errors.deinit(gpa); - var aw: std.Io.Writer.Allocating = .init(gpa); + var aw: Io.Writer.Allocating = .init(gpa); defer aw.deinit(); try errors.renderToWriter(.{ .ttyconf = .no_color }, &aw.writer); try std.testing.expectEqualStrings( @@ -2057,6 +2069,7 @@ test "tarball with duplicate paths" { // const gpa = std.testing.allocator; + const io = std.testing.io; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); @@ -2067,7 +2080,7 @@ test "tarball with duplicate paths" { // Run tarball fetch, expect to fail var fb: TestFetchBuilder = undefined; - var fetch = try fb.build(gpa, tmp.dir, tarball_path); + var fetch = try fb.build(gpa, io, tmp.dir, tarball_path); defer fb.deinit(); try std.testing.expectError(error.FetchFailed, fetch.run()); @@ -2089,6 +2102,7 @@ test "tarball with excluded duplicate paths" { // const gpa = std.testing.allocator; + const io = std.testing.io; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); @@ -2099,7 +2113,7 @@ test "tarball with excluded duplicate paths" { // Run tarball fetch, should succeed var fb: TestFetchBuilder = undefined; - var fetch = try fb.build(gpa, tmp.dir, tarball_path); + var fetch = try fb.build(gpa, io, tmp.dir, tarball_path); defer fb.deinit(); try fetch.run(); @@ -2133,6 +2147,8 @@ test "tarball without root folder" { // const gpa = std.testing.allocator; + const io = std.testing.io; + var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); @@ -2143,7 +2159,7 @@ test "tarball without root folder" { // Run tarball fetch, should succeed var fb: TestFetchBuilder = undefined; - var fetch = try fb.build(gpa, tmp.dir, tarball_path); + var fetch = try fb.build(gpa, io, tmp.dir, tarball_path); defer fb.deinit(); try fetch.run(); @@ -2164,6 +2180,8 @@ test "tarball without root folder" { test "set executable bit based on file content" { if (!std.fs.has_executable_bit) return error.SkipZigTest; const gpa = std.testing.allocator; + const io = std.testing.io; + var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); @@ -2182,7 +2200,7 @@ test "set executable bit based on file content" { // -rwxrwxr-x 17 executables/script var fb: TestFetchBuilder = undefined; - var fetch = try fb.build(gpa, tmp.dir, tarball_path); + var fetch = try fb.build(gpa, io, tmp.dir, tarball_path); defer fb.deinit(); try fetch.run(); @@ -2232,13 +2250,14 @@ const TestFetchBuilder = struct { fn build( self: *TestFetchBuilder, allocator: std.mem.Allocator, + io: Io, cache_parent_dir: std.fs.Dir, path_or_url: []const u8, ) !*Fetch { const cache_dir = try cache_parent_dir.makeOpenPath("zig-global-cache", .{}); try self.thread_pool.init(.{ .allocator = allocator }); - self.http_client = .{ .allocator = allocator }; + self.http_client = .{ .allocator = allocator, .io = io }; self.global_cache_directory = .{ .handle = cache_dir, .path = null }; self.job_queue = .{ @@ -2254,6 +2273,7 @@ const TestFetchBuilder = struct { self.fetch = .{ .arena = std.heap.ArenaAllocator.init(allocator), + .io = io, .location = .{ .path_or_url = path_or_url }, .location_tok = 0, .hash_tok = .none, @@ -2338,7 +2358,7 @@ const TestFetchBuilder = struct { if (notes_len > 0) { try std.testing.expectEqual(notes_len, em.notes_len); } - var aw: std.Io.Writer.Allocating = .init(std.testing.allocator); + var aw: Io.Writer.Allocating = .init(std.testing.allocator); defer aw.deinit(); try errors.renderToWriter(.{ .ttyconf = .no_color }, &aw.writer); try std.testing.expectEqualStrings(msg, aw.written()); diff --git a/src/Package/Fetch/git.zig b/src/Package/Fetch/git.zig index df0366c783..1d01b58633 100644 --- a/src/Package/Fetch/git.zig +++ b/src/Package/Fetch/git.zig @@ -5,6 +5,7 @@ //! a package. const std = @import("std"); +const Io = std.Io; const mem = std.mem; const testing = std.testing; const Allocator = mem.Allocator; @@ -67,8 +68,8 @@ pub const Oid = union(Format) { }; const Hashing = union(Format) { - sha1: std.Io.Writer.Hashing(Sha1), - sha256: std.Io.Writer.Hashing(Sha256), + sha1: Io.Writer.Hashing(Sha1), + sha256: Io.Writer.Hashing(Sha256), fn init(oid_format: Format, buffer: []u8) Hashing { return switch (oid_format) { @@ -77,7 +78,7 @@ pub const Oid = union(Format) { }; } - fn writer(h: *@This()) *std.Io.Writer { + fn writer(h: *@This()) *Io.Writer { return switch (h.*) { inline else => |*inner| &inner.writer, }; @@ -100,7 +101,7 @@ pub const Oid = union(Format) { }; } - pub fn readBytes(oid_format: Format, reader: *std.Io.Reader) !Oid { + pub fn readBytes(oid_format: Format, reader: *Io.Reader) !Oid { return switch (oid_format) { inline else => |tag| @unionInit(Oid, @tagName(tag), (try reader.takeArray(tag.byteLength())).*), }; @@ -146,7 +147,7 @@ pub const Oid = union(Format) { } else error.InvalidOid; } - pub fn format(oid: Oid, writer: *std.Io.Writer) std.Io.Writer.Error!void { + pub fn format(oid: Oid, writer: *Io.Writer) Io.Writer.Error!void { try writer.print("{x}", .{oid.slice()}); } @@ -594,7 +595,7 @@ pub const Packet = union(enum) { pub const max_data_length = 65516; /// Reads a packet in pkt-line format. - fn read(reader: *std.Io.Reader) !Packet { + fn read(reader: *Io.Reader) !Packet { const packet: Packet = try .peek(reader); switch (packet) { .data => |data| reader.toss(data.len), @@ -605,7 +606,7 @@ pub const Packet = union(enum) { /// Consumes the header of a pkt-line packet and reads any associated data /// into the reader's buffer, but does not consume the data. - fn peek(reader: *std.Io.Reader) !Packet { + fn peek(reader: *Io.Reader) !Packet { const length = std.fmt.parseUnsigned(u16, try reader.take(4), 16) catch return error.InvalidPacket; switch (length) { 0 => return .flush, @@ -618,7 +619,7 @@ pub const Packet = union(enum) { } /// Writes a packet in pkt-line format. - fn write(packet: Packet, writer: *std.Io.Writer) !void { + fn write(packet: Packet, writer: *Io.Writer) !void { switch (packet) { .flush => try writer.writeAll("0000"), .delimiter => try writer.writeAll("0001"), @@ -812,7 +813,7 @@ pub const Session = struct { const CapabilityIterator = struct { request: std.http.Client.Request, - reader: *std.Io.Reader, + reader: *Io.Reader, decompress: std.http.Decompress, const Capability = struct { @@ -869,7 +870,7 @@ pub const Session = struct { upload_pack_uri.query = null; upload_pack_uri.fragment = null; - var body: std.Io.Writer = .fixed(options.buffer); + var body: Io.Writer = .fixed(options.buffer); try Packet.write(.{ .data = "command=ls-refs\n" }, &body); if (session.supports_agent) { try Packet.write(.{ .data = agent_capability }, &body); @@ -918,7 +919,7 @@ pub const Session = struct { pub const RefIterator = struct { format: Oid.Format, request: std.http.Client.Request, - reader: *std.Io.Reader, + reader: *Io.Reader, decompress: std.http.Decompress, pub const Ref = struct { @@ -986,7 +987,7 @@ pub const Session = struct { upload_pack_uri.query = null; upload_pack_uri.fragment = null; - var body: std.Io.Writer = .fixed(response_buffer); + var body: Io.Writer = .fixed(response_buffer); try Packet.write(.{ .data = "command=fetch\n" }, &body); if (session.supports_agent) { try Packet.write(.{ .data = agent_capability }, &body); @@ -1068,8 +1069,8 @@ pub const Session = struct { pub const FetchStream = struct { request: std.http.Client.Request, - input: *std.Io.Reader, - reader: std.Io.Reader, + input: *Io.Reader, + reader: Io.Reader, err: ?Error = null, remaining_len: usize, decompress: std.http.Decompress, @@ -1094,7 +1095,7 @@ pub const Session = struct { _, }; - pub fn stream(r: *std.Io.Reader, w: *std.Io.Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize { + pub fn stream(r: *Io.Reader, w: *Io.Writer, limit: Io.Limit) Io.Reader.StreamError!usize { const fs: *FetchStream = @alignCast(@fieldParentPtr("reader", r)); const input = fs.input; if (fs.remaining_len == 0) { @@ -1139,7 +1140,7 @@ const PackHeader = struct { const signature = "PACK"; const supported_version = 2; - fn read(reader: *std.Io.Reader) !PackHeader { + fn read(reader: *Io.Reader) !PackHeader { const actual_signature = reader.take(4) catch |e| switch (e) { error.EndOfStream => return error.InvalidHeader, else => |other| return other, @@ -1202,7 +1203,7 @@ const EntryHeader = union(Type) { }; } - fn read(format: Oid.Format, reader: *std.Io.Reader) !EntryHeader { + fn read(format: Oid.Format, reader: *Io.Reader) !EntryHeader { const InitialByte = packed struct { len: u4, type: u3, has_next: bool }; const initial: InitialByte = @bitCast(reader.takeByte() catch |e| switch (e) { error.EndOfStream => return error.InvalidFormat, @@ -1231,7 +1232,7 @@ const EntryHeader = union(Type) { } }; -fn readOffsetVarInt(r: *std.Io.Reader) !u64 { +fn readOffsetVarInt(r: *Io.Reader) !u64 { const Byte = packed struct { value: u7, has_next: bool }; var b: Byte = @bitCast(try r.takeByte()); var value: u64 = b.value; @@ -1250,7 +1251,7 @@ const IndexHeader = struct { const supported_version = 2; const size = 4 + 4 + @sizeOf([256]u32); - fn read(index_header: *IndexHeader, reader: *std.Io.Reader) !void { + fn read(index_header: *IndexHeader, reader: *Io.Reader) !void { const sig = try reader.take(4); if (!mem.eql(u8, sig, signature)) return error.InvalidHeader; const version = try reader.takeInt(u32, .big); @@ -1324,7 +1325,7 @@ pub fn indexPack( } @memset(fan_out_table[fan_out_index..], count); - var index_hashed_writer = std.Io.Writer.hashed(&index_writer.interface, Oid.Hasher.init(format), &.{}); + var index_hashed_writer = Io.Writer.hashed(&index_writer.interface, Oid.Hasher.init(format), &.{}); const writer = &index_hashed_writer.writer; try writer.writeAll(IndexHeader.signature); try writer.writeInt(u32, IndexHeader.supported_version, .big); @@ -1489,14 +1490,14 @@ fn resolveDeltaChain( const delta_header = try EntryHeader.read(format, &pack.interface); const delta_data = try readObjectRaw(allocator, &pack.interface, delta_header.uncompressedLength()); defer allocator.free(delta_data); - var delta_reader: std.Io.Reader = .fixed(delta_data); + var delta_reader: Io.Reader = .fixed(delta_data); _ = try delta_reader.takeLeb128(u64); // base object size const expanded_size = try delta_reader.takeLeb128(u64); const expanded_alloc_size = std.math.cast(usize, expanded_size) orelse return error.ObjectTooLarge; const expanded_data = try allocator.alloc(u8, expanded_alloc_size); errdefer allocator.free(expanded_data); - var expanded_delta_stream: std.Io.Writer = .fixed(expanded_data); + var expanded_delta_stream: Io.Writer = .fixed(expanded_data); try expandDelta(base_data, &delta_reader, &expanded_delta_stream); if (expanded_delta_stream.end != expanded_size) return error.InvalidObject; @@ -1509,9 +1510,9 @@ fn resolveDeltaChain( /// Reads the complete contents of an object from `reader`. This function may /// read more bytes than required from `reader`, so the reader position after /// returning is not reliable. -fn readObjectRaw(allocator: Allocator, reader: *std.Io.Reader, size: u64) ![]u8 { +fn readObjectRaw(allocator: Allocator, reader: *Io.Reader, size: u64) ![]u8 { const alloc_size = std.math.cast(usize, size) orelse return error.ObjectTooLarge; - var aw: std.Io.Writer.Allocating = .init(allocator); + var aw: Io.Writer.Allocating = .init(allocator); try aw.ensureTotalCapacity(alloc_size + std.compress.flate.max_window_len); defer aw.deinit(); var decompress: std.compress.flate.Decompress = .init(reader, .zlib, &.{}); @@ -1523,7 +1524,7 @@ fn readObjectRaw(allocator: Allocator, reader: *std.Io.Reader, size: u64) ![]u8 /// /// The format of the delta data is documented in /// [pack-format](https://git-scm.com/docs/pack-format). -fn expandDelta(base_object: []const u8, delta_reader: *std.Io.Reader, writer: *std.Io.Writer) !void { +fn expandDelta(base_object: []const u8, delta_reader: *Io.Reader, writer: *Io.Writer) !void { while (true) { const inst: packed struct { value: u7, copy: bool } = @bitCast(delta_reader.takeByte() catch |e| switch (e) { error.EndOfStream => return, @@ -1576,7 +1577,7 @@ fn expandDelta(base_object: []const u8, delta_reader: *std.Io.Reader, writer: *s /// - SHA-1: `dd582c0720819ab7130b103635bd7271b9fd4feb` /// - SHA-256: `7f444a92bd4572ee4a28b2c63059924a9ca1829138553ef3e7c41ee159afae7a` /// 4. `git checkout $commit` -fn runRepositoryTest(comptime format: Oid.Format, head_commit: []const u8) !void { +fn runRepositoryTest(io: Io, comptime format: Oid.Format, head_commit: []const u8) !void { const testrepo_pack = @embedFile("git/testdata/testrepo-" ++ @tagName(format) ++ ".pack"); var git_dir = testing.tmpDir(.{}); @@ -1586,7 +1587,7 @@ fn runRepositoryTest(comptime format: Oid.Format, head_commit: []const u8) !void try pack_file.writeAll(testrepo_pack); var pack_file_buffer: [2000]u8 = undefined; - var pack_file_reader = pack_file.reader(&pack_file_buffer); + var pack_file_reader = pack_file.reader(io, &pack_file_buffer); var index_file = try git_dir.dir.createFile("testrepo.idx", .{ .read = true }); defer index_file.close(); @@ -1608,7 +1609,7 @@ fn runRepositoryTest(comptime format: Oid.Format, head_commit: []const u8) !void try testing.expectEqualSlices(u8, testrepo_idx, index_file_data); } - var index_file_reader = index_file.reader(&index_file_buffer); + var index_file_reader = index_file.reader(io, &index_file_buffer); var repository: Repository = undefined; try repository.init(testing.allocator, format, &pack_file_reader, &index_file_reader); defer repository.deinit(); @@ -1687,11 +1688,11 @@ fn runRepositoryTest(comptime format: Oid.Format, head_commit: []const u8) !void const skip_checksums = true; test "SHA-1 packfile indexing and checkout" { - try runRepositoryTest(.sha1, "dd582c0720819ab7130b103635bd7271b9fd4feb"); + try runRepositoryTest(std.testing.io, .sha1, "dd582c0720819ab7130b103635bd7271b9fd4feb"); } test "SHA-256 packfile indexing and checkout" { - try runRepositoryTest(.sha256, "7f444a92bd4572ee4a28b2c63059924a9ca1829138553ef3e7c41ee159afae7a"); + try runRepositoryTest(std.testing.io, .sha256, "7f444a92bd4572ee4a28b2c63059924a9ca1829138553ef3e7c41ee159afae7a"); } /// Checks out a commit of a packfile. Intended for experimenting with and @@ -1699,6 +1700,10 @@ test "SHA-256 packfile indexing and checkout" { pub fn main() !void { const allocator = std.heap.smp_allocator; + var threaded: Io.Threaded = .init(allocator); + defer threaded.deinit(); + const io = threaded.io(); + const args = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, args); if (args.len != 5) { @@ -1710,7 +1715,7 @@ pub fn main() !void { var pack_file = try std.fs.cwd().openFile(args[2], .{}); defer pack_file.close(); var pack_file_buffer: [4096]u8 = undefined; - var pack_file_reader = pack_file.reader(&pack_file_buffer); + var pack_file_reader = pack_file.reader(io, &pack_file_buffer); const commit = try Oid.parse(format, args[3]); var worktree = try std.fs.cwd().makeOpenPath(args[4], .{}); @@ -1727,7 +1732,7 @@ pub fn main() !void { try indexPack(allocator, format, &pack_file_reader, &index_file_writer); std.debug.print("Starting checkout...\n", .{}); - var index_file_reader = index_file.reader(&index_file_buffer); + var index_file_reader = index_file.reader(io, &index_file_buffer); var repository: Repository = undefined; try repository.init(allocator, format, &pack_file_reader, &index_file_reader); defer repository.deinit(); diff --git a/src/Zcu.zig b/src/Zcu.zig index 5af60dfa6c..ed1ae0eece 100644 --- a/src/Zcu.zig +++ b/src/Zcu.zig @@ -4,9 +4,12 @@ //! //! Each `Compilation` has exactly one or zero `Zcu`, depending on whether //! there is or is not any zig source code, respectively. +const Zcu = @This(); +const builtin = @import("builtin"); const std = @import("std"); -const builtin = @import("builtin"); +const Io = std.Io; +const Writer = std.Io.Writer; const mem = std.mem; const Allocator = std.mem.Allocator; const assert = std.debug.assert; @@ -15,9 +18,7 @@ const BigIntConst = std.math.big.int.Const; const BigIntMutable = std.math.big.int.Mutable; const Target = std.Target; const Ast = std.zig.Ast; -const Writer = std.Io.Writer; -const Zcu = @This(); const Compilation = @import("Compilation.zig"); const Cache = std.Build.Cache; pub const Value = @import("Value.zig"); @@ -1037,10 +1038,15 @@ pub const File = struct { stat: Cache.File.Stat, }; - pub const GetSourceError = error{ OutOfMemory, FileTooBig } || std.fs.File.OpenError || std.fs.File.ReadError; + pub const GetSourceError = error{ + OutOfMemory, + FileTooBig, + Streaming, + } || std.fs.File.OpenError || std.fs.File.ReadError; pub fn getSource(file: *File, zcu: *const Zcu) GetSourceError!Source { const gpa = zcu.gpa; + const io = zcu.comp.io; if (file.source) |source| return .{ .bytes = source, @@ -1061,7 +1067,7 @@ pub const File = struct { const source = try gpa.allocSentinel(u8, @intCast(stat.size), 0); errdefer gpa.free(source); - var file_reader = f.reader(&.{}); + var file_reader = f.reader(io, &.{}); file_reader.size = stat.size; file_reader.interface.readSliceAll(source) catch return file_reader.err.?; @@ -2859,9 +2865,9 @@ comptime { } } -pub fn loadZirCache(gpa: Allocator, cache_file: std.fs.File) !Zir { +pub fn loadZirCache(gpa: Allocator, io: Io, cache_file: std.fs.File) !Zir { var buffer: [2000]u8 = undefined; - var file_reader = cache_file.reader(&buffer); + var file_reader = cache_file.reader(io, &buffer); return result: { const header = file_reader.interface.takeStructPointer(Zir.Header) catch |err| break :result err; break :result loadZirCacheBody(gpa, header.*, &file_reader.interface); @@ -2871,7 +2877,7 @@ pub fn loadZirCache(gpa: Allocator, cache_file: std.fs.File) !Zir { }; } -pub fn loadZirCacheBody(gpa: Allocator, header: Zir.Header, cache_br: *std.Io.Reader) !Zir { +pub fn loadZirCacheBody(gpa: Allocator, header: Zir.Header, cache_br: *Io.Reader) !Zir { var instructions: std.MultiArrayList(Zir.Inst) = .{}; errdefer instructions.deinit(gpa); @@ -2940,7 +2946,7 @@ pub fn saveZirCache(gpa: Allocator, cache_file: std.fs.File, stat: std.fs.File.S .stat_size = stat.size, .stat_inode = stat.inode, - .stat_mtime = stat.mtime, + .stat_mtime = stat.mtime.toNanoseconds(), }; var vecs = [_][]const u8{ @ptrCast((&header)[0..1]), @@ -2969,7 +2975,7 @@ pub fn saveZoirCache(cache_file: std.fs.File, stat: std.fs.File.Stat, zoir: Zoir .stat_size = stat.size, .stat_inode = stat.inode, - .stat_mtime = stat.mtime, + .stat_mtime = stat.mtime.toNanoseconds(), }; var vecs = [_][]const u8{ @ptrCast((&header)[0..1]), @@ -2988,7 +2994,7 @@ pub fn saveZoirCache(cache_file: std.fs.File, stat: std.fs.File.Stat, zoir: Zoir }; } -pub fn loadZoirCacheBody(gpa: Allocator, header: Zoir.Header, cache_br: *std.Io.Reader) !Zoir { +pub fn loadZoirCacheBody(gpa: Allocator, header: Zoir.Header, cache_br: *Io.Reader) !Zoir { var zoir: Zoir = .{ .nodes = .empty, .extra = &.{}, @@ -4283,7 +4289,7 @@ const FormatAnalUnit = struct { zcu: *Zcu, }; -fn formatAnalUnit(data: FormatAnalUnit, writer: *std.Io.Writer) std.Io.Writer.Error!void { +fn formatAnalUnit(data: FormatAnalUnit, writer: *Io.Writer) Io.Writer.Error!void { const zcu = data.zcu; const ip = &zcu.intern_pool; switch (data.unit.unwrap()) { @@ -4309,7 +4315,7 @@ fn formatAnalUnit(data: FormatAnalUnit, writer: *std.Io.Writer) std.Io.Writer.Er const FormatDependee = struct { dependee: InternPool.Dependee, zcu: *Zcu }; -fn formatDependee(data: FormatDependee, writer: *std.Io.Writer) std.Io.Writer.Error!void { +fn formatDependee(data: FormatDependee, writer: *Io.Writer) Io.Writer.Error!void { const zcu = data.zcu; const ip = &zcu.intern_pool; switch (data.dependee) { diff --git a/src/Zcu/PerThread.zig b/src/Zcu/PerThread.zig index 8f2656e78a..474ccc710d 100644 --- a/src/Zcu/PerThread.zig +++ b/src/Zcu/PerThread.zig @@ -87,6 +87,7 @@ pub fn updateFile( const zcu = pt.zcu; const comp = zcu.comp; const gpa = zcu.gpa; + const io = comp.io; // In any case we need to examine the stat of the file to determine the course of action. var source_file = f: { @@ -127,7 +128,7 @@ pub fn updateFile( .astgen_failure, .success => lock: { const unchanged_metadata = stat.size == file.stat.size and - stat.mtime == file.stat.mtime and + stat.mtime.nanoseconds == file.stat.mtime.nanoseconds and stat.inode == file.stat.inode; if (unchanged_metadata) { @@ -173,8 +174,6 @@ pub fn updateFile( .lock = lock, }) catch |err| switch (err) { error.NotDir => unreachable, // no dir components - error.InvalidUtf8 => unreachable, // it's a hex encoded name - error.InvalidWtf8 => unreachable, // it's a hex encoded name error.BadPathName => unreachable, // it's a hex encoded name error.NameTooLong => unreachable, // it's a fixed size name error.PipeBusy => unreachable, // it's not a pipe @@ -255,7 +254,7 @@ pub fn updateFile( const source = try gpa.allocSentinel(u8, @intCast(stat.size), 0); defer if (file.source == null) gpa.free(source); - var source_fr = source_file.reader(&.{}); + var source_fr = source_file.reader(io, &.{}); source_fr.size = stat.size; source_fr.interface.readSliceAll(source) catch |err| switch (err) { error.ReadFailed => return source_fr.err.?, @@ -353,6 +352,7 @@ fn loadZirZoirCache( assert(file.getMode() == mode); const gpa = zcu.gpa; + const io = zcu.comp.io; const Header = switch (mode) { .zig => Zir.Header, @@ -360,7 +360,7 @@ fn loadZirZoirCache( }; var buffer: [2000]u8 = undefined; - var cache_fr = cache_file.reader(&buffer); + var cache_fr = cache_file.reader(io, &buffer); cache_fr.size = stat.size; const cache_br = &cache_fr.interface; @@ -375,7 +375,7 @@ fn loadZirZoirCache( const unchanged_metadata = stat.size == header.stat_size and - stat.mtime == header.stat_mtime and + stat.mtime.nanoseconds == header.stat_mtime and stat.inode == header.stat_inode; if (!unchanged_metadata) { @@ -2436,6 +2436,7 @@ fn updateEmbedFileInner( const tid = pt.tid; const zcu = pt.zcu; const gpa = zcu.gpa; + const io = zcu.comp.io; const ip = &zcu.intern_pool; var file = f: { @@ -2450,7 +2451,7 @@ fn updateEmbedFileInner( const old_stat = ef.stat; const unchanged_metadata = stat.size == old_stat.size and - stat.mtime == old_stat.mtime and + stat.mtime.nanoseconds == old_stat.mtime.nanoseconds and stat.inode == old_stat.inode; if (unchanged_metadata) return; } @@ -2464,7 +2465,7 @@ fn updateEmbedFileInner( const old_len = string_bytes.mutate.len; errdefer string_bytes.shrinkRetainingCapacity(old_len); const bytes = (try string_bytes.addManyAsSlice(size_plus_one))[0]; - var fr = file.reader(&.{}); + var fr = file.reader(io, &.{}); fr.size = stat.size; fr.interface.readSliceAll(bytes[0..size]) catch |err| switch (err) { error.ReadFailed => return fr.err.?, diff --git a/src/codegen/llvm.zig b/src/codegen/llvm.zig index 5829401e21..6b891c1ab5 100644 --- a/src/codegen/llvm.zig +++ b/src/codegen/llvm.zig @@ -782,10 +782,10 @@ pub const Object = struct { pub const EmitOptions = struct { pre_ir_path: ?[]const u8, pre_bc_path: ?[]const u8, - bin_path: ?[*:0]const u8, - asm_path: ?[*:0]const u8, - post_ir_path: ?[*:0]const u8, - post_bc_path: ?[*:0]const u8, + bin_path: ?[:0]const u8, + asm_path: ?[:0]const u8, + post_ir_path: ?[:0]const u8, + post_bc_path: ?[]const u8, is_debug: bool, is_small: bool, @@ -989,7 +989,7 @@ pub const Object = struct { options.post_ir_path == null and options.post_bc_path == null) return; if (options.post_bc_path) |path| { - var file = std.fs.cwd().createFileZ(path, .{}) catch |err| + var file = std.fs.cwd().createFile(path, .{}) catch |err| return diags.fail("failed to create '{s}': {s}", .{ path, @errorName(err) }); defer file.close(); @@ -1098,8 +1098,8 @@ pub const Object = struct { // though it's clearly not ready and produces multiple miscompilations in our std tests. .allow_machine_outliner = !comp.root_mod.resolved_target.result.cpu.arch.isRISCV(), .asm_filename = null, - .bin_filename = options.bin_path, - .llvm_ir_filename = options.post_ir_path, + .bin_filename = if (options.bin_path) |x| x.ptr else null, + .llvm_ir_filename = if (options.post_ir_path) |x| x.ptr else null, .bitcode_filename = null, // `.coverage` value is only used when `.sancov` is enabled. @@ -1146,7 +1146,7 @@ pub const Object = struct { lowered_options.time_report_out = &time_report_c_str; } - lowered_options.asm_filename = options.asm_path; + lowered_options.asm_filename = if (options.asm_path) |x| x.ptr else null; if (target_machine.emitToFile(module, &error_message, &lowered_options)) { defer llvm.disposeMessage(error_message); return diags.fail("LLVM failed to emit asm={s} bin={s} ir={s} bc={s}: {s}", .{ diff --git a/src/codegen/wasm/Emit.zig b/src/codegen/wasm/Emit.zig index 272d5519a6..e18bd12213 100644 --- a/src/codegen/wasm/Emit.zig +++ b/src/codegen/wasm/Emit.zig @@ -188,8 +188,8 @@ pub fn lowerToCode(emit: *Emit) Error!void { .fromInterned(fn_info.return_type), target, ).?; - code.appendAssumeCapacity(@intFromEnum(std.wasm.Opcode.call_indirect)); if (is_obj) { + code.appendAssumeCapacity(@intFromEnum(std.wasm.Opcode.call_indirect)); try wasm.out_relocs.append(gpa, .{ .offset = @intCast(code.items.len), .pointee = .{ .type_index = func_ty_index }, @@ -198,7 +198,19 @@ pub fn lowerToCode(emit: *Emit) Error!void { }); code.appendNTimesAssumeCapacity(0, 5); } else { - const index: Wasm.Flush.FuncTypeIndex = .fromTypeIndex(func_ty_index, &wasm.flush_buffer); + const index: Wasm.Flush.FuncTypeIndex = @enumFromInt(wasm.flush_buffer.func_types.getIndex(func_ty_index) orelse { + // In this case we tried to call a function pointer for + // which the type signature does not match any function + // body or function import in the entire wasm executable. + // + // Since there is no way to create a reference to a + // function without it being in the function table or + // import table, this instruction is unreachable. + code.appendAssumeCapacity(@intFromEnum(std.wasm.Opcode.@"unreachable")); + inst += 1; + continue :loop tags[inst]; + }); + code.appendAssumeCapacity(@intFromEnum(std.wasm.Opcode.call_indirect)); writeUleb128(code, @intFromEnum(index)); } writeUleb128(code, @as(u32, 0)); // table index diff --git a/src/fmt.zig b/src/fmt.zig index 4a3c321877..344a89d6ed 100644 --- a/src/fmt.zig +++ b/src/fmt.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const Io = std.Io; const mem = std.mem; const fs = std.fs; const process = std.process; @@ -34,13 +35,14 @@ const Fmt = struct { color: Color, gpa: Allocator, arena: Allocator, + io: Io, out_buffer: std.Io.Writer.Allocating, stdout_writer: *fs.File.Writer, const SeenMap = std.AutoHashMap(fs.File.INode, void); }; -pub fn run(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { +pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !void { var color: Color = .auto; var stdin_flag = false; var check_flag = false; @@ -99,7 +101,7 @@ pub fn run(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { const stdin: fs.File = .stdin(); var stdio_buffer: [1024]u8 = undefined; - var file_reader: fs.File.Reader = stdin.reader(&stdio_buffer); + var file_reader: fs.File.Reader = stdin.reader(io, &stdio_buffer); const source_code = std.zig.readSourceFileToEndAlloc(gpa, &file_reader) catch |err| { fatal("unable to read stdin: {}", .{err}); }; @@ -165,6 +167,7 @@ pub fn run(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { var fmt: Fmt = .{ .gpa = gpa, .arena = arena, + .io = io, .seen = .init(gpa), .any_error = false, .check_ast = check_ast_flag, @@ -255,6 +258,8 @@ fn fmtPathFile( dir: fs.Dir, sub_path: []const u8, ) !void { + const io = fmt.io; + const source_file = try dir.openFile(sub_path, .{}); var file_closed = false; errdefer if (!file_closed) source_file.close(); @@ -265,7 +270,7 @@ fn fmtPathFile( return error.IsDir; var read_buffer: [1024]u8 = undefined; - var file_reader: fs.File.Reader = source_file.reader(&read_buffer); + var file_reader: fs.File.Reader = source_file.reader(io, &read_buffer); file_reader.size = stat.size; const gpa = fmt.gpa; @@ -363,5 +368,8 @@ pub fn main() !void { var arena_instance = std.heap.ArenaAllocator.init(gpa); const arena = arena_instance.allocator(); const args = try process.argsAlloc(arena); - return run(gpa, arena, args[1..]); + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + return run(gpa, arena, io, args[1..]); } diff --git a/src/libs/freebsd.zig b/src/libs/freebsd.zig index 2315964a50..ff32b4649a 100644 --- a/src/libs/freebsd.zig +++ b/src/libs/freebsd.zig @@ -426,6 +426,7 @@ pub fn buildSharedObjects(comp: *Compilation, prog_node: std.Progress.Node) anye } const gpa = comp.gpa; + const io = comp.io; var arena_allocator = std.heap.ArenaAllocator.init(gpa); defer arena_allocator.deinit(); @@ -438,6 +439,7 @@ pub fn buildSharedObjects(comp: *Compilation, prog_node: std.Progress.Node) anye // Use the global cache directory. var cache: Cache = .{ .gpa = gpa, + .io = io, .manifest_dir = try comp.dirs.global_cache.handle.makeOpenPath("h", .{}), }; cache.addPrefix(.{ .path = null, .handle = fs.cwd() }); @@ -1017,6 +1019,7 @@ fn buildSharedLib( const tracy = trace(@src()); defer tracy.end(); + const io = comp.io; const basename = try std.fmt.allocPrint(arena, "lib{s}.so.{d}", .{ lib.name, lib.sover }); const version: Version = .{ .major = lib.sover, .minor = 0, .patch = 0 }; const ld_basename = path.basename(comp.getTarget().standardDynamicLinkerPath().get().?); @@ -1071,7 +1074,7 @@ fn buildSharedLib( const misc_task: Compilation.MiscTask = .@"freebsd libc shared object"; var sub_create_diag: Compilation.CreateDiagnostic = undefined; - const sub_compilation = Compilation.create(comp.gpa, arena, &sub_create_diag, .{ + const sub_compilation = Compilation.create(comp.gpa, arena, io, &sub_create_diag, .{ .dirs = comp.dirs.withoutLocalCache(), .thread_pool = comp.thread_pool, .self_exe_path = comp.self_exe_path, diff --git a/src/libs/glibc.zig b/src/libs/glibc.zig index 8281bee303..76048239e0 100644 --- a/src/libs/glibc.zig +++ b/src/libs/glibc.zig @@ -666,6 +666,7 @@ pub fn buildSharedObjects(comp: *Compilation, prog_node: std.Progress.Node) anye } const gpa = comp.gpa; + const io = comp.io; var arena_allocator = std.heap.ArenaAllocator.init(gpa); defer arena_allocator.deinit(); @@ -677,6 +678,7 @@ pub fn buildSharedObjects(comp: *Compilation, prog_node: std.Progress.Node) anye // Use the global cache directory. var cache: Cache = .{ .gpa = gpa, + .io = io, .manifest_dir = try comp.dirs.global_cache.handle.makeOpenPath("h", .{}), }; cache.addPrefix(.{ .path = null, .handle = fs.cwd() }); @@ -1175,6 +1177,7 @@ fn buildSharedLib( const tracy = trace(@src()); defer tracy.end(); + const io = comp.io; const basename = try std.fmt.allocPrint(arena, "lib{s}.so.{d}", .{ lib.name, lib.sover }); const version: Version = .{ .major = lib.sover, .minor = 0, .patch = 0 }; const ld_basename = path.basename(comp.getTarget().standardDynamicLinkerPath().get().?); @@ -1229,7 +1232,7 @@ fn buildSharedLib( const misc_task: Compilation.MiscTask = .@"glibc shared object"; var sub_create_diag: Compilation.CreateDiagnostic = undefined; - const sub_compilation = Compilation.create(comp.gpa, arena, &sub_create_diag, .{ + const sub_compilation = Compilation.create(comp.gpa, arena, io, &sub_create_diag, .{ .dirs = comp.dirs.withoutLocalCache(), .thread_pool = comp.thread_pool, .self_exe_path = comp.self_exe_path, diff --git a/src/libs/libcxx.zig b/src/libs/libcxx.zig index 0956f2e299..405a06ba59 100644 --- a/src/libs/libcxx.zig +++ b/src/libs/libcxx.zig @@ -120,6 +120,7 @@ pub fn buildLibCxx(comp: *Compilation, prog_node: std.Progress.Node) BuildError! defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); + const io = comp.io; const root_name = "c++"; const output_mode = .Lib; const link_mode = .static; @@ -254,7 +255,7 @@ pub fn buildLibCxx(comp: *Compilation, prog_node: std.Progress.Node) BuildError! const misc_task: Compilation.MiscTask = .libcxx; var sub_create_diag: Compilation.CreateDiagnostic = undefined; - const sub_compilation = Compilation.create(comp.gpa, arena, &sub_create_diag, .{ + const sub_compilation = Compilation.create(comp.gpa, arena, io, &sub_create_diag, .{ .dirs = comp.dirs.withoutLocalCache(), .self_exe_path = comp.self_exe_path, .cache_mode = .whole, @@ -309,6 +310,7 @@ pub fn buildLibCxxAbi(comp: *Compilation, prog_node: std.Progress.Node) BuildErr defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); + const io = comp.io; const root_name = "c++abi"; const output_mode = .Lib; const link_mode = .static; @@ -446,7 +448,7 @@ pub fn buildLibCxxAbi(comp: *Compilation, prog_node: std.Progress.Node) BuildErr const misc_task: Compilation.MiscTask = .libcxxabi; var sub_create_diag: Compilation.CreateDiagnostic = undefined; - const sub_compilation = Compilation.create(comp.gpa, arena, &sub_create_diag, .{ + const sub_compilation = Compilation.create(comp.gpa, arena, io, &sub_create_diag, .{ .dirs = comp.dirs.withoutLocalCache(), .self_exe_path = comp.self_exe_path, .cache_mode = .whole, diff --git a/src/libs/libtsan.zig b/src/libs/libtsan.zig index 259fccd26f..12642bc452 100644 --- a/src/libs/libtsan.zig +++ b/src/libs/libtsan.zig @@ -25,6 +25,7 @@ pub fn buildTsan(comp: *Compilation, prog_node: std.Progress.Node) BuildError!vo defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); + const io = comp.io; const target = comp.getTarget(); const root_name = switch (target.os.tag) { // On Apple platforms, we use the same name as LLVM because the @@ -277,7 +278,7 @@ pub fn buildTsan(comp: *Compilation, prog_node: std.Progress.Node) BuildError!vo const misc_task: Compilation.MiscTask = .libtsan; var sub_create_diag: Compilation.CreateDiagnostic = undefined; - const sub_compilation = Compilation.create(comp.gpa, arena, &sub_create_diag, .{ + const sub_compilation = Compilation.create(comp.gpa, arena, io, &sub_create_diag, .{ .dirs = comp.dirs.withoutLocalCache(), .thread_pool = comp.thread_pool, .self_exe_path = comp.self_exe_path, diff --git a/src/libs/libunwind.zig b/src/libs/libunwind.zig index 6bd13bad51..26b8ae59b4 100644 --- a/src/libs/libunwind.zig +++ b/src/libs/libunwind.zig @@ -26,6 +26,7 @@ pub fn buildStaticLib(comp: *Compilation, prog_node: std.Progress.Node) BuildErr defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); + const io = comp.io; const output_mode = .Lib; const target = &comp.root_mod.resolved_target.result; const unwind_tables: std.builtin.UnwindTables = @@ -143,7 +144,7 @@ pub fn buildStaticLib(comp: *Compilation, prog_node: std.Progress.Node) BuildErr const misc_task: Compilation.MiscTask = .libunwind; var sub_create_diag: Compilation.CreateDiagnostic = undefined; - const sub_compilation = Compilation.create(comp.gpa, arena, &sub_create_diag, .{ + const sub_compilation = Compilation.create(comp.gpa, arena, io, &sub_create_diag, .{ .dirs = comp.dirs.withoutLocalCache(), .self_exe_path = comp.self_exe_path, .config = config, diff --git a/src/libs/mingw.zig b/src/libs/mingw.zig index ce78809892..1773c321e1 100644 --- a/src/libs/mingw.zig +++ b/src/libs/mingw.zig @@ -235,6 +235,7 @@ pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void { dev.check(.build_import_lib); const gpa = comp.gpa; + const io = comp.io; var arena_allocator = std.heap.ArenaAllocator.init(gpa); defer arena_allocator.deinit(); @@ -255,6 +256,7 @@ pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void { // Use the global cache directory. var cache: Cache = .{ .gpa = gpa, + .io = io, .manifest_dir = try comp.dirs.global_cache.handle.makeOpenPath("h", .{}), }; cache.addPrefix(.{ .path = null, .handle = std.fs.cwd() }); @@ -302,7 +304,7 @@ pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void { .output = .{ .to_list = .{ .arena = .init(gpa) } }, }; defer diagnostics.deinit(); - var aro_comp = aro.Compilation.init(gpa, arena, &diagnostics, std.fs.cwd()); + var aro_comp = aro.Compilation.init(gpa, arena, io, &diagnostics, std.fs.cwd()); defer aro_comp.deinit(); aro_comp.target = target.*; diff --git a/src/libs/musl.zig b/src/libs/musl.zig index ae91425470..69bd892b3b 100644 --- a/src/libs/musl.zig +++ b/src/libs/musl.zig @@ -26,6 +26,7 @@ pub fn buildCrtFile(comp: *Compilation, in_crt_file: CrtFile, prog_node: std.Pro var arena_allocator = std.heap.ArenaAllocator.init(gpa); defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); + const io = comp.io; switch (in_crt_file) { .crt1_o => { @@ -246,7 +247,7 @@ pub fn buildCrtFile(comp: *Compilation, in_crt_file: CrtFile, prog_node: std.Pro const misc_task: Compilation.MiscTask = .@"musl libc.so"; var sub_create_diag: Compilation.CreateDiagnostic = undefined; - const sub_compilation = Compilation.create(comp.gpa, arena, &sub_create_diag, .{ + const sub_compilation = Compilation.create(comp.gpa, arena, io, &sub_create_diag, .{ .dirs = comp.dirs.withoutLocalCache(), .self_exe_path = comp.self_exe_path, .cache_mode = .whole, diff --git a/src/libs/netbsd.zig b/src/libs/netbsd.zig index e47bdce3af..07a7da7f6f 100644 --- a/src/libs/netbsd.zig +++ b/src/libs/netbsd.zig @@ -372,6 +372,7 @@ pub fn buildSharedObjects(comp: *Compilation, prog_node: std.Progress.Node) anye } const gpa = comp.gpa; + const io = comp.io; var arena_allocator = std.heap.ArenaAllocator.init(gpa); defer arena_allocator.deinit(); @@ -383,6 +384,7 @@ pub fn buildSharedObjects(comp: *Compilation, prog_node: std.Progress.Node) anye // Use the global cache directory. var cache: Cache = .{ .gpa = gpa, + .io = io, .manifest_dir = try comp.dirs.global_cache.handle.makeOpenPath("h", .{}), }; cache.addPrefix(.{ .path = null, .handle = fs.cwd() }); @@ -680,6 +682,7 @@ fn buildSharedLib( const tracy = trace(@src()); defer tracy.end(); + const io = comp.io; const basename = try std.fmt.allocPrint(arena, "lib{s}.so.{d}", .{ lib.name, lib.sover }); const version: Version = .{ .major = lib.sover, .minor = 0, .patch = 0 }; const ld_basename = path.basename(comp.getTarget().standardDynamicLinkerPath().get().?); @@ -733,7 +736,7 @@ fn buildSharedLib( const misc_task: Compilation.MiscTask = .@"netbsd libc shared object"; var sub_create_diag: Compilation.CreateDiagnostic = undefined; - const sub_compilation = Compilation.create(comp.gpa, arena, &sub_create_diag, .{ + const sub_compilation = Compilation.create(comp.gpa, arena, io, &sub_create_diag, .{ .dirs = comp.dirs.withoutLocalCache(), .thread_pool = comp.thread_pool, .self_exe_path = comp.self_exe_path, diff --git a/src/link.zig b/src/link.zig index 0efc46c4f1..7cf8e5c1a6 100644 --- a/src/link.zig +++ b/src/link.zig @@ -1,19 +1,22 @@ -const std = @import("std"); -const build_options = @import("build_options"); const builtin = @import("builtin"); +const build_options = @import("build_options"); + +const std = @import("std"); +const Io = std.Io; const assert = std.debug.assert; const fs = std.fs; const mem = std.mem; const log = std.log.scoped(.link); -const trace = @import("tracy.zig").trace; -const wasi_libc = @import("libs/wasi_libc.zig"); - const Allocator = std.mem.Allocator; const Cache = std.Build.Cache; const Path = std.Build.Cache.Path; const Directory = std.Build.Cache.Directory; const Compilation = @import("Compilation.zig"); const LibCInstallation = std.zig.LibCInstallation; + +const trace = @import("tracy.zig").trace; +const wasi_libc = @import("libs/wasi_libc.zig"); + const Zcu = @import("Zcu.zig"); const InternPool = @import("InternPool.zig"); const Type = @import("Type.zig"); @@ -572,6 +575,7 @@ pub const File = struct { dev.check(.make_writable); const comp = base.comp; const gpa = comp.gpa; + const io = comp.io; switch (base.tag) { .lld => assert(base.file == null), .elf, .macho, .wasm => { @@ -616,22 +620,9 @@ pub const File = struct { &coff.mf else unreachable; - var attempt: u5 = 0; - mf.file = while (true) break base.emit.root_dir.handle.openFile(base.emit.sub_path, .{ + mf.file = .adaptFromNewApi(try Io.Dir.openFile(base.emit.root_dir.handle.adaptToNewApi(), io, base.emit.sub_path, .{ .mode = .read_write, - }) catch |err| switch (err) { - error.AccessDenied => switch (builtin.os.tag) { - .windows => { - if (attempt == 13) return error.AccessDenied; - // give the kernel a chance to finish closing the executable handle - std.os.windows.kernel32.Sleep(@as(u32, 1) << attempt >> 1); - attempt += 1; - continue; - }, - else => return error.AccessDenied, - }, - else => |e| return e, - }; + })); base.file = mf.file; try mf.ensureTotalCapacity(@intCast(mf.nodes.items[0].location().resolve(mf)[1])); }, diff --git a/src/link/Coff.zig b/src/link/Coff.zig index ac78f75af5..32889684dc 100644 --- a/src/link/Coff.zig +++ b/src/link/Coff.zig @@ -610,7 +610,7 @@ fn create( .Obj => false, }; const machine = target.toCoffMachine(); - const timestamp: u32 = if (options.repro) 0 else @truncate(@as(u64, @bitCast(std.time.timestamp()))); + const timestamp: u32 = 0; const major_subsystem_version = options.major_subsystem_version orelse 6; const minor_subsystem_version = options.minor_subsystem_version orelse 0; const magic: std.coff.OptionalHeader.Magic = switch (target.ptrBitWidth()) { diff --git a/src/link/Lld.zig b/src/link/Lld.zig index 28a26175b5..44b889365c 100644 --- a/src/link/Lld.zig +++ b/src/link/Lld.zig @@ -1613,11 +1613,9 @@ fn wasmLink(lld: *Lld, arena: Allocator) !void { } } -fn spawnLld( - comp: *Compilation, - arena: Allocator, - argv: []const []const u8, -) !void { +fn spawnLld(comp: *Compilation, arena: Allocator, argv: []const []const u8) !void { + const io = comp.io; + if (comp.verbose_link) { // Skip over our own name so that the LLD linker name is the first argv item. Compilation.dump_argv(argv[1..]); @@ -1649,7 +1647,7 @@ fn spawnLld( child.stderr_behavior = .Pipe; child.spawn() catch |err| break :term err; - var stderr_reader = child.stderr.?.readerStreaming(&.{}); + var stderr_reader = child.stderr.?.readerStreaming(io, &.{}); stderr = try stderr_reader.interface.allocRemaining(comp.gpa, .unlimited); break :term child.wait(); }) catch |first_err| term: { @@ -1659,7 +1657,7 @@ fn spawnLld( const rand_int = std.crypto.random.int(u64); const rsp_path = "tmp" ++ s ++ std.fmt.hex(rand_int) ++ ".rsp"; - const rsp_file = try comp.dirs.local_cache.handle.createFileZ(rsp_path, .{}); + const rsp_file = try comp.dirs.local_cache.handle.createFile(rsp_path, .{}); defer comp.dirs.local_cache.handle.deleteFileZ(rsp_path) catch |err| log.warn("failed to delete response file {s}: {s}", .{ rsp_path, @errorName(err) }); { @@ -1699,7 +1697,7 @@ fn spawnLld( rsp_child.stderr_behavior = .Pipe; rsp_child.spawn() catch |err| break :err err; - var stderr_reader = rsp_child.stderr.?.readerStreaming(&.{}); + var stderr_reader = rsp_child.stderr.?.readerStreaming(io, &.{}); stderr = try stderr_reader.interface.allocRemaining(comp.gpa, .unlimited); break :term rsp_child.wait() catch |err| break :err err; } diff --git a/src/link/MachO.zig b/src/link/MachO.zig index 11127495ab..ef5c837fd1 100644 --- a/src/link/MachO.zig +++ b/src/link/MachO.zig @@ -915,7 +915,7 @@ pub fn readArMagic(file: std.fs.File, offset: usize, buffer: *[Archive.SARMAG]u8 return buffer[0..Archive.SARMAG]; } -fn addObject(self: *MachO, path: Path, handle: File.HandleIndex, offset: u64) !void { +fn addObject(self: *MachO, path: Path, handle_index: File.HandleIndex, offset: u64) !void { const tracy = trace(@src()); defer tracy.end(); @@ -929,17 +929,15 @@ fn addObject(self: *MachO, path: Path, handle: File.HandleIndex, offset: u64) !v }); errdefer gpa.free(abs_path); - const mtime: u64 = mtime: { - const file = self.getFileHandle(handle); - const stat = file.stat() catch break :mtime 0; - break :mtime @as(u64, @intCast(@divFloor(stat.mtime, 1_000_000_000))); - }; - const index = @as(File.Index, @intCast(try self.files.addOne(gpa))); + const file = self.getFileHandle(handle_index); + const stat = try file.stat(); + const mtime = stat.mtime.toSeconds(); + const index: File.Index = @intCast(try self.files.addOne(gpa)); self.files.set(index, .{ .object = .{ .offset = offset, .path = abs_path, - .file_handle = handle, - .mtime = mtime, + .file_handle = handle_index, + .mtime = @intCast(mtime), .index = index, } }); try self.objects.append(gpa, index); diff --git a/src/link/MappedFile.zig b/src/link/MappedFile.zig index 97c250f758..a9d874c23f 100644 --- a/src/link/MappedFile.zig +++ b/src/link/MappedFile.zig @@ -16,11 +16,13 @@ writers: std.SinglyLinkedList, pub const growth_factor = 4; -pub const Error = std.posix.MMapError || - std.posix.MRemapError || - std.fs.File.SetEndPosError || - std.fs.File.CopyRangeError || - error{NotFile}; +pub const Error = std.posix.MMapError || std.posix.MRemapError || std.fs.File.SetEndPosError || error{ + NotFile, + SystemResources, + IsDir, + Unseekable, + NoSpaceLeft, +}; pub fn init(file: std.fs.File, gpa: std.mem.Allocator) !MappedFile { var mf: MappedFile = .{ @@ -402,7 +404,7 @@ pub const Node = extern struct { const w: *Writer = @fieldParentPtr("interface", interface); const copy_size: usize = @intCast(w.mf.copyFileRange( - file_reader.file, + .adaptFromNewApi(file_reader.file), file_reader.pos, w.ni.fileLocation(w.mf, true).offset + interface.end, limit.minInt(interface.unusedCapacityLen()), diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index c0aeb01fc0..f47f7fbe2a 100644 --- a/src/link/Wasm.zig +++ b/src/link/Wasm.zig @@ -3029,18 +3029,22 @@ fn openParseObjectReportingFailure(wasm: *Wasm, path: Path) void { fn parseObject(wasm: *Wasm, obj: link.Input.Object) !void { log.debug("parseObject {f}", .{obj.path}); const gpa = wasm.base.comp.gpa; + const io = wasm.base.comp.io; const gc_sections = wasm.base.gc_sections; defer obj.file.close(); + var file_reader = obj.file.reader(io, &.{}); + try wasm.objects.ensureUnusedCapacity(gpa, 1); - const stat = try obj.file.stat(); - const size = std.math.cast(usize, stat.size) orelse return error.FileTooBig; + const size = std.math.cast(usize, try file_reader.getSize()) orelse return error.FileTooBig; const file_contents = try gpa.alloc(u8, size); defer gpa.free(file_contents); - const n = try obj.file.preadAll(file_contents, 0); + const n = file_reader.interface.readSliceShort(file_contents) catch |err| switch (err) { + error.ReadFailed => return file_reader.err.?, + }; if (n != file_contents.len) return error.UnexpectedEndOfFile; var ss: Object.ScratchSpace = .{}; @@ -3053,17 +3057,21 @@ fn parseObject(wasm: *Wasm, obj: link.Input.Object) !void { fn parseArchive(wasm: *Wasm, obj: link.Input.Object) !void { log.debug("parseArchive {f}", .{obj.path}); const gpa = wasm.base.comp.gpa; + const io = wasm.base.comp.io; const gc_sections = wasm.base.gc_sections; defer obj.file.close(); - const stat = try obj.file.stat(); - const size = std.math.cast(usize, stat.size) orelse return error.FileTooBig; + var file_reader = obj.file.reader(io, &.{}); + + const size = std.math.cast(usize, try file_reader.getSize()) orelse return error.FileTooBig; const file_contents = try gpa.alloc(u8, size); defer gpa.free(file_contents); - const n = try obj.file.preadAll(file_contents, 0); + const n = file_reader.interface.readSliceShort(file_contents) catch |err| switch (err) { + error.ReadFailed => return file_reader.err.?, + }; if (n != file_contents.len) return error.UnexpectedEndOfFile; var archive = try Archive.parse(gpa, file_contents); diff --git a/src/link/Wasm/Flush.zig b/src/link/Wasm/Flush.zig index b5d93259e2..6bc1887855 100644 --- a/src/link/Wasm/Flush.zig +++ b/src/link/Wasm/Flush.zig @@ -1064,9 +1064,14 @@ pub fn finish(f: *Flush, wasm: *Wasm) !void { } // Finally, write the entire binary into the file. - const file = wasm.base.file.?; - try file.pwriteAll(binary_bytes.items, 0); - try file.setEndPos(binary_bytes.items.len); + var file_writer = wasm.base.file.?.writer(&.{}); + file_writer.interface.writeAll(binary_bytes.items) catch |err| switch (err) { + error.WriteFailed => return file_writer.err.?, + }; + file_writer.end() catch |err| switch (err) { + error.WriteFailed => return file_writer.err.?, + else => |e| return e, + }; } const VirtualAddrs = struct { diff --git a/src/main.zig b/src/main.zig index f84fc36d80..03d42ba898 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,5 +1,8 @@ -const std = @import("std"); const builtin = @import("builtin"); +const native_os = builtin.os.tag; + +const std = @import("std"); +const Io = std.Io; const assert = std.debug.assert; const fs = std.fs; const mem = std.mem; @@ -10,7 +13,6 @@ const Color = std.zig.Color; const warn = std.log.warn; const ThreadPool = std.Thread.Pool; const cleanExit = std.process.cleanExit; -const native_os = builtin.os.tag; const Cache = std.Build.Cache; const Path = std.Build.Cache.Path; const Directory = std.Build.Cache.Directory; @@ -245,26 +247,30 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { } } + var threaded: Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + const cmd = args[1]; const cmd_args = args[2..]; if (mem.eql(u8, cmd, "build-exe")) { dev.check(.build_exe_command); - return buildOutputType(gpa, arena, args, .{ .build = .Exe }); + return buildOutputType(gpa, arena, io, args, .{ .build = .Exe }); } else if (mem.eql(u8, cmd, "build-lib")) { dev.check(.build_lib_command); - return buildOutputType(gpa, arena, args, .{ .build = .Lib }); + return buildOutputType(gpa, arena, io, args, .{ .build = .Lib }); } else if (mem.eql(u8, cmd, "build-obj")) { dev.check(.build_obj_command); - return buildOutputType(gpa, arena, args, .{ .build = .Obj }); + return buildOutputType(gpa, arena, io, args, .{ .build = .Obj }); } else if (mem.eql(u8, cmd, "test")) { dev.check(.test_command); - return buildOutputType(gpa, arena, args, .zig_test); + return buildOutputType(gpa, arena, io, args, .zig_test); } else if (mem.eql(u8, cmd, "test-obj")) { dev.check(.test_command); - return buildOutputType(gpa, arena, args, .zig_test_obj); + return buildOutputType(gpa, arena, io, args, .zig_test_obj); } else if (mem.eql(u8, cmd, "run")) { dev.check(.run_command); - return buildOutputType(gpa, arena, args, .run); + return buildOutputType(gpa, arena, io, args, .run); } else if (mem.eql(u8, cmd, "dlltool") or mem.eql(u8, cmd, "ranlib") or mem.eql(u8, cmd, "lib") or @@ -274,7 +280,7 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { return process.exit(try llvmArMain(arena, args)); } else if (mem.eql(u8, cmd, "build")) { dev.check(.build_command); - return cmdBuild(gpa, arena, cmd_args); + return cmdBuild(gpa, arena, io, cmd_args); } else if (mem.eql(u8, cmd, "clang") or mem.eql(u8, cmd, "-cc1") or mem.eql(u8, cmd, "-cc1as")) { @@ -288,16 +294,16 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { return process.exit(try lldMain(arena, args, true)); } else if (mem.eql(u8, cmd, "cc")) { dev.check(.cc_command); - return buildOutputType(gpa, arena, args, .cc); + return buildOutputType(gpa, arena, io, args, .cc); } else if (mem.eql(u8, cmd, "c++")) { dev.check(.cc_command); - return buildOutputType(gpa, arena, args, .cpp); + return buildOutputType(gpa, arena, io, args, .cpp); } else if (mem.eql(u8, cmd, "translate-c")) { dev.check(.translate_c_command); - return buildOutputType(gpa, arena, args, .translate_c); + return buildOutputType(gpa, arena, io, args, .translate_c); } else if (mem.eql(u8, cmd, "rc")) { const use_server = cmd_args.len > 0 and std.mem.eql(u8, cmd_args[0], "--zig-integration"); - return jitCmd(gpa, arena, cmd_args, .{ + return jitCmd(gpa, arena, io, cmd_args, .{ .cmd_name = "resinator", .root_src_path = "resinator/main.zig", .depend_on_aro = true, @@ -306,22 +312,22 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { }); } else if (mem.eql(u8, cmd, "fmt")) { dev.check(.fmt_command); - return @import("fmt.zig").run(gpa, arena, cmd_args); + return @import("fmt.zig").run(gpa, arena, io, cmd_args); } else if (mem.eql(u8, cmd, "objcopy")) { - return jitCmd(gpa, arena, cmd_args, .{ + return jitCmd(gpa, arena, io, cmd_args, .{ .cmd_name = "objcopy", .root_src_path = "objcopy.zig", }); } else if (mem.eql(u8, cmd, "fetch")) { - return cmdFetch(gpa, arena, cmd_args); + return cmdFetch(gpa, arena, io, cmd_args); } else if (mem.eql(u8, cmd, "libc")) { - return jitCmd(gpa, arena, cmd_args, .{ + return jitCmd(gpa, arena, io, cmd_args, .{ .cmd_name = "libc", .root_src_path = "libc.zig", .prepend_zig_lib_dir_path = true, }); } else if (mem.eql(u8, cmd, "std")) { - return jitCmd(gpa, arena, cmd_args, .{ + return jitCmd(gpa, arena, io, cmd_args, .{ .cmd_name = "std", .root_src_path = "std-docs.zig", .prepend_zig_lib_dir_path = true, @@ -332,7 +338,7 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { return cmdInit(gpa, arena, cmd_args); } else if (mem.eql(u8, cmd, "targets")) { dev.check(.targets_command); - const host = std.zig.resolveTargetQueryOrFatal(.{}); + const host = std.zig.resolveTargetQueryOrFatal(io, .{}); var stdout_writer = fs.File.stdout().writer(&stdout_buffer); try @import("print_targets.zig").cmdTargets(arena, cmd_args, &stdout_writer.interface, &host); return stdout_writer.interface.flush(); @@ -342,16 +348,18 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { return; } else if (mem.eql(u8, cmd, "env")) { dev.check(.env_command); + const host = std.zig.resolveTargetQueryOrFatal(io, .{}); var stdout_writer = fs.File.stdout().writer(&stdout_buffer); try @import("print_env.zig").cmdEnv( arena, &stdout_writer.interface, args, if (native_os == .wasi) wasi_preopens, + &host, ); return stdout_writer.interface.flush(); } else if (mem.eql(u8, cmd, "reduce")) { - return jitCmd(gpa, arena, cmd_args, .{ + return jitCmd(gpa, arena, io, cmd_args, .{ .cmd_name = "reduce", .root_src_path = "reduce.zig", }); @@ -362,13 +370,13 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { dev.check(.help_command); return fs.File.stdout().writeAll(usage); } else if (mem.eql(u8, cmd, "ast-check")) { - return cmdAstCheck(arena, cmd_args); + return cmdAstCheck(arena, io, cmd_args); } else if (mem.eql(u8, cmd, "detect-cpu")) { - return cmdDetectCpu(cmd_args); + return cmdDetectCpu(io, cmd_args); } else if (build_options.enable_debug_extensions and mem.eql(u8, cmd, "changelist")) { - return cmdChangelist(arena, cmd_args); + return cmdChangelist(arena, io, cmd_args); } else if (build_options.enable_debug_extensions and mem.eql(u8, cmd, "dump-zir")) { - return cmdDumpZir(arena, cmd_args); + return cmdDumpZir(arena, io, cmd_args); } else if (build_options.enable_debug_extensions and mem.eql(u8, cmd, "llvm-ints")) { return cmdDumpLlvmInts(gpa, arena, cmd_args); } else { @@ -735,7 +743,7 @@ const ArgMode = union(enum) { const Listen = union(enum) { none, stdio: if (dev.env.supports(.stdio_listen)) void else noreturn, - ip4: if (dev.env.supports(.network_listen)) std.net.Ip4Address else noreturn, + ip4: if (dev.env.supports(.network_listen)) Io.net.Ip4Address else noreturn, }; const ArgsIterator = struct { @@ -792,6 +800,7 @@ const CliModule = struct { fn buildOutputType( gpa: Allocator, arena: Allocator, + io: Io, all_args: []const []const u8, arg_mode: ArgMode, ) !void { @@ -1328,7 +1337,7 @@ fn buildOutputType( const host, const port_text = mem.cutScalar(u8, next_arg, ':') orelse .{ next_arg, "14735" }; const port = std.fmt.parseInt(u16, port_text, 10) catch |err| fatal("invalid port number: '{s}': {s}", .{ port_text, @errorName(err) }); - listen = .{ .ip4 = std.net.Ip4Address.parse(host, port) catch |err| + listen = .{ .ip4 = Io.net.Ip4Address.parse(host, port) catch |err| fatal("invalid host: '{s}': {s}", .{ host, @errorName(err) }) }; } } else if (mem.eql(u8, arg, "--listen=-")) { @@ -3017,7 +3026,7 @@ fn buildOutputType( create_module.opts.emit_bin = emit_bin != .no; create_module.opts.any_c_source_files = create_module.c_source_files.items.len != 0; - const main_mod = try createModule(gpa, arena, &create_module, 0, null, color); + const main_mod = try createModule(gpa, arena, io, &create_module, 0, null, color); for (create_module.modules.keys(), create_module.modules.values()) |key, cli_mod| { if (cli_mod.resolved == null) fatal("module '{s}' declared but not used", .{key}); @@ -3311,7 +3320,7 @@ fn buildOutputType( var file_writer = f.writer(&.{}); var buffer: [1000]u8 = undefined; var hasher = file_writer.interface.hashed(Cache.Hasher.init("0123456789abcdef"), &buffer); - var stdin_reader = fs.File.stdin().readerStreaming(&.{}); + var stdin_reader = fs.File.stdin().readerStreaming(io, &.{}); _ = hasher.writer.sendFileAll(&stdin_reader, .unlimited) catch |err| switch (err) { error.WriteFailed => fatal("failed to write {s}: {t}", .{ dump_path, file_writer.err.? }), else => fatal("failed to pipe stdin to {s}: {t}", .{ dump_path, err }), @@ -3367,7 +3376,7 @@ fn buildOutputType( try create_module.rpath_list.appendSlice(arena, rpath_dedup.keys()); var create_diag: Compilation.CreateDiagnostic = undefined; - const comp = Compilation.create(gpa, arena, &create_diag, .{ + const comp = Compilation.create(gpa, arena, io, &create_diag, .{ .dirs = dirs, .thread_pool = &thread_pool, .self_exe_path = switch (native_os) { @@ -3542,7 +3551,7 @@ fn buildOutputType( switch (listen) { .none => {}, .stdio => { - var stdin_reader = fs.File.stdin().reader(&stdin_buffer); + var stdin_reader = fs.File.stdin().reader(io, &stdin_buffer); var stdout_writer = fs.File.stdout().writer(&stdout_buffer); try serve( comp, @@ -3557,22 +3566,22 @@ fn buildOutputType( return cleanExit(); }, .ip4 => |ip4_addr| { - const addr: std.net.Address = .{ .in = ip4_addr }; + const addr: Io.net.IpAddress = .{ .ip4 = ip4_addr }; - var server = try addr.listen(.{ + var server = try addr.listen(io, .{ .reuse_address = true, }); - defer server.deinit(); + defer server.deinit(io); - const conn = try server.accept(); - defer conn.stream.close(); + var stream = try server.accept(io); + defer stream.close(io); - var input = conn.stream.reader(&stdin_buffer); - var output = conn.stream.writer(&stdout_buffer); + var input = stream.reader(io, &stdin_buffer); + var output = stream.writer(io, &stdout_buffer); try serve( comp, - input.interface(), + &input.interface, &output.interface, test_exec_args.items, self_exe_path, @@ -3646,6 +3655,7 @@ fn buildOutputType( comp, gpa, arena, + io, test_exec_args.items, self_exe_path, arg_mode, @@ -3704,6 +3714,7 @@ const CreateModule = struct { fn createModule( gpa: Allocator, arena: Allocator, + io: Io, create_module: *CreateModule, index: usize, parent: ?*Package.Module, @@ -3777,7 +3788,7 @@ fn createModule( } const target_query = std.zig.parseTargetQueryOrReportFatalError(arena, target_parse_options); - const target = std.zig.resolveTargetQueryOrFatal(target_query); + const target = std.zig.resolveTargetQueryOrFatal(io, target_query); break :t .{ .result = target, .is_native_os = target_query.isNativeOs(), @@ -4022,7 +4033,7 @@ fn createModule( for (cli_mod.deps) |dep| { const dep_index = create_module.modules.getIndex(dep.value) orelse fatal("module '{s}' depends on non-existent module '{s}'", .{ name, dep.key }); - const dep_mod = try createModule(gpa, arena, create_module, dep_index, mod, color); + const dep_mod = try createModule(gpa, arena, io, create_module, dep_index, mod, color); try mod.deps.put(arena, dep.key, dep_mod); } @@ -4039,8 +4050,8 @@ fn saveState(comp: *Compilation, incremental: bool) void { fn serve( comp: *Compilation, - in: *std.Io.Reader, - out: *std.Io.Writer, + in: *Io.Reader, + out: *Io.Writer, test_exec_args: []const ?[]const u8, self_exe_path: ?[]const u8, arg_mode: ArgMode, @@ -4126,6 +4137,7 @@ fn serve( // comp, // gpa, // arena, + // io, // test_exec_args, // self_exe_path.?, // arg_mode, @@ -4280,6 +4292,7 @@ fn runOrTest( comp: *Compilation, gpa: Allocator, arena: Allocator, + io: Io, test_exec_args: []const ?[]const u8, self_exe_path: []const u8, arg_mode: ArgMode, @@ -4334,7 +4347,7 @@ fn runOrTest( std.debug.lockStdErr(); const err = process.execve(gpa, argv.items, &env_map); std.debug.unlockStdErr(); - try warnAboutForeignBinaries(arena, arg_mode, target, link_libc); + try warnAboutForeignBinaries(io, arena, arg_mode, target, link_libc); const cmd = try std.mem.join(arena, " ", argv.items); fatal("the following command failed to execve with '{s}':\n{s}", .{ @errorName(err), cmd }); } else if (process.can_spawn) { @@ -4355,7 +4368,7 @@ fn runOrTest( break :t child.spawnAndWait(); }; const term = term_result catch |err| { - try warnAboutForeignBinaries(arena, arg_mode, target, link_libc); + try warnAboutForeignBinaries(io, arena, arg_mode, target, link_libc); const cmd = try std.mem.join(arena, " ", argv.items); fatal("the following command failed with '{s}':\n{s}", .{ @errorName(err), cmd }); }; @@ -4521,6 +4534,8 @@ fn cmdTranslateC( ) !void { dev.check(.translate_c_command); + const io = comp.io; + assert(comp.c_source_files.len == 1); const c_source_file = comp.c_source_files[0]; @@ -4584,7 +4599,7 @@ fn cmdTranslateC( }; defer zig_file.close(); var stdout_writer = fs.File.stdout().writer(&stdout_buffer); - var file_reader = zig_file.reader(&.{}); + var file_reader = zig_file.reader(io, &.{}); _ = try stdout_writer.interface.sendFileAll(&file_reader, .unlimited); try stdout_writer.interface.flush(); return cleanExit(); @@ -4594,11 +4609,12 @@ fn cmdTranslateC( pub fn translateC( gpa: Allocator, arena: Allocator, + io: Io, argv: []const []const u8, prog_node: std.Progress.Node, capture: ?*[]u8, ) !void { - try jitCmd(gpa, arena, argv, .{ + try jitCmd(gpa, arena, io, argv, .{ .cmd_name = "translate-c", .root_src_path = "translate-c/main.zig", .depend_on_aro = true, @@ -4755,7 +4771,7 @@ test sanitizeExampleName { try std.testing.expectEqualStrings("test_project", try sanitizeExampleName(arena, "test project")); } -fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { +fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !void { dev.check(.build_command); var build_file: ?[]const u8 = null; @@ -4983,7 +4999,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { .arch_os_abi = triple, }); break :t .{ - .result = std.zig.resolveTargetQueryOrFatal(target_query), + .result = std.zig.resolveTargetQueryOrFatal(io, target_query), .is_native_os = false, .is_native_abi = false, .is_explicit_dynamic_linker = false, @@ -4991,7 +5007,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { } } break :t .{ - .result = std.zig.resolveTargetQueryOrFatal(.{}), + .result = std.zig.resolveTargetQueryOrFatal(io, .{}), .is_native_os = true, .is_native_abi = true, .is_explicit_dynamic_linker = false, @@ -5046,8 +5062,9 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { // Prevents bootstrap from depending on a bunch of unnecessary stuff. var http_client: if (dev.env.supports(.fetch_command)) std.http.Client else struct { allocator: Allocator, + io: Io, fn deinit(_: @This()) void {} - } = .{ .allocator = gpa }; + } = .{ .allocator = gpa, .io = io }; defer http_client.deinit(); var unlazy_set: Package.Fetch.JobQueue.UnlazySet = .{}; @@ -5139,6 +5156,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { var fetch: Package.Fetch = .{ .arena = std.heap.ArenaAllocator.init(gpa), + .io = io, .location = .{ .relative_path = phantom_package_root }, .location_tok = 0, .hash_tok = .none, @@ -5261,7 +5279,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { try root_mod.deps.put(arena, "@build", build_mod); var create_diag: Compilation.CreateDiagnostic = undefined; - const comp = Compilation.create(gpa, arena, &create_diag, .{ + const comp = Compilation.create(gpa, arena, io, &create_diag, .{ .libc_installation = libc_installation, .dirs = dirs, .root_name = "build", @@ -5400,6 +5418,7 @@ const JitCmdOptions = struct { fn jitCmd( gpa: Allocator, arena: Allocator, + io: Io, args: []const []const u8, options: JitCmdOptions, ) !void { @@ -5412,7 +5431,7 @@ fn jitCmd( const target_query: std.Target.Query = .{}; const resolved_target: Package.Module.ResolvedTarget = .{ - .result = std.zig.resolveTargetQueryOrFatal(target_query), + .result = std.zig.resolveTargetQueryOrFatal(io, target_query), .is_native_os = true, .is_native_abi = true, .is_explicit_dynamic_linker = false, @@ -5504,7 +5523,7 @@ fn jitCmd( } var create_diag: Compilation.CreateDiagnostic = undefined; - const comp = Compilation.create(gpa, arena, &create_diag, .{ + const comp = Compilation.create(gpa, arena, io, &create_diag, .{ .dirs = dirs, .root_name = options.cmd_name, .config = config, @@ -5584,7 +5603,7 @@ fn jitCmd( try child.spawn(); if (options.capture) |ptr| { - var stdout_reader = child.stdout.?.readerStreaming(&.{}); + var stdout_reader = child.stdout.?.readerStreaming(io, &.{}); ptr.* = try stdout_reader.interface.allocRemaining(arena, .limited(std.math.maxInt(u32))); } @@ -6039,10 +6058,7 @@ const usage_ast_check = \\ ; -fn cmdAstCheck( - arena: Allocator, - args: []const []const u8, -) !void { +fn cmdAstCheck(arena: Allocator, io: Io, args: []const []const u8) !void { dev.check(.ast_check_command); const Zir = std.zig.Zir; @@ -6090,7 +6106,7 @@ fn cmdAstCheck( }; } else fs.File.stdin(); defer if (zig_source_path != null) f.close(); - var file_reader: fs.File.Reader = f.reader(&stdin_buffer); + var file_reader: fs.File.Reader = f.reader(io, &stdin_buffer); break :s std.zig.readSourceFileToEndAlloc(arena, &file_reader) catch |err| { fatal("unable to load file '{s}' for ast-check: {s}", .{ display_path, @errorName(err) }); }; @@ -6209,7 +6225,7 @@ fn cmdAstCheck( } } -fn cmdDetectCpu(args: []const []const u8) !void { +fn cmdDetectCpu(io: Io, args: []const []const u8) !void { dev.check(.detect_cpu_command); const detect_cpu_usage = @@ -6254,7 +6270,7 @@ fn cmdDetectCpu(args: []const []const u8) !void { const cpu = try detectNativeCpuWithLLVM(builtin.cpu.arch, name, features); try printCpu(cpu); } else { - const host_target = std.zig.resolveTargetQueryOrFatal(.{}); + const host_target = std.zig.resolveTargetQueryOrFatal(io, .{}); try printCpu(host_target.cpu); } } @@ -6385,10 +6401,7 @@ fn cmdDumpLlvmInts( } /// This is only enabled for debug builds. -fn cmdDumpZir( - arena: Allocator, - args: []const []const u8, -) !void { +fn cmdDumpZir(arena: Allocator, io: Io, args: []const []const u8) !void { dev.check(.dump_zir_command); const Zir = std.zig.Zir; @@ -6400,7 +6413,7 @@ fn cmdDumpZir( }; defer f.close(); - const zir = try Zcu.loadZirCache(arena, f); + const zir = try Zcu.loadZirCache(arena, io, f); var stdout_writer = fs.File.stdout().writerStreaming(&stdout_buffer); const stdout_bw = &stdout_writer.interface; { @@ -6432,10 +6445,7 @@ fn cmdDumpZir( } /// This is only enabled for debug builds. -fn cmdChangelist( - arena: Allocator, - args: []const []const u8, -) !void { +fn cmdChangelist(arena: Allocator, io: Io, args: []const []const u8) !void { dev.check(.changelist_command); const color: Color = .auto; @@ -6448,7 +6458,7 @@ fn cmdChangelist( var f = fs.cwd().openFile(old_source_path, .{}) catch |err| fatal("unable to open old source file '{s}': {s}", .{ old_source_path, @errorName(err) }); defer f.close(); - var file_reader: fs.File.Reader = f.reader(&stdin_buffer); + var file_reader: fs.File.Reader = f.reader(io, &stdin_buffer); break :source std.zig.readSourceFileToEndAlloc(arena, &file_reader) catch |err| fatal("unable to read old source file '{s}': {s}", .{ old_source_path, @errorName(err) }); }; @@ -6456,7 +6466,7 @@ fn cmdChangelist( var f = fs.cwd().openFile(new_source_path, .{}) catch |err| fatal("unable to open new source file '{s}': {s}", .{ new_source_path, @errorName(err) }); defer f.close(); - var file_reader: fs.File.Reader = f.reader(&stdin_buffer); + var file_reader: fs.File.Reader = f.reader(io, &stdin_buffer); break :source std.zig.readSourceFileToEndAlloc(arena, &file_reader) catch |err| fatal("unable to read new source file '{s}': {s}", .{ new_source_path, @errorName(err) }); }; @@ -6521,13 +6531,14 @@ fn prefixedIntArg(arg: []const u8, prefix: []const u8) ?u64 { } fn warnAboutForeignBinaries( + io: Io, arena: Allocator, arg_mode: ArgMode, target: *const std.Target, link_libc: bool, ) !void { const host_query: std.Target.Query = .{}; - const host_target = std.zig.resolveTargetQueryOrFatal(host_query); + const host_target = std.zig.resolveTargetQueryOrFatal(io, host_query); switch (std.zig.system.getExternalExecutor(&host_target, target, .{ .link_libc = link_libc })) { .native => return, @@ -6812,6 +6823,7 @@ const usage_fetch = fn cmdFetch( gpa: Allocator, arena: Allocator, + io: Io, args: []const []const u8, ) !void { dev.check(.fetch_command); @@ -6867,7 +6879,7 @@ fn cmdFetch( try thread_pool.init(.{ .allocator = gpa }); defer thread_pool.deinit(); - var http_client: std.http.Client = .{ .allocator = gpa }; + var http_client: std.http.Client = .{ .allocator = gpa, .io = io }; defer http_client.deinit(); try http_client.initDefaultProxies(arena); @@ -6900,6 +6912,7 @@ fn cmdFetch( var fetch: Package.Fetch = .{ .arena = std.heap.ArenaAllocator.init(gpa), + .io = io, .location = .{ .path_or_url = path_or_url }, .location_tok = 0, .hash_tok = .none, @@ -7080,7 +7093,7 @@ fn cmdFetch( try fixups.append_string_after_node.put(gpa, manifest.version_node, dependencies_text); } - var aw: std.Io.Writer.Allocating = .init(gpa); + var aw: Io.Writer.Allocating = .init(gpa); defer aw.deinit(); try ast.render(gpa, &aw.writer, fixups); const rendered = aw.written(); diff --git a/src/print_env.zig b/src/print_env.zig index e1b2b1eb83..e1847688ad 100644 --- a/src/print_env.zig +++ b/src/print_env.zig @@ -14,6 +14,7 @@ pub fn cmdEnv( .wasi => std.fs.wasi.Preopens, else => void, }, + host: *const std.Target, ) !void { const override_lib_dir: ?[]const u8 = try EnvVar.ZIG_LIB_DIR.get(arena); const override_global_cache_dir: ?[]const u8 = try EnvVar.ZIG_GLOBAL_CACHE_DIR.get(arena); @@ -38,8 +39,6 @@ pub fn cmdEnv( const zig_lib_dir = dirs.zig_lib.path orelse ""; const zig_std_dir = try dirs.zig_lib.join(arena, &.{"std"}); const global_cache_dir = dirs.global_cache.path orelse ""; - - const host = try std.zig.system.resolveTargetQuery(.{}); const triple = try host.zigTriple(arena); var serializer: std.zon.Serializer = .{ .writer = out }; diff --git a/test/src/Cases.zig b/test/src/Cases.zig index 7edb66aede..2fdcc45195 100644 --- a/test/src/Cases.zig +++ b/test/src/Cases.zig @@ -370,6 +370,10 @@ fn addFromDirInner( const resolved_target = b.resolveTargetQuery(target_query); const target = &resolved_target.result; for (backends) |backend| { + if (backend == .selfhosted and target.cpu.arch == .wasm32) { + // https://github.com/ziglang/zig/issues/25684 + continue; + } if (backend == .selfhosted and target.cpu.arch != .aarch64 and target.cpu.arch != .wasm32 and target.cpu.arch != .x86_64 and target.cpu.arch != .spirv64) { @@ -455,8 +459,7 @@ pub fn lowerToBuildSteps( parent_step: *std.Build.Step, options: CaseTestOptions, ) void { - const host = std.zig.system.resolveTargetQuery(.{}) catch |err| - std.debug.panic("unable to detect native host: {s}\n", .{@errorName(err)}); + const host = b.resolveTargetQuery(.{}); const cases_dir_path = b.build_root.join(b.allocator, &.{ "test", "cases" }) catch @panic("OOM"); for (self.cases.items) |case| { @@ -587,7 +590,7 @@ pub fn lowerToBuildSteps( }, .Execution => |expected_stdout| no_exec: { const run = if (case.target.result.ofmt == .c) run_step: { - if (getExternalExecutor(&host, &case.target.result, .{ .link_libc = true }) != .native) { + if (getExternalExecutor(&host.result, &case.target.result, .{ .link_libc = true }) != .native) { // We wouldn't be able to run the compiled C code. break :no_exec; } @@ -972,14 +975,6 @@ const TestManifest = struct { } }; -fn resolveTargetQuery(query: std.Target.Query) std.Build.ResolvedTarget { - return .{ - .query = query, - .target = std.zig.system.resolveTargetQuery(query) catch - @panic("unable to resolve target query"), - }; -} - fn knownFileExtension(filename: []const u8) bool { // List taken from `Compilation.classifyFileExt` in the compiler. for ([_][]const u8{ diff --git a/test/src/convert-stack-trace.zig b/test/src/convert-stack-trace.zig index c7cd01a460..91be53a8e5 100644 --- a/test/src/convert-stack-trace.zig +++ b/test/src/convert-stack-trace.zig @@ -32,6 +32,12 @@ pub fn main() !void { const args = try std.process.argsAlloc(arena); if (args.len != 2) std.process.fatal("usage: convert-stack-trace path/to/test/output", .{}); + const gpa = arena; + + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + var read_buf: [1024]u8 = undefined; var write_buf: [1024]u8 = undefined; @@ -40,7 +46,7 @@ pub fn main() !void { const out_file: std.fs.File = .stdout(); - var in_fr = in_file.reader(&read_buf); + var in_fr = in_file.reader(io, &read_buf); var out_fw = out_file.writer(&write_buf); const w = &out_fw.interface; diff --git a/test/standalone/child_process/child.zig b/test/standalone/child_process/child.zig index b02bec3500..2e74f30882 100644 --- a/test/standalone/child_process/child.zig +++ b/test/standalone/child_process/child.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const Io = std.Io; // 42 is expected by parent; other values result in test failure var exit_code: u8 = 42; @@ -6,12 +7,17 @@ var exit_code: u8 = 42; pub fn main() !void { var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator); const arena = arena_state.allocator(); - try run(arena); + + var threaded: std.Io.Threaded = .init(arena); + defer threaded.deinit(); + const io = threaded.io(); + + try run(arena, io); arena_state.deinit(); std.process.exit(exit_code); } -fn run(allocator: std.mem.Allocator) !void { +fn run(allocator: std.mem.Allocator, io: Io) !void { var args = try std.process.argsWithAllocator(allocator); defer args.deinit(); _ = args.next() orelse unreachable; // skip binary name @@ -33,7 +39,8 @@ fn run(allocator: std.mem.Allocator) !void { const hello_stdin = "hello from stdin"; var buf: [hello_stdin.len]u8 = undefined; const stdin: std.fs.File = .stdin(); - const n = try stdin.readAll(&buf); + var reader = stdin.reader(io, &.{}); + const n = try reader.interface.readSliceShort(&buf); if (!std.mem.eql(u8, buf[0..n], hello_stdin)) { testError("stdin: '{s}'; want '{s}'", .{ buf[0..n], hello_stdin }); } diff --git a/test/standalone/child_process/main.zig b/test/standalone/child_process/main.zig index 4be5e1fec3..5970cdd952 100644 --- a/test/standalone/child_process/main.zig +++ b/test/standalone/child_process/main.zig @@ -20,6 +20,10 @@ pub fn main() !void { }; defer if (needs_free) gpa.free(child_path); + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + var child = std.process.Child.init(&.{ child_path, "hello arg" }, gpa); child.stdin_behavior = .Pipe; child.stdout_behavior = .Pipe; @@ -32,7 +36,7 @@ pub fn main() !void { const hello_stdout = "hello from stdout"; var buf: [hello_stdout.len]u8 = undefined; - var stdout_reader = child.stdout.?.readerStreaming(&.{}); + var stdout_reader = child.stdout.?.readerStreaming(io, &.{}); const n = try stdout_reader.interface.readSliceShort(&buf); if (!std.mem.eql(u8, buf[0..n], hello_stdout)) { testError("child stdout: '{s}'; want '{s}'", .{ buf[0..n], hello_stdout }); diff --git a/test/standalone/coff_dwarf/main.zig b/test/standalone/coff_dwarf/main.zig index 6707bab4dc..e7590f3f07 100644 --- a/test/standalone/coff_dwarf/main.zig +++ b/test/standalone/coff_dwarf/main.zig @@ -11,10 +11,14 @@ pub fn main() void { var di: std.debug.SelfInfo = .init; defer di.deinit(gpa); + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + var add_addr: usize = undefined; _ = add(1, 2, &add_addr); - const symbol = di.getSymbol(gpa, add_addr) catch |err| fatal("failed to get symbol: {t}", .{err}); + const symbol = di.getSymbol(gpa, io, add_addr) catch |err| fatal("failed to get symbol: {t}", .{err}); defer if (symbol.source_location) |sl| gpa.free(sl.file_name); if (symbol.name == null) fatal("failed to resolve symbol name", .{}); diff --git a/test/standalone/libfuzzer/main.zig b/test/standalone/libfuzzer/main.zig index b21e9be250..b275b6d593 100644 --- a/test/standalone/libfuzzer/main.zig +++ b/test/standalone/libfuzzer/main.zig @@ -15,6 +15,10 @@ pub fn main() !void { defer args.deinit(); _ = args.skip(); // executable name + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + const cache_dir_path = args.next() orelse @panic("expected cache directory path argument"); var cache_dir = try std.fs.cwd().openDir(cache_dir_path, .{}); defer cache_dir.close(); @@ -30,7 +34,7 @@ pub fn main() !void { defer coverage_file.close(); var read_buf: [@sizeOf(abi.SeenPcsHeader)]u8 = undefined; - var r = coverage_file.reader(&read_buf); + var r = coverage_file.reader(io, &read_buf); const pcs_header = r.interface.takeStruct(abi.SeenPcsHeader, native_endian) catch return r.err.?; if (pcs_header.pcs_len == 0) diff --git a/test/standalone/posix/sigaction.zig b/test/standalone/posix/sigaction.zig index 4f5f81ad66..8b2a6f3b98 100644 --- a/test/standalone/posix/sigaction.zig +++ b/test/standalone/posix/sigaction.zig @@ -17,12 +17,12 @@ fn test_sigaction() !void { return; // https://github.com/ziglang/zig/issues/15381 } - const test_signo = std.posix.SIG.URG; // URG only because it is ignored by default in debuggers + const test_signo: std.posix.SIG = .URG; // URG only because it is ignored by default in debuggers const S = struct { var handler_called_count: u32 = 0; - fn handler(sig: i32, info: *const std.posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) void { + fn handler(sig: std.posix.SIG, info: *const std.posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) void { _ = ctx_ptr; // Check that we received the correct signal. const info_sig = switch (native_os) { @@ -80,20 +80,18 @@ fn test_sigaction() !void { } fn test_sigset_bits() !void { - const NO_SIG: i32 = 0; - const S = struct { - var expected_sig: i32 = undefined; - var seen_sig: i32 = NO_SIG; + var expected_sig: std.posix.SIG = undefined; + var seen_sig: ?std.posix.SIG = null; - fn handler(sig: i32, info: *const std.posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) void { + fn handler(sig: std.posix.SIG, info: *const std.posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) void { _ = ctx_ptr; const info_sig = switch (native_os) { .netbsd => info.info.signo, else => info.signo, }; - if (seen_sig == NO_SIG and sig == expected_sig and sig == info_sig) { + if (seen_sig == null and sig == expected_sig and sig == info_sig) { seen_sig = sig; } } @@ -107,11 +105,9 @@ fn test_sigset_bits() !void { // big-endian), try sending a blocked signal to make sure the mask matches the // signal. (Send URG and CHLD because they're ignored by default in the // debugger, vs. USR1 or other named signals) - inline for ([_]i32{ std.posix.SIG.URG, std.posix.SIG.CHLD, 62, 94, 126 }) |test_signo| { - if (test_signo >= std.posix.NSIG) continue; - + inline for ([_]std.posix.SIG{ .URG, .CHLD }) |test_signo| { S.expected_sig = test_signo; - S.seen_sig = NO_SIG; + S.seen_sig = null; const sa: std.posix.Sigaction = .{ .handler = .{ .sigaction = &S.handler }, @@ -135,14 +131,14 @@ fn test_sigset_bits() !void { switch (std.posix.errno(rc)) { .SUCCESS => { // See that the signal is blocked, then unblocked - try std.testing.expectEqual(NO_SIG, S.seen_sig); + try std.testing.expectEqual(null, S.seen_sig); std.posix.sigprocmask(std.posix.SIG.UNBLOCK, &block_one, null); try std.testing.expectEqual(test_signo, S.seen_sig); }, .INVAL => { // Signal won't get delviered. Just clean up. std.posix.sigprocmask(std.posix.SIG.UNBLOCK, &block_one, null); - try std.testing.expectEqual(NO_SIG, S.seen_sig); + try std.testing.expectEqual(null, S.seen_sig); }, else => |errno| return std.posix.unexpectedErrno(errno), } diff --git a/test/standalone/simple/cat/main.zig b/test/standalone/simple/cat/main.zig index 61b7fbe7ce..9ea980aecc 100644 --- a/test/standalone/simple/cat/main.zig +++ b/test/standalone/simple/cat/main.zig @@ -9,6 +9,10 @@ pub fn main() !void { defer arena_instance.deinit(); const arena = arena_instance.allocator(); + var threaded: std.Io.Threaded = .init(arena); + defer threaded.deinit(); + const io = threaded.io(); + const args = try std.process.argsAlloc(arena); const exe = args[0]; @@ -16,7 +20,7 @@ pub fn main() !void { var stdout_buffer: [4096]u8 = undefined; var stdout_writer = fs.File.stdout().writerStreaming(&stdout_buffer); const stdout = &stdout_writer.interface; - var stdin_reader = fs.File.stdin().readerStreaming(&.{}); + var stdin_reader = fs.File.stdin().readerStreaming(io, &.{}); const cwd = fs.cwd(); @@ -32,7 +36,7 @@ pub fn main() !void { defer file.close(); catted_anything = true; - var file_reader = file.reader(&.{}); + var file_reader = file.reader(io, &.{}); _ = try stdout.sendFileAll(&file_reader, .unlimited); try stdout.flush(); } diff --git a/test/standalone/test_obj_link_run/build.zig b/test/standalone/test_obj_link_run/build.zig index 45d35865c6..19d52ce68c 100644 --- a/test/standalone/test_obj_link_run/build.zig +++ b/test/standalone/test_obj_link_run/build.zig @@ -11,6 +11,7 @@ pub fn build(b: *std.Build) void { if (is_windows) { test_obj.linkSystemLibrary("ntdll"); test_obj.linkSystemLibrary("kernel32"); + test_obj.linkSystemLibrary("ws2_32"); } const test_exe_mod = b.createModule(.{ diff --git a/test/standalone/windows_spawn/main.zig b/test/standalone/windows_spawn/main.zig index 8802f6efff..10ee35f4df 100644 --- a/test/standalone/windows_spawn/main.zig +++ b/test/standalone/windows_spawn/main.zig @@ -224,7 +224,7 @@ fn renameExe(dir: std.fs.Dir, old_sub_path: []const u8, new_sub_path: []const u8 error.AccessDenied => { if (attempt == 13) return error.AccessDenied; // give the kernel a chance to finish closing the executable handle - std.os.windows.kernel32.Sleep(@as(u32, 1) << attempt >> 1); + _ = std.os.windows.kernel32.SleepEx(@as(u32, 1) << attempt >> 1, std.os.windows.FALSE); attempt += 1; continue; }, diff --git a/tools/docgen.zig b/tools/docgen.zig index 18311b0d54..d23892e06c 100644 --- a/tools/docgen.zig +++ b/tools/docgen.zig @@ -36,6 +36,12 @@ pub fn main() !void { var args_it = try process.argsWithAllocator(arena); if (!args_it.skip()) @panic("expected self arg"); + const gpa = arena; + + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + var opt_code_dir: ?[]const u8 = null; var opt_input: ?[]const u8 = null; var opt_output: ?[]const u8 = null; @@ -77,7 +83,7 @@ pub fn main() !void { var code_dir = try fs.cwd().openDir(code_dir_path, .{}); defer code_dir.close(); - var in_file_reader = in_file.reader(&.{}); + var in_file_reader = in_file.reader(io, &.{}); const input_file_bytes = try in_file_reader.interface.allocRemaining(arena, .limited(max_doc_file_size)); var tokenizer = Tokenizer.init(input_path, input_file_bytes); diff --git a/tools/doctest.zig b/tools/doctest.zig index ccdbc6b065..de8d3a80ee 100644 --- a/tools/doctest.zig +++ b/tools/doctest.zig @@ -1,5 +1,8 @@ const builtin = @import("builtin"); + const std = @import("std"); +const Io = std.Io; +const Writer = std.Io.Writer; const fatal = std.process.fatal; const mem = std.mem; const fs = std.fs; @@ -7,7 +10,6 @@ const process = std.process; const Allocator = std.mem.Allocator; const testing = std.testing; const getExternalExecutor = std.zig.system.getExternalExecutor; -const Writer = std.Io.Writer; const max_doc_file_size = 10 * 1024 * 1024; @@ -36,6 +38,12 @@ pub fn main() !void { var args_it = try process.argsWithAllocator(arena); if (!args_it.skip()) fatal("missing argv[0]", .{}); + const gpa = arena; + + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + var opt_input: ?[]const u8 = null; var opt_output: ?[]const u8 = null; var opt_zig: ?[]const u8 = null; @@ -93,6 +101,7 @@ pub fn main() !void { try printSourceBlock(arena, out, source, fs.path.basename(input_path)); try printOutput( arena, + io, out, code, tmp_dir_path, @@ -109,6 +118,7 @@ pub fn main() !void { fn printOutput( arena: Allocator, + io: Io, out: *Writer, code: Code, /// Relative to this process' cwd. @@ -123,11 +133,11 @@ fn printOutput( var env_map = try process.getEnvMap(arena); try env_map.put("CLICOLOR_FORCE", "1"); - const host = try std.zig.system.resolveTargetQuery(.{}); + const host = try std.zig.system.resolveTargetQuery(io, .{}); const obj_ext = builtin.object_format.fileExt(builtin.cpu.arch); const print = std.debug.print; - var shell_buffer: std.Io.Writer.Allocating = .init(arena); + var shell_buffer: Writer.Allocating = .init(arena); defer shell_buffer.deinit(); const shell_out = &shell_buffer.writer; @@ -238,7 +248,7 @@ fn printOutput( const target_query = try std.Target.Query.parse(.{ .arch_os_abi = code.target_str orelse "native", }); - const target = try std.zig.system.resolveTargetQuery(target_query); + const target = try std.zig.system.resolveTargetQuery(io, target_query); const path_to_exe = try std.fmt.allocPrint(arena, "./{s}{s}", .{ code_name, target.exeFileExt(), @@ -316,9 +326,7 @@ fn printOutput( const target_query = try std.Target.Query.parse(.{ .arch_os_abi = triple, }); - const target = try std.zig.system.resolveTargetQuery( - target_query, - ); + const target = try std.zig.system.resolveTargetQuery(io, target_query); switch (getExternalExecutor(&host, &target, .{ .link_libc = code.link_libc, })) { @@ -1397,7 +1405,7 @@ test "printShell" { \\ ; - var buffer: std.Io.Writer.Allocating = .init(test_allocator); + var buffer: Writer.Allocating = .init(test_allocator); defer buffer.deinit(); try printShell(&buffer.writer, shell_out, false); @@ -1414,7 +1422,7 @@ test "printShell" { \\ ; - var buffer: std.Io.Writer.Allocating = .init(test_allocator); + var buffer: Writer.Allocating = .init(test_allocator); defer buffer.deinit(); try printShell(&buffer.writer, shell_out, false); @@ -1428,7 +1436,7 @@ test "printShell" { \\ ; - var buffer: std.Io.Writer.Allocating = .init(test_allocator); + var buffer: Writer.Allocating = .init(test_allocator); defer buffer.deinit(); try printShell(&buffer.writer, shell_out, false); @@ -1447,7 +1455,7 @@ test "printShell" { \\ ; - var buffer: std.Io.Writer.Allocating = .init(test_allocator); + var buffer: Writer.Allocating = .init(test_allocator); defer buffer.deinit(); try printShell(&buffer.writer, shell_out, false); @@ -1468,7 +1476,7 @@ test "printShell" { \\ ; - var buffer: std.Io.Writer.Allocating = .init(test_allocator); + var buffer: Writer.Allocating = .init(test_allocator); defer buffer.deinit(); try printShell(&buffer.writer, shell_out, false); @@ -1487,7 +1495,7 @@ test "printShell" { \\ ; - var buffer: std.Io.Writer.Allocating = .init(test_allocator); + var buffer: Writer.Allocating = .init(test_allocator); defer buffer.deinit(); try printShell(&buffer.writer, shell_out, false); @@ -1510,7 +1518,7 @@ test "printShell" { \\ ; - var buffer: std.Io.Writer.Allocating = .init(test_allocator); + var buffer: Writer.Allocating = .init(test_allocator); defer buffer.deinit(); try printShell(&buffer.writer, shell_out, false); @@ -1532,7 +1540,7 @@ test "printShell" { \\ ; - var buffer: std.Io.Writer.Allocating = .init(test_allocator); + var buffer: Writer.Allocating = .init(test_allocator); defer buffer.deinit(); try printShell(&buffer.writer, shell_out, false); @@ -1549,7 +1557,7 @@ test "printShell" { \\ ; - var buffer: std.Io.Writer.Allocating = .init(test_allocator); + var buffer: Writer.Allocating = .init(test_allocator); defer buffer.deinit(); try printShell(&buffer.writer, shell_out, false); @@ -1568,7 +1576,7 @@ test "printShell" { \\ ; - var buffer: std.Io.Writer.Allocating = .init(test_allocator); + var buffer: Writer.Allocating = .init(test_allocator); defer buffer.deinit(); try printShell(&buffer.writer, shell_out, false); @@ -1583,7 +1591,7 @@ test "printShell" { \\ ; - var buffer: std.Io.Writer.Allocating = .init(test_allocator); + var buffer: Writer.Allocating = .init(test_allocator); defer buffer.deinit(); try printShell(&buffer.writer, shell_out, false); diff --git a/tools/fetch_them_macos_headers.zig b/tools/fetch_them_macos_headers.zig index ca022e9b0c..2a2a2452e7 100644 --- a/tools/fetch_them_macos_headers.zig +++ b/tools/fetch_them_macos_headers.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const Io = std.Io; const fs = std.fs; const mem = std.mem; const process = std.process; @@ -85,8 +86,12 @@ pub fn main() anyerror!void { } else try argv.append(arg); } + var threaded: Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + const sysroot_path = sysroot orelse blk: { - const target = try std.zig.system.resolveTargetQuery(.{}); + const target = try std.zig.system.resolveTargetQuery(io, .{}); break :blk std.zig.system.darwin.getSdk(allocator, &target) orelse fatal("no SDK found; you can provide one explicitly with '--sysroot' flag", .{}); }; @@ -114,12 +119,13 @@ pub fn main() anyerror!void { .arch = arch, .os_ver = os_ver, }; - try fetchTarget(allocator, argv.items, sysroot_path, target, version, tmp); + try fetchTarget(allocator, io, argv.items, sysroot_path, target, version, tmp); } } fn fetchTarget( arena: Allocator, + io: Io, args: []const []const u8, sysroot: []const u8, target: Target, @@ -190,7 +196,7 @@ fn fetchTarget( var dirs = std.StringHashMap(fs.Dir).init(arena); try dirs.putNoClobber(".", dest_dir); - var headers_list_file_reader = headers_list_file.reader(&.{}); + var headers_list_file_reader = headers_list_file.reader(io, &.{}); const headers_list_str = try headers_list_file_reader.interface.allocRemaining(arena, .unlimited); const prefix = "/usr/include"; @@ -263,8 +269,8 @@ const Version = struct { pub fn format( v: Version, - writer: *std.Io.Writer, - ) std.Io.Writer.Error!void { + writer: *Io.Writer, + ) Io.Writer.Error!void { try writer.print("{d}.{d}.{d}", .{ v.major, v.minor, v.patch }); } }; diff --git a/tools/gen_macos_headers_c.zig b/tools/gen_macos_headers_c.zig index f95023adb7..fe036cf6b7 100644 --- a/tools/gen_macos_headers_c.zig +++ b/tools/gen_macos_headers_c.zig @@ -33,7 +33,7 @@ pub fn main() anyerror!void { if (positionals.items.len != 1) fatal("expected one positional argument: [dir]", .{}); - var dir = try std.fs.cwd().openDir(positionals.items[0], .{ .no_follow = true }); + var dir = try std.fs.cwd().openDir(positionals.items[0], .{ .follow_symlinks = false }); defer dir.close(); var paths = std.array_list.Managed([]const u8).init(arena); try findHeaders(arena, dir, "", &paths); @@ -73,7 +73,7 @@ fn findHeaders( switch (entry.kind) { .directory => { const path = try std.fs.path.join(arena, &.{ prefix, entry.name }); - var subdir = try dir.openDir(entry.name, .{ .no_follow = true }); + var subdir = try dir.openDir(entry.name, .{ .follow_symlinks = false }); defer subdir.close(); try findHeaders(arena, subdir, path, paths); }, diff --git a/tools/generate_c_size_and_align_checks.zig b/tools/generate_c_size_and_align_checks.zig index 8c278407e4..3663756533 100644 --- a/tools/generate_c_size_and_align_checks.zig +++ b/tools/generate_c_size_and_align_checks.zig @@ -39,8 +39,12 @@ pub fn main() !void { std.process.exit(1); } + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + const query = try std.Target.Query.parse(.{ .arch_os_abi = args[1] }); - const target = try std.zig.system.resolveTargetQuery(query); + const target = try std.zig.system.resolveTargetQuery(io, query); var buffer: [2000]u8 = undefined; var stdout_writer = std.fs.File.stdout().writerStreaming(&buffer); diff --git a/tools/incr-check.zig b/tools/incr-check.zig index 183d59bf88..22031e147a 100644 --- a/tools/incr-check.zig +++ b/tools/incr-check.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const Io = std.Io; const Allocator = std.mem.Allocator; const Cache = std.Build.Cache; @@ -11,6 +12,12 @@ pub fn main() !void { defer arena_instance.deinit(); const arena = arena_instance.allocator(); + const gpa = arena; + + var threaded: Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + var opt_zig_exe: ?[]const u8 = null; var opt_input_file_name: ?[]const u8 = null; var opt_lib_dir: ?[]const u8 = null; @@ -53,7 +60,7 @@ pub fn main() !void { const input_file_name = opt_input_file_name orelse fatal("missing input file\n{s}", .{usage}); const input_file_bytes = try std.fs.cwd().readFileAlloc(input_file_name, arena, .limited(std.math.maxInt(u32))); - const case = try Case.parse(arena, input_file_bytes); + const case = try Case.parse(arena, io, input_file_bytes); // Check now: if there are any targets using the `cbe` backend, we need the lib dir. if (opt_lib_dir == null) { @@ -86,22 +93,21 @@ pub fn main() !void { else null; - const host = try std.zig.system.resolveTargetQuery(.{}); + const host = try std.zig.system.resolveTargetQuery(io, .{}); const debug_log_verbose = debug_zcu or debug_dwarf or debug_link; for (case.targets) |target| { const target_prog_node = node: { var name_buf: [std.Progress.Node.max_name_len]u8 = undefined; - const name = std.fmt.bufPrint(&name_buf, "{s}-{s}", .{ target.query, @tagName(target.backend) }) catch &name_buf; + const name = std.fmt.bufPrint(&name_buf, "{s}-{t}", .{ target.query, target.backend }) catch &name_buf; break :node prog_node.start(name, case.updates.len); }; defer target_prog_node.end(); if (debug_log_verbose) { - std.log.scoped(.status).info("target: '{s}-{s}'", .{ target.query, @tagName(target.backend) }); + std.log.scoped(.status).info("target: '{s}-{t}'", .{ target.query, target.backend }); } - var child_args: std.ArrayListUnmanaged([]const u8) = .empty; try child_args.appendSlice(arena, &.{ resolved_zig_exe, @@ -114,8 +120,10 @@ pub fn main() !void { ".local-cache", "--global-cache-dir", ".global-cache", - "--listen=-", }); + if (target.resolved.os.tag == .windows) try child_args.append(arena, "-lws2_32"); + try child_args.append(arena, "--listen=-"); + if (opt_resolved_lib_dir) |resolved_lib_dir| { try child_args.appendSlice(arena, &.{ "--zig-lib-dir", resolved_lib_dir }); } @@ -167,8 +175,12 @@ pub fn main() !void { target.query, "-I", opt_resolved_lib_dir.?, // verified earlier - "-o", }); + + if (target.resolved.os.tag == .windows) + try cc_child_args.append(arena, "-lws2_32"); + + try cc_child_args.append(arena, "-o"); } var eval: Eval = .{ @@ -186,7 +198,7 @@ pub fn main() !void { try child.spawn(); - var poller = std.Io.poll(arena, Eval.StreamEnum, .{ + var poller = Io.poll(arena, Eval.StreamEnum, .{ .stdout = child.stdout.?, .stderr = child.stderr.?, }); @@ -226,7 +238,7 @@ const Eval = struct { cc_child_args: *std.ArrayListUnmanaged([]const u8), const StreamEnum = enum { stdout, stderr }; - const Poller = std.Io.Poller(StreamEnum); + const Poller = Io.Poller(StreamEnum); /// Currently this function assumes the previous updates have already been written. fn write(eval: *Eval, update: Case.Update) void { @@ -647,7 +659,7 @@ const Case = struct { msg: []const u8, }; - fn parse(arena: Allocator, bytes: []const u8) !Case { + fn parse(arena: Allocator, io: Io, bytes: []const u8) !Case { const fatal = std.process.fatal; var targets: std.ArrayListUnmanaged(Target) = .empty; @@ -683,7 +695,7 @@ const Case = struct { }, }) catch fatal("line {d}: invalid target query '{s}'", .{ line_n, query }); - const resolved = try std.zig.system.resolveTargetQuery(parsed_query); + const resolved = try std.zig.system.resolveTargetQuery(io, parsed_query); try targets.append(arena, .{ .query = query, diff --git a/tools/migrate_langref.zig b/tools/migrate_langref.zig index d880db1c25..3544cee175 100644 --- a/tools/migrate_langref.zig +++ b/tools/migrate_langref.zig @@ -13,10 +13,16 @@ pub fn main() !void { defer arena_instance.deinit(); const arena = arena_instance.allocator(); + const gpa = arena; + const args = try std.process.argsAlloc(arena); const input_file = args[1]; const output_file = args[2]; + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + var in_file = try fs.cwd().openFile(input_file, .{ .mode = .read_only }); defer in_file.close(); @@ -28,7 +34,7 @@ pub fn main() !void { var out_dir = try fs.cwd().openDir(fs.path.dirname(output_file).?, .{}); defer out_dir.close(); - var in_file_reader = in_file.reader(&.{}); + var in_file_reader = in_file.reader(io, &.{}); const input_file_bytes = try in_file_reader.interface.allocRemaining(arena, .unlimited); var tokenizer = Tokenizer.init(input_file, input_file_bytes);