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 }