mirror of
https://codeberg.org/ziglang/zig.git
synced 2026-05-01 15:31:25 +03:00
bcb5218a2b
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).
996 lines
36 KiB
Zig
996 lines
36 KiB
Zig
const Environ = @This();
|
|
|
|
const builtin = @import("builtin");
|
|
const native_os = builtin.os.tag;
|
|
|
|
const std = @import("../std.zig");
|
|
const Allocator = mem.Allocator;
|
|
const assert = std.debug.assert;
|
|
const testing = std.testing;
|
|
const unicode = std.unicode;
|
|
const posix = std.posix;
|
|
const mem = std.mem;
|
|
|
|
/// Unmodified, unprocessed data provided by the operating system.
|
|
block: Block,
|
|
|
|
pub const empty: Environ = .{ .block = .empty };
|
|
|
|
/// On WASI without libc, this is `void` because the environment has to be
|
|
/// queried and heap-allocated at runtime.
|
|
///
|
|
/// On Windows, the memory pointed at by the PEB changes when the environment
|
|
/// is modified, so a long-lived pointer cannot be used. Therefore, on this
|
|
/// operating system `void` is also used.
|
|
pub const Block = switch (native_os) {
|
|
.windows => GlobalBlock,
|
|
.wasi => switch (builtin.link_libc) {
|
|
false => GlobalBlock,
|
|
true => PosixBlock,
|
|
},
|
|
.freestanding, .other => GlobalBlock,
|
|
else => PosixBlock,
|
|
};
|
|
|
|
pub const GlobalBlock = struct {
|
|
use_global: bool,
|
|
|
|
pub const empty: GlobalBlock = .{ .use_global = false };
|
|
pub const global: GlobalBlock = .{ .use_global = true };
|
|
|
|
pub fn deinit(_: GlobalBlock, _: Allocator) void {}
|
|
};
|
|
|
|
pub const PosixBlock = struct {
|
|
slice: [:null]const ?[*:0]const u8,
|
|
|
|
pub const empty: PosixBlock = .{ .slice = &.{} };
|
|
|
|
pub fn deinit(block: PosixBlock, gpa: Allocator) void {
|
|
for (block.slice) |entry| gpa.free(mem.span(entry.?));
|
|
gpa.free(block.slice);
|
|
}
|
|
|
|
pub const View = struct {
|
|
slice: []const [*:0]const u8,
|
|
|
|
pub fn isEmpty(v: View) bool {
|
|
return v.slice.len == 0;
|
|
}
|
|
};
|
|
pub fn view(block: PosixBlock) View {
|
|
return .{ .slice = @ptrCast(block.slice) };
|
|
}
|
|
};
|
|
|
|
pub const WindowsBlock = struct {
|
|
slice: [:0]const u16,
|
|
|
|
pub const empty: WindowsBlock = .{ .slice = &.{0} };
|
|
|
|
pub fn deinit(block: WindowsBlock, gpa: Allocator) void {
|
|
gpa.free(block.slice);
|
|
}
|
|
|
|
pub const View = struct {
|
|
ptr: [*:0]const u16,
|
|
|
|
pub fn isEmpty(v: View) bool {
|
|
return v.ptr[0] == 0;
|
|
}
|
|
};
|
|
pub fn view(block: WindowsBlock) View {
|
|
return .{ .ptr = block.slice.ptr };
|
|
}
|
|
};
|
|
|
|
pub const Map = struct {
|
|
array_hash_map: ArrayHashMap,
|
|
allocator: Allocator,
|
|
|
|
const ArrayHashMap = std.ArrayHashMapUnmanaged([]const u8, []const u8, EnvNameHashContext, false);
|
|
|
|
pub const Size = usize;
|
|
|
|
pub const EnvNameHashContext = struct {
|
|
pub fn hash(self: @This(), s: []const u8) u32 {
|
|
_ = self;
|
|
switch (native_os) {
|
|
else => return std.array_hash_map.hashString(s),
|
|
.windows => {
|
|
var h = std.hash.Wyhash.init(0);
|
|
var it = unicode.Wtf8View.initUnchecked(s).iterator();
|
|
while (it.nextCodepoint()) |cp| {
|
|
const cp_upper = if (std.math.cast(u16, cp)) |wtf16|
|
|
std.os.windows.toUpperWtf16(wtf16)
|
|
else
|
|
cp;
|
|
h.update(&[_]u8{
|
|
@truncate(cp_upper >> 0),
|
|
@truncate(cp_upper >> 8),
|
|
@truncate(cp_upper >> 16),
|
|
});
|
|
}
|
|
return @truncate(h.final());
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn eql(self: @This(), a: []const u8, b: []const u8, b_index: usize) bool {
|
|
_ = self;
|
|
_ = b_index;
|
|
return eqlKeys(a, b);
|
|
}
|
|
};
|
|
fn eqlKeys(a: []const u8, b: []const u8) bool {
|
|
return switch (native_os) {
|
|
else => std.array_hash_map.eqlString(a, b),
|
|
.windows => std.os.windows.eqlIgnoreCaseWtf8(a, b),
|
|
};
|
|
}
|
|
|
|
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;
|
|
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.
|
|
pub fn init(allocator: Allocator) Map {
|
|
return .{ .array_hash_map = .empty, .allocator = allocator };
|
|
}
|
|
|
|
/// Free the backing storage of the map, as well as all
|
|
/// of the stored keys and values.
|
|
pub fn deinit(self: *Map) void {
|
|
const gpa = self.allocator;
|
|
for (self.keys()) |key| gpa.free(key);
|
|
for (self.values()) |value| gpa.free(value);
|
|
self.array_hash_map.deinit(gpa);
|
|
self.* = undefined;
|
|
}
|
|
|
|
pub fn keys(map: *const Map) [][]const u8 {
|
|
return map.array_hash_map.keys();
|
|
}
|
|
|
|
pub fn values(map: *const Map) [][]const u8 {
|
|
return map.array_hash_map.values();
|
|
}
|
|
|
|
pub fn putPosixBlock(map: *Map, view: PosixBlock.View) Allocator.Error!void {
|
|
for (view.slice) |entry| {
|
|
var entry_i: usize = 0;
|
|
while (entry[entry_i] != 0 and entry[entry_i] != '=') : (entry_i += 1) {}
|
|
const key = entry[0..entry_i];
|
|
|
|
var end_i: usize = entry_i;
|
|
while (entry[end_i] != 0) : (end_i += 1) {}
|
|
const value = entry[entry_i + 1 .. end_i];
|
|
|
|
try map.put(key, value);
|
|
}
|
|
}
|
|
|
|
pub fn putWindowsBlock(map: *Map, view: WindowsBlock.View) Allocator.Error!void {
|
|
var i: usize = 0;
|
|
while (view.ptr[i] != 0) {
|
|
const key_start = i;
|
|
|
|
// There are some special environment variables that start with =,
|
|
// so we need a special case to not treat = as a key/value separator
|
|
// if it's the first character.
|
|
// https://devblogs.microsoft.com/oldnewthing/20100506-00/?p=14133
|
|
if (view.ptr[key_start] == '=') i += 1;
|
|
|
|
while (view.ptr[i] != 0 and view.ptr[i] != '=') : (i += 1) {}
|
|
const key_w = view.ptr[key_start..i];
|
|
const key = try unicode.wtf16LeToWtf8Alloc(map.allocator, key_w);
|
|
errdefer map.allocator.free(key);
|
|
|
|
if (view.ptr[i] == '=') i += 1;
|
|
|
|
const value_start = i;
|
|
while (view.ptr[i] != 0) : (i += 1) {}
|
|
const value_w = view.ptr[value_start..i];
|
|
const value = try unicode.wtf16LeToWtf8Alloc(map.allocator, value_w);
|
|
errdefer map.allocator.free(value);
|
|
|
|
i += 1; // skip over null byte
|
|
|
|
try map.putMove(key, value);
|
|
}
|
|
}
|
|
|
|
/// 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.
|
|
///
|
|
/// 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(validateKeyForPut(key));
|
|
const gpa = self.allocator;
|
|
const get_or_put = try self.array_hash_map.getOrPut(gpa, key);
|
|
if (get_or_put.found_existing) {
|
|
gpa.free(get_or_put.key_ptr.*);
|
|
gpa.free(get_or_put.value_ptr.*);
|
|
get_or_put.key_ptr.* = key;
|
|
}
|
|
get_or_put.value_ptr.* = value;
|
|
}
|
|
|
|
/// `key` and `value` are copied into the Map.
|
|
///
|
|
/// 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(validateKeyForPut(key));
|
|
const gpa = self.allocator;
|
|
const value_copy = try gpa.dupe(u8, value);
|
|
errdefer gpa.free(value_copy);
|
|
const get_or_put = try self.array_hash_map.getOrPut(gpa, key);
|
|
errdefer {
|
|
if (!get_or_put.found_existing) assert(self.array_hash_map.pop() != null);
|
|
}
|
|
if (get_or_put.found_existing) {
|
|
gpa.free(get_or_put.value_ptr.*);
|
|
} else {
|
|
get_or_put.key_ptr.* = try gpa.dupe(u8, key);
|
|
}
|
|
get_or_put.value_ptr.* = value_copy;
|
|
}
|
|
|
|
/// Find the address of the value associated with a key.
|
|
/// The returned pointer is invalidated if the map resizes.
|
|
/// 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(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, asserts that `key` is valid [WTF-8](https://wtf-8.codeberg.page/).
|
|
pub fn get(self: Map, key: []const u8) ?[]const u8 {
|
|
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(validateKeyForFetch(key));
|
|
return m.array_hash_map.contains(key);
|
|
}
|
|
|
|
/// If there is an entry with a matching key, it is deleted from the hash
|
|
/// map. The entry is removed from the underlying array by swapping it with
|
|
/// the last element.
|
|
///
|
|
/// Returns true if an entry was removed, false otherwise.
|
|
///
|
|
/// This invalidates the value returned by get() for this key.
|
|
/// On Windows, asserts that `key` is valid [WTF-8](https://wtf-8.codeberg.page/).
|
|
pub fn swapRemove(self: *Map, key: []const u8) bool {
|
|
assert(validateKeyForFetch(key));
|
|
const kv = self.array_hash_map.fetchSwapRemove(key) orelse return false;
|
|
const gpa = self.allocator;
|
|
gpa.free(kv.key);
|
|
gpa.free(kv.value);
|
|
return true;
|
|
}
|
|
|
|
/// If there is an entry with a matching key, it is deleted from the map.
|
|
/// The entry is removed from the underlying array by shifting all elements
|
|
/// forward, thereby maintaining the current ordering.
|
|
///
|
|
/// Returns true if an entry was removed, false otherwise.
|
|
///
|
|
/// This invalidates the value returned by get() for this key.
|
|
/// On Windows, asserts that `key` is valid [WTF-8](https://wtf-8.codeberg.page/).
|
|
pub fn orderedRemove(self: *Map, key: []const u8) bool {
|
|
assert(validateKeyForFetch(key));
|
|
const kv = self.array_hash_map.fetchOrderedRemove(key) orelse return false;
|
|
const gpa = self.allocator;
|
|
gpa.free(kv.key);
|
|
gpa.free(kv.value);
|
|
return true;
|
|
}
|
|
|
|
/// Returns the number of KV pairs stored in the map.
|
|
pub fn count(self: Map) Size {
|
|
return self.array_hash_map.count();
|
|
}
|
|
|
|
/// Returns an iterator over entries in the map.
|
|
pub fn iterator(self: *const Map) ArrayHashMap.Iterator {
|
|
return self.array_hash_map.iterator();
|
|
}
|
|
|
|
/// Returns a full copy of `em` allocated with `gpa`, which is not necessarily
|
|
/// the same allocator used to allocate `em`.
|
|
pub fn clone(m: *const Map, gpa: Allocator) Allocator.Error!Map {
|
|
// Since we need to dupe the keys and values, the only way for error handling to not be a
|
|
// nightmare is to add keys to an empty map one-by-one. This could be avoided if this
|
|
// abstraction were a bit less... OOP-esque.
|
|
var new: Map = .init(gpa);
|
|
errdefer new.deinit();
|
|
try new.array_hash_map.ensureUnusedCapacity(gpa, m.array_hash_map.count());
|
|
for (m.array_hash_map.keys(), m.array_hash_map.values()) |key, value| {
|
|
try new.put(key, value);
|
|
}
|
|
return new;
|
|
}
|
|
|
|
/// Creates a null-delimited environment variable block in the format
|
|
/// expected by POSIX, from a hash map plus options.
|
|
pub fn createPosixBlock(
|
|
map: *const Map,
|
|
gpa: Allocator,
|
|
options: CreatePosixBlockOptions,
|
|
) Allocator.Error!PosixBlock {
|
|
const ZigProgressAction = enum { nothing, edit, delete, add };
|
|
const zig_progress_action: ZigProgressAction = action: {
|
|
const fd = options.zig_progress_fd orelse break :action .nothing;
|
|
const exists = map.contains("ZIG_PROGRESS");
|
|
if (fd >= 0) {
|
|
break :action if (exists) .edit else .add;
|
|
} else {
|
|
if (exists) break :action .delete;
|
|
}
|
|
break :action .nothing;
|
|
};
|
|
|
|
const envp = try gpa.allocSentinel(?[*:0]u8, len: {
|
|
var len: usize = map.count();
|
|
switch (zig_progress_action) {
|
|
.add => len += 1,
|
|
.delete => len -= 1,
|
|
.nothing, .edit => {},
|
|
}
|
|
break :len len;
|
|
}, null);
|
|
var envp_len: usize = 0;
|
|
errdefer {
|
|
envp[envp_len] = null;
|
|
PosixBlock.deinit(.{ .slice = envp[0..envp_len :null] }, gpa);
|
|
}
|
|
|
|
if (zig_progress_action == .add) {
|
|
envp[envp_len] = try std.fmt.allocPrintSentinel(gpa, "ZIG_PROGRESS={d}", .{options.zig_progress_fd.?}, 0);
|
|
envp_len += 1;
|
|
}
|
|
|
|
for (map.keys(), map.values()) |key, value| {
|
|
if (mem.eql(u8, key, "ZIG_PROGRESS")) switch (zig_progress_action) {
|
|
.add => unreachable,
|
|
.delete => continue,
|
|
.edit => {
|
|
envp[envp_len] = try std.fmt.allocPrintSentinel(gpa, "{s}={d}", .{
|
|
key, options.zig_progress_fd.?,
|
|
}, 0);
|
|
envp_len += 1;
|
|
continue;
|
|
},
|
|
.nothing => {},
|
|
};
|
|
|
|
envp[envp_len] = try std.fmt.allocPrintSentinel(gpa, "{s}={s}", .{ key, value }, 0);
|
|
envp_len += 1;
|
|
}
|
|
|
|
assert(envp_len == envp.len);
|
|
return .{ .slice = envp };
|
|
}
|
|
|
|
/// Caller owns result.
|
|
pub fn createWindowsBlock(
|
|
map: *const Map,
|
|
gpa: Allocator,
|
|
options: CreateWindowsBlockOptions,
|
|
) error{ OutOfMemory, InvalidWtf8 }!WindowsBlock {
|
|
// count bytes needed
|
|
const max_chars_needed = max_chars_needed: {
|
|
var max_chars_needed: usize = "\x00".len;
|
|
if (options.zig_progress_handle) |handle| if (handle != std.os.windows.INVALID_HANDLE_VALUE) {
|
|
max_chars_needed += std.fmt.count("ZIG_PROGRESS={d}\x00", .{@intFromPtr(handle)});
|
|
};
|
|
for (map.keys(), map.values()) |key, value| {
|
|
if (options.zig_progress_handle != null and eqlKeys(key, "ZIG_PROGRESS")) continue;
|
|
max_chars_needed += key.len + "=".len + value.len + "\x00".len;
|
|
}
|
|
break :max_chars_needed @max("\x00\x00".len, max_chars_needed);
|
|
};
|
|
const block = try gpa.alloc(u16, max_chars_needed);
|
|
errdefer gpa.free(block);
|
|
|
|
var i: usize = 0;
|
|
if (options.zig_progress_handle) |handle| if (handle != std.os.windows.INVALID_HANDLE_VALUE) {
|
|
@memcpy(
|
|
block[i..][0.."ZIG_PROGRESS=".len],
|
|
&[_]u16{ 'Z', 'I', 'G', '_', 'P', 'R', 'O', 'G', 'R', 'E', 'S', 'S', '=' },
|
|
);
|
|
i += "ZIG_PROGRESS=".len;
|
|
var value_buf: [std.fmt.count("{d}", .{std.math.maxInt(usize)})]u8 = undefined;
|
|
const value = std.fmt.bufPrint(&value_buf, "{d}", .{@intFromPtr(handle)}) catch unreachable;
|
|
for (block[i..][0..value.len], value) |*r, v| r.* = v;
|
|
i += value.len;
|
|
block[i] = 0;
|
|
i += 1;
|
|
};
|
|
for (map.keys(), map.values()) |key, value| {
|
|
if (options.zig_progress_handle != null and eqlKeys(key, "ZIG_PROGRESS")) continue;
|
|
i += try unicode.wtf8ToWtf16Le(block[i..], key);
|
|
block[i] = '=';
|
|
i += 1;
|
|
i += try unicode.wtf8ToWtf16Le(block[i..], value);
|
|
block[i] = 0;
|
|
i += 1;
|
|
}
|
|
// An empty environment is a special case that requires a redundant
|
|
// NUL terminator. CreateProcess will read the second code unit even
|
|
// though theoretically the first should be enough to recognize that the
|
|
// environment is empty (see https://nullprogram.com/blog/2023/08/23/)
|
|
for (0..2) |_| {
|
|
block[i] = 0;
|
|
i += 1;
|
|
if (i >= 2) break;
|
|
} else unreachable;
|
|
const reallocated = try gpa.realloc(block, i);
|
|
return .{ .slice = reallocated[0 .. i - 1 :0] };
|
|
}
|
|
};
|
|
|
|
pub const CreateMapError = error{
|
|
OutOfMemory,
|
|
/// WASI-only. `environ_sizes_get` or `environ_get` failed for an
|
|
/// unanticipated, undocumented reason.
|
|
Unexpected,
|
|
};
|
|
|
|
/// Allocates a `Map` and copies environment block into it.
|
|
pub fn createMap(env: Environ, allocator: Allocator) CreateMapError!Map {
|
|
var map = Map.init(allocator);
|
|
errdefer map.deinit();
|
|
if (native_os == .windows) empty: {
|
|
if (!env.block.use_global) break :empty;
|
|
|
|
const peb = std.os.windows.peb();
|
|
assert(std.os.windows.ntdll.RtlEnterCriticalSection(peb.FastPebLock) == .SUCCESS);
|
|
defer assert(std.os.windows.ntdll.RtlLeaveCriticalSection(peb.FastPebLock) == .SUCCESS);
|
|
try map.putWindowsBlock(.{ .ptr = peb.ProcessParameters.Environment });
|
|
} else if (native_os == .wasi and !builtin.link_libc) empty: {
|
|
if (!env.block.use_global) break :empty;
|
|
|
|
var environ_count: usize = undefined;
|
|
var environ_buf_size: usize = undefined;
|
|
|
|
const environ_sizes_get_ret = std.os.wasi.environ_sizes_get(&environ_count, &environ_buf_size);
|
|
if (environ_sizes_get_ret != .SUCCESS) {
|
|
return posix.unexpectedErrno(environ_sizes_get_ret);
|
|
}
|
|
|
|
if (environ_count == 0) {
|
|
return map;
|
|
}
|
|
|
|
const environ = try allocator.alloc([*:0]u8, environ_count);
|
|
defer allocator.free(environ);
|
|
const environ_buf = try allocator.alloc(u8, environ_buf_size);
|
|
defer allocator.free(environ_buf);
|
|
|
|
const environ_get_ret = std.os.wasi.environ_get(environ.ptr, environ_buf.ptr);
|
|
if (environ_get_ret != .SUCCESS) {
|
|
return posix.unexpectedErrno(environ_get_ret);
|
|
}
|
|
|
|
try map.putPosixBlock(.{ .slice = environ });
|
|
} else try map.putPosixBlock(env.block.view());
|
|
return map;
|
|
}
|
|
|
|
pub const ContainsError = error{
|
|
OutOfMemory,
|
|
/// On Windows, environment variable keys provided by the user must be
|
|
/// valid [WTF-8](https://wtf-8.codeberg.page/). This error is unreachable
|
|
/// if the key is statically known to be valid.
|
|
InvalidWtf8,
|
|
/// WASI-only. `environ_sizes_get` or `environ_get` failed for an
|
|
/// unexpected reason.
|
|
Unexpected,
|
|
};
|
|
|
|
/// On Windows, if `key` is not valid [WTF-8](https://wtf-8.codeberg.page/),
|
|
/// then `error.InvalidWtf8` is returned.
|
|
///
|
|
/// See also:
|
|
/// * `createMap`
|
|
/// * `containsConstant`
|
|
/// * `containsUnempty`
|
|
pub fn contains(environ: Environ, gpa: Allocator, key: []const u8) ContainsError!bool {
|
|
if (native_os == .windows and !unicode.wtf8ValidateSlice(key)) return error.InvalidWtf8;
|
|
var map = try createMap(environ, gpa);
|
|
defer map.deinit();
|
|
return map.contains(key);
|
|
}
|
|
|
|
/// On Windows, if `key` is not valid [WTF-8](https://wtf-8.codeberg.page/),
|
|
/// then `error.InvalidWtf8` is returned.
|
|
///
|
|
/// See also:
|
|
/// * `createMap`
|
|
/// * `containsUnemptyConstant`
|
|
/// * `contains`
|
|
pub fn containsUnempty(environ: Environ, gpa: Allocator, key: []const u8) ContainsError!bool {
|
|
if (native_os == .windows and !unicode.wtf8ValidateSlice(key)) return error.InvalidWtf8;
|
|
var map = try createMap(environ, gpa);
|
|
defer map.deinit();
|
|
const value = map.get(key) orelse return false;
|
|
return value.len != 0;
|
|
}
|
|
|
|
/// This function is unavailable on WASI without libc due to the memory
|
|
/// allocation requirement.
|
|
///
|
|
/// On Windows, `key` must be valid [WTF-8](https://wtf-8.codeberg.page/),
|
|
///
|
|
/// See also:
|
|
/// * `contains`
|
|
/// * `containsUnemptyConstant`
|
|
/// * `createMap`
|
|
pub inline fn containsConstant(environ: Environ, comptime key: []const u8) bool {
|
|
if (native_os == .windows) {
|
|
const key_w = comptime unicode.wtf8ToWtf16LeStringLiteral(key);
|
|
return getWindows(environ, key_w) != null;
|
|
} else {
|
|
return getPosix(environ, key) != null;
|
|
}
|
|
}
|
|
|
|
/// This function is unavailable on WASI without libc due to the memory
|
|
/// allocation requirement.
|
|
///
|
|
/// On Windows, `key` must be valid [WTF-8](https://wtf-8.codeberg.page/),
|
|
///
|
|
/// See also:
|
|
/// * `containsUnempty`
|
|
/// * `containsConstant`
|
|
/// * `createMap`
|
|
pub inline fn containsUnemptyConstant(environ: Environ, comptime key: []const u8) bool {
|
|
if (native_os == .windows) {
|
|
const key_w = comptime unicode.wtf8ToWtf16LeStringLiteral(key);
|
|
const value = getWindows(environ, key_w) orelse return false;
|
|
return value.len != 0;
|
|
} else {
|
|
const value = getPosix(environ, key) orelse return false;
|
|
return value.len != 0;
|
|
}
|
|
}
|
|
|
|
/// This function is unavailable on WASI without libc due to the memory
|
|
/// allocation requirement.
|
|
///
|
|
/// See also:
|
|
/// * `getWindows`
|
|
/// * `createMap`
|
|
pub fn getPosix(environ: Environ, key: []const u8) ?[:0]const u8 {
|
|
if (mem.findScalar(u8, key, '=') != null) return null;
|
|
for (environ.block.view().slice) |entry| {
|
|
var entry_i: usize = 0;
|
|
while (entry[entry_i] != 0) : (entry_i += 1) {
|
|
if (entry_i == key.len) break;
|
|
if (entry[entry_i] != key[entry_i]) break;
|
|
}
|
|
if ((entry_i != key.len) or (entry[entry_i] != '=')) continue;
|
|
|
|
return mem.sliceTo(entry + entry_i + 1, 0);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Windows-only. Get an environment variable with a null-terminated, WTF-16
|
|
/// encoded name.
|
|
///
|
|
/// This function performs a Unicode-aware case-insensitive lookup using
|
|
/// RtlEqualUnicodeString.
|
|
///
|
|
/// See also:
|
|
/// * `createMap`
|
|
/// * `containsConstant`
|
|
/// * `contains`
|
|
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);
|
|
if (key_slice.len == 0 or mem.findScalar(u16, key_slice[1..], '=') != null) return null;
|
|
|
|
if (!environ.block.use_global) return null;
|
|
|
|
const peb = std.os.windows.peb();
|
|
assert(std.os.windows.ntdll.RtlEnterCriticalSection(peb.FastPebLock) == .SUCCESS);
|
|
defer assert(std.os.windows.ntdll.RtlLeaveCriticalSection(peb.FastPebLock) == .SUCCESS);
|
|
const ptr = peb.ProcessParameters.Environment;
|
|
|
|
var i: usize = 0;
|
|
while (ptr[i] != 0) {
|
|
const key_value = mem.sliceTo(ptr[i..], 0);
|
|
|
|
// There are some special environment variables that start with =,
|
|
// so we need a special case to not treat = as a key/value separator
|
|
// if it's the first character.
|
|
// https://devblogs.microsoft.com/oldnewthing/20100506-00/?p=14133
|
|
const equal_index = mem.findScalarPos(u16, key_value, 1, '=') orelse {
|
|
// This is enforced by CreateProcess.
|
|
// If violated, CreateProcess will fail with INVALID_PARAMETER.
|
|
unreachable; // must contain a =
|
|
};
|
|
|
|
const this_key = key_value[0..equal_index];
|
|
if (std.os.windows.eqlIgnoreCaseWtf16(key_slice, this_key)) {
|
|
return key_value[equal_index + 1 ..];
|
|
}
|
|
|
|
// skip past the NUL terminator
|
|
i += key_value.len + 1;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
pub const GetAllocError = error{
|
|
OutOfMemory,
|
|
EnvironmentVariableMissing,
|
|
/// On Windows, environment variable keys provided by the user must be
|
|
/// valid [WTF-8](https://wtf-8.codeberg.page/). This error is unreachable
|
|
/// if the key is statically known to be valid.
|
|
InvalidWtf8,
|
|
};
|
|
|
|
/// Caller owns returned memory.
|
|
///
|
|
/// On Windows:
|
|
/// * If `key` is not valid [WTF-8](https://wtf-8.codeberg.page/), then
|
|
/// `error.InvalidWtf8` is returned.
|
|
/// * The returned value is encoded as [WTF-8](https://wtf-8.codeberg.page/).
|
|
///
|
|
/// On other platforms, the value is an opaque sequence of bytes with no
|
|
/// particular encoding.
|
|
///
|
|
/// See also:
|
|
/// * `createMap`
|
|
pub fn getAlloc(environ: Environ, gpa: Allocator, key: []const u8) GetAllocError![]u8 {
|
|
if (native_os == .windows and !unicode.wtf8ValidateSlice(key)) return error.InvalidWtf8;
|
|
var map = createMap(environ, gpa) catch return error.OutOfMemory;
|
|
defer map.deinit();
|
|
const val = map.get(key) orelse return error.EnvironmentVariableMissing;
|
|
return gpa.dupe(u8, val);
|
|
}
|
|
|
|
pub const CreatePosixBlockOptions = struct {
|
|
/// `null` means to leave the `ZIG_PROGRESS` environment variable unmodified.
|
|
/// If non-null, negative means to remove the environment variable, and >= 0
|
|
/// means to provide it with the given integer.
|
|
zig_progress_fd: ?i32 = null,
|
|
};
|
|
|
|
/// Creates a null-delimited environment variable block in the format expected
|
|
/// by POSIX, from a different one.
|
|
pub fn createPosixBlock(
|
|
existing: Environ,
|
|
gpa: Allocator,
|
|
options: CreatePosixBlockOptions,
|
|
) Allocator.Error!PosixBlock {
|
|
const contains_zig_progress = for (existing.block.view().slice) |entry| {
|
|
if (mem.eql(u8, mem.sliceTo(entry, '='), "ZIG_PROGRESS")) break true;
|
|
} else false;
|
|
|
|
const ZigProgressAction = enum { nothing, edit, delete, add };
|
|
const zig_progress_action: ZigProgressAction = action: {
|
|
const fd = options.zig_progress_fd orelse break :action .nothing;
|
|
if (fd >= 0) {
|
|
break :action if (contains_zig_progress) .edit else .add;
|
|
} else {
|
|
if (contains_zig_progress) break :action .delete;
|
|
}
|
|
break :action .nothing;
|
|
};
|
|
|
|
const envp = try gpa.allocSentinel(?[*:0]u8, len: {
|
|
var len: usize = existing.block.slice.len;
|
|
switch (zig_progress_action) {
|
|
.add => len += 1,
|
|
.delete => len -= 1,
|
|
.nothing, .edit => {},
|
|
}
|
|
break :len len;
|
|
}, null);
|
|
var envp_len: usize = 0;
|
|
errdefer {
|
|
envp[envp_len] = null;
|
|
PosixBlock.deinit(.{ .slice = envp[0..envp_len :null] }, gpa);
|
|
}
|
|
if (zig_progress_action == .add) {
|
|
envp[envp_len] = try std.fmt.allocPrintSentinel(gpa, "ZIG_PROGRESS={d}", .{options.zig_progress_fd.?}, 0);
|
|
envp_len += 1;
|
|
}
|
|
|
|
var existing_index: usize = 0;
|
|
while (existing.block.slice[existing_index]) |entry| : (existing_index += 1) {
|
|
if (mem.eql(u8, mem.sliceTo(entry, '='), "ZIG_PROGRESS")) switch (zig_progress_action) {
|
|
.add => unreachable,
|
|
.delete => continue,
|
|
.edit => {
|
|
envp[envp_len] = try std.fmt.allocPrintSentinel(gpa, "ZIG_PROGRESS={d}", .{options.zig_progress_fd.?}, 0);
|
|
envp_len += 1;
|
|
continue;
|
|
},
|
|
.nothing => {},
|
|
};
|
|
envp[envp_len] = try gpa.dupeZ(u8, mem.span(entry));
|
|
envp_len += 1;
|
|
}
|
|
|
|
assert(envp_len == envp.len);
|
|
return .{ .slice = envp };
|
|
}
|
|
|
|
pub const CreateWindowsBlockOptions = struct {
|
|
/// `null` means to leave the `ZIG_PROGRESS` environment variable unmodified.
|
|
/// If non-null, `std.os.windows.INVALID_HANDLE_VALUE` means to remove the
|
|
/// environment variable, otherwise provide it with the given handle as an integer.
|
|
zig_progress_handle: ?std.os.windows.HANDLE = null,
|
|
};
|
|
|
|
/// Creates a null-delimited environment variable block in the format expected
|
|
/// by POSIX, from a different one.
|
|
pub fn createWindowsBlock(
|
|
existing: Environ,
|
|
gpa: Allocator,
|
|
options: CreateWindowsBlockOptions,
|
|
) Allocator.Error!WindowsBlock {
|
|
if (!existing.block.use_global) return .{
|
|
.slice = try gpa.dupeSentinel(u16, WindowsBlock.empty.slice, 0),
|
|
};
|
|
const peb = std.os.windows.peb();
|
|
assert(std.os.windows.ntdll.RtlEnterCriticalSection(peb.FastPebLock) == .SUCCESS);
|
|
defer assert(std.os.windows.ntdll.RtlLeaveCriticalSection(peb.FastPebLock) == .SUCCESS);
|
|
const existing_block = peb.ProcessParameters.Environment;
|
|
var ranges: [2]struct { start: usize, end: usize } = undefined;
|
|
var ranges_len: usize = 0;
|
|
ranges[ranges_len].start = 0;
|
|
const zig_progress_key = [_]u16{ 'Z', 'I', 'G', '_', 'P', 'R', 'O', 'G', 'R', 'E', 'S', 'S', '=' };
|
|
const needed_len = needed_len: {
|
|
var needed_len: usize = "\x00".len;
|
|
if (options.zig_progress_handle) |handle| if (handle != std.os.windows.INVALID_HANDLE_VALUE) {
|
|
needed_len += std.fmt.count("ZIG_PROGRESS={d}\x00", .{@intFromPtr(handle)});
|
|
};
|
|
var i: usize = 0;
|
|
while (existing_block[i] != 0) {
|
|
const start = i;
|
|
const entry = mem.sliceTo(existing_block[start..], 0);
|
|
i += entry.len + "\x00".len;
|
|
if (options.zig_progress_handle != null and entry.len >= zig_progress_key.len and
|
|
std.os.windows.eqlIgnoreCaseWtf16(entry[0..zig_progress_key.len], &zig_progress_key))
|
|
{
|
|
ranges[ranges_len].end = start;
|
|
ranges_len += 1;
|
|
ranges[ranges_len].start = i;
|
|
} else needed_len += entry.len + "\x00".len;
|
|
}
|
|
ranges[ranges_len].end = i;
|
|
ranges_len += 1;
|
|
break :needed_len @max("\x00\x00".len, needed_len);
|
|
};
|
|
const block = try gpa.alloc(u16, needed_len);
|
|
errdefer gpa.free(block);
|
|
var i: usize = 0;
|
|
if (options.zig_progress_handle) |handle| if (handle != std.os.windows.INVALID_HANDLE_VALUE) {
|
|
@memcpy(block[i..][0..zig_progress_key.len], &zig_progress_key);
|
|
i += zig_progress_key.len;
|
|
var value_buf: [std.fmt.count("{d}", .{std.math.maxInt(usize)})]u8 = undefined;
|
|
const value = std.fmt.bufPrint(&value_buf, "{d}", .{@intFromPtr(handle)}) catch unreachable;
|
|
for (block[i..][0..value.len], value) |*r, v| r.* = v;
|
|
i += value.len;
|
|
block[i] = 0;
|
|
i += 1;
|
|
};
|
|
for (ranges[0..ranges_len]) |range| {
|
|
const range_len = range.end - range.start;
|
|
@memcpy(block[i..][0..range_len], existing_block[range.start..range.end]);
|
|
i += range_len;
|
|
}
|
|
// An empty environment is a special case that requires a redundant
|
|
// NUL terminator. CreateProcess will read the second code unit even
|
|
// though theoretically the first should be enough to recognize that the
|
|
// environment is empty (see https://nullprogram.com/blog/2023/08/23/)
|
|
for (0..2) |_| {
|
|
block[i] = 0;
|
|
i += 1;
|
|
if (i >= 2) break;
|
|
} else unreachable;
|
|
assert(i == block.len);
|
|
return .{ .slice = block[0 .. i - 1 :0] };
|
|
}
|
|
|
|
test "Map.createPosixBlock" {
|
|
const gpa = testing.allocator;
|
|
|
|
var envmap = Map.init(gpa);
|
|
defer envmap.deinit();
|
|
|
|
try envmap.put("HOME", "/home/ifreund");
|
|
try envmap.put("WAYLAND_DISPLAY", "wayland-1");
|
|
try envmap.put("DISPLAY", ":1");
|
|
try envmap.put("DEBUGINFOD_URLS", " ");
|
|
try envmap.put("XCURSOR_SIZE", "24");
|
|
|
|
const block = try envmap.createPosixBlock(gpa, .{});
|
|
defer block.deinit(gpa);
|
|
|
|
try testing.expectEqual(@as(usize, 5), block.slice.len);
|
|
|
|
for (&[_][]const u8{
|
|
"HOME=/home/ifreund",
|
|
"WAYLAND_DISPLAY=wayland-1",
|
|
"DISPLAY=:1",
|
|
"DEBUGINFOD_URLS= ",
|
|
"XCURSOR_SIZE=24",
|
|
}, block.slice) |expected, actual| try testing.expectEqualStrings(expected, mem.span(actual.?));
|
|
}
|
|
|
|
test Map {
|
|
const gpa = testing.allocator;
|
|
|
|
var env: Map = .init(gpa);
|
|
defer env.deinit();
|
|
|
|
try env.put("SOMETHING_NEW", "hello");
|
|
try testing.expectEqualStrings("hello", env.get("SOMETHING_NEW").?);
|
|
try testing.expectEqual(@as(Map.Size, 1), env.count());
|
|
|
|
// overwrite
|
|
try env.put("SOMETHING_NEW", "something");
|
|
try testing.expectEqualStrings("something", env.get("SOMETHING_NEW").?);
|
|
try testing.expectEqual(@as(Map.Size, 1), env.count());
|
|
|
|
// a new longer name to test the Windows-specific conversion buffer
|
|
try env.put("SOMETHING_NEW_AND_LONGER", "1");
|
|
try testing.expectEqualStrings("1", env.get("SOMETHING_NEW_AND_LONGER").?);
|
|
try testing.expectEqual(@as(Map.Size, 2), env.count());
|
|
|
|
// case insensitivity on Windows only
|
|
if (native_os == .windows) {
|
|
try testing.expectEqualStrings("1", env.get("something_New_aNd_LONGER").?);
|
|
} else {
|
|
try testing.expect(null == env.get("something_New_aNd_LONGER"));
|
|
}
|
|
|
|
var it = env.iterator();
|
|
var count: Map.Size = 0;
|
|
while (it.next()) |entry| {
|
|
const is_an_expected_name = mem.eql(u8, "SOMETHING_NEW", entry.key_ptr.*) or mem.eql(u8, "SOMETHING_NEW_AND_LONGER", entry.key_ptr.*);
|
|
try testing.expect(is_an_expected_name);
|
|
count += 1;
|
|
}
|
|
try testing.expectEqual(@as(Map.Size, 2), count);
|
|
|
|
try testing.expect(env.swapRemove("SOMETHING_NEW"));
|
|
try testing.expect(!env.swapRemove("SOMETHING_NEW"));
|
|
try testing.expect(env.get("SOMETHING_NEW") == null);
|
|
try testing.expect(!env.contains("SOMETHING_NEW"));
|
|
|
|
try testing.expectEqual(@as(Map.Size, 1), env.count());
|
|
|
|
if (native_os == .windows) {
|
|
// test Unicode case-insensitivity on Windows
|
|
try env.put("КИРиллИЦА", "something else");
|
|
try testing.expectEqualStrings("something else", env.get("кириллица").?);
|
|
|
|
// and WTF-8 that's not valid UTF-8
|
|
const wtf8_with_surrogate_pair = try unicode.wtf16LeToWtf8Alloc(gpa, &[_]u16{
|
|
mem.nativeToLittle(u16, 0xD83D), // unpaired high surrogate
|
|
});
|
|
defer gpa.free(wtf8_with_surrogate_pair);
|
|
|
|
try env.put(wtf8_with_surrogate_pair, wtf8_with_surrogate_pair);
|
|
try testing.expectEqualSlices(u8, wtf8_with_surrogate_pair, env.get(wtf8_with_surrogate_pair).?);
|
|
}
|
|
}
|
|
|
|
test "convert from Environ to Map and back again" {
|
|
if (native_os == .windows) return;
|
|
if (native_os == .wasi and !builtin.link_libc) return;
|
|
|
|
const gpa = testing.allocator;
|
|
|
|
var map: Map = .init(gpa);
|
|
defer map.deinit();
|
|
try map.put("FOO", "BAR");
|
|
try map.put("A", "");
|
|
|
|
const environ: Environ = .{ .block = try map.createPosixBlock(gpa, .{}) };
|
|
defer environ.block.deinit(gpa);
|
|
|
|
try testing.expectEqual(true, environ.contains(gpa, "FOO"));
|
|
try testing.expectEqual(false, environ.contains(gpa, "BAR"));
|
|
try testing.expectEqual(true, environ.contains(gpa, "A"));
|
|
try testing.expectEqual(true, environ.containsConstant("A"));
|
|
try testing.expectEqual(false, environ.containsUnempty(gpa, "A"));
|
|
try testing.expectEqual(false, environ.containsUnemptyConstant("A"));
|
|
try testing.expectEqual(false, environ.contains(gpa, "B"));
|
|
|
|
try testing.expectError(error.EnvironmentVariableMissing, environ.getAlloc(gpa, "BOGUS"));
|
|
{
|
|
const value = try environ.getAlloc(gpa, "FOO");
|
|
defer gpa.free(value);
|
|
try testing.expectEqualStrings("BAR", value);
|
|
}
|
|
|
|
var map2 = try environ.createMap(gpa);
|
|
defer map2.deinit();
|
|
|
|
try testing.expectEqualDeep(map.keys(), map2.keys());
|
|
try testing.expectEqualDeep(map.values(), map2.values());
|
|
}
|
|
|
|
test "Map.putPosixBlock" {
|
|
const gpa = testing.allocator;
|
|
|
|
var map: Map = .init(gpa);
|
|
defer map.deinit();
|
|
|
|
try map.put("FOO", "BAR");
|
|
try map.put("A", "");
|
|
try map.put("ZIG_PROGRESS", "unchanged");
|
|
|
|
const block = try map.createPosixBlock(gpa, .{});
|
|
defer block.deinit(gpa);
|
|
|
|
var map2: Map = .init(gpa);
|
|
defer map2.deinit();
|
|
try map2.putPosixBlock(block.view());
|
|
|
|
try testing.expectEqualDeep(&[_][]const u8{ "FOO", "A", "ZIG_PROGRESS" }, map2.keys());
|
|
try testing.expectEqualDeep(&[_][]const u8{ "BAR", "", "unchanged" }, map2.values());
|
|
}
|
|
|
|
test "Map.putWindowsBlock" {
|
|
if (native_os != .windows) return;
|
|
|
|
const gpa = testing.allocator;
|
|
|
|
var map: Map = .init(gpa);
|
|
defer map.deinit();
|
|
|
|
try map.put("FOO", "BAR");
|
|
try map.put("A", "");
|
|
try map.put("=B", "");
|
|
try map.put("ZIG_PROGRESS", "unchanged");
|
|
|
|
const block = try map.createWindowsBlock(gpa, .{});
|
|
defer block.deinit(gpa);
|
|
|
|
var map2: Map = .init(gpa);
|
|
defer map2.deinit();
|
|
try map2.putWindowsBlock(block.view());
|
|
|
|
try testing.expectEqualDeep(&[_][]const u8{ "FOO", "A", "=B", "ZIG_PROGRESS" }, map2.keys());
|
|
try testing.expectEqualDeep(&[_][]const u8{ "BAR", "", "", "unchanged" }, map2.values());
|
|
}
|