dsh-ops 0.0.0-stage → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +202 -0
- package/LICENSE +30 -0
- package/NOTICE +106 -0
- package/PROVENANCE.md +435 -0
- package/README.en.md +126 -0
- package/README.md +115 -2
- package/README.zh.md +116 -0
- package/bin/dsh-ops.mjs +1216 -0
- package/cordis.patch.yml +160 -0
- package/docs/manual-validation.md +53 -0
- package/docs/release-0.2.1.md +72 -0
- package/docs/schema-baseline.json +64 -0
- package/docs/schema-current.json +84 -0
- package/docs/schema-measurement.md +17 -0
- package/dsh-plugin.json +88 -0
- package/icon.svg +12 -0
- package/lib/binary.js +409 -0
- package/lib/config.js +198 -0
- package/lib/handshake.js +252 -0
- package/lib/index.js +108 -0
- package/lib/jobs.js +42 -0
- package/lib/policy.js +64 -0
- package/lib/presentation.js +63 -0
- package/lib/profile-install.js +61 -0
- package/lib/rust.js +194 -0
- package/lib/session-shells.js +78 -0
- package/lib/shells.js +998 -0
- package/lib/tools.js +657 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +114 -4
- package/vendor/fastctx/Cargo.lock +3210 -0
- package/vendor/fastctx/Cargo.toml +94 -0
- package/vendor/fastctx/FORK.md +119 -0
- package/vendor/fastctx/LICENSE-APACHE +201 -0
- package/vendor/fastctx/NOTICE +40 -0
- package/vendor/fastctx/README.md +439 -0
- package/vendor/fastctx/THIRD_PARTY_LICENSES.md +17 -0
- package/vendor/fastctx/THIRD_PARTY_LICENSES_RUST.md +7914 -0
- package/vendor/fastctx/UPSTREAM.md +49 -0
- package/vendor/fastctx/build.rs +413 -0
- package/vendor/fastctx/src/background_status.rs +403 -0
- package/vendor/fastctx/src/binary.rs +75 -0
- package/vendor/fastctx/src/bounded_sort.rs +500 -0
- package/vendor/fastctx/src/budget.rs +781 -0
- package/vendor/fastctx/src/cli/mod.rs +110 -0
- package/vendor/fastctx/src/context_guard.rs +289 -0
- package/vendor/fastctx/src/control/mod.rs +6 -0
- package/vendor/fastctx/src/control/paths.rs +49 -0
- package/vendor/fastctx/src/control/settings.rs +753 -0
- package/vendor/fastctx/src/control/transaction.rs +531 -0
- package/vendor/fastctx/src/edit/document.rs +535 -0
- package/vendor/fastctx/src/edit/locks.rs +371 -0
- package/vendor/fastctx/src/edit/mod.rs +213 -0
- package/vendor/fastctx/src/edit/private_storage/unix.rs +315 -0
- package/vendor/fastctx/src/edit/private_storage/windows.rs +793 -0
- package/vendor/fastctx/src/edit/private_storage.rs +234 -0
- package/vendor/fastctx/src/edit/replace.rs +1030 -0
- package/vendor/fastctx/src/edit_server.rs +53 -0
- package/vendor/fastctx/src/encoding/reference_v011.rs +587 -0
- package/vendor/fastctx/src/encoding/snapshot_pipeline.rs +1678 -0
- package/vendor/fastctx/src/encoding.rs +1118 -0
- package/vendor/fastctx/src/file_executor.rs +1151 -0
- package/vendor/fastctx/src/file_snapshot.rs +1491 -0
- package/vendor/fastctx/src/glob_filter.rs +98 -0
- package/vendor/fastctx/src/glob_tool.rs +653 -0
- package/vendor/fastctx/src/grep_sink.rs +1162 -0
- package/vendor/fastctx/src/grep_tool.rs +2449 -0
- package/vendor/fastctx/src/lib.rs +45 -0
- package/vendor/fastctx/src/main.rs +15 -0
- package/vendor/fastctx/src/model.rs +51 -0
- package/vendor/fastctx/src/model_guidance.rs +62 -0
- package/vendor/fastctx/src/operation.rs +356 -0
- package/vendor/fastctx/src/ordered_window.rs +1235 -0
- package/vendor/fastctx/src/os_environment.rs +414 -0
- package/vendor/fastctx/src/path_codec.rs +850 -0
- package/vendor/fastctx/src/paths.rs +244 -0
- package/vendor/fastctx/src/process_identity.rs +763 -0
- package/vendor/fastctx/src/process_policy.rs +74 -0
- package/vendor/fastctx/src/read_tool/batch.rs +496 -0
- package/vendor/fastctx/src/read_tool/hex_file.rs +141 -0
- package/vendor/fastctx/src/read_tool/image_file.rs +88 -0
- package/vendor/fastctx/src/read_tool/mod.rs +245 -0
- package/vendor/fastctx/src/read_tool/pdf.rs +470 -0
- package/vendor/fastctx/src/read_tool/pdf_disabled.rs +47 -0
- package/vendor/fastctx/src/read_tool/pdf_engine.rs +664 -0
- package/vendor/fastctx/src/read_tool/text_file.rs +351 -0
- package/vendor/fastctx/src/render_plan.rs +468 -0
- package/vendor/fastctx/src/runtime/activity.rs +159 -0
- package/vendor/fastctx/src/runtime/hosts.rs +99 -0
- package/vendor/fastctx/src/runtime/journal.rs +556 -0
- package/vendor/fastctx/src/runtime/local_ipc.rs +186 -0
- package/vendor/fastctx/src/runtime/mod.rs +746 -0
- package/vendor/fastctx/src/runtime/protocol.rs +296 -0
- package/vendor/fastctx/src/runtime/session.rs +536 -0
- package/vendor/fastctx/src/runtime/windows_process.rs +66 -0
- package/vendor/fastctx/src/search_parallelism.rs +106 -0
- package/vendor/fastctx/src/search_text.rs +227 -0
- package/vendor/fastctx/src/server.rs +359 -0
- package/vendor/fastctx/src/server_manifest.rs +468 -0
- package/vendor/fastctx/src/server_support.rs +826 -0
- package/vendor/fastctx/src/session.rs +629 -0
- package/vendor/fastctx/src/shell/apply_patch_hint.rs +41 -0
- package/vendor/fastctx/src/shell/bash.rs +263 -0
- package/vendor/fastctx/src/shell/buffer.rs +108 -0
- package/vendor/fastctx/src/shell/encoding.rs +403 -0
- package/vendor/fastctx/src/shell/foreground.rs +115 -0
- package/vendor/fastctx/src/shell/jobs/admission.rs +91 -0
- package/vendor/fastctx/src/shell/jobs/background.rs +146 -0
- package/vendor/fastctx/src/shell/jobs/host.rs +830 -0
- package/vendor/fastctx/src/shell/jobs/identity.rs +29 -0
- package/vendor/fastctx/src/shell/jobs/mod.rs +1513 -0
- package/vendor/fastctx/src/shell/jobs/model.rs +244 -0
- package/vendor/fastctx/src/shell/jobs/output_log.rs +1148 -0
- package/vendor/fastctx/src/shell/jobs/store.rs +1300 -0
- package/vendor/fastctx/src/shell/mod.rs +345 -0
- package/vendor/fastctx/src/shell/normalize.rs +389 -0
- package/vendor/fastctx/src/shell/output.rs +406 -0
- package/vendor/fastctx/src/shell/process.rs +493 -0
- package/vendor/fastctx/src/shell_server.rs +156 -0
- package/vendor/fastctx/src/skip_report.rs +83 -0
- package/vendor/fastctx/src/stdio_transport.rs +177 -0
- package/vendor/fastctx/src/tool_schema.rs +204 -0
- package/vendor/fastctx/src/traversal.rs +846 -0
- package/vendor/fastctx/third-party/pdfium-7763/LICENSE +9 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/abseil.txt +202 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/agg23.txt +14 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/fast_float.txt +27 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/freetype.txt +169 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/icu.txt +542 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/lcms.txt +27 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.ijg +260 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.md +135 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libopenjpeg.txt +32 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libpng.txt +134 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libtiff.txt +21 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/llvm-libc.txt +278 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/pdfium.txt +230 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/simdutf.txt +18 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/zlib.txt +29 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
//! Restores the machine's persisted environment for the commands the shell tools execute.
|
|
2
|
+
//!
|
|
3
|
+
//! A stdio MCP server does not receive the environment its user configured. Codex clears the
|
|
4
|
+
//! child environment and re-adds a fixed list of about twenty names, so variables like
|
|
5
|
+
//! `JAVA_HOME`, `GOPATH`, or `CUDA_PATH` never reach this process and therefore never reach a
|
|
6
|
+
//! command the model runs — while Codex's own shell tool, on the same machine, inherits all of
|
|
7
|
+
//! them. This module closes that asymmetry by reading the environment the operating system
|
|
8
|
+
//! persists for the user and placing it *underneath* whatever the host did provide.
|
|
9
|
+
//!
|
|
10
|
+
//! The result is used only for commands the user runs. FastCtx keeps resolving its own state
|
|
11
|
+
//! directories, endpoint identity, and output budgets from the unmodified host environment: a
|
|
12
|
+
//! machine that persists `HOME` must not silently relocate `~/.fastctx` on upgrade.
|
|
13
|
+
|
|
14
|
+
use crate::session::SessionEnvironment;
|
|
15
|
+
use std::ffi::{OsStr, OsString};
|
|
16
|
+
|
|
17
|
+
/// Name of the search-path variable rebuilt by union instead of replacement.
|
|
18
|
+
const SEARCH_PATH: &str = "PATH";
|
|
19
|
+
|
|
20
|
+
/// Opt-out for anyone who wants commands to see the host's exact environment and nothing else.
|
|
21
|
+
const INHERIT_ENV: &str = "FASTCTX_INHERIT_ENVIRONMENT";
|
|
22
|
+
|
|
23
|
+
#[cfg(windows)]
|
|
24
|
+
const PATH_LIST_SEPARATOR: char = ';';
|
|
25
|
+
#[cfg(not(windows))]
|
|
26
|
+
const PATH_LIST_SEPARATOR: char = ':';
|
|
27
|
+
|
|
28
|
+
/// Builds the environment a user command should see for one session.
|
|
29
|
+
///
|
|
30
|
+
/// Host-provided values always win: they are either live session state or literals the user wrote
|
|
31
|
+
/// into the host's own server configuration, and both are more specific than a persisted default.
|
|
32
|
+
/// `PATH` is the one exception — it is a union, because dropping either side loses real tools.
|
|
33
|
+
pub(crate) fn command_environment(host: &SessionEnvironment) -> SessionEnvironment {
|
|
34
|
+
let persisted = persisted_environment(host.variables());
|
|
35
|
+
if persisted.is_empty() {
|
|
36
|
+
return host.clone();
|
|
37
|
+
}
|
|
38
|
+
let merged = overlay(persisted, host.variables());
|
|
39
|
+
// Checked against the merged view so the opt-out works from the machine's own environment
|
|
40
|
+
// settings, which is where someone who wants it turned off will reach for it first.
|
|
41
|
+
if !inheritance_enabled(&merged) {
|
|
42
|
+
return host.clone();
|
|
43
|
+
}
|
|
44
|
+
SessionEnvironment::new(host.cwd().to_path_buf(), merged)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Reports whether the persisted environment should be restored for this session.
|
|
48
|
+
fn inheritance_enabled(variables: &[(OsString, OsString)]) -> bool {
|
|
49
|
+
variables
|
|
50
|
+
.iter()
|
|
51
|
+
.find(|(name, _)| crate::session::environment_name_eq(name, INHERIT_ENV))
|
|
52
|
+
.and_then(|(_, value)| value.to_str())
|
|
53
|
+
.is_none_or(|value| {
|
|
54
|
+
!matches!(
|
|
55
|
+
value.trim().to_ascii_lowercase().as_str(),
|
|
56
|
+
"0" | "false" | "off" | "no"
|
|
57
|
+
)
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Lays the host environment over the persisted one, keeping the host authoritative.
|
|
62
|
+
fn overlay(
|
|
63
|
+
persisted: Vec<(OsString, OsString)>,
|
|
64
|
+
host: &[(OsString, OsString)],
|
|
65
|
+
) -> Vec<(OsString, OsString)> {
|
|
66
|
+
let mut merged: Vec<(OsString, OsString)> = Vec::with_capacity(persisted.len() + host.len());
|
|
67
|
+
for (name, value) in persisted {
|
|
68
|
+
if merged
|
|
69
|
+
.iter()
|
|
70
|
+
.any(|(existing, _)| crate::session::environment_name_eq_os(existing, &name))
|
|
71
|
+
{
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
merged.push((name, value));
|
|
75
|
+
}
|
|
76
|
+
for (name, value) in host {
|
|
77
|
+
match merged
|
|
78
|
+
.iter_mut()
|
|
79
|
+
.find(|(existing, _)| crate::session::environment_name_eq_os(existing, name))
|
|
80
|
+
{
|
|
81
|
+
Some(entry) => {
|
|
82
|
+
entry.1 = if crate::session::environment_name_eq(name, SEARCH_PATH) {
|
|
83
|
+
union_search_path(value, &entry.1)
|
|
84
|
+
} else {
|
|
85
|
+
value.clone()
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
None => merged.push((name.clone(), value.clone())),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
merged
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// Appends persisted search-path entries the host's own search path does not already contain.
|
|
95
|
+
///
|
|
96
|
+
/// The host value is never rewritten: it is the live path, it comes first, and anything it already
|
|
97
|
+
/// resolves keeps resolving to the same place. Only genuinely absent directories are added.
|
|
98
|
+
fn union_search_path(host: &OsStr, persisted: &OsStr) -> OsString {
|
|
99
|
+
let host_entries = split_search_path(host)
|
|
100
|
+
.map(comparable_entry)
|
|
101
|
+
.collect::<Vec<_>>();
|
|
102
|
+
let mut merged = host.to_os_string();
|
|
103
|
+
for entry in split_search_path(persisted) {
|
|
104
|
+
let comparable = comparable_entry(entry);
|
|
105
|
+
if comparable.is_empty() || host_entries.contains(&comparable) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if !merged.is_empty() {
|
|
109
|
+
merged.push(PATH_LIST_SEPARATOR.to_string());
|
|
110
|
+
}
|
|
111
|
+
merged.push(entry);
|
|
112
|
+
}
|
|
113
|
+
merged
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fn split_search_path(value: &OsStr) -> impl Iterator<Item = &OsStr> {
|
|
117
|
+
value
|
|
118
|
+
.to_str()
|
|
119
|
+
.into_iter()
|
|
120
|
+
.flat_map(|value| value.split(PATH_LIST_SEPARATOR))
|
|
121
|
+
.map(OsStr::new)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Normalizes one search-path entry so that spelling differences do not duplicate a directory.
|
|
125
|
+
fn comparable_entry(entry: &OsStr) -> String {
|
|
126
|
+
let entry = entry.to_string_lossy();
|
|
127
|
+
let trimmed = entry.trim().trim_end_matches(['/', '\\']);
|
|
128
|
+
if cfg!(windows) {
|
|
129
|
+
trimmed.to_ascii_lowercase().replace('\\', "/")
|
|
130
|
+
} else {
|
|
131
|
+
trimmed.to_string()
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// Expands `%NAME%` references against an already-resolved lookup, leaving unknown names literal.
|
|
136
|
+
///
|
|
137
|
+
/// Values that are not valid Unicode are returned unchanged rather than mangled; Windows stores
|
|
138
|
+
/// expandable strings as UTF-16, so a value that fails this test cannot contain a usable reference.
|
|
139
|
+
#[cfg(windows)]
|
|
140
|
+
fn expand_references(value: &OsStr, lookup: &[(OsString, OsString)]) -> OsString {
|
|
141
|
+
let Some(value) = value.to_str() else {
|
|
142
|
+
return value.to_os_string();
|
|
143
|
+
};
|
|
144
|
+
if !value.contains('%') {
|
|
145
|
+
return OsString::from(value);
|
|
146
|
+
}
|
|
147
|
+
let mut expanded = OsString::new();
|
|
148
|
+
let mut rest = value;
|
|
149
|
+
while let Some(start) = rest.find('%') {
|
|
150
|
+
expanded.push(&rest[..start]);
|
|
151
|
+
let tail = &rest[start + 1..];
|
|
152
|
+
let Some(end) = tail.find('%') else {
|
|
153
|
+
expanded.push(&rest[start..]);
|
|
154
|
+
return expanded;
|
|
155
|
+
};
|
|
156
|
+
let name = &tail[..end];
|
|
157
|
+
match lookup
|
|
158
|
+
.iter()
|
|
159
|
+
.find(|(candidate, _)| crate::session::environment_name_eq(candidate, name))
|
|
160
|
+
{
|
|
161
|
+
Some((_, resolved)) => expanded.push(resolved),
|
|
162
|
+
None => {
|
|
163
|
+
expanded.push("%");
|
|
164
|
+
expanded.push(name);
|
|
165
|
+
expanded.push("%");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
rest = &tail[end + 1..];
|
|
169
|
+
}
|
|
170
|
+
expanded.push(rest);
|
|
171
|
+
expanded
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#[cfg(not(windows))]
|
|
175
|
+
/// Unix persists a user's environment in the login profile, which `bash -lc` already sources.
|
|
176
|
+
fn persisted_environment(_host: &[(OsString, OsString)]) -> Vec<(OsString, OsString)> {
|
|
177
|
+
Vec::new()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#[cfg(windows)]
|
|
181
|
+
fn persisted_environment(host: &[(OsString, OsString)]) -> Vec<(OsString, OsString)> {
|
|
182
|
+
use windows_sys::Win32::System::Registry::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
|
|
183
|
+
|
|
184
|
+
// System values are laid down first so the user's own hive overrides them, matching how
|
|
185
|
+
// Windows composes a logon environment. PATH is unioned rather than replaced for both.
|
|
186
|
+
let system = read_environment_key(
|
|
187
|
+
HKEY_LOCAL_MACHINE,
|
|
188
|
+
r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment",
|
|
189
|
+
);
|
|
190
|
+
let user = read_environment_key(HKEY_CURRENT_USER, "Environment");
|
|
191
|
+
let merged = overlay(system, &user);
|
|
192
|
+
// References are resolved against the host environment as well: names computed at logon —
|
|
193
|
+
// `USERPROFILE`, `SystemRoot` — are never stored in these keys, so a lookup limited to the
|
|
194
|
+
// registry would leave `%USERPROFILE%\go` as literal text and hand a command a broken path.
|
|
195
|
+
// Every value is resolved rather than only the ones stored as expandable: a plain string
|
|
196
|
+
// holding a resolvable `%NAME%` is a mistyped entry, and expanding it is what its author meant.
|
|
197
|
+
let lookup = overlay(merged.clone(), host);
|
|
198
|
+
merged
|
|
199
|
+
.into_iter()
|
|
200
|
+
.map(|(name, value)| {
|
|
201
|
+
let value = expand_references(&value, &lookup);
|
|
202
|
+
(name, value)
|
|
203
|
+
})
|
|
204
|
+
.collect()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#[cfg(windows)]
|
|
208
|
+
fn read_environment_key(
|
|
209
|
+
root: windows_sys::Win32::System::Registry::HKEY,
|
|
210
|
+
subkey: &str,
|
|
211
|
+
) -> Vec<(OsString, OsString)> {
|
|
212
|
+
use std::os::windows::ffi::{OsStrExt, OsStringExt};
|
|
213
|
+
use windows_sys::Win32::Foundation::{ERROR_MORE_DATA, ERROR_SUCCESS};
|
|
214
|
+
use windows_sys::Win32::System::Registry::{
|
|
215
|
+
HKEY, KEY_READ, REG_EXPAND_SZ, REG_SZ, RegCloseKey, RegEnumValueW, RegOpenKeyExW,
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// Registry value names are bounded at 16383 characters, so one maximum-size buffer removes
|
|
219
|
+
// the retry path that RegEnumValueW does not report a required name length for.
|
|
220
|
+
const MAX_VALUE_NAME: usize = 16_384;
|
|
221
|
+
|
|
222
|
+
let wide_subkey = OsStr::new(subkey)
|
|
223
|
+
.encode_wide()
|
|
224
|
+
.chain(std::iter::once(0))
|
|
225
|
+
.collect::<Vec<u16>>();
|
|
226
|
+
let mut key: HKEY = std::ptr::null_mut();
|
|
227
|
+
// SAFETY: the subkey is NUL-terminated and `key` receives a handle closed below.
|
|
228
|
+
let status =
|
|
229
|
+
unsafe { RegOpenKeyExW(root, wide_subkey.as_ptr(), 0, KEY_READ, &raw mut key) } as u32;
|
|
230
|
+
if status != ERROR_SUCCESS {
|
|
231
|
+
// An unreadable key degrades to the environment the host supplied, which is what every
|
|
232
|
+
// release before this one used. Failing the call instead would take the shell tools down
|
|
233
|
+
// over an enhancement, so absence is reported as "nothing persisted" rather than an error.
|
|
234
|
+
return Vec::new();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let mut values = Vec::new();
|
|
238
|
+
let mut name = vec![0u16; MAX_VALUE_NAME];
|
|
239
|
+
let mut data = vec![0u8; 4096];
|
|
240
|
+
let mut index = 0u32;
|
|
241
|
+
loop {
|
|
242
|
+
let mut name_length = name.len() as u32;
|
|
243
|
+
let mut kind = 0u32;
|
|
244
|
+
let mut data_length = data.len() as u32;
|
|
245
|
+
// SAFETY: every pointer refers to a live buffer whose length is passed alongside it.
|
|
246
|
+
let status = unsafe {
|
|
247
|
+
RegEnumValueW(
|
|
248
|
+
key,
|
|
249
|
+
index,
|
|
250
|
+
name.as_mut_ptr(),
|
|
251
|
+
&raw mut name_length,
|
|
252
|
+
std::ptr::null_mut(),
|
|
253
|
+
&raw mut kind,
|
|
254
|
+
data.as_mut_ptr(),
|
|
255
|
+
&raw mut data_length,
|
|
256
|
+
)
|
|
257
|
+
} as u32;
|
|
258
|
+
if status == ERROR_MORE_DATA {
|
|
259
|
+
// Only the data buffer can be short; it now holds the required byte count.
|
|
260
|
+
data.resize((data_length as usize).max(data.len() * 2), 0);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if status != ERROR_SUCCESS {
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
index += 1;
|
|
267
|
+
if kind != REG_SZ && kind != REG_EXPAND_SZ {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
let name_value = OsString::from_wide(&name[..name_length as usize]);
|
|
271
|
+
if name_value.is_empty() {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
values.push((name_value, wide_value(&data[..data_length as usize])));
|
|
275
|
+
}
|
|
276
|
+
// SAFETY: `key` was opened above and is not used afterwards.
|
|
277
|
+
unsafe { RegCloseKey(key) };
|
|
278
|
+
values
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/// Decodes a registry string payload, dropping the terminator the API counts but callers must not keep.
|
|
282
|
+
#[cfg(windows)]
|
|
283
|
+
fn wide_value(bytes: &[u8]) -> OsString {
|
|
284
|
+
use std::os::windows::ffi::OsStringExt;
|
|
285
|
+
let units = bytes
|
|
286
|
+
.as_chunks::<2>()
|
|
287
|
+
.0
|
|
288
|
+
.iter()
|
|
289
|
+
.map(|pair| u16::from_le_bytes(*pair))
|
|
290
|
+
.collect::<Vec<u16>>();
|
|
291
|
+
let end = units
|
|
292
|
+
.iter()
|
|
293
|
+
.position(|unit| *unit == 0)
|
|
294
|
+
.unwrap_or(units.len());
|
|
295
|
+
OsString::from_wide(&units[..end])
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
#[cfg(test)]
|
|
299
|
+
mod tests {
|
|
300
|
+
use super::{command_environment, inheritance_enabled, overlay, union_search_path};
|
|
301
|
+
use crate::session::SessionEnvironment;
|
|
302
|
+
use std::ffi::OsString;
|
|
303
|
+
use std::path::PathBuf;
|
|
304
|
+
|
|
305
|
+
fn pairs(entries: &[(&str, &str)]) -> Vec<(OsString, OsString)> {
|
|
306
|
+
entries
|
|
307
|
+
.iter()
|
|
308
|
+
.map(|(name, value)| (OsString::from(name), OsString::from(value)))
|
|
309
|
+
.collect()
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
fn value<'a>(merged: &'a [(OsString, OsString)], name: &str) -> Option<&'a OsString> {
|
|
313
|
+
merged
|
|
314
|
+
.iter()
|
|
315
|
+
.find(|(candidate, _)| candidate == name)
|
|
316
|
+
.map(|(_, value)| value)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
#[test]
|
|
320
|
+
fn the_host_wins_every_name_it_provides_and_absent_names_survive() {
|
|
321
|
+
let merged = overlay(
|
|
322
|
+
pairs(&[("JAVA_HOME", "C:/jdk21"), ("TEMP", "C:/persisted-temp")]),
|
|
323
|
+
&pairs(&[("TEMP", "C:/live-temp"), ("FASTCTX_TOKEN_BUDGET", "9000")]),
|
|
324
|
+
);
|
|
325
|
+
assert_eq!(value(&merged, "JAVA_HOME").unwrap(), "C:/jdk21");
|
|
326
|
+
assert_eq!(value(&merged, "TEMP").unwrap(), "C:/live-temp");
|
|
327
|
+
assert_eq!(value(&merged, "FASTCTX_TOKEN_BUDGET").unwrap(), "9000");
|
|
328
|
+
assert_eq!(
|
|
329
|
+
merged.len(),
|
|
330
|
+
3,
|
|
331
|
+
"a name must never appear twice: {merged:?}"
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
#[test]
|
|
336
|
+
fn the_search_path_keeps_the_live_value_first_and_only_gains_absent_entries() {
|
|
337
|
+
// Entries carry no drive letter: off Windows a colon is the list separator, so a
|
|
338
|
+
// `C:/...` fixture would be read as two entries there and prove nothing.
|
|
339
|
+
let separator = super::PATH_LIST_SEPARATOR;
|
|
340
|
+
let host = format!("/live/bin{separator}/Shared/Bin");
|
|
341
|
+
let persisted = format!("/shared/bin/{separator}/persisted/only{separator}");
|
|
342
|
+
let merged = union_search_path(&OsString::from(&host), &OsString::from(&persisted));
|
|
343
|
+
let merged = merged.to_string_lossy();
|
|
344
|
+
assert!(
|
|
345
|
+
merged.starts_with(&host),
|
|
346
|
+
"live path was rewritten: {merged}"
|
|
347
|
+
);
|
|
348
|
+
assert!(merged.ends_with("/persisted/only"), "{merged}");
|
|
349
|
+
// Windows compares entries case-insensitively and ignores a trailing separator, so the
|
|
350
|
+
// shared directory must not reappear under a second spelling; elsewhere case matters.
|
|
351
|
+
let occurrences = merged.to_ascii_lowercase().matches("shared/bin").count();
|
|
352
|
+
let expected = if cfg!(windows) { 1 } else { 2 };
|
|
353
|
+
assert_eq!(occurrences, expected, "{merged}");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
#[cfg(windows)]
|
|
357
|
+
#[test]
|
|
358
|
+
fn unresolvable_references_stay_literal_instead_of_expanding_to_nothing() {
|
|
359
|
+
use super::expand_references;
|
|
360
|
+
let lookup = pairs(&[("SystemRoot", "C:/Windows")]);
|
|
361
|
+
assert_eq!(
|
|
362
|
+
expand_references(&OsString::from("%SystemRoot%/System32"), &lookup),
|
|
363
|
+
OsString::from("C:/Windows/System32")
|
|
364
|
+
);
|
|
365
|
+
assert_eq!(
|
|
366
|
+
expand_references(&OsString::from("%NOT_SET%/bin"), &lookup),
|
|
367
|
+
OsString::from("%NOT_SET%/bin")
|
|
368
|
+
);
|
|
369
|
+
assert_eq!(
|
|
370
|
+
expand_references(&OsString::from("100% done"), &lookup),
|
|
371
|
+
OsString::from("100% done")
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
#[test]
|
|
376
|
+
fn the_opt_out_is_honoured_only_for_values_that_clearly_mean_off() {
|
|
377
|
+
for setting in ["0", "false", "OFF", "no", " No "] {
|
|
378
|
+
assert!(
|
|
379
|
+
!inheritance_enabled(&pairs(&[(super::INHERIT_ENV, setting)])),
|
|
380
|
+
"{setting}"
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
for setting in ["1", "true", "on", "yes", ""] {
|
|
384
|
+
assert!(
|
|
385
|
+
inheritance_enabled(&pairs(&[(super::INHERIT_ENV, setting)])),
|
|
386
|
+
"{setting}"
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
assert!(inheritance_enabled(&pairs(&[("PATH", "C:/live/bin")])));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/// Holds on whichever machine runs it: nothing is persisted off Windows, and on Windows the
|
|
393
|
+
/// real registry is read — either way the session's own cwd and search path lead the result.
|
|
394
|
+
#[test]
|
|
395
|
+
fn a_derived_environment_keeps_the_session_cwd_and_leads_with_the_live_search_path() {
|
|
396
|
+
let host = SessionEnvironment::new(
|
|
397
|
+
PathBuf::from("."),
|
|
398
|
+
pairs(&[("PATH", "/live/bin"), ("FASTCTX_TOKEN_BUDGET", "9000")]),
|
|
399
|
+
);
|
|
400
|
+
let derived = command_environment(&host);
|
|
401
|
+
assert_eq!(derived.cwd(), host.cwd());
|
|
402
|
+
assert!(
|
|
403
|
+
derived
|
|
404
|
+
.var_os("PATH")
|
|
405
|
+
.is_some_and(|path| path.to_string_lossy().starts_with("/live/bin")),
|
|
406
|
+
"the live search path must survive verbatim as the prefix"
|
|
407
|
+
);
|
|
408
|
+
assert_eq!(
|
|
409
|
+
derived.var("FASTCTX_TOKEN_BUDGET").unwrap(),
|
|
410
|
+
"9000",
|
|
411
|
+
"a host-provided value must never be displaced by a persisted one"
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
}
|