1const Environ = @This();
2
3const builtin = @import("builtin");
4const native_os = builtin.os.tag;
5
6const std = @import("../std.zig");
7const Allocator = mem.Allocator;
8const assert = std.debug.assert;
9const testing = std.testing;
10const unicode = std.unicode;
11const posix = std.posix;
12const mem = std.mem;
13
14/// Unmodified, unprocessed data provided by the operating system.
15block: Block,
16
17pub const empty: Environ = .{ .block = .empty };
18
19/// On WASI without libc, this is `void` because the environment has to be
20/// queried and heap-allocated at runtime.
21///
22/// On Windows, the memory pointed at by the PEB changes when the environment
23/// is modified, so a long-lived pointer cannot be used. Therefore, on this
24/// operating system `void` is also used.
25pub const Block = switch (native_os) {
26 .windows => GlobalBlock,
27 .wasi, .emscripten => switch (builtin.link_libc) {
28 false => GlobalBlock,
29 true => PosixBlock,
30 },
31 .freestanding, .other => GlobalBlock,
32 else => PosixBlock,
33};
34
35pub const GlobalBlock = struct {
36 use_global: bool,
37
38 pub const empty: GlobalBlock = .{ .use_global = false };
39 pub const global: GlobalBlock = .{ .use_global = true };
40
41 pub fn deinit(_: GlobalBlock, _: Allocator) void {}
42
43 pub fn isEmpty(block: GlobalBlock) bool {
44 return !block.use_global;
45 }
46};
47
48pub const PosixBlock = struct {
49 slice: [:null]const ?[*:0]const u8,
50
51 pub const empty: PosixBlock = .{ .slice = &.{} };
52
53 pub fn deinit(block: PosixBlock, gpa: Allocator) void {
54 for (block.slice) |entry| gpa.free(mem.span(entry.?));
55 gpa.free(block.slice);
56 }
57
58 pub fn isEmpty(block: PosixBlock) bool {
59 return block.slice.len == 0;
60 }
61
62 pub const View = struct {
63 slice: []const [*:0]const u8,
64
65 pub fn isEmpty(v: View) bool {
66 return v.slice.len == 0;
67 }
68 };
69 pub fn view(block: PosixBlock) View {
70 return .{ .slice = @ptrCast(block.slice) };
71 }
72};
73
74pub const WindowsBlock = struct {
75 slice: [:0]const u16,
76
77 pub const empty: WindowsBlock = .{ .slice = &.{0} };
78
79 pub fn deinit(block: WindowsBlock, gpa: Allocator) void {
80 gpa.free(block.slice);
81 }
82
83 pub fn isEmpty(block: WindowsBlock) bool {
84 return block.slice[0] == 0;
85 }
86
87 pub const View = struct {
88 ptr: [*:0]const u16,
89
90 pub fn isEmpty(v: View) bool {
91 return v.ptr[0] == 0;
92 }
93 };
94 pub fn view(block: WindowsBlock) View {
95 return .{ .ptr = block.slice.ptr };
96 }
97};
98
99/// Each key and each value are allocated independently and owned by this data structure.
100pub const Map = struct {
101 array_hash_map: ArrayHashMap,
102 allocator: Allocator,
103
104 const ArrayHashMap = std.array_hash_map.Custom([]const u8, []const u8, EnvNameHashContext, false);
105
106 pub const Size = usize;
107
108 pub const EnvNameHashContext = struct {
109 pub fn hash(self: @This(), s: []const u8) u32 {
110 _ = self;
111 switch (native_os) {
112 else => return std.array_hash_map.hashString(s),
113 .windows => {
114 var h = std.hash.Wyhash.init(0);
115 var it = unicode.Wtf8View.initUnchecked(s).iterator();
116 while (it.nextCodepoint()) |cp| {
117 const cp_upper = if (std.math.cast(u16, cp)) |wtf16|
118 std.os.windows.toUpperWtf16(wtf16)
119 else
120 cp;
121 h.update(&[_]u8{
122 @truncate(cp_upper >> 0),
123 @truncate(cp_upper >> 8),
124 @truncate(cp_upper >> 16),
125 });
126 }
127 return @truncate(h.final());
128 },
129 }
130 }
131
132 pub fn eql(self: @This(), a: []const u8, b: []const u8, b_index: usize) bool {
133 _ = self;
134 _ = b_index;
135 return eqlKeys(a, b);
136 }
137 };
138 fn eqlKeys(a: []const u8, b: []const u8) bool {
139 return switch (native_os) {
140 else => std.array_hash_map.eqlString(a, b),
141 .windows => std.os.windows.eqlIgnoreCaseWtf8(a, b),
142 };
143 }
144
145 pub fn validateKeyForPut(key: []const u8) bool {
146 switch (native_os) {
147 else => return key.len > 0 and mem.findAny(u8, key, &.{ 0, '=' }) == null,
148 .windows => {
149 if (!unicode.wtf8ValidateSlice(key)) return false;
150 return key.len > 0 and key[0] != 0 and mem.findAnyPos(u8, key, 1, &.{ 0, '=' }) == null;
151 },
152 }
153 }
154
155 pub fn validateKeyForFetch(key: []const u8) bool {
156 if (native_os == .windows and !unicode.wtf8ValidateSlice(key)) return false;
157 return true;
158 }
159
160 /// Create a Map backed by a specific allocator.
161 /// That allocator will be used for both backing allocations
162 /// and string deduplication.
163 pub fn init(allocator: Allocator) Map {
164 return .{ .array_hash_map = .empty, .allocator = allocator };
165 }
166
167 /// Free the backing storage of the map, as well as all
168 /// of the stored keys and values.
169 pub fn deinit(self: *Map) void {
170 const gpa = self.allocator;
171 for (self.keys()) |key| gpa.free(key);
172 for (self.values()) |value| gpa.free(value);
173 self.array_hash_map.deinit(gpa);
174 self.* = undefined;
175 }
176
177 pub fn keys(map: *const Map) [][]const u8 {
178 return map.array_hash_map.keys();
179 }
180
181 pub fn values(map: *const Map) [][]const u8 {
182 return map.array_hash_map.values();
183 }
184
185 pub fn putPosixBlock(map: *Map, view: PosixBlock.View) Allocator.Error!void {
186 for (view.slice) |entry| {
187 var entry_i: usize = 0;
188 while (entry[entry_i] != 0 and entry[entry_i] != '=') : (entry_i += 1) {}
189 const key = entry[0..entry_i];
190
191 var end_i: usize = entry_i;
192 while (entry[end_i] != 0) : (end_i += 1) {}
193 const value = entry[entry_i + 1 .. end_i];
194
195 try map.put(key, value);
196 }
197 }
198
199 pub fn putWindowsBlock(map: *Map, view: WindowsBlock.View) Allocator.Error!void {
200 var i: usize = 0;
201 while (view.ptr[i] != 0) {
202 const key_start = i;
203
204 // There are some special environment variables that start with =,
205 // so we need a special case to not treat = as a key/value separator
206 // if it's the first character.
207 // https://devblogs.microsoft.com/oldnewthing/20100506-00/?p=14133
208 if (view.ptr[key_start] == '=') i += 1;
209
210 while (view.ptr[i] != 0 and view.ptr[i] != '=') : (i += 1) {}
211 const key_w = view.ptr[key_start..i];
212 const key = try unicode.wtf16LeToWtf8Alloc(map.allocator, key_w);
213 errdefer map.allocator.free(key);
214
215 if (view.ptr[i] == '=') i += 1;
216
217 const value_start = i;
218 while (view.ptr[i] != 0) : (i += 1) {}
219 const value_w = view.ptr[value_start..i];
220 const value = try unicode.wtf16LeToWtf8Alloc(map.allocator, value_w);
221 errdefer map.allocator.free(value);
222
223 i += 1; // skip over null byte
224
225 try map.putMove(key, value);
226 }
227 }
228
229 /// Same as `put` but the key and value become owned by the Map rather
230 /// than being copied.
231 /// If `putMove` fails, the ownership of key and value does not transfer.
232 ///
233 /// Asserts that `key` is valid:
234 /// - It cannot contain a NUL (`'\x00') byte.
235 /// - It must have a length > 0.
236 /// - It cannot contain `=`, except on Windows where only the first code point is allowed to be `=`.
237 /// - On Windows, it must be valid [WTF-8](https://wtf-8.codeberg.page/).
238 pub fn putMove(self: *Map, key: []u8, value: []u8) Allocator.Error!void {
239 assert(validateKeyForPut(key));
240 const gpa = self.allocator;
241 const get_or_put = try self.array_hash_map.getOrPut(gpa, key);
242 if (get_or_put.found_existing) {
243 gpa.free(get_or_put.key_ptr.*);
244 gpa.free(get_or_put.value_ptr.*);
245 get_or_put.key_ptr.* = key;
246 }
247 get_or_put.value_ptr.* = value;
248 }
249
250 /// `key` and `value` are copied into the Map.
251 ///
252 /// Asserts that `key` is valid:
253 /// - It cannot contain a NUL (`'\x00') byte.
254 /// - It must have a length > 0.
255 /// - It cannot contain `=`, except on Windows where only the first code point is allowed to be `=`.
256 /// - On Windows, it must be valid [WTF-8](https://wtf-8.codeberg.page/).
257 pub fn put(self: *Map, key: []const u8, value: []const u8) Allocator.Error!void {
258 assert(validateKeyForPut(key));
259 const gpa = self.allocator;
260 const value_copy = try gpa.dupe(u8, value);
261 errdefer gpa.free(value_copy);
262 const get_or_put = try self.array_hash_map.getOrPut(gpa, key);
263 errdefer {
264 if (!get_or_put.found_existing) assert(self.array_hash_map.pop() != null);
265 }
266 if (get_or_put.found_existing) {
267 gpa.free(get_or_put.value_ptr.*);
268 } else {
269 get_or_put.key_ptr.* = try gpa.dupe(u8, key);
270 }
271 get_or_put.value_ptr.* = value_copy;
272 }
273
274 /// Find the address of the value associated with a key.
275 /// The returned pointer is invalidated if the map resizes.
276 /// On Windows, asserts that `key` is valid [WTF-8](https://wtf-8.codeberg.page/).
277 pub fn getPtr(self: Map, key: []const u8) ?*[]const u8 {
278 assert(validateKeyForFetch(key));
279 return self.array_hash_map.getPtr(key);
280 }
281
282 /// Return the map's copy of the value associated with
283 /// a key. The returned string is invalidated if this
284 /// key is removed from the map.
285 /// On Windows, asserts that `key` is valid [WTF-8](https://wtf-8.codeberg.page/).
286 pub fn get(self: Map, key: []const u8) ?[]const u8 {
287 assert(validateKeyForFetch(key));
288 return self.array_hash_map.get(key);
289 }
290
291 /// On Windows, asserts that `key` is valid [WTF-8](https://wtf-8.codeberg.page/).
292 pub fn contains(m: *const Map, key: []const u8) bool {
293 assert(validateKeyForFetch(key));
294 return m.array_hash_map.contains(key);
295 }
296
297 /// If there is an entry with a matching key, it is deleted from the hash
298 /// map. The entry is removed from the underlying array by swapping it with
299 /// the last element.
300 ///
301 /// Returns true if an entry was removed, false otherwise.
302 ///
303 /// This invalidates the value returned by get() for this key.
304 /// On Windows, asserts that `key` is valid [WTF-8](https://wtf-8.codeberg.page/).
305 pub fn swapRemove(self: *Map, key: []const u8) bool {
306 assert(validateKeyForFetch(key));
307 const kv = self.array_hash_map.fetchSwapRemove(key) orelse return false;
308 const gpa = self.allocator;
309 gpa.free(kv.key);
310 gpa.free(kv.value);
311 return true;
312 }
313
314 /// If there is an entry with a matching key, it is deleted from the map.
315 /// The entry is removed from the underlying array by shifting all elements
316 /// forward, thereby maintaining the current ordering.
317 ///
318 /// Returns true if an entry was removed, false otherwise.
319 ///
320 /// This invalidates the value returned by get() for this key.
321 /// On Windows, asserts that `key` is valid [WTF-8](https://wtf-8.codeberg.page/).
322 pub fn orderedRemove(self: *Map, key: []const u8) bool {
323 assert(validateKeyForFetch(key));
324 const kv = self.array_hash_map.fetchOrderedRemove(key) orelse return false;
325 const gpa = self.allocator;
326 gpa.free(kv.key);
327 gpa.free(kv.value);
328 return true;
329 }
330
331 /// Returns the number of KV pairs stored in the map.
332 pub fn count(self: Map) Size {
333 return self.array_hash_map.count();
334 }
335
336 /// Returns an iterator over entries in the map.
337 pub fn iterator(self: *const Map) ArrayHashMap.Iterator {
338 return self.array_hash_map.iterator();
339 }
340
341 /// Returns a full copy of `em` allocated with `gpa`, which is not necessarily
342 /// the same allocator used to allocate `em`.
343 pub fn clone(m: *const Map, gpa: Allocator) Allocator.Error!Map {
344 var new: Map = .init(gpa);
345 errdefer new.deinit();
346 try new.array_hash_map.ensureUnusedCapacity(gpa, m.array_hash_map.count());
347 for (m.array_hash_map.keys(), m.array_hash_map.values()) |key, value| {
348 try new.put(key, value);
349 }
350 return new;
351 }
352
353 /// Adds all the key-value pairs from `other` into this `m`.
354 pub fn putAll(m: *Map, other: *const Map) Allocator.Error!void {
355 const gpa = m.allocator;
356 try m.array_hash_map.ensureUnusedCapacity(gpa, other.array_hash_map.count());
357 const start = m.count();
358 errdefer while (m.array_hash_map.count() > start) {
359 const kv = m.array_hash_map.pop().?;
360 gpa.free(kv.key);
361 gpa.free(kv.value);
362 };
363 for (other.array_hash_map.keys(), other.array_hash_map.values()) |key, value| {
364 try m.put(key, value);
365 }
366 }
367
368 /// Set the length to zero, freeing all key and value memory, not freeing
369 /// the allocation for the entries.
370 pub fn clearRetainingCapacity(m: *Map) void {
371 const gpa = m.allocator;
372 for (m.array_hash_map.keys(), m.array_hash_map.values()) |k, v| {
373 gpa.free(k);
374 gpa.free(v);
375 }
376 m.array_hash_map.clearRetainingCapacity();
377 }
378
379 /// Creates a null-delimited environment variable block in the format
380 /// expected by POSIX, from a hash map plus options.
381 pub fn createPosixBlock(
382 map: *const Map,
383 gpa: Allocator,
384 options: CreatePosixBlockOptions,
385 ) Allocator.Error!PosixBlock {
386 const ZigProgressAction = enum { nothing, edit, delete, add };
387 const zig_progress_action: ZigProgressAction = action: {
388 const fd = options.zig_progress_fd orelse break :action .nothing;
389 const exists = map.contains("ZIG_PROGRESS");
390 if (fd >= 0) {
391 break :action if (exists) .edit else .add;
392 } else {
393 if (exists) break :action .delete;
394 }
395 break :action .nothing;
396 };
397
398 const envp = try gpa.allocSentinel(?[*:0]u8, len: {
399 var len: usize = map.count();
400 switch (zig_progress_action) {
401 .add => len += 1,
402 .delete => len -= 1,
403 .nothing, .edit => {},
404 }
405 break :len len;
406 }, null);
407 var envp_len: usize = 0;
408 errdefer {
409 envp[envp_len] = null;
410 PosixBlock.deinit(.{ .slice = envp[0..envp_len :null] }, gpa);
411 }
412
413 if (zig_progress_action == .add) {
414 envp[envp_len] = try std.fmt.allocPrintSentinel(gpa, "ZIG_PROGRESS={d}", .{options.zig_progress_fd.?}, 0);
415 envp_len += 1;
416 }
417
418 for (map.keys(), map.values()) |key, value| {
419 if (mem.eql(u8, key, "ZIG_PROGRESS")) switch (zig_progress_action) {
420 .add => unreachable,
421 .delete => continue,
422 .edit => {
423 envp[envp_len] = try std.fmt.allocPrintSentinel(gpa, "{s}={d}", .{
424 key, options.zig_progress_fd.?,
425 }, 0);
426 envp_len += 1;
427 continue;
428 },
429 .nothing => {},
430 };
431
432 envp[envp_len] = try std.fmt.allocPrintSentinel(gpa, "{s}={s}", .{ key, value }, 0);
433 envp_len += 1;
434 }
435
436 assert(envp_len == envp.len);
437 return .{ .slice = envp };
438 }
439
440 /// Caller owns result.
441 pub fn createWindowsBlock(
442 map: *const Map,
443 gpa: Allocator,
444 options: CreateWindowsBlockOptions,
445 ) error{ OutOfMemory, InvalidWtf8 }!WindowsBlock {
446 // count bytes needed
447 const max_chars_needed = max_chars_needed: {
448 var max_chars_needed: usize = "\x00".len;
449 if (options.zig_progress_handle) |handle| if (handle != std.os.windows.INVALID_HANDLE_VALUE) {
450 max_chars_needed += std.fmt.count("ZIG_PROGRESS={d}\x00", .{@intFromPtr(handle)});
451 };
452 for (map.keys(), map.values()) |key, value| {
453 if (options.zig_progress_handle != null and eqlKeys(key, "ZIG_PROGRESS")) continue;
454 max_chars_needed += key.len + "=".len + value.len + "\x00".len;
455 }
456 break :max_chars_needed @max("\x00\x00".len, max_chars_needed);
457 };
458 const block = try gpa.alloc(u16, max_chars_needed);
459 errdefer gpa.free(block);
460
461 var i: usize = 0;
462 if (options.zig_progress_handle) |handle| if (handle != std.os.windows.INVALID_HANDLE_VALUE) {
463 @memcpy(
464 block[i..][0.."ZIG_PROGRESS=".len],
465 &[_]u16{ 'Z', 'I', 'G', '_', 'P', 'R', 'O', 'G', 'R', 'E', 'S', 'S', '=' },
466 );
467 i += "ZIG_PROGRESS=".len;
468 var value_buf: [std.fmt.count("{d}", .{std.math.maxInt(usize)})]u8 = undefined;
469 const value = std.mem.print(&value_buf, "{d}", .{@intFromPtr(handle)}) catch unreachable;
470 for (block[i..][0..value.len], value) |*r, v| r.* = v;
471 i += value.len;
472 block[i] = 0;
473 i += 1;
474 };
475 for (map.keys(), map.values()) |key, value| {
476 if (options.zig_progress_handle != null and eqlKeys(key, "ZIG_PROGRESS")) continue;
477 i += try unicode.wtf8ToWtf16Le(block[i..], key);
478 block[i] = '=';
479 i += 1;
480 i += try unicode.wtf8ToWtf16Le(block[i..], value);
481 block[i] = 0;
482 i += 1;
483 }
484 // An empty environment is a special case that requires a redundant
485 // NUL terminator. CreateProcess will read the second code unit even
486 // though theoretically the first should be enough to recognize that the
487 // environment is empty (see https://nullprogram.com/blog/2023/08/23/)
488 for (0..2) |_| {
489 block[i] = 0;
490 i += 1;
491 if (i >= 2) break;
492 } else unreachable;
493 const reallocated = try gpa.realloc(block, i);
494 return .{ .slice = reallocated[0 .. i - 1 :0] };
495 }
496};
497
498pub const CreateMapError = error{
499 OutOfMemory,
500 /// WASI-only. `environ_sizes_get` or `environ_get` failed for an
501 /// unanticipated, undocumented reason.
502 Unexpected,
503};
504
505/// Allocates a `Map` and copies environment block into it.
506pub fn createMap(env: Environ, allocator: Allocator) CreateMapError!Map {
507 var map = Map.init(allocator);
508 errdefer map.deinit();
509 if (native_os == .windows) empty: {
510 if (!env.block.use_global) break :empty;
511
512 const peb = std.os.windows.peb();
513 assert(std.os.windows.ntdll.RtlEnterCriticalSection(peb.FastPebLock) == .SUCCESS);
514 defer assert(std.os.windows.ntdll.RtlLeaveCriticalSection(peb.FastPebLock) == .SUCCESS);
515 try map.putWindowsBlock(.{ .ptr = peb.ProcessParameters.Environment });
516 } else if (native_os == .wasi and !builtin.link_libc) empty: {
517 if (!env.block.use_global) break :empty;
518
519 var environ_count: usize = undefined;
520 var environ_buf_size: usize = undefined;
521
522 const environ_sizes_get_ret = std.os.wasi.environ_sizes_get(&environ_count, &environ_buf_size);
523 if (environ_sizes_get_ret != .SUCCESS) {
524 return posix.unexpectedErrno(environ_sizes_get_ret);
525 }
526
527 if (environ_count == 0) {
528 return map;
529 }
530
531 const environ = try allocator.alloc([*:0]u8, environ_count);
532 defer allocator.free(environ);
533 const environ_buf = try allocator.alloc(u8, environ_buf_size);
534 defer allocator.free(environ_buf);
535
536 const environ_get_ret = std.os.wasi.environ_get(environ.ptr, environ_buf.ptr);
537 if (environ_get_ret != .SUCCESS) {
538 return posix.unexpectedErrno(environ_get_ret);
539 }
540
541 try map.putPosixBlock(.{ .slice = environ });
542 } else try map.putPosixBlock(env.block.view());
543 return map;
544}
545
546pub const ContainsError = error{
547 OutOfMemory,
548 /// On Windows, environment variable keys provided by the user must be
549 /// valid [WTF-8](https://wtf-8.codeberg.page/). This error is unreachable
550 /// if the key is statically known to be valid.
551 InvalidWtf8,
552 /// WASI-only. `environ_sizes_get` or `environ_get` failed for an
553 /// unexpected reason.
554 Unexpected,
555};
556
557/// On Windows, if `key` is not valid [WTF-8](https://wtf-8.codeberg.page/),
558/// then `error.InvalidWtf8` is returned.
559///
560/// See also:
561/// * `createMap`
562/// * `containsConstant`
563/// * `containsUnempty`
564pub fn contains(environ: Environ, gpa: Allocator, key: []const u8) ContainsError!bool {
565 if (native_os == .windows and !unicode.wtf8ValidateSlice(key)) return error.InvalidWtf8;
566 var map = try createMap(environ, gpa);
567 defer map.deinit();
568 return map.contains(key);
569}
570
571/// On Windows, if `key` is not valid [WTF-8](https://wtf-8.codeberg.page/),
572/// then `error.InvalidWtf8` is returned.
573///
574/// See also:
575/// * `createMap`
576/// * `containsUnemptyConstant`
577/// * `contains`
578pub fn containsUnempty(environ: Environ, gpa: Allocator, key: []const u8) ContainsError!bool {
579 if (native_os == .windows and !unicode.wtf8ValidateSlice(key)) return error.InvalidWtf8;
580 var map = try createMap(environ, gpa);
581 defer map.deinit();
582 const value = map.get(key) orelse return false;
583 return value.len != 0;
584}
585
586/// This function is unavailable on WASI without libc due to the memory
587/// allocation requirement.
588///
589/// On Windows, `key` must be valid [WTF-8](https://wtf-8.codeberg.page/),
590///
591/// See also:
592/// * `contains`
593/// * `containsUnemptyConstant`
594/// * `createMap`
595pub inline fn containsConstant(environ: Environ, comptime key: []const u8) bool {
596 if (native_os == .windows) {
597 const key_w = comptime unicode.wtf8ToWtf16LeStringLiteral(key);
598 return getWindows(environ, key_w) != null;
599 } else {
600 return getPosix(environ, key) != null;
601 }
602}
603
604/// This function is unavailable on WASI without libc due to the memory
605/// allocation requirement.
606///
607/// On Windows, `key` must be valid [WTF-8](https://wtf-8.codeberg.page/),
608///
609/// See also:
610/// * `containsUnempty`
611/// * `containsConstant`
612/// * `createMap`
613pub inline fn containsUnemptyConstant(environ: Environ, comptime key: []const u8) bool {
614 if (native_os == .windows) {
615 const key_w = comptime unicode.wtf8ToWtf16LeStringLiteral(key);
616 const value = getWindows(environ, key_w) orelse return false;
617 return value.len != 0;
618 } else {
619 const value = getPosix(environ, key) orelse return false;
620 return value.len != 0;
621 }
622}
623
624/// This function is unavailable on WASI without libc due to the memory
625/// allocation requirement.
626///
627/// See also:
628/// * `getWindows`
629/// * `createMap`
630pub fn getPosix(environ: Environ, key: []const u8) ?[:0]const u8 {
631 if (mem.findScalar(u8, key, '=') != null) return null;
632 for (environ.block.view().slice) |entry| {
633 var entry_i: usize = 0;
634 while (entry[entry_i] != 0) : (entry_i += 1) {
635 if (entry_i == key.len) break;
636 if (entry[entry_i] != key[entry_i]) break;
637 }
638 if ((entry_i != key.len) or (entry[entry_i] != '=')) continue;
639
640 return mem.sliceTo(entry + entry_i + 1, 0);
641 }
642 return null;
643}
644
645/// Windows-only. Get an environment variable with a null-terminated, WTF-16
646/// encoded name.
647///
648/// This function performs a Unicode-aware case-insensitive lookup using
649/// RtlEqualUnicodeString.
650///
651/// See also:
652/// * `createMap`
653/// * `containsConstant`
654/// * `contains`
655pub fn getWindows(environ: Environ, key: [*:0]const u16) ?[:0]const u16 {
656 // '=' anywhere but the start makes this an invalid environment variable name.
657 const key_slice = mem.sliceTo(key, 0);
658 if (key_slice.len == 0 or mem.findScalar(u16, key_slice[1..], '=') != null) return null;
659
660 if (!environ.block.use_global) return null;
661
662 const peb = std.os.windows.peb();
663 assert(std.os.windows.ntdll.RtlEnterCriticalSection(peb.FastPebLock) == .SUCCESS);
664 defer assert(std.os.windows.ntdll.RtlLeaveCriticalSection(peb.FastPebLock) == .SUCCESS);
665 const ptr = peb.ProcessParameters.Environment;
666
667 var i: usize = 0;
668 while (ptr[i] != 0) {
669 const key_value = mem.sliceTo(ptr[i..], 0);
670
671 // There are some special environment variables that start with =,
672 // so we need a special case to not treat = as a key/value separator
673 // if it's the first character.
674 // https://devblogs.microsoft.com/oldnewthing/20100506-00/?p=14133
675 const equal_index = mem.findScalarPos(u16, key_value, 1, '=') orelse {
676 // This is enforced by CreateProcess.
677 // If violated, CreateProcess will fail with INVALID_PARAMETER.
678 unreachable; // must contain a =
679 };
680
681 const this_key = key_value[0..equal_index];
682 if (std.os.windows.eqlIgnoreCaseWtf16(key_slice, this_key)) {
683 return key_value[equal_index + 1 ..];
684 }
685
686 // skip past the NUL terminator
687 i += key_value.len + 1;
688 }
689 return null;
690}
691
692pub const GetAllocError = error{
693 OutOfMemory,
694 EnvironmentVariableMissing,
695 /// On Windows, environment variable keys provided by the user must be
696 /// valid [WTF-8](https://wtf-8.codeberg.page/). This error is unreachable
697 /// if the key is statically known to be valid.
698 InvalidWtf8,
699};
700
701/// Caller owns returned memory.
702///
703/// On Windows:
704/// * If `key` is not valid [WTF-8](https://wtf-8.codeberg.page/), then
705/// `error.InvalidWtf8` is returned.
706/// * The returned value is encoded as [WTF-8](https://wtf-8.codeberg.page/).
707///
708/// On other platforms, the value is an opaque sequence of bytes with no
709/// particular encoding.
710///
711/// See also:
712/// * `createMap`
713pub fn getAlloc(environ: Environ, gpa: Allocator, key: []const u8) GetAllocError![]u8 {
714 if (native_os == .windows and !unicode.wtf8ValidateSlice(key)) return error.InvalidWtf8;
715 var map = createMap(environ, gpa) catch return error.OutOfMemory;
716 defer map.deinit();
717 const val = map.get(key) orelse return error.EnvironmentVariableMissing;
718 return gpa.dupe(u8, val);
719}
720
721pub const CreatePosixBlockOptions = struct {
722 /// `null` means to leave the `ZIG_PROGRESS` environment variable unmodified.
723 /// If non-null, negative means to remove the environment variable, and >= 0
724 /// means to provide it with the given integer.
725 zig_progress_fd: ?i32 = null,
726};
727
728/// Creates a null-delimited environment variable block in the format expected
729/// by POSIX, from a different one.
730pub fn createPosixBlock(
731 existing: Environ,
732 gpa: Allocator,
733 options: CreatePosixBlockOptions,
734) Allocator.Error!PosixBlock {
735 const contains_zig_progress = for (existing.block.view().slice) |entry| {
736 if (mem.eql(u8, mem.sliceTo(entry, '='), "ZIG_PROGRESS")) break true;
737 } else false;
738
739 const ZigProgressAction = enum { nothing, edit, delete, add };
740 const zig_progress_action: ZigProgressAction = action: {
741 const fd = options.zig_progress_fd orelse break :action .nothing;
742 if (fd >= 0) {
743 break :action if (contains_zig_progress) .edit else .add;
744 } else {
745 if (contains_zig_progress) break :action .delete;
746 }
747 break :action .nothing;
748 };
749
750 const envp = try gpa.allocSentinel(?[*:0]u8, len: {
751 var len: usize = existing.block.slice.len;
752 switch (zig_progress_action) {
753 .add => len += 1,
754 .delete => len -= 1,
755 .nothing, .edit => {},
756 }
757 break :len len;
758 }, null);
759 var envp_len: usize = 0;
760 errdefer {
761 envp[envp_len] = null;
762 PosixBlock.deinit(.{ .slice = envp[0..envp_len :null] }, gpa);
763 }
764 if (zig_progress_action == .add) {
765 envp[envp_len] = try std.fmt.allocPrintSentinel(gpa, "ZIG_PROGRESS={d}", .{options.zig_progress_fd.?}, 0);
766 envp_len += 1;
767 }
768
769 var existing_index: usize = 0;
770 while (existing.block.slice[existing_index]) |entry| : (existing_index += 1) {
771 if (mem.eql(u8, mem.sliceTo(entry, '='), "ZIG_PROGRESS")) switch (zig_progress_action) {
772 .add => unreachable,
773 .delete => continue,
774 .edit => {
775 envp[envp_len] = try std.fmt.allocPrintSentinel(gpa, "ZIG_PROGRESS={d}", .{options.zig_progress_fd.?}, 0);
776 envp_len += 1;
777 continue;
778 },
779 .nothing => {},
780 };
781 envp[envp_len] = try gpa.dupeSentinel(u8, mem.span(entry), 0);
782 envp_len += 1;
783 }
784
785 assert(envp_len == envp.len);
786 return .{ .slice = envp };
787}
788
789pub const CreateWindowsBlockOptions = struct {
790 /// `null` means to leave the `ZIG_PROGRESS` environment variable unmodified.
791 /// If non-null, `std.os.windows.INVALID_HANDLE_VALUE` means to remove the
792 /// environment variable, otherwise provide it with the given handle as an integer.
793 zig_progress_handle: ?std.os.windows.HANDLE = null,
794};
795
796/// Creates a null-delimited environment variable block in the format expected
797/// by POSIX, from a different one.
798pub fn createWindowsBlock(
799 existing: Environ,
800 gpa: Allocator,
801 options: CreateWindowsBlockOptions,
802) Allocator.Error!WindowsBlock {
803 if (!existing.block.use_global) return .{
804 .slice = try gpa.dupeSentinel(u16, WindowsBlock.empty.slice, 0),
805 };
806 const peb = std.os.windows.peb();
807 assert(std.os.windows.ntdll.RtlEnterCriticalSection(peb.FastPebLock) == .SUCCESS);
808 defer assert(std.os.windows.ntdll.RtlLeaveCriticalSection(peb.FastPebLock) == .SUCCESS);
809 const existing_block = peb.ProcessParameters.Environment;
810 var ranges: [2]struct { start: usize, end: usize } = undefined;
811 var ranges_len: usize = 0;
812 ranges[ranges_len].start = 0;
813 const zig_progress_key = [_]u16{ 'Z', 'I', 'G', '_', 'P', 'R', 'O', 'G', 'R', 'E', 'S', 'S', '=' };
814 const needed_len = needed_len: {
815 var needed_len: usize = "\x00".len;
816 if (options.zig_progress_handle) |handle| if (handle != std.os.windows.INVALID_HANDLE_VALUE) {
817 needed_len += std.fmt.count("ZIG_PROGRESS={d}\x00", .{@intFromPtr(handle)});
818 };
819 var i: usize = 0;
820 while (existing_block[i] != 0) {
821 const start = i;
822 const entry = mem.sliceTo(existing_block[start..], 0);
823 i += entry.len + "\x00".len;
824 if (options.zig_progress_handle != null and entry.len >= zig_progress_key.len and
825 std.os.windows.eqlIgnoreCaseWtf16(entry[0..zig_progress_key.len], &zig_progress_key))
826 {
827 ranges[ranges_len].end = start;
828 ranges_len += 1;
829 ranges[ranges_len].start = i;
830 } else needed_len += entry.len + "\x00".len;
831 }
832 ranges[ranges_len].end = i;
833 ranges_len += 1;
834 break :needed_len @max("\x00\x00".len, needed_len);
835 };
836 const block = try gpa.alloc(u16, needed_len);
837 errdefer gpa.free(block);
838 var i: usize = 0;
839 if (options.zig_progress_handle) |handle| if (handle != std.os.windows.INVALID_HANDLE_VALUE) {
840 @memcpy(block[i..][0..zig_progress_key.len], &zig_progress_key);
841 i += zig_progress_key.len;
842 var value_buf: [std.fmt.count("{d}", .{std.math.maxInt(usize)})]u8 = undefined;
843 const value = std.mem.print(&value_buf, "{d}", .{@intFromPtr(handle)}) catch unreachable;
844 for (block[i..][0..value.len], value) |*r, v| r.* = v;
845 i += value.len;
846 block[i] = 0;
847 i += 1;
848 };
849 for (ranges[0..ranges_len]) |range| {
850 const range_len = range.end - range.start;
851 @memcpy(block[i..][0..range_len], existing_block[range.start..range.end]);
852 i += range_len;
853 }
854 // An empty environment is a special case that requires a redundant
855 // NUL terminator. CreateProcess will read the second code unit even
856 // though theoretically the first should be enough to recognize that the
857 // environment is empty (see https://nullprogram.com/blog/2023/08/23/)
858 for (0..2) |_| {
859 block[i] = 0;
860 i += 1;
861 if (i >= 2) break;
862 } else unreachable;
863 assert(i == block.len);
864 return .{ .slice = block[0 .. i - 1 :0] };
865}
866
867test "Map.createPosixBlock" {
868 const gpa = testing.allocator;
869
870 var envmap = Map.init(gpa);
871 defer envmap.deinit();
872
873 try envmap.put("HOME", "/home/ifreund");
874 try envmap.put("WAYLAND_DISPLAY", "wayland-1");
875 try envmap.put("DISPLAY", ":1");
876 try envmap.put("DEBUGINFOD_URLS", " ");
877 try envmap.put("XCURSOR_SIZE", "24");
878
879 const block = try envmap.createPosixBlock(gpa, .{});
880 defer block.deinit(gpa);
881
882 try testing.expectEqual(@as(usize, 5), block.slice.len);
883
884 for (&[_][]const u8{
885 "HOME=/home/ifreund",
886 "WAYLAND_DISPLAY=wayland-1",
887 "DISPLAY=:1",
888 "DEBUGINFOD_URLS= ",
889 "XCURSOR_SIZE=24",
890 }, block.slice) |expected, actual| try testing.expectEqualStrings(expected, mem.span(actual.?));
891}
892
893test Map {
894 const gpa = testing.allocator;
895
896 var env: Map = .init(gpa);
897 defer env.deinit();
898
899 try env.put("SOMETHING_NEW", "hello");
900 try testing.expectEqualStrings("hello", env.get("SOMETHING_NEW").?);
901 try testing.expectEqual(@as(Map.Size, 1), env.count());
902
903 // overwrite
904 try env.put("SOMETHING_NEW", "something");
905 try testing.expectEqualStrings("something", env.get("SOMETHING_NEW").?);
906 try testing.expectEqual(@as(Map.Size, 1), env.count());
907
908 // a new longer name to test the Windows-specific conversion buffer
909 try env.put("SOMETHING_NEW_AND_LONGER", "1");
910 try testing.expectEqualStrings("1", env.get("SOMETHING_NEW_AND_LONGER").?);
911 try testing.expectEqual(@as(Map.Size, 2), env.count());
912
913 // case insensitivity on Windows only
914 if (native_os == .windows) {
915 try testing.expectEqualStrings("1", env.get("something_New_aNd_LONGER").?);
916 } else {
917 try testing.expect(null == env.get("something_New_aNd_LONGER"));
918 }
919
920 var it = env.iterator();
921 var count: Map.Size = 0;
922 while (it.next()) |entry| {
923 const is_an_expected_name = mem.eql(u8, "SOMETHING_NEW", entry.key_ptr.*) or mem.eql(u8, "SOMETHING_NEW_AND_LONGER", entry.key_ptr.*);
924 try testing.expect(is_an_expected_name);
925 count += 1;
926 }
927 try testing.expectEqual(@as(Map.Size, 2), count);
928
929 try testing.expect(env.swapRemove("SOMETHING_NEW"));
930 try testing.expect(!env.swapRemove("SOMETHING_NEW"));
931 try testing.expect(env.get("SOMETHING_NEW") == null);
932 try testing.expect(!env.contains("SOMETHING_NEW"));
933
934 try testing.expectEqual(@as(Map.Size, 1), env.count());
935
936 if (native_os == .windows) {
937 // test Unicode case-insensitivity on Windows
938 try env.put("КИРиллИЦА", "something else");
939 try testing.expectEqualStrings("something else", env.get("кириллица").?);
940
941 // and WTF-8 that's not valid UTF-8
942 const wtf8_with_surrogate_pair = try unicode.wtf16LeToWtf8Alloc(gpa, &[_]u16{
943 mem.nativeToLittle(u16, 0xD83D), // unpaired high surrogate
944 });
945 defer gpa.free(wtf8_with_surrogate_pair);
946
947 try env.put(wtf8_with_surrogate_pair, wtf8_with_surrogate_pair);
948 try testing.expectEqualSlices(u8, wtf8_with_surrogate_pair, env.get(wtf8_with_surrogate_pair).?);
949 }
950}
951
952test "convert from Environ to Map and back again" {
953 if (native_os == .windows) return;
954 if (native_os == .wasi and !builtin.link_libc) return;
955
956 const gpa = testing.allocator;
957
958 var map: Map = .init(gpa);
959 defer map.deinit();
960 try map.put("FOO", "BAR");
961 try map.put("A", "");
962
963 const environ: Environ = .{ .block = try map.createPosixBlock(gpa, .{}) };
964 defer environ.block.deinit(gpa);
965
966 try testing.expectEqual(true, environ.contains(gpa, "FOO"));
967 try testing.expectEqual(false, environ.contains(gpa, "BAR"));
968 try testing.expectEqual(true, environ.contains(gpa, "A"));
969 try testing.expectEqual(true, environ.containsConstant("A"));
970 try testing.expectEqual(false, environ.containsUnempty(gpa, "A"));
971 try testing.expectEqual(false, environ.containsUnemptyConstant("A"));
972 try testing.expectEqual(false, environ.contains(gpa, "B"));
973
974 try testing.expectError(error.EnvironmentVariableMissing, environ.getAlloc(gpa, "BOGUS"));
975 {
976 const value = try environ.getAlloc(gpa, "FOO");
977 defer gpa.free(value);
978 try testing.expectEqualStrings("BAR", value);
979 }
980
981 var map2 = try environ.createMap(gpa);
982 defer map2.deinit();
983
984 try testing.expectEqualDeep(map.keys(), map2.keys());
985 try testing.expectEqualDeep(map.values(), map2.values());
986}
987
988test "Map.putPosixBlock" {
989 const gpa = testing.allocator;
990
991 var map: Map = .init(gpa);
992 defer map.deinit();
993
994 try map.put("FOO", "BAR");
995 try map.put("A", "");
996 try map.put("ZIG_PROGRESS", "unchanged");
997
998 const block = try map.createPosixBlock(gpa, .{});
999 defer block.deinit(gpa);
1000
1001 var map2: Map = .init(gpa);
1002 defer map2.deinit();
1003 try map2.putPosixBlock(block.view());
1004
1005 try testing.expectEqualDeep(&[_][]const u8{ "FOO", "A", "ZIG_PROGRESS" }, map2.keys());
1006 try testing.expectEqualDeep(&[_][]const u8{ "BAR", "", "unchanged" }, map2.values());
1007}
1008
1009test "Map.putWindowsBlock" {
1010 if (native_os != .windows) return;
1011
1012 const gpa = testing.allocator;
1013
1014 var map: Map = .init(gpa);
1015 defer map.deinit();
1016
1017 try map.put("FOO", "BAR");
1018 try map.put("A", "");
1019 try map.put("=B", "");
1020 try map.put("ZIG_PROGRESS", "unchanged");
1021
1022 const block = try map.createWindowsBlock(gpa, .{});
1023 defer block.deinit(gpa);
1024
1025 var map2: Map = .init(gpa);
1026 defer map2.deinit();
1027 try map2.putWindowsBlock(block.view());
1028
1029 try testing.expectEqualDeep(&[_][]const u8{ "FOO", "A", "=B", "ZIG_PROGRESS" }, map2.keys());
1030 try testing.expectEqualDeep(&[_][]const u8{ "BAR", "", "", "unchanged" }, map2.values());
1031}