| 1 | const Environ = @This(); |
| 2 | |
| 3 | const builtin = @import("builtin"); |
| 4 | const native_os = builtin.os.tag; |
| 5 | |
| 6 | const std = @import("../std.zig"); |
| 7 | const Allocator = mem.Allocator; |
| 8 | const assert = std.debug.assert; |
| 9 | const testing = std.testing; |
| 10 | const unicode = std.unicode; |
| 11 | const posix = std.posix; |
| 12 | const mem = std.mem; |
| 13 | |
| 14 | /// Unmodified, unprocessed data provided by the operating system. |
| 15 | block: Block, |
| 16 | |
| 17 | pub 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. |
| 25 | pub 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 | |
| 35 | pub 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 | |
| 48 | pub 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 | |
| 74 | pub 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. |
| 100 | pub 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 | |
| 498 | pub 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. |
| 506 | pub 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 | |
| 546 | pub 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` |
| 564 | pub 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` |
| 578 | pub 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` |
| 595 | pub 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` |
| 613 | pub 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` |
| 630 | pub 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` |
| 655 | pub 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 | |
| 692 | pub 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` |
| 713 | pub 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 | |
| 721 | pub 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. |
| 730 | pub 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 | |
| 789 | pub 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. |
| 798 | pub 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 | |
| 867 | test "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 | |
| 893 | test 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 | |
| 952 | test "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 | |
| 988 | test "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 | |
| 1009 | test "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 | } |