authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2026-02-04 18:12:06-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-02-05 20:24:31+01:00
logbcb5218a2b2bce189831e68b3396cfd6f246caa2
tree79d594257171e1a93b71f81612254bbcc8d4a77d
parentfa3228ae42d3bc92ad66fe91e108511583129ffd

Environ: reinstate `null` return on `=` in environment variable keys

Changes an assert back into a conditional to match the behavior of `getPosix`, see https://codeberg.org/ziglang/zig/pulls/31113#issuecomment-10371698 and https://github.com/ziglang/zig/issues/23331. Note: the conditional has been updated to also return null early on 0-length key lookups, since there's no need to iterate the block in that case. For `Environ.Map`, validation of keys has been split into two categories: 'put' and 'fetch', each of which are tailored to the constraints that the implementation actually relies upon. Specifically: - Hashing (fetching) requires the keys to be valid WTF-8 on Windows, but does not rely on any other properties of the keys (attempting to fetch `F\x00=` is not a problem, it just won't be found) - `create{Posix,Windows}Block` relies on the Map to always have fully valid keys (no NUL, no `=` in an invalid location, no zero-length keys), which means that the 'put' APIs need to validate that incoming keys adhere to those properties. The relevant assertions are now documented on each of the Map functions. Also reinstates some test cases in the `env_vars` standalone test. Some of the reinstated tests are effectively just testing the Environ.Map implementation due to how `Environ.contains`, `Environ.getAlloc`, etc are implemented, but that is not inherent to those functions so the tests are still potentially relevant if e.g. `contains` is implemented in terms of `getPosix`/`getWindows` in the future (which is totally possible and maybe a good idea since constructing the whole map is not necessary for looking up one key).

2 files changed, 55 insertions(+), 25 deletions(-)

