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).
This commit is contained in:
Ryan Liptak 2026-02-04 18:12:06 -08:00 committed by Andrew Kelley
parent fa3228ae42
commit bcb5218a2b
2 changed files with 55 additions and 25 deletions

View file

@ -129,25 +129,21 @@ pub const Map = struct {
};
}
pub fn validateKey(key: []const u8) bool {
pub fn validateKeyForPut(key: []const u8) bool {
switch (native_os) {
else => return key.len > 0 and mem.findAny(u8, key, &.{ 0, '=' }) == null,
.windows => {
if (!unicode.wtf8ValidateSlice(key)) return false;
var it = unicode.Wtf8View.initUnchecked(key).iterator();
switch (it.nextCodepoint() orelse return false) {
0 => return false,
else => {},
}
while (it.nextCodepoint()) |cp| switch (cp) {
0, '=' => return false,
else => {},
};
return true;
return key.len > 0 and key[0] != 0 and mem.findAnyPos(u8, key, 1, &.{ 0, '=' }) == null;
},
}
}
pub fn validateKeyForFetch(key: []const u8) bool {
if (native_os == .windows and !unicode.wtf8ValidateSlice(key)) return false;
return true;
}
/// Create a Map backed by a specific allocator.
/// That allocator will be used for both backing allocations
/// and string deduplication.
@ -220,9 +216,14 @@ pub const Map = struct {
/// Same as `put` but the key and value become owned by the Map rather
/// than being copied.
/// If `putMove` fails, the ownership of key and value does not transfer.
/// On Windows `key` must be a valid [WTF-8](https://wtf-8.codeberg.page/) string.
///
/// Asserts that `key` is valid:
/// - It cannot contain a NUL (`'\x00') byte.
/// - It must have a length > 0.
/// - It cannot contain `=`, except on Windows where only the first code point is allowed to be `=`.
/// - On Windows, it must be valid [WTF-8](https://wtf-8.codeberg.page/).
pub fn putMove(self: *Map, key: []u8, value: []u8) Allocator.Error!void {
assert(validateKey(key));
assert(validateKeyForPut(key));
const gpa = self.allocator;
const get_or_put = try self.array_hash_map.getOrPut(gpa, key);
if (get_or_put.found_existing) {
@ -234,9 +235,14 @@ pub const Map = struct {
}
/// `key` and `value` are copied into the Map.
/// On Windows `key` must be a valid [WTF-8](https://wtf-8.codeberg.page/) string.
///
/// Asserts that `key` is valid:
/// - It cannot contain a NUL (`'\x00') byte.
/// - It must have a length > 0.
/// - It cannot contain `=`, except on Windows where only the first code point is allowed to be `=`.
/// - On Windows, it must be valid [WTF-8](https://wtf-8.codeberg.page/).
pub fn put(self: *Map, key: []const u8, value: []const u8) Allocator.Error!void {
assert(validateKey(key));
assert(validateKeyForPut(key));
const gpa = self.allocator;
const value_copy = try gpa.dupe(u8, value);
errdefer gpa.free(value_copy);
@ -254,23 +260,24 @@ pub const Map = struct {
/// Find the address of the value associated with a key.
/// The returned pointer is invalidated if the map resizes.
/// On Windows `key` must be a valid [WTF-8](https://wtf-8.codeberg.page/) string.
/// On Windows, asserts that `key` is valid [WTF-8](https://wtf-8.codeberg.page/).
pub fn getPtr(self: Map, key: []const u8) ?*[]const u8 {
assert(validateKey(key));
assert(validateKeyForFetch(key));
return self.array_hash_map.getPtr(key);
}
/// Return the map's copy of the value associated with
/// a key. The returned string is invalidated if this
/// key is removed from the map.
/// On Windows `key` must be a valid [WTF-8](https://wtf-8.codeberg.page/) string.
/// On Windows, asserts that `key` is valid [WTF-8](https://wtf-8.codeberg.page/).
pub fn get(self: Map, key: []const u8) ?[]const u8 {
assert(validateKey(key));
assert(validateKeyForFetch(key));
return self.array_hash_map.get(key);
}
/// On Windows, asserts that `key` is valid [WTF-8](https://wtf-8.codeberg.page/).
pub fn contains(m: *const Map, key: []const u8) bool {
assert(validateKey(key));
assert(validateKeyForFetch(key));
return m.array_hash_map.contains(key);
}
@ -281,9 +288,9 @@ pub const Map = struct {
/// Returns true if an entry was removed, false otherwise.
///
/// This invalidates the value returned by get() for this key.
/// On Windows `key` must be a valid [WTF-8](https://wtf-8.codeberg.page/) string.
/// On Windows, asserts that `key` is valid [WTF-8](https://wtf-8.codeberg.page/).
pub fn swapRemove(self: *Map, key: []const u8) bool {
assert(validateKey(key));
assert(validateKeyForFetch(key));
const kv = self.array_hash_map.fetchSwapRemove(key) orelse return false;
const gpa = self.allocator;
gpa.free(kv.key);
@ -298,9 +305,9 @@ pub const Map = struct {
/// Returns true if an entry was removed, false otherwise.
///
/// This invalidates the value returned by get() for this key.
/// On Windows `key` must be a valid [WTF-8](https://wtf-8.codeberg.page/) string.
/// On Windows, asserts that `key` is valid [WTF-8](https://wtf-8.codeberg.page/).
pub fn orderedRemove(self: *Map, key: []const u8) bool {
assert(validateKey(key));
assert(validateKeyForFetch(key));
const kv = self.array_hash_map.fetchOrderedRemove(key) orelse return false;
const gpa = self.allocator;
gpa.free(kv.key);
@ -612,7 +619,7 @@ pub fn getPosix(environ: Environ, key: []const u8) ?[:0]const u8 {
pub fn getWindows(environ: Environ, key: [*:0]const u16) ?[:0]const u16 {
// '=' anywhere but the start makes this an invalid environment variable name.
const key_slice = mem.sliceTo(key, 0);
assert(key_slice.len > 0 and mem.findScalar(u16, key_slice[1..], '=') == null);
if (key_slice.len == 0 or mem.findScalar(u16, key_slice[1..], '=') != null) return null;
if (!environ.block.use_global) return null;

View file

@ -12,10 +12,14 @@ pub fn main(init: std.process.Init) !void {
// containsUnempty
{
try std.testing.expect(try environ.containsUnempty(allocator, "FOO"));
try std.testing.expect(!(try environ.containsUnempty(allocator, "FOO=")));
try std.testing.expect(!(try environ.containsUnempty(allocator, "FO")));
try std.testing.expect(!(try environ.containsUnempty(allocator, "FOOO")));
if (builtin.os.tag == .windows) {
try std.testing.expect(try environ.containsUnempty(allocator, "foo"));
}
try std.testing.expect(try environ.containsUnempty(allocator, "EQUALS"));
try std.testing.expect(!(try environ.containsUnempty(allocator, "EQUALS=ABC")));
try std.testing.expect(try environ.containsUnempty(allocator, "КИРиллИЦА"));
if (builtin.os.tag == .windows) {
try std.testing.expect(try environ.containsUnempty(allocator, "кирИЛЛица"));
@ -31,10 +35,14 @@ pub fn main(init: std.process.Init) !void {
// containsUnemptyConstant
{
try std.testing.expect(environ.containsUnemptyConstant("FOO"));
try std.testing.expect(!environ.containsUnemptyConstant("FOO="));
try std.testing.expect(!environ.containsUnemptyConstant("FO"));
try std.testing.expect(!environ.containsUnemptyConstant("FOOO"));
if (builtin.os.tag == .windows) {
try std.testing.expect(environ.containsUnemptyConstant("foo"));
}
try std.testing.expect(environ.containsUnemptyConstant("EQUALS"));
try std.testing.expect(!environ.containsUnemptyConstant("EQUALS=ABC"));
try std.testing.expect(environ.containsUnemptyConstant("КИРиллИЦА"));
if (builtin.os.tag == .windows) {
try std.testing.expect(environ.containsUnemptyConstant("кирИЛЛица"));
@ -50,10 +58,14 @@ pub fn main(init: std.process.Init) !void {
// contains
{
try std.testing.expect(try environ.contains(allocator, "FOO"));
try std.testing.expect(!(try environ.contains(allocator, "FOO=")));
try std.testing.expect(!(try environ.contains(allocator, "FO")));
try std.testing.expect(!(try environ.contains(allocator, "FOOO")));
if (builtin.os.tag == .windows) {
try std.testing.expect(try environ.contains(allocator, "foo"));
}
try std.testing.expect(try environ.contains(allocator, "EQUALS"));
try std.testing.expect(!(try environ.contains(allocator, "EQUALS=ABC")));
try std.testing.expect(try environ.contains(allocator, "КИРиллИЦА"));
if (builtin.os.tag == .windows) {
try std.testing.expect(try environ.contains(allocator, "кирИЛЛица"));
@ -69,10 +81,14 @@ pub fn main(init: std.process.Init) !void {
// containsConstant
{
try std.testing.expect(environ.containsConstant("FOO"));
try std.testing.expect(!environ.containsConstant("FOO="));
try std.testing.expect(!environ.containsConstant("FO"));
try std.testing.expect(!environ.containsConstant("FOOO"));
if (builtin.os.tag == .windows) {
try std.testing.expect(environ.containsConstant("foo"));
}
try std.testing.expect(environ.containsConstant("EQUALS"));
try std.testing.expect(!environ.containsConstant("EQUALS=ABC"));
try std.testing.expect(environ.containsConstant("КИРиллИЦА"));
if (builtin.os.tag == .windows) {
try std.testing.expect(environ.containsConstant("кирИЛЛица"));
@ -88,10 +104,14 @@ pub fn main(init: std.process.Init) !void {
// getAlloc
{
try std.testing.expectEqualSlices(u8, "123", try environ.getAlloc(arena, "FOO"));
try std.testing.expectError(error.EnvironmentVariableMissing, environ.getAlloc(arena, "FOO="));
try std.testing.expectError(error.EnvironmentVariableMissing, environ.getAlloc(arena, "FO"));
try std.testing.expectError(error.EnvironmentVariableMissing, environ.getAlloc(arena, "FOOO"));
if (builtin.os.tag == .windows) {
try std.testing.expectEqualSlices(u8, "123", try environ.getAlloc(arena, "foo"));
}
try std.testing.expectEqualSlices(u8, "ABC=123", try environ.getAlloc(arena, "EQUALS"));
try std.testing.expectError(error.EnvironmentVariableMissing, environ.getAlloc(arena, "EQUALS=ABC"));
try std.testing.expectEqualSlices(u8, "non-ascii አማርኛ \u{10FFFF}", try environ.getAlloc(arena, "КИРиллИЦА"));
if (builtin.os.tag == .windows) {
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 {
defer environ_map.deinit();
try std.testing.expectEqualSlices(u8, "123", environ_map.get("FOO").?);
try std.testing.expectEqual(null, environ_map.get("FO"));
try std.testing.expectEqual(null, environ_map.get("FOOO"));
if (builtin.os.tag == .windows) {
try std.testing.expectEqualSlices(u8, "123", environ_map.get("foo").?);
}
try std.testing.expectEqualSlices(u8, "ABC=123", environ_map.get("EQUALS").?);
try std.testing.expectEqual(null, environ_map.get("EQUALS=ABC"));
try std.testing.expectEqualSlices(u8, "non-ascii አማርኛ \u{10FFFF}", environ_map.get("КИРиллИЦА").?);
if (builtin.os.tag == .windows) {
try std.testing.expectEqualSlices(u8, "non-ascii አማርኛ \u{10FFFF}", environ_map.get("кирИЛЛица").?);