diff --git a/build/app.zig b/build/app.zig index 8374a6589..526a48de1 100644 --- a/build/app.zig +++ b/build/app.zig @@ -353,7 +353,7 @@ pub fn readConfig(b: *std.Build, target: std.Build.ResolvedTarget, opts: Options // package's, so its VERSION file is out of reach here), else 0.0.0. const app_version: []const u8 = if (app_version_opt) |v| v else if (opts.own) |own| blk: { b.dependOnFileContents(b.path(own.version_file)); - const raw = b.root.root_dir.handle.readFileAlloc(b.graph.io, own.version_file, b.allocator, std.Io.Limit.limited(256)) catch |e| std.debug.panic("read VERSION: {}", .{e}); + const raw = std.Io.Dir.cwd().readFileAlloc(b.graph.io, b.root.joinString(b.allocator, own.version_file) catch @panic("OOM"), b.allocator, std.Io.Limit.limited(256)) catch |e| std.debug.panic("read VERSION: {}", .{e}); version_owned = raw; break :blk std.mem.trimEnd(u8, raw, "\r\n"); } else "0.0.0"; diff --git a/build/web.zig b/build/web.zig index b0895e4e3..36750ad65 100644 --- a/build/web.zig +++ b/build/web.zig @@ -355,7 +355,7 @@ fn stampStorage(b: *std.Build) std.Build.LazyPath { }; const token = "__FIZZY_WEB_STORAGE__"; b.dependOnFileContents(b.path("web/index.html")); - const html = b.root.root_dir.handle.readFileAlloc(b.graph.io, "web/index.html", b.allocator, .unlimited) catch |err| + const html = std.Io.Dir.cwd().readFileAlloc(b.graph.io, b.root.joinString(b.allocator, "web/index.html") catch @panic("OOM"), b.allocator, .unlimited) catch |err| std.debug.panic("web/index.html: {t}", .{err}); if (std.mem.indexOf(u8, html, token) == null) std.debug.panic("web/index.html: no {s} to stamp", .{token}); const stamped = std.mem.replaceOwned(u8, b.allocator, html, token, name) catch @panic("OOM"); diff --git a/plugins/shared/build/helpers.zig b/plugins/shared/build/helpers.zig index 5beda65ff..723fddd1a 100644 --- a/plugins/shared/build/helpers.zig +++ b/plugins/shared/build/helpers.zig @@ -57,7 +57,7 @@ pub const IdentityManifest = @import("../../../sdk/src/manifest_identity.zig").I /// `@import`s it via `plugin_sdk.readManifest`). Panics clearly on a missing/invalid manifest. pub fn readManifestAt(b: *std.Build, zon_rel_path: []const u8) IdentityManifest { b.dependOnFileContents(b.path(zon_rel_path)); - const raw = b.root.root_dir.handle.readFileAlloc(b.graph.io, zon_rel_path, b.allocator, std.Io.Limit.limited(256 * 1024)) catch |e| + const raw = std.Io.Dir.cwd().readFileAlloc(b.graph.io, b.root.joinString(b.allocator, zon_rel_path) catch @panic("OOM"), b.allocator, std.Io.Limit.limited(256 * 1024)) catch |e| std.debug.panic("fizzy: could not read {s}: {}", .{ zon_rel_path, e }); const src = b.allocator.dupeSentinel(u8, raw, 0) catch @panic("OOM"); const Target = struct { diff --git a/sdk/plugin_sdk.zig b/sdk/plugin_sdk.zig index b5edb1e6b..80466f23d 100644 --- a/sdk/plugin_sdk.zig +++ b/sdk/plugin_sdk.zig @@ -31,8 +31,7 @@ const core_module = @import("core_module.zig"); /// package — so the access check below picks whichever layout is present. fn repoPath(b: *std.Build, sub_path: []const u8) std.Build.LazyPath { b.root.access(b.graph.io, sub_path, .{}) catch { - const root = b.root.root_dir.path orelse @panic("fizzy sdk: missing build_root"); - return .{ .cwd_relative = b.pathJoin(&.{ root, "..", sub_path }) }; + return .{ .cwd_relative = b.root.joinString(b.allocator, b.pathJoin(&.{ "..", sub_path })) catch @panic("OOM") }; }; return b.path(sub_path); } @@ -99,7 +98,7 @@ pub fn readManifest(b: *std.Build) IdentityManifest { fn readManifestAt(b: *std.Build, rel_path: []const u8) IdentityManifest { b.dependOnFileContents(b.path(rel_path)); - const raw = b.root.root_dir.handle.readFileAlloc(b.graph.io, rel_path, b.allocator, std.Io.Limit.limited(256 * 1024)) catch |e| + const raw = std.Io.Dir.cwd().readFileAlloc(b.graph.io, b.root.joinString(b.allocator, rel_path) catch @panic("OOM"), b.allocator, std.Io.Limit.limited(256 * 1024)) catch |e| std.debug.panic( "fizzy: could not read {s}: {}\nEvery plugin needs a `plugin.zig.zon` manifest at its root (id/name/version) — see docs/PLUGINS.md.", .{ rel_path, e }, diff --git a/sdk/src/settings.zig b/sdk/src/settings.zig index 189a00127..657032c22 100644 --- a/sdk/src/settings.zig +++ b/sdk/src/settings.zig @@ -141,6 +141,24 @@ pub fn isCell(comptime T: type) bool { /// Compile error for a settings struct field that isn't a `Value(...)` cell. Bare fields used to /// be the whole API, so this names the fix rather than just the rule. +/// True when a value of `T` holds a pointer anywhere in it. A settings payload may hold one only +/// when it is a string itself: the schema copies strings out of a parse and frees them, and owns +/// nothing else, so any other pointer would point into a parse's arena after it is freed. +fn holdsPointers(comptime T: type) bool { + return comptime switch (@typeInfo(T)) { + .pointer => true, + .optional => |o| holdsPointers(o.child), + .array => |a| holdsPointers(a.child), + .@"struct" => |st| for (st.field_types) |F| { + if (holdsPointers(F)) break true; + } else false, + .@"union" => |u| for (u.field_types) |F| { + if (holdsPointers(F)) break true; + } else false, + else => false, + }; +} + fn requireCell(comptime T: type, comptime field_name: []const u8, comptime owner: type) void { if (!isCell(T)) @compileError( "sdk.settings.Schema: field '" ++ field_name ++ "' of " ++ @typeName(owner) ++ @@ -247,6 +265,11 @@ fn Plain(comptime T: type) type { inline for (info.field_names, info.field_types, info.field_attrs, 0..) |f_name, Cell, cell_attrs, i| { requireCell(Cell, f_name, T); const P = Cell.Payload; + if (typeTagFor(P) != .string and holdsPointers(P)) @compileError( + "sdk.settings.Schema: field '" ++ f_name ++ "' of " ++ @typeName(T) ++ " is a " ++ + @typeName(P) ++ ", which holds pointers. A setting owns strings and plain values " ++ + "only; keep a list as a string, or as several settings.", + ); const cell_default = cell_attrs.defaultValue(Cell) orelse @compileError( "sdk.settings.Schema: field '" ++ f_name ++ "' of " ++ @typeName(T) ++ " needs a default value (`= .init()`) — required so settings.zon only " ++ @@ -475,17 +498,29 @@ pub fn Schema(comptime T: type) type { return; }; // The parse's strings are in `arena`. One that is not the declared default is copied - // out, so it is this schema's to free (`freeOwned`); a default stays the default. - inline for (info.field_names, info.field_types) |f_name, Cell| { - if (comptime typeTagFor(Cell.Payload) == .string) { - const parsed_str = @field(parsed, f_name); - if (parsed_str.ptr != @field(default_payloads, f_name).ptr) { - @field(parsed, f_name) = gpa.dupe(u8, parsed_str) catch |err| { - dvui.log.warn("sdk.settings: out of memory keeping settings: {s}", .{@errorName(err)}); - return; - }; + // out, so it is this schema's to free (`freeOwned`); a default stays the default. A + // copy that fails frees the ones before it, and the live value is left as it was. + var copied: usize = 0; + const all_copied = copy: { + inline for (info.field_names, info.field_types, 0..) |f_name, Cell, i| { + if (comptime typeTagFor(Cell.Payload) == .string) { + const parsed_str = @field(parsed, f_name); + if (parsed_str.ptr != @field(default_payloads, f_name).ptr) { + @field(parsed, f_name) = gpa.dupe(u8, parsed_str) catch break :copy false; + } + } + copied = i + 1; + } + break :copy true; + }; + if (!all_copied) { + inline for (info.field_names, info.field_types, 0..) |f_name, Cell, i| { + if (comptime typeTagFor(Cell.Payload) == .string) { + if (i < copied and @field(parsed, f_name).ptr != @field(default_payloads, f_name).ptr) gpa.free(@field(parsed, f_name)); } } + dvui.log.warn("sdk.settings: out of memory keeping settings", .{}); + return; } freeOwned(gpa, out.*); fromPayloads(out, parsed); @@ -1021,3 +1056,13 @@ test "a type with no dedicated control is still a legal setting, edited as zon t // `.other` payloads take part in diff-vs-default like anything else. try testing.expect(try S.diffSerialize(testing.allocator, .{}) == null); } + +test "a settings payload holds pointers only as a string" { + try std.testing.expect(!holdsPointers(u32)); + try std.testing.expect(!holdsPointers(?[4]f32)); + try std.testing.expect(!holdsPointers(enum { a, b })); + try std.testing.expect(holdsPointers([]const u8)); + try std.testing.expect(holdsPointers([]const []const u8)); + try std.testing.expect(holdsPointers(struct { n: u8, name: []const u8 })); + try std.testing.expect(holdsPointers(?struct { inner: [2]?*const u8 })); +}