lib/std/process/Environ.zig+32-25
...@@ -129,25 +129,21 @@ pub const Map = struct {...@@ -129,25 +129,21 @@ pub const Map = struct {
129 };129 };
130 }130 }
131131
132 pub fn validateKey(key: []const u8) bool {132 pub fn validateKeyForPut(key: []const u8) bool {
133 switch (native_os) {133 switch (native_os) {
134 else => return key.len > 0 and mem.findAny(u8, key, &.{ 0, '=' }) == null,134 else => return key.len > 0 and mem.findAny(u8, key, &.{ 0, '=' }) == null,
135 .windows => {135 .windows => {
136 if (!unicode.wtf8ValidateSlice(key)) return false;136 if (!unicode.wtf8ValidateSlice(key)) return false;
137 var it = unicode.Wtf8View.initUnchecked(key).iterator();137 return key.len > 0 and key[0] != 0 and mem.findAnyPos(u8, key, 1, &.{ 0, '=' }) == null;
138 switch (it.nextCodepoint() orelse return false) {
139 0 => return false,
140 else => {},
141 }
142 while (it.nextCodepoint()) |cp| switch (cp) {
143 0, '=' => return false,
144 else => {},
145 };
146 return true;
147 },138 },
148 }139 }
149 }140 }
150141
142 pub fn validateKeyForFetch(key: []const u8) bool {
143 if (native_os == .windows and !unicode.wtf8ValidateSlice(key)) return false;
144 return true;
145 }
146
151 /// Create a Map backed by a specific allocator.147 /// Create a Map backed by a specific allocator.
152 /// That allocator will be used for both backing allocations148 /// That allocator will be used for both backing allocations
153 /// and string deduplication.149 /// and string deduplication.
...@@ -220,9 +216,14 @@ pub const Map = struct {...@@ -220,9 +216,14 @@ pub const Map = struct {
220 /// Same as `put` but the key and value become owned by the Map rather216 /// Same as `put` but the key and value become owned by the Map rather
221 /// than being copied.217 /// than being copied.
222 /// If `putMove` fails, the ownership of key and value does not transfer.218 /// If `putMove` fails, the ownership of key and value does not transfer.
223 /// On Windows `key` must be a valid [WTF-8](https://wtf-8.codeberg.page/) string.219 ///
220 /// Asserts that `key` is valid:
221 /// - It cannot contain a NUL (`'\x00') byte.
222 /// - It must have a length > 0.
223 /// - It cannot contain `=`, except on Windows where only the first code point is allowed to be `=`.
224 /// - On Windows, it must be valid [WTF-8](https://wtf-8.codeberg.page/).
224 pub fn putMove(self: *Map, key: []u8, value: []u8) Allocator.Error!void {225 pub fn putMove(self: *Map, key: []u8, value: []u8) Allocator.Error!void {
225 assert(validateKey(key));226 assert(validateKeyForPut(key));
226 const gpa = self.allocator;227 const gpa = self.allocator;
227 const get_or_put = try self.array_hash_map.getOrPut(gpa, key);228 const get_or_put = try self.array_hash_map.getOrPut(gpa, key);
228 if (get_or_put.found_existing) {229 if (get_or_put.found_existing) {
...@@ -234,9 +235,14 @@ pub const Map = struct {...@@ -234,9 +235,14 @@ pub const Map = struct {
234 }235 }
235236
236 /// `key` and `value` are copied into the Map.237 /// `key` and `value` are copied into the Map.
237 /// On Windows `key` must be a valid [WTF-8](https://wtf-8.codeberg.page/) string.238 ///
239 /// Asserts that `key` is valid:
240 /// - It cannot contain a NUL (`'\x00') byte.
241 /// - It must have a length > 0.
242 /// - It cannot contain `=`, except on Windows where only the first code point is allowed to be `=`.
243 /// - On Windows, it must be valid [WTF-8](https://wtf-8.codeberg.page/).
238 pub fn put(self: *Map, key: []const u8, value: []const u8) Allocator.Error!void {244 pub fn put(self: *Map, key: []const u8, value: []const u8) Allocator.Error!void {
239 assert(validateKey(key));245 assert(validateKeyForPut(key));
240 const gpa = self.allocator;246 const gpa = self.allocator;
241 const value_copy = try gpa.dupe(u8, value);247 const value_copy = try gpa.dupe(u8, value);
242 errdefer gpa.free(value_copy);248 errdefer gpa.free(value_copy);
...@@ -254,23 +260,24 @@ pub const Map = struct {...@@ -254,23 +260,24 @@ pub const Map = struct {
254260
255 /// Find the address of the value associated with a key.261 /// Find the address of the value associated with a key.
256 /// The returned pointer is invalidated if the map resizes.262 /// The returned pointer is invalidated if the map resizes.
257 /// On Windows `key` must be a valid [WTF-8](https://wtf-8.codeberg.page/) string.263 /// On Windows, asserts that `key` is valid [WTF-8](https://wtf-8.codeberg.page/).
258 pub fn getPtr(self: Map, key: []const u8) ?*[]const u8 {264 pub fn getPtr(self: Map, key: []const u8) ?*[]const u8 {
259 assert(validateKey(key));265 assert(validateKeyForFetch(key));
260 return self.array_hash_map.getPtr(key);266 return self.array_hash_map.getPtr(key);
261 }267 }
262268
263 /// Return the map's copy of the value associated with269 /// Return the map's copy of the value associated with
264 /// a key. The returned string is invalidated if this270 /// a key. The returned string is invalidated if this
265 /// key is removed from the map.271 /// key is removed from the map.
266 /// On Windows `key` must be a valid [WTF-8](https://wtf-8.codeberg.page/) string.272 /// On Windows, asserts that `key` is valid [WTF-8](https://wtf-8.codeberg.page/).
267 pub fn get(self: Map, key: []const u8) ?[]const u8 {273 pub fn get(self: Map, key: []const u8) ?[]const u8 {
268 assert(validateKey(key));274 assert(validateKeyForFetch(key));
269 return self.array_hash_map.get(key);275 return self.array_hash_map.get(key);
270 }276 }
271277
278 /// On Windows, asserts that `key` is valid [WTF-8](https://wtf-8.codeberg.page/).
272 pub fn contains(m: *const Map, key: []const u8) bool {279 pub fn contains(m: *const Map, key: []const u8) bool {
273 assert(validateKey(key));280 assert(validateKeyForFetch(key));
274 return m.array_hash_map.contains(key);281 return m.array_hash_map.contains(key);
275 }282 }
276283
...@@ -281,9 +288,9 @@ pub const Map = struct {...@@ -281,9 +288,9 @@ pub const Map = struct {
281 /// Returns true if an entry was removed, false otherwise.288 /// Returns true if an entry was removed, false otherwise.
282 ///289 ///
283 /// This invalidates the value returned by get() for this key.290 /// This invalidates the value returned by get() for this key.
284 /// On Windows `key` must be a valid [WTF-8](https://wtf-8.codeberg.page/) string.291 /// On Windows, asserts that `key` is valid [WTF-8](https://wtf-8.codeberg.page/).
285 pub fn swapRemove(self: *Map, key: []const u8) bool {292 pub fn swapRemove(self: *Map, key: []const u8) bool {
286 assert(validateKey(key));293 assert(validateKeyForFetch(key));
287 const kv = self.array_hash_map.fetchSwapRemove(key) orelse return false;294 const kv = self.array_hash_map.fetchSwapRemove(key) orelse return false;
288 const gpa = self.allocator;295 const gpa = self.allocator;
289 gpa.free(kv.key);296 gpa.free(kv.key);
...@@ -298,9 +305,9 @@ pub const Map = struct {...@@ -298,9 +305,9 @@ pub const Map = struct {
298 /// Returns true if an entry was removed, false otherwise.305 /// Returns true if an entry was removed, false otherwise.
299 ///306 ///
300 /// This invalidates the value returned by get() for this key.307 /// This invalidates the value returned by get() for this key.
301 /// On Windows `key` must be a valid [WTF-8](https://wtf-8.codeberg.page/) string.308 /// On Windows, asserts that `key` is valid [WTF-8](https://wtf-8.codeberg.page/).
302 pub fn orderedRemove(self: *Map, key: []const u8) bool {309 pub fn orderedRemove(self: *Map, key: []const u8) bool {
303 assert(validateKey(key));310 assert(validateKeyForFetch(key));
304 const kv = self.array_hash_map.fetchOrderedRemove(key) orelse return false;311 const kv = self.array_hash_map.fetchOrderedRemove(key) orelse return false;
305 const gpa = self.allocator;312 const gpa = self.allocator;
306 gpa.free(kv.key);313 gpa.free(kv.key);
...@@ -612,7 +619,7 @@ pub fn getPosix(environ: Environ, key: []const u8) ?[:0]const u8 {...@@ -612,7 +619,7 @@ pub fn getPosix(environ: Environ, key: []const u8) ?[:0]const u8 {
612pub fn getWindows(environ: Environ, key: [*:0]const u16) ?[:0]const u16 {619pub fn getWindows(environ: Environ, key: [*:0]const u16) ?[:0]const u16 {
613 // '=' anywhere but the start makes this an invalid environment variable name.620 // '=' anywhere but the start makes this an invalid environment variable name.
614 const key_slice = mem.sliceTo(key, 0);621 const key_slice = mem.sliceTo(key, 0);
615 assert(key_slice.len > 0 and mem.findScalar(u16, key_slice[1..], '=') == null);622 if (key_slice.len == 0 or mem.findScalar(u16, key_slice[1..], '=') != null) return null;
616623
617 if (!environ.block.use_global) return null;624 if (!environ.block.use_global) return null;
618625
test/standalone/env_vars/main.zig+23
...@@ -12,10 +12,14 @@ pub fn main(init: std.process.Init) !void {...@@ -12,10 +12,14 @@ pub fn main(init: std.process.Init) !void {
12 // containsUnempty12 // containsUnempty
13 {13 {
14 try std.testing.expect(try environ.containsUnempty(allocator, "FOO"));14 try std.testing.expect(try environ.containsUnempty(allocator, "FOO"));
15 try std.testing.expect(!(try environ.containsUnempty(allocator, "FOO=")));
16 try std.testing.expect(!(try environ.containsUnempty(allocator, "FO")));
17 try std.testing.expect(!(try environ.containsUnempty(allocator, "FOOO")));
15 if (builtin.os.tag == .windows) {18 if (builtin.os.tag == .windows) {
16 try std.testing.expect(try environ.containsUnempty(allocator, "foo"));19 try std.testing.expect(try environ.containsUnempty(allocator, "foo"));
17 }20 }
18 try std.testing.expect(try environ.containsUnempty(allocator, "EQUALS"));21 try std.testing.expect(try environ.containsUnempty(allocator, "EQUALS"));
22 try std.testing.expect(!(try environ.containsUnempty(allocator, "EQUALS=ABC")));
19 try std.testing.expect(try environ.containsUnempty(allocator, "КИРиллИЦА"));23 try std.testing.expect(try environ.containsUnempty(allocator, "КИРиллИЦА"));
20 if (builtin.os.tag == .windows) {24 if (builtin.os.tag == .windows) {
21 try std.testing.expect(try environ.containsUnempty(allocator, "кирИЛЛица"));25 try std.testing.expect(try environ.containsUnempty(allocator, "кирИЛЛица"));
...@@ -31,10 +35,14 @@ pub fn main(init: std.process.Init) !void {...@@ -31,10 +35,14 @@ pub fn main(init: std.process.Init) !void {
31 // containsUnemptyConstant35 // containsUnemptyConstant
32 {36 {
33 try std.testing.expect(environ.containsUnemptyConstant("FOO"));37 try std.testing.expect(environ.containsUnemptyConstant("FOO"));
38 try std.testing.expect(!environ.containsUnemptyConstant("FOO="));
39 try std.testing.expect(!environ.containsUnemptyConstant("FO"));
40 try std.testing.expect(!environ.containsUnemptyConstant("FOOO"));
34 if (builtin.os.tag == .windows) {41 if (builtin.os.tag == .windows) {
35 try std.testing.expect(environ.containsUnemptyConstant("foo"));42 try std.testing.expect(environ.containsUnemptyConstant("foo"));
36 }43 }
37 try std.testing.expect(environ.containsUnemptyConstant("EQUALS"));44 try std.testing.expect(environ.containsUnemptyConstant("EQUALS"));
45 try std.testing.expect(!environ.containsUnemptyConstant("EQUALS=ABC"));
38 try std.testing.expect(environ.containsUnemptyConstant("КИРиллИЦА"));46 try std.testing.expect(environ.containsUnemptyConstant("КИРиллИЦА"));
39 if (builtin.os.tag == .windows) {47 if (builtin.os.tag == .windows) {
40 try std.testing.expect(environ.containsUnemptyConstant("кирИЛЛица"));48 try std.testing.expect(environ.containsUnemptyConstant("кирИЛЛица"));
...@@ -50,10 +58,14 @@ pub fn main(init: std.process.Init) !void {...@@ -50,10 +58,14 @@ pub fn main(init: std.process.Init) !void {
50 // contains58 // contains
51 {59 {
52 try std.testing.expect(try environ.contains(allocator, "FOO"));60 try std.testing.expect(try environ.contains(allocator, "FOO"));
61 try std.testing.expect(!(try environ.contains(allocator, "FOO=")));
62 try std.testing.expect(!(try environ.contains(allocator, "FO")));
63 try std.testing.expect(!(try environ.contains(allocator, "FOOO")));
53 if (builtin.os.tag == .windows) {64 if (builtin.os.tag == .windows) {
54 try std.testing.expect(try environ.contains(allocator, "foo"));65 try std.testing.expect(try environ.contains(allocator, "foo"));
55 }66 }
56 try std.testing.expect(try environ.contains(allocator, "EQUALS"));67 try std.testing.expect(try environ.contains(allocator, "EQUALS"));
68 try std.testing.expect(!(try environ.contains(allocator, "EQUALS=ABC")));
57 try std.testing.expect(try environ.contains(allocator, "КИРиллИЦА"));69 try std.testing.expect(try environ.contains(allocator, "КИРиллИЦА"));
58 if (builtin.os.tag == .windows) {70 if (builtin.os.tag == .windows) {
59 try std.testing.expect(try environ.contains(allocator, "кирИЛЛица"));71 try std.testing.expect(try environ.contains(allocator, "кирИЛЛица"));
...@@ -69,10 +81,14 @@ pub fn main(init: std.process.Init) !void {...@@ -69,10 +81,14 @@ pub fn main(init: std.process.Init) !void {
69 // containsConstant81 // containsConstant
70 {82 {
71 try std.testing.expect(environ.containsConstant("FOO"));83 try std.testing.expect(environ.containsConstant("FOO"));
84 try std.testing.expect(!environ.containsConstant("FOO="));
85 try std.testing.expect(!environ.containsConstant("FO"));
86 try std.testing.expect(!environ.containsConstant("FOOO"));
72 if (builtin.os.tag == .windows) {87 if (builtin.os.tag == .windows) {
73 try std.testing.expect(environ.containsConstant("foo"));88 try std.testing.expect(environ.containsConstant("foo"));
74 }89 }
75 try std.testing.expect(environ.containsConstant("EQUALS"));90 try std.testing.expect(environ.containsConstant("EQUALS"));
91 try std.testing.expect(!environ.containsConstant("EQUALS=ABC"));
76 try std.testing.expect(environ.containsConstant("КИРиллИЦА"));92 try std.testing.expect(environ.containsConstant("КИРиллИЦА"));
77 if (builtin.os.tag == .windows) {93 if (builtin.os.tag == .windows) {
78 try std.testing.expect(environ.containsConstant("кирИЛЛица"));94 try std.testing.expect(environ.containsConstant("кирИЛЛица"));
...@@ -88,10 +104,14 @@ pub fn main(init: std.process.Init) !void {...@@ -88,10 +104,14 @@ pub fn main(init: std.process.Init) !void {
88 // getAlloc104 // getAlloc
89 {105 {
90 try std.testing.expectEqualSlices(u8, "123", try environ.getAlloc(arena, "FOO"));106 try std.testing.expectEqualSlices(u8, "123", try environ.getAlloc(arena, "FOO"));
107 try std.testing.expectError(error.EnvironmentVariableMissing, environ.getAlloc(arena, "FOO="));
108 try std.testing.expectError(error.EnvironmentVariableMissing, environ.getAlloc(arena, "FO"));
109 try std.testing.expectError(error.EnvironmentVariableMissing, environ.getAlloc(arena, "FOOO"));
91 if (builtin.os.tag == .windows) {110 if (builtin.os.tag == .windows) {
92 try std.testing.expectEqualSlices(u8, "123", try environ.getAlloc(arena, "foo"));111 try std.testing.expectEqualSlices(u8, "123", try environ.getAlloc(arena, "foo"));
93 }112 }
94 try std.testing.expectEqualSlices(u8, "ABC=123", try environ.getAlloc(arena, "EQUALS"));113 try std.testing.expectEqualSlices(u8, "ABC=123", try environ.getAlloc(arena, "EQUALS"));
114 try std.testing.expectError(error.EnvironmentVariableMissing, environ.getAlloc(arena, "EQUALS=ABC"));
95 try std.testing.expectEqualSlices(u8, "non-ascii አማርኛ \u{10FFFF}", try environ.getAlloc(arena, "КИРиллИЦА"));115 try std.testing.expectEqualSlices(u8, "non-ascii አማርኛ \u{10FFFF}", try environ.getAlloc(arena, "КИРиллИЦА"));
96 if (builtin.os.tag == .windows) {116 if (builtin.os.tag == .windows) {
97 try std.testing.expectEqualSlices(u8, "non-ascii አማርኛ \u{10FFFF}", try environ.getAlloc(arena, "кирИЛЛица"));117 try std.testing.expectEqualSlices(u8, "non-ascii አማርኛ \u{10FFFF}", try environ.getAlloc(arena, "кирИЛЛица"));
...@@ -110,10 +130,13 @@ pub fn main(init: std.process.Init) !void {...@@ -110,10 +130,13 @@ pub fn main(init: std.process.Init) !void {
110 defer environ_map.deinit();130 defer environ_map.deinit();
111131
112 try std.testing.expectEqualSlices(u8, "123", environ_map.get("FOO").?);132 try std.testing.expectEqualSlices(u8, "123", environ_map.get("FOO").?);
133 try std.testing.expectEqual(null, environ_map.get("FO"));
134 try std.testing.expectEqual(null, environ_map.get("FOOO"));
113 if (builtin.os.tag == .windows) {135 if (builtin.os.tag == .windows) {
114 try std.testing.expectEqualSlices(u8, "123", environ_map.get("foo").?);136 try std.testing.expectEqualSlices(u8, "123", environ_map.get("foo").?);
115 }137 }
116 try std.testing.expectEqualSlices(u8, "ABC=123", environ_map.get("EQUALS").?);138 try std.testing.expectEqualSlices(u8, "ABC=123", environ_map.get("EQUALS").?);
139 try std.testing.expectEqual(null, environ_map.get("EQUALS=ABC"));
117 try std.testing.expectEqualSlices(u8, "non-ascii አማርኛ \u{10FFFF}", environ_map.get("КИРиллИЦА").?);140 try std.testing.expectEqualSlices(u8, "non-ascii አማርኛ \u{10FFFF}", environ_map.get("КИРиллИЦА").?);
118 if (builtin.os.tag == .windows) {141 if (builtin.os.tag == .windows) {
119 try std.testing.expectEqualSlices(u8, "non-ascii አማርኛ \u{10FFFF}", environ_map.get("кирИЛЛица").?);142 try std.testing.expectEqualSlices(u8, "non-ascii አማርኛ \u{10FFFF}", environ_map.get("кирИЛЛица").?);