lib/sys/src/env.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const capabilities = @import("capabilities.zig");
  3 const memory = @import("memory.zig");
  4 
  5 const Allocator = std.mem.Allocator;
  6 
  7 pub const required_capabilities = capabilities.host(&.{.environment});
  8 
  9 pub const Map = std.process.Environ.Map;
 10 pub const CreateMapError = std.process.Environ.CreateMapError;
 11 pub const ClaimError = error{
 12     ConditionRejected,
 13     GenerationStorageAlreadyUsed,
 14     GenerationStorageTooSmall,
 15 };
 16 
 17 pub const GenerationStorage = struct {
 18     entries: []?[*:0]const u8,
 19     used: bool = false,
 20 
 21     pub fn init(entries: []?[*:0]const u8) GenerationStorage {
 22         return .{ .entries = entries };
 23     }
 24 };
 25 
 26 pub fn get(name: [:0]const u8) ?[]const u8 {
 27     if (!validPosixName(name)) return null;
 28     if (comptime !@hasDecl(std.process.Environ.Block, "view")) return null;
 29     const threaded = std.Options.debug_threaded_io orelse return null;
 30     std.Io.Threaded.mutexLock(&threaded.mutex);
 31     defer std.Io.Threaded.mutexUnlock(&threaded.mutex);
 32     return lookupPosixEntries(threaded.environ.process_environ.block.view().slice, name);
 33 }
 34 
 35 pub fn getConstant(comptime name: []const u8) ?[]const u8 {
 36     const name_z: [:0]const u8 = name ++ "";
 37     return get(name_z);
 38 }
 39 
 40 pub fn contains(name: [:0]const u8) bool {
 41     return get(name) != null;
 42 }
 43 
 44 pub fn containsNonEmpty(name: [:0]const u8) bool {
 45     const value = get(name) orelse return false;
 46     return value.len != 0;
 47 }
 48 
 49 pub fn installProcessEnvironment(environ: std.process.Environ) void {
 50     const threaded = std.Options.debug_threaded_io orelse return;
 51     std.Io.Threaded.mutexLock(&threaded.mutex);
 52     defer std.Io.Threaded.mutexUnlock(&threaded.mutex);
 53     threaded.environ = .{ .process_environ = environ };
 54     threaded.environ_initialized = environ.block.isEmpty();
 55 }
 56 
 57 pub fn processEnviron() std.process.Environ {
 58     const threaded = std.Options.debug_threaded_io orelse return .empty;
 59     std.Io.Threaded.mutexLock(&threaded.mutex);
 60     defer std.Io.Threaded.mutexUnlock(&threaded.mutex);
 61     return threaded.environ.process_environ;
 62 }
 63 
 64 pub fn getOwned(allocator: Allocator, name: []const u8) Allocator.Error!?[]u8 {
 65     if (!validPosixName(name)) return null;
 66     if (comptime !@hasDecl(std.process.Environ.Block, "view")) return null;
 67     const threaded = std.Options.debug_threaded_io orelse return null;
 68     std.Io.Threaded.mutexLock(&threaded.mutex);
 69     defer std.Io.Threaded.mutexUnlock(&threaded.mutex);
 70     const block = threaded.environ.process_environ.block;
 71     if (lookupPosixEntries(block.view().slice, name)) |value| return try allocator.dupe(u8, value);
 72     return null;
 73 }
 74 
 75 pub fn claimIf(
 76     storage: *GenerationStorage,
 77     name: []const u8,
 78     context: anytype,
 79     comptime condition: fn (@TypeOf(context), []const u8) bool,
 80 ) ClaimError!bool {
 81     if (!validPosixName(name)) return false;
 82     if (comptime !@hasDecl(std.process.Environ.Block, "view")) return false;
 83     const threaded = std.Options.debug_threaded_io orelse return false;
 84     std.Io.Threaded.mutexLock(&threaded.mutex);
 85     defer std.Io.Threaded.mutexUnlock(&threaded.mutex);
 86 
 87     const entries = threaded.environ.process_environ.block.view().slice;
 88     const match = findPosixEntry(entries, name) orelse return false;
 89     if (storage.used) return error.GenerationStorageAlreadyUsed;
 90     if (entries.len > storage.entries.len) return error.GenerationStorageTooSmall;
 91     if (!condition(context, match.value)) return error.ConditionRejected;
 92     commitClaim(storage, threaded, entries, match);
 93     return true;
 94 }
 95 
 96 fn commitClaim(
 97     storage: *GenerationStorage,
 98     threaded: *std.Io.Threaded,
 99     entries: []const [*:0]const u8,
100     match: PosixEntry,
101 ) void {
102     std.debug.assert(!storage.used);
103     std.debug.assert(entries.len <= storage.entries.len);
104     var write_index: usize = 0;
105     for (entries, 0..) |entry, read_index| {
106         if (read_index == match.index) continue;
107         storage.entries[write_index] = entry;
108         write_index += 1;
109     }
110     storage.entries[write_index] = null;
111     storage.used = true;
112     const slice: [:null]const ?[*:0]const u8 = storage.entries[0..write_index :null];
113     threaded.environ = .{ .process_environ = .{ .block = .{ .slice = slice } } };
114     threaded.environ_initialized = slice.len == 0;
115 }
116 
117 pub fn createMap(allocator: Allocator) CreateMapError!Map {
118     const threaded = std.Options.debug_threaded_io orelse return .init(allocator);
119     std.Io.Threaded.mutexLock(&threaded.mutex);
120     defer std.Io.Threaded.mutexUnlock(&threaded.mutex);
121     return threaded.environ.process_environ.createMap(allocator);
122 }
123 
124 pub fn current() std.process.Environ {
125     const threaded = std.Options.debug_threaded_io orelse return .empty;
126     std.Io.Threaded.mutexLock(&threaded.mutex);
127     defer std.Io.Threaded.mutexUnlock(&threaded.mutex);
128     return threaded.environ.process_environ;
129 }
130 
131 fn lookupPosixEntries(entries: []const [*:0]const u8, name: []const u8) ?[]const u8 {
132     return if (findPosixEntry(entries, name)) |match| match.value else null;
133 }
134 
135 const PosixEntry = struct {
136     index: usize,
137     value: []const u8,
138 };
139 
140 fn findPosixEntry(entries: []const [*:0]const u8, name: []const u8) ?PosixEntry {
141     for (entries, 0..) |entry, index| {
142         var separator_index: usize = 0;
143         while (entry[separator_index] != 0 and entry[separator_index] != '=') : (separator_index += 1) {}
144         if (entry[separator_index] != '=') continue;
145         if (!std.mem.eql(u8, name, entry[0..separator_index])) continue;
146 
147         const value_start = separator_index + 1;
148         var value_end = value_start;
149         while (entry[value_end] != 0) : (value_end += 1) {}
150         return .{ .index = index, .value = entry[value_start..value_end] };
151     }
152     return null;
153 }
154 
155 fn validPosixName(name: []const u8) bool {
156     return name.len > 0 and std.mem.indexOfAny(u8, name, "\x00=") == null;
157 }
158 
159 const ClaimWorker = struct {
160     storage: *GenerationStorage,
161     claimed: bool = false,
162     saw_value: bool = false,
163     failure: ?ClaimError = null,
164 
165     fn run(self: *@This()) void {
166         self.claimed = claimIf(
167             self.storage,
168             "TINY_SYS_ENV_TAKE",
169             self,
170             accept,
171         ) catch |failure| {
172             self.failure = failure;
173             return;
174         };
175     }
176 
177     fn accept(self: *@This(), value: []const u8) bool {
178         self.saw_value = std.mem.eql(u8, value, "transferred");
179         return self.saw_value;
180     }
181 };
182 
183 fn acceptClaimValue(_: void, _: []const u8) bool {
184     return true;
185 }
186 
187 fn rejectClaimValue(_: void, _: []const u8) bool {
188     return false;
189 }
190 
191 fn recordClaimCondition(called: *bool, _: []const u8) bool {
192     called.* = true;
193     return true;
194 }
195 
196 const SnapshotReader = struct {
197     failed: bool = false,
198 
199     fn run(self: *@This()) void {
200         for (0..64) |_| {
201             var snapshot = createMap(memory.page_allocator) catch {
202                 self.failed = true;
203                 return;
204             };
205             defer snapshot.deinit();
206             const stable = snapshot.get("TINY_SYS_ENV_SNAPSHOT_KEEP") orelse {
207                 self.failed = true;
208                 return;
209             };
210             if (!std.mem.eql(u8, stable, "stable")) {
211                 self.failed = true;
212                 return;
213             }
214             if (snapshot.get("TINY_SYS_ENV_SNAPSHOT_TAKE")) |transient| {
215                 if (!std.mem.eql(u8, transient, "transient")) {
216                     self.failed = true;
217                     return;
218                 }
219             }
220         }
221     }
222 };
223 
224 test "lookupPosixEntries matches only exact names" {
225     const entries = [_][*:0]const u8{
226         "TINY_SYS_ENV_TEST=ok",
227         "TINY_SYS_ENV_TEST_EXTRA=no",
228     };
229     try std.testing.expectEqualStrings("ok", lookupPosixEntries(&entries, "TINY_SYS_ENV_TEST").?);
230 }
231 
232 test "lookupPosixEntries preserves empty values" {
233     const entries = [_][*:0]const u8{"TINY_SYS_ENV_EMPTY="};
234     try std.testing.expectEqualStrings("", lookupPosixEntries(&entries, "TINY_SYS_ENV_EMPTY").?);
235 }
236 
237 test "containsNonEmpty distinguishes missing and empty values" {
238     const entries = [_][*:0]const u8{"TINY_SYS_ENV_EMPTY="};
239     try std.testing.expect(lookupPosixEntries(&entries, "TINY_SYS_ENV_MISSING") == null);
240     try std.testing.expectEqual(@as(usize, 0), lookupPosixEntries(&entries, "TINY_SYS_ENV_EMPTY").?.len);
241 }
242 
243 test "invalid names do not query the environment" {
244     try std.testing.expect(!validPosixName(""));
245     try std.testing.expect(!validPosixName("A=B"));
246     try std.testing.expect(!validPosixName("A\x00B"));
247 }
248 
249 test "getOwned duplicates installed process block values" {
250     if (comptime !@hasDecl(std.process.Environ.Block, "view")) return error.SkipZigTest;
251 
252     const allocator = std.testing.allocator;
253     var map = Map.init(allocator);
254     defer map.deinit();
255     try map.put("TINY_SYS_ENV_OWNED", "from-block");
256 
257     const block = try map.createPosixBlock(allocator, .{});
258     defer block.deinit(allocator);
259 
260     const previous = current();
261     installProcessEnvironment(.{ .block = block });
262     defer installProcessEnvironment(previous);
263 
264     try std.testing.expectEqualStrings("from-block", getConstant("TINY_SYS_ENV_OWNED").?);
265 
266     const value = (try getOwned(allocator, "TINY_SYS_ENV_OWNED")).?;
267     defer allocator.free(value);
268     try std.testing.expectEqualStrings("from-block", value);
269 }
270 
271 test "claimIf removes one process value atomically from inherited child state" {
272     if (comptime !@hasDecl(std.process.Environ.Block, "view")) return error.SkipZigTest;
273 
274     const allocator = std.testing.allocator;
275     var map = Map.init(allocator);
276     defer map.deinit();
277     try map.put("TINY_SYS_ENV_TAKE", "transferred");
278     try map.put("TINY_SYS_ENV_RETAIN", "visible");
279 
280     const block = try map.createPosixBlock(allocator, .{});
281     defer block.deinit(allocator);
282     const previous = current();
283     installProcessEnvironment(.{ .block = block });
284     defer installProcessEnvironment(previous);
285 
286     var generation_entries: [2]?[*:0]const u8 = undefined;
287     var generation = GenerationStorage.init(&generation_entries);
288     var workers: [8]ClaimWorker = undefined;
289     for (&workers) |*worker| worker.* = .{ .storage = &generation };
290     var threads: [workers.len]std.Thread = undefined;
291     for (&threads, &workers) |*thread, *worker| {
292         thread.* = try std.Thread.spawn(.{}, ClaimWorker.run, .{worker});
293     }
294     for (&threads) |*thread| thread.join();
295 
296     var transfers: usize = 0;
297     for (workers) |worker| {
298         try std.testing.expect(worker.failure == null);
299         if (worker.claimed) {
300             transfers += 1;
301             try std.testing.expect(worker.saw_value);
302         }
303     }
304     try std.testing.expectEqual(@as(usize, 1), transfers);
305     try std.testing.expect(generation.used);
306     try std.testing.expect(getConstant("TINY_SYS_ENV_TAKE") == null);
307     try std.testing.expectEqualStrings("visible", getConstant("TINY_SYS_ENV_RETAIN").?);
308 
309     var inherited = try createMap(allocator);
310     defer inherited.deinit();
311     try std.testing.expect(!inherited.contains("TINY_SYS_ENV_TAKE"));
312     try std.testing.expectEqualStrings("visible", inherited.get("TINY_SYS_ENV_RETAIN").?);
313 }
314 
315 test "claimIf leaves rejected process values and generation storage untouched" {
316     if (comptime !@hasDecl(std.process.Environ.Block, "view")) return error.SkipZigTest;
317 
318     const allocator = std.testing.allocator;
319     var map = Map.init(allocator);
320     defer map.deinit();
321     try map.put("TINY_SYS_ENV_REJECT", "unchanged");
322 
323     const block = try map.createPosixBlock(allocator, .{});
324     defer block.deinit(allocator);
325     const previous = current();
326     installProcessEnvironment(.{ .block = block });
327     defer installProcessEnvironment(previous);
328 
329     var generation_entries: [1]?[*:0]const u8 = undefined;
330     var generation = GenerationStorage.init(&generation_entries);
331     try std.testing.expectError(
332         error.ConditionRejected,
333         claimIf(&generation, "TINY_SYS_ENV_REJECT", {}, rejectClaimValue),
334     );
335     try std.testing.expect(!generation.used);
336     try std.testing.expectEqualStrings("unchanged", getConstant("TINY_SYS_ENV_REJECT").?);
337 
338     var inherited = try createMap(allocator);
339     defer inherited.deinit();
340     try std.testing.expectEqualStrings("unchanged", inherited.get("TINY_SYS_ENV_REJECT").?);
341 }
342 
343 test "claimIf reserves exact generation capacity before condition effects" {
344     if (comptime !@hasDecl(std.process.Environ.Block, "view")) return error.SkipZigTest;
345 
346     const allocator = std.testing.allocator;
347     var map = Map.init(allocator);
348     defer map.deinit();
349     try map.put("TINY_SYS_ENV_TAKE", "transferred");
350     try map.put("TINY_SYS_ENV_RETAIN", "visible");
351 
352     const block = try map.createPosixBlock(allocator, .{});
353     defer block.deinit(allocator);
354     const previous = current();
355     installProcessEnvironment(.{ .block = block });
356     defer installProcessEnvironment(previous);
357 
358     var too_small_entries: [1]?[*:0]const u8 = undefined;
359     var too_small = GenerationStorage.init(&too_small_entries);
360     var condition_called = false;
361     try std.testing.expectError(
362         error.GenerationStorageTooSmall,
363         claimIf(
364             &too_small,
365             "TINY_SYS_ENV_TAKE",
366             &condition_called,
367             recordClaimCondition,
368         ),
369     );
370     try std.testing.expect(!condition_called);
371     try std.testing.expect(!too_small.used);
372     try std.testing.expectEqualStrings("transferred", getConstant("TINY_SYS_ENV_TAKE").?);
373 
374     var exact_entries: [2]?[*:0]const u8 = undefined;
375     var exact = GenerationStorage.init(&exact_entries);
376     try std.testing.expect(try claimIf(
377         &exact,
378         "TINY_SYS_ENV_TAKE",
379         &condition_called,
380         recordClaimCondition,
381     ));
382     try std.testing.expect(condition_called);
383     try std.testing.expect(exact.used);
384     try std.testing.expect(getConstant("TINY_SYS_ENV_TAKE") == null);
385     try std.testing.expectEqualStrings("visible", getConstant("TINY_SYS_ENV_RETAIN").?);
386 }
387 
388 test "claimIf never reuses a populated generation" {
389     if (comptime !@hasDecl(std.process.Environ.Block, "view")) return error.SkipZigTest;
390 
391     const allocator = std.testing.allocator;
392     var first = Map.init(allocator);
393     defer first.deinit();
394     try first.put("TINY_SYS_ENV_FIRST", "first");
395     const first_block = try first.createPosixBlock(allocator, .{});
396     defer first_block.deinit(allocator);
397 
398     var second = Map.init(allocator);
399     defer second.deinit();
400     try second.put("TINY_SYS_ENV_SECOND", "second");
401     const second_block = try second.createPosixBlock(allocator, .{});
402     defer second_block.deinit(allocator);
403 
404     const previous = current();
405     installProcessEnvironment(.{ .block = first_block });
406     defer installProcessEnvironment(previous);
407 
408     var generation_entries: [1]?[*:0]const u8 = undefined;
409     var generation = GenerationStorage.init(&generation_entries);
410     try std.testing.expect(try claimIf(
411         &generation,
412         "TINY_SYS_ENV_FIRST",
413         {},
414         acceptClaimValue,
415     ));
416 
417     installProcessEnvironment(.{ .block = second_block });
418     try std.testing.expectError(
419         error.GenerationStorageAlreadyUsed,
420         claimIf(&generation, "TINY_SYS_ENV_SECOND", {}, acceptClaimValue),
421     );
422     try std.testing.expectEqualStrings("second", getConstant("TINY_SYS_ENV_SECOND").?);
423 }
424 
425 test "createMap snapshots process blocks during concurrent removal" {
426     if (comptime !@hasDecl(std.process.Environ.Block, "view")) return error.SkipZigTest;
427 
428     const allocator = std.testing.allocator;
429     var map = Map.init(allocator);
430     defer map.deinit();
431     try map.put("TINY_SYS_ENV_SNAPSHOT_TAKE", "transient");
432     try map.put("TINY_SYS_ENV_SNAPSHOT_KEEP", "stable");
433 
434     const block = try map.createPosixBlock(allocator, .{});
435     defer block.deinit(allocator);
436     const previous = current();
437     installProcessEnvironment(.{ .block = block });
438     defer installProcessEnvironment(previous);
439 
440     var readers: [8]SnapshotReader = @splat(.{});
441     var threads: [readers.len]std.Thread = undefined;
442     for (&threads, &readers) |*thread, *reader| {
443         thread.* = try std.Thread.spawn(.{}, SnapshotReader.run, .{reader});
444     }
445     var generation_entries: [2]?[*:0]const u8 = undefined;
446     var generation = GenerationStorage.init(&generation_entries);
447     try std.testing.expect(try claimIf(
448         &generation,
449         "TINY_SYS_ENV_SNAPSHOT_TAKE",
450         {},
451         acceptClaimValue,
452     ));
453     for (&threads) |*thread| thread.join();
454 
455     for (readers) |reader| try std.testing.expect(!reader.failed);
456 }
457 
458 test "saved process environment generations survive later removals" {
459     if (comptime !@hasDecl(std.process.Environ.Block, "view")) return error.SkipZigTest;
460 
461     const allocator = std.testing.allocator;
462     var first = Map.init(allocator);
463     defer first.deinit();
464     try first.put("WAYLAND_SOCKET", "11");
465     try first.put("TINY_SYS_ENV_GENERATION_KEEP", "A");
466     const first_block = try first.createPosixBlock(allocator, .{});
467     defer first_block.deinit(allocator);
468 
469     var second = Map.init(allocator);
470     defer second.deinit();
471     try second.put("TINY_SYS_ENV_GENERATION_TAKE", "second");
472     try second.put("TINY_SYS_ENV_GENERATION_KEEP", "B");
473     const second_block = try second.createPosixBlock(allocator, .{});
474     defer second_block.deinit(allocator);
475 
476     const initial = current();
477     installProcessEnvironment(.{ .block = first_block });
478     defer installProcessEnvironment(initial);
479 
480     var first_generation_entries: [2]?[*:0]const u8 = undefined;
481     var first_generation = GenerationStorage.init(&first_generation_entries);
482     try std.testing.expect(try claimIf(
483         &first_generation,
484         "WAYLAND_SOCKET",
485         {},
486         acceptClaimValue,
487     ));
488     const previous = current();
489 
490     installProcessEnvironment(.{ .block = second_block });
491     var second_generation_entries: [2]?[*:0]const u8 = undefined;
492     var second_generation = GenerationStorage.init(&second_generation_entries);
493     try std.testing.expect(try claimIf(
494         &second_generation,
495         "TINY_SYS_ENV_GENERATION_TAKE",
496         {},
497         acceptClaimValue,
498     ));
499     installProcessEnvironment(previous);
500 
501     var restored = try createMap(allocator);
502     defer restored.deinit();
503     try std.testing.expect(!restored.contains("WAYLAND_SOCKET"));
504     try std.testing.expect(!restored.contains("TINY_SYS_ENV_GENERATION_TAKE"));
505     try std.testing.expectEqualStrings("A", restored.get("TINY_SYS_ENV_GENERATION_KEEP").?);
506 }