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 {
129129 };
130130 }
131131
132 pub fn validateKey(key: []const u8) bool {
132 pub fn validateKeyForPut(key: []const u8) bool {
133133 switch (native_os) {
134134 else => return key.len > 0 and mem.findAny(u8, key, &.{ 0, '=' }) == null,
135135 .windows => {
136136 if (!unicode.wtf8ValidateSlice(key)) return false;
137 var it = unicode.Wtf8View.initUnchecked(key).iterator();
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;
137 return key.len > 0 and key[0] != 0 and mem.findAnyPos(u8, key, 1, &.{ 0, '=' }) == null;
147138 },
148139 }
149140 }
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
151147 /// Create a Map backed by a specific allocator.
152148 /// That allocator will be used for both backing allocations
153149 /// and string deduplication.
......@@ -220,9 +216,14 @@ pub const Map = struct {
220216 /// Same as `put` but the key and value become owned by the Map rather
221217 /// than being copied.
222218 /// 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/).
224225 pub fn putMove(self: *Map, key: []u8, value: []u8) Allocator.Error!void {
225 assert(validateKey(key));
226 assert(validateKeyForPut(key));
226227 const gpa = self.allocator;
227228 const get_or_put = try self.array_hash_map.getOrPut(gpa, key);
228229 if (get_or_put.found_existing) {
......@@ -234,9 +235,14 @@ pub const Map = struct {
234235 }
235236
236237 /// `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/).
238244 pub fn put(self: *Map, key: []const u8, value: []const u8) Allocator.Error!void {
239 assert(validateKey(key));
245 assert(validateKeyForPut(key));
240246 const gpa = self.allocator;
241247 const value_copy = try gpa.dupe(u8, value);
242248 errdefer gpa.free(value_copy);
......@@ -254,23 +260,24 @@ pub const Map = struct {
254260
255261 /// Find the address of the value associated with a key.
256262 /// 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/).
258264 pub fn getPtr(self: Map, key: []const u8) ?*[]const u8 {
259 assert(validateKey(key));
265 assert(validateKeyForFetch(key));
260266 return self.array_hash_map.getPtr(key);
261267 }
262268
263269 /// Return the map's copy of the value associated with
264270 /// a key. The returned string is invalidated if this
265271 /// 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/).
267273 pub fn get(self: Map, key: []const u8) ?[]const u8 {
268 assert(validateKey(key));
274 assert(validateKeyForFetch(key));
269275 return self.array_hash_map.get(key);
270276 }
271277
278 /// On Windows, asserts that `key` is valid [WTF-8](https://wtf-8.codeberg.page/).
272279 pub fn contains(m: *const Map, key: []const u8) bool {
273 assert(validateKey(key));
280 assert(validateKeyForFetch(key));
274281 return m.array_hash_map.contains(key);
275282 }
276283
......@@ -281,9 +288,9 @@ pub const Map = struct {
281288 /// Returns true if an entry was removed, false otherwise.
282289 ///
283290 /// 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/).
285292 pub fn swapRemove(self: *Map, key: []const u8) bool {
286 assert(validateKey(key));
293 assert(validateKeyForFetch(key));
287294 const kv = self.array_hash_map.fetchSwapRemove(key) orelse return false;
288295 const gpa = self.allocator;
289296 gpa.free(kv.key);
......@@ -298,9 +305,9 @@ pub const Map = struct {
298305 /// Returns true if an entry was removed, false otherwise.
299306 ///
300307 /// 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/).
302309 pub fn orderedRemove(self: *Map, key: []const u8) bool {
303 assert(validateKey(key));
310 assert(validateKeyForFetch(key));
304311 const kv = self.array_hash_map.fetchOrderedRemove(key) orelse return false;
305312 const gpa = self.allocator;
306313 gpa.free(kv.key);
......@@ -612,7 +619,7 @@ pub fn getPosix(environ: Environ, key: []const u8) ?[:0]const u8 {
612619pub fn getWindows(environ: Environ, key: [*:0]const u16) ?[:0]const u16 {
613620 // '=' anywhere but the start makes this an invalid environment variable name.
614621 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
617624 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 {
1212 // containsUnempty
1313 {
1414 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")));
1518 if (builtin.os.tag == .windows) {
1619 try std.testing.expect(try environ.containsUnempty(allocator, "foo"));
1720 }
1821 try std.testing.expect(try environ.containsUnempty(allocator, "EQUALS"));
22 try std.testing.expect(!(try environ.containsUnempty(allocator, "EQUALS=ABC")));
1923 try std.testing.expect(try environ.containsUnempty(allocator, "КИРиллИЦА"));
2024 if (builtin.os.tag == .windows) {
2125 try std.testing.expect(try environ.containsUnempty(allocator, "кирИЛЛица"));
......@@ -31,10 +35,14 @@ pub fn main(init: std.process.Init) !void {
3135 // containsUnemptyConstant
3236 {
3337 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"));
3441 if (builtin.os.tag == .windows) {
3542 try std.testing.expect(environ.containsUnemptyConstant("foo"));
3643 }
3744 try std.testing.expect(environ.containsUnemptyConstant("EQUALS"));
45 try std.testing.expect(!environ.containsUnemptyConstant("EQUALS=ABC"));
3846 try std.testing.expect(environ.containsUnemptyConstant("КИРиллИЦА"));
3947 if (builtin.os.tag == .windows) {
4048 try std.testing.expect(environ.containsUnemptyConstant("кирИЛЛица"));
......@@ -50,10 +58,14 @@ pub fn main(init: std.process.Init) !void {
5058 // contains
5159 {
5260 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")));
5364 if (builtin.os.tag == .windows) {
5465 try std.testing.expect(try environ.contains(allocator, "foo"));
5566 }
5667 try std.testing.expect(try environ.contains(allocator, "EQUALS"));
68 try std.testing.expect(!(try environ.contains(allocator, "EQUALS=ABC")));
5769 try std.testing.expect(try environ.contains(allocator, "КИРиллИЦА"));
5870 if (builtin.os.tag == .windows) {
5971 try std.testing.expect(try environ.contains(allocator, "кирИЛЛица"));
......@@ -69,10 +81,14 @@ pub fn main(init: std.process.Init) !void {
6981 // containsConstant
7082 {
7183 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"));
7287 if (builtin.os.tag == .windows) {
7388 try std.testing.expect(environ.containsConstant("foo"));
7489 }
7590 try std.testing.expect(environ.containsConstant("EQUALS"));
91 try std.testing.expect(!environ.containsConstant("EQUALS=ABC"));
7692 try std.testing.expect(environ.containsConstant("КИРиллИЦА"));
7793 if (builtin.os.tag == .windows) {
7894 try std.testing.expect(environ.containsConstant("кирИЛЛица"));
......@@ -88,10 +104,14 @@ pub fn main(init: std.process.Init) !void {
88104 // getAlloc
89105 {
90106 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"));
91110 if (builtin.os.tag == .windows) {
92111 try std.testing.expectEqualSlices(u8, "123", try environ.getAlloc(arena, "foo"));
93112 }
94113 try std.testing.expectEqualSlices(u8, "ABC=123", try environ.getAlloc(arena, "EQUALS"));
114 try std.testing.expectError(error.EnvironmentVariableMissing, environ.getAlloc(arena, "EQUALS=ABC"));
95115 try std.testing.expectEqualSlices(u8, "non-ascii አማርኛ \u{10FFFF}", try environ.getAlloc(arena, "КИРиллИЦА"));
96116 if (builtin.os.tag == .windows) {
97117 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 {
110130 defer environ_map.deinit();
111131
112132 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"));
113135 if (builtin.os.tag == .windows) {
114136 try std.testing.expectEqualSlices(u8, "123", environ_map.get("foo").?);
115137 }
116138 try std.testing.expectEqualSlices(u8, "ABC=123", environ_map.get("EQUALS").?);
139 try std.testing.expectEqual(null, environ_map.get("EQUALS=ABC"));
117140 try std.testing.expectEqualSlices(u8, "non-ascii አማርኛ \u{10FFFF}", environ_map.get("КИРиллИЦА").?);
118141 if (builtin.os.tag == .windows) {
119142 try std.testing.expectEqualSlices(u8, "non-ascii አማርኛ \u{10FFFF}", environ_map.get("кирИЛЛица").?);