mirror of
https://codeberg.org/ziglang/zig.git
synced 2026-03-08 04:24:33 +01:00
1007 lines
36 KiB
Zig
1007 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 fn isEmpty(block: GlobalBlock) bool {
|
|
return !block.use_global;
|
|
}
|
|
};
|
|
|
|
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 fn isEmpty(block: PosixBlock) bool {
|
|
return block.slice.len == 0;
|
|
}
|
|
|
|
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 fn isEmpty(block: WindowsBlock) bool {
|
|
return block.slice[0] == 0;
|
|
}
|
|
|
|
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());
|
|
}
|