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,793 @@
|
|
|
1
|
+
//! Windows private-directory ACLs and reparse-point enforcement.
|
|
2
|
+
|
|
3
|
+
use std::fs::{self, OpenOptions};
|
|
4
|
+
use std::os::windows::ffi::OsStrExt;
|
|
5
|
+
use std::os::windows::io::{AsRawHandle, FromRawHandle};
|
|
6
|
+
use std::path::{Path, PathBuf};
|
|
7
|
+
|
|
8
|
+
pub(super) fn runtime_component(component: &str) -> PathBuf {
|
|
9
|
+
let base = std::env::var_os("LOCALAPPDATA")
|
|
10
|
+
.map(PathBuf::from)
|
|
11
|
+
.filter(|path| path.is_absolute())
|
|
12
|
+
.or_else(|| {
|
|
13
|
+
std::env::var_os("USERPROFILE")
|
|
14
|
+
.map(PathBuf::from)
|
|
15
|
+
.filter(|path| path.is_absolute())
|
|
16
|
+
.map(|path| path.join("AppData").join("Local"))
|
|
17
|
+
})
|
|
18
|
+
.unwrap_or_else(std::env::temp_dir);
|
|
19
|
+
base.join("fastctx").join("runtime").join(component)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
pub(super) fn ensure_private_directory(path: &Path, label: &str) -> Result<(), String> {
|
|
23
|
+
let user_sid = current_process_user_sid_string()?;
|
|
24
|
+
let descriptor = PrivateAcl::new(path, label, &user_sid)?;
|
|
25
|
+
let chain = managed_chain(path);
|
|
26
|
+
let mut opened_chain = Vec::with_capacity(chain.len());
|
|
27
|
+
for directory in &chain {
|
|
28
|
+
let opened = match open_directory_for_security(directory, label) {
|
|
29
|
+
Ok(opened) => opened,
|
|
30
|
+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
|
|
31
|
+
descriptor.create_directory(directory, label)?;
|
|
32
|
+
open_directory_for_security(directory, label).map_err(|error| {
|
|
33
|
+
directory_io_error("open the newly created", directory, label, error)
|
|
34
|
+
})?
|
|
35
|
+
}
|
|
36
|
+
Err(error) => return Err(directory_io_error("open", directory, label, error)),
|
|
37
|
+
};
|
|
38
|
+
descriptor.secure_open_directory(&opened, directory, label)?;
|
|
39
|
+
opened_chain.push(opened);
|
|
40
|
+
}
|
|
41
|
+
super::verify_directory(path, label)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fn managed_chain(path: &Path) -> Vec<PathBuf> {
|
|
45
|
+
let root = path
|
|
46
|
+
.ancestors()
|
|
47
|
+
.find_map(|ancestor| {
|
|
48
|
+
let parent = ancestor.parent()?;
|
|
49
|
+
if file_name_eq(ancestor, "runtime") && file_name_eq(parent, "fastctx") {
|
|
50
|
+
Some(parent)
|
|
51
|
+
} else if file_name_eq(ancestor, ".fastctx") {
|
|
52
|
+
Some(ancestor)
|
|
53
|
+
} else {
|
|
54
|
+
None
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
.unwrap_or(path);
|
|
58
|
+
let mut chain = Vec::new();
|
|
59
|
+
for ancestor in path.ancestors() {
|
|
60
|
+
chain.push(ancestor.to_path_buf());
|
|
61
|
+
if ancestor == root {
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
chain.reverse();
|
|
66
|
+
chain
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fn file_name_eq(path: &Path, expected: &str) -> bool {
|
|
70
|
+
path.file_name()
|
|
71
|
+
.is_some_and(|name| name.to_string_lossy().eq_ignore_ascii_case(expected))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fn current_process_user_sid_string() -> Result<String, String> {
|
|
75
|
+
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE};
|
|
76
|
+
use windows_sys::Win32::Security::Authorization::ConvertSidToStringSidW;
|
|
77
|
+
use windows_sys::Win32::Security::{GetTokenInformation, TOKEN_QUERY, TOKEN_USER, TokenUser};
|
|
78
|
+
use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
|
|
79
|
+
|
|
80
|
+
let mut token: HANDLE = std::ptr::null_mut();
|
|
81
|
+
let opened = unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) };
|
|
82
|
+
if opened == 0 {
|
|
83
|
+
return Err(format!(
|
|
84
|
+
"Cannot inspect the current Windows user for private runtime storage: {}",
|
|
85
|
+
std::io::Error::last_os_error()
|
|
86
|
+
));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let result = (|| {
|
|
90
|
+
let mut required = 0_u32;
|
|
91
|
+
unsafe {
|
|
92
|
+
GetTokenInformation(token, TokenUser, std::ptr::null_mut(), 0, &mut required);
|
|
93
|
+
}
|
|
94
|
+
if required < std::mem::size_of::<TOKEN_USER>() as u32 {
|
|
95
|
+
return Err(format!(
|
|
96
|
+
"Cannot size the current Windows user identity: {}",
|
|
97
|
+
std::io::Error::last_os_error()
|
|
98
|
+
));
|
|
99
|
+
}
|
|
100
|
+
let mut buffer = aligned_buffer(required);
|
|
101
|
+
let read = unsafe {
|
|
102
|
+
GetTokenInformation(
|
|
103
|
+
token,
|
|
104
|
+
TokenUser,
|
|
105
|
+
buffer.as_mut_ptr().cast(),
|
|
106
|
+
required,
|
|
107
|
+
&mut required,
|
|
108
|
+
)
|
|
109
|
+
};
|
|
110
|
+
if read == 0 {
|
|
111
|
+
return Err(format!(
|
|
112
|
+
"Cannot read the current Windows user identity: {}",
|
|
113
|
+
std::io::Error::last_os_error()
|
|
114
|
+
));
|
|
115
|
+
}
|
|
116
|
+
let token_user = unsafe { &*buffer.as_ptr().cast::<TOKEN_USER>() };
|
|
117
|
+
let mut string_sid = std::ptr::null_mut();
|
|
118
|
+
let converted = unsafe { ConvertSidToStringSidW(token_user.User.Sid, &mut string_sid) };
|
|
119
|
+
if converted == 0 {
|
|
120
|
+
return Err(format!(
|
|
121
|
+
"Cannot format the current Windows user identity: {}",
|
|
122
|
+
std::io::Error::last_os_error()
|
|
123
|
+
));
|
|
124
|
+
}
|
|
125
|
+
let mut length = 0_usize;
|
|
126
|
+
unsafe {
|
|
127
|
+
while *string_sid.add(length) != 0 {
|
|
128
|
+
length += 1;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
let sid = String::from_utf16(unsafe { std::slice::from_raw_parts(string_sid, length) })
|
|
132
|
+
.map_err(|_| "Windows returned a malformed user SID string.".to_string());
|
|
133
|
+
unsafe {
|
|
134
|
+
windows_sys::Win32::Foundation::LocalFree(string_sid.cast());
|
|
135
|
+
}
|
|
136
|
+
sid
|
|
137
|
+
})();
|
|
138
|
+
unsafe {
|
|
139
|
+
CloseHandle(token);
|
|
140
|
+
}
|
|
141
|
+
result
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fn aligned_buffer(required_bytes: u32) -> Vec<usize> {
|
|
145
|
+
let word_size = std::mem::size_of::<usize>();
|
|
146
|
+
vec![0_usize; (required_bytes as usize).div_ceil(word_size)]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
struct PrivateAcl {
|
|
150
|
+
descriptor: windows_sys::Win32::Security::PSECURITY_DESCRIPTOR,
|
|
151
|
+
dacl: *mut windows_sys::Win32::Security::ACL,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// Owner-and-System descriptor used while creating one private named-pipe instance.
|
|
155
|
+
pub(crate) struct PrivateSecurityDescriptor {
|
|
156
|
+
descriptor: windows_sys::Win32::Security::PSECURITY_DESCRIPTOR,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
impl PrivateSecurityDescriptor {
|
|
160
|
+
pub(crate) fn new() -> Result<Self, String> {
|
|
161
|
+
let user_sid = current_process_user_sid_string()?;
|
|
162
|
+
let descriptor = private_descriptor(
|
|
163
|
+
Path::new(r"\\.\pipe\fastctx-engine"),
|
|
164
|
+
"control-center IPC",
|
|
165
|
+
&user_sid,
|
|
166
|
+
false,
|
|
167
|
+
)?;
|
|
168
|
+
Ok(Self { descriptor })
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
pub(crate) fn security_attributes(&self) -> windows_sys::Win32::Security::SECURITY_ATTRIBUTES {
|
|
172
|
+
windows_sys::Win32::Security::SECURITY_ATTRIBUTES {
|
|
173
|
+
nLength: std::mem::size_of::<windows_sys::Win32::Security::SECURITY_ATTRIBUTES>()
|
|
174
|
+
as u32,
|
|
175
|
+
lpSecurityDescriptor: self.descriptor,
|
|
176
|
+
bInheritHandle: 0,
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
impl Drop for PrivateSecurityDescriptor {
|
|
182
|
+
fn drop(&mut self) {
|
|
183
|
+
unsafe {
|
|
184
|
+
windows_sys::Win32::Foundation::LocalFree(self.descriptor);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
impl PrivateAcl {
|
|
190
|
+
fn new(path: &Path, label: &str, user_sid: &str) -> Result<Self, String> {
|
|
191
|
+
use windows_sys::Win32::Security::GetSecurityDescriptorDacl;
|
|
192
|
+
let descriptor = private_descriptor(path, label, user_sid, true)?;
|
|
193
|
+
|
|
194
|
+
let mut present = 0;
|
|
195
|
+
let mut defaulted = 0;
|
|
196
|
+
let mut dacl = std::ptr::null_mut();
|
|
197
|
+
let read = unsafe {
|
|
198
|
+
GetSecurityDescriptorDacl(descriptor, &mut present, &mut dacl, &mut defaulted)
|
|
199
|
+
};
|
|
200
|
+
if read == 0 || present == 0 || dacl.is_null() {
|
|
201
|
+
let error = std::io::Error::last_os_error();
|
|
202
|
+
unsafe {
|
|
203
|
+
windows_sys::Win32::Foundation::LocalFree(descriptor);
|
|
204
|
+
}
|
|
205
|
+
return Err(format!(
|
|
206
|
+
"Cannot inspect the private ACL for the {label} directory {}: {error}",
|
|
207
|
+
crate::paths::display_path(path)
|
|
208
|
+
));
|
|
209
|
+
}
|
|
210
|
+
Ok(Self { descriptor, dacl })
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
fn create_directory(&self, path: &Path, label: &str) -> Result<(), String> {
|
|
214
|
+
use windows_sys::Win32::Security::SECURITY_ATTRIBUTES;
|
|
215
|
+
use windows_sys::Win32::Storage::FileSystem::CreateDirectoryW;
|
|
216
|
+
|
|
217
|
+
let wide = wide_path(path);
|
|
218
|
+
let attributes = SECURITY_ATTRIBUTES {
|
|
219
|
+
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
|
|
220
|
+
lpSecurityDescriptor: self.descriptor,
|
|
221
|
+
bInheritHandle: 0,
|
|
222
|
+
};
|
|
223
|
+
let created = unsafe { CreateDirectoryW(wide.as_ptr(), &attributes) };
|
|
224
|
+
if created != 0 {
|
|
225
|
+
return Ok(());
|
|
226
|
+
}
|
|
227
|
+
let error = std::io::Error::last_os_error();
|
|
228
|
+
if error.kind() == std::io::ErrorKind::AlreadyExists {
|
|
229
|
+
return Ok(());
|
|
230
|
+
}
|
|
231
|
+
Err(directory_io_error("create", path, label, error))
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
fn secure_open_directory(
|
|
235
|
+
&self,
|
|
236
|
+
directory: &fs::File,
|
|
237
|
+
path: &Path,
|
|
238
|
+
label: &str,
|
|
239
|
+
) -> Result<(), String> {
|
|
240
|
+
use windows_sys::Win32::Security::Authorization::{SE_FILE_OBJECT, SetSecurityInfo};
|
|
241
|
+
use windows_sys::Win32::Security::{
|
|
242
|
+
DACL_SECURITY_INFORMATION, PROTECTED_DACL_SECURITY_INFORMATION,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
let result = unsafe {
|
|
246
|
+
SetSecurityInfo(
|
|
247
|
+
directory.as_raw_handle(),
|
|
248
|
+
SE_FILE_OBJECT,
|
|
249
|
+
DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
|
|
250
|
+
std::ptr::null_mut(),
|
|
251
|
+
std::ptr::null_mut(),
|
|
252
|
+
self.dacl,
|
|
253
|
+
std::ptr::null(),
|
|
254
|
+
)
|
|
255
|
+
};
|
|
256
|
+
if result != 0 {
|
|
257
|
+
return Err(directory_io_error(
|
|
258
|
+
"secure",
|
|
259
|
+
path,
|
|
260
|
+
label,
|
|
261
|
+
std::io::Error::from_raw_os_error(result as i32),
|
|
262
|
+
));
|
|
263
|
+
}
|
|
264
|
+
Ok(())
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
fn private_descriptor(
|
|
269
|
+
path: &Path,
|
|
270
|
+
label: &str,
|
|
271
|
+
user_sid: &str,
|
|
272
|
+
inheritable: bool,
|
|
273
|
+
) -> Result<windows_sys::Win32::Security::PSECURITY_DESCRIPTOR, String> {
|
|
274
|
+
use windows_sys::Win32::Security::Authorization::{
|
|
275
|
+
ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1,
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// 2026-07-22: TokenUser is stable across UAC's elevated and filtered tokens;
|
|
279
|
+
// Owner Rights can resolve to Administrators and lock the filtered token out.
|
|
280
|
+
let ace_flags = if inheritable { "OICI" } else { "" };
|
|
281
|
+
let rights = if inheritable { "FA" } else { "GA" };
|
|
282
|
+
let system_ace = if user_sid.eq_ignore_ascii_case("S-1-5-18") {
|
|
283
|
+
String::new()
|
|
284
|
+
} else {
|
|
285
|
+
format!("(A;{ace_flags};{rights};;;SY)")
|
|
286
|
+
};
|
|
287
|
+
let descriptor_text = format!("D:P(A;{ace_flags};{rights};;;{user_sid}){system_ace}")
|
|
288
|
+
.encode_utf16()
|
|
289
|
+
.chain(Some(0))
|
|
290
|
+
.collect::<Vec<_>>();
|
|
291
|
+
let mut descriptor = std::ptr::null_mut();
|
|
292
|
+
let converted = unsafe {
|
|
293
|
+
ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
|
294
|
+
descriptor_text.as_ptr(),
|
|
295
|
+
SDDL_REVISION_1,
|
|
296
|
+
&mut descriptor,
|
|
297
|
+
std::ptr::null_mut(),
|
|
298
|
+
)
|
|
299
|
+
};
|
|
300
|
+
if converted == 0 {
|
|
301
|
+
return Err(format!(
|
|
302
|
+
"Cannot build the private ACL for the {label} path {}: {}",
|
|
303
|
+
crate::paths::display_path(path),
|
|
304
|
+
std::io::Error::last_os_error()
|
|
305
|
+
));
|
|
306
|
+
}
|
|
307
|
+
Ok(descriptor)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
impl Drop for PrivateAcl {
|
|
311
|
+
fn drop(&mut self) {
|
|
312
|
+
unsafe {
|
|
313
|
+
windows_sys::Win32::Foundation::LocalFree(self.descriptor);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
fn open_directory_for_security(path: &Path, label: &str) -> Result<fs::File, std::io::Error> {
|
|
319
|
+
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
|
|
320
|
+
use windows_sys::Win32::Storage::FileSystem::{
|
|
321
|
+
CreateFileW, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT,
|
|
322
|
+
FILE_READ_ATTRIBUTES, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING, READ_CONTROL,
|
|
323
|
+
WRITE_DAC,
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
let wide = wide_path(path);
|
|
327
|
+
let handle = unsafe {
|
|
328
|
+
CreateFileW(
|
|
329
|
+
wide.as_ptr(),
|
|
330
|
+
READ_CONTROL | WRITE_DAC | FILE_READ_ATTRIBUTES,
|
|
331
|
+
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
|
332
|
+
std::ptr::null(),
|
|
333
|
+
OPEN_EXISTING,
|
|
334
|
+
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
|
|
335
|
+
std::ptr::null_mut(),
|
|
336
|
+
)
|
|
337
|
+
};
|
|
338
|
+
if handle == INVALID_HANDLE_VALUE {
|
|
339
|
+
return Err(std::io::Error::last_os_error());
|
|
340
|
+
}
|
|
341
|
+
let directory = unsafe { fs::File::from_raw_handle(handle) };
|
|
342
|
+
let metadata = directory.metadata()?;
|
|
343
|
+
if let Err(message) = super::validate_directory_metadata(path, label, &metadata) {
|
|
344
|
+
return Err(std::io::Error::new(
|
|
345
|
+
std::io::ErrorKind::InvalidInput,
|
|
346
|
+
message,
|
|
347
|
+
));
|
|
348
|
+
}
|
|
349
|
+
Ok(directory)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
fn wide_path(path: &Path) -> Vec<u16> {
|
|
353
|
+
path.as_os_str().encode_wide().chain(Some(0)).collect()
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
fn directory_io_error(action: &str, path: &Path, label: &str, error: std::io::Error) -> String {
|
|
357
|
+
let mut message = format!(
|
|
358
|
+
"Cannot {action} the {label} directory {}: {error}",
|
|
359
|
+
crate::paths::display_path(path)
|
|
360
|
+
);
|
|
361
|
+
if error.kind() == std::io::ErrorKind::PermissionDenied {
|
|
362
|
+
message.push_str(
|
|
363
|
+
"\nNote: an earlier FastCtx version may have left an owner-only Windows ACL. Retry this operation once from an elevated process under the same Windows account; FastCtx can repair that ACL in place and never deletes the directory. If access is still denied, restore that account's Full control on the listed FastCtx directory or ask an administrator to do so.",
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
message
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
pub(super) fn configure_no_follow(options: &mut OpenOptions) {
|
|
370
|
+
use std::os::windows::fs::OpenOptionsExt;
|
|
371
|
+
use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OPEN_REPARSE_POINT;
|
|
372
|
+
options.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
pub(super) fn is_reparse_or_symlink(metadata: &fs::Metadata) -> bool {
|
|
376
|
+
use std::os::windows::fs::MetadataExt;
|
|
377
|
+
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT;
|
|
378
|
+
metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
#[cfg(test)]
|
|
382
|
+
mod tests {
|
|
383
|
+
use super::{
|
|
384
|
+
aligned_buffer, directory_io_error, ensure_private_directory, managed_chain, wide_path,
|
|
385
|
+
};
|
|
386
|
+
use std::mem::size_of;
|
|
387
|
+
use std::path::{Path, PathBuf};
|
|
388
|
+
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, LocalFree};
|
|
389
|
+
use windows_sys::Win32::Security::Authorization::{
|
|
390
|
+
ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1,
|
|
391
|
+
};
|
|
392
|
+
use windows_sys::Win32::Security::{
|
|
393
|
+
ACCESS_ALLOWED_ACE, ACE_HEADER, ACL, ACL_SIZE_INFORMATION, AclSizeInformation,
|
|
394
|
+
CONTAINER_INHERIT_ACE, CreateWellKnownSid, DACL_SECURITY_INFORMATION, EqualSid, GetAce,
|
|
395
|
+
GetAclInformation, GetFileSecurityW, GetSecurityDescriptorControl,
|
|
396
|
+
GetSecurityDescriptorDacl, GetTokenInformation, OBJECT_INHERIT_ACE,
|
|
397
|
+
PROTECTED_DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, PSID, SE_DACL_PROTECTED,
|
|
398
|
+
SetFileSecurityW, TOKEN_QUERY, TOKEN_USER, TokenUser, WinLocalSystemSid,
|
|
399
|
+
};
|
|
400
|
+
use windows_sys::Win32::Storage::FileSystem::FILE_ALL_ACCESS;
|
|
401
|
+
use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
|
|
402
|
+
|
|
403
|
+
const ACCESS_ALLOWED_ACE_TYPE_VALUE: u8 = 0;
|
|
404
|
+
|
|
405
|
+
struct SidBuffer {
|
|
406
|
+
_storage: Vec<usize>,
|
|
407
|
+
sid: PSID,
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
fn current_user_sid() -> SidBuffer {
|
|
411
|
+
let mut token: HANDLE = std::ptr::null_mut();
|
|
412
|
+
assert_ne!(
|
|
413
|
+
unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) },
|
|
414
|
+
0,
|
|
415
|
+
"{}",
|
|
416
|
+
std::io::Error::last_os_error()
|
|
417
|
+
);
|
|
418
|
+
let mut required = 0_u32;
|
|
419
|
+
unsafe {
|
|
420
|
+
GetTokenInformation(token, TokenUser, std::ptr::null_mut(), 0, &mut required);
|
|
421
|
+
}
|
|
422
|
+
assert!(required >= size_of::<TOKEN_USER>() as u32);
|
|
423
|
+
let mut storage = aligned_buffer(required);
|
|
424
|
+
assert_ne!(
|
|
425
|
+
unsafe {
|
|
426
|
+
GetTokenInformation(
|
|
427
|
+
token,
|
|
428
|
+
TokenUser,
|
|
429
|
+
storage.as_mut_ptr().cast(),
|
|
430
|
+
required,
|
|
431
|
+
&mut required,
|
|
432
|
+
)
|
|
433
|
+
},
|
|
434
|
+
0,
|
|
435
|
+
"{}",
|
|
436
|
+
std::io::Error::last_os_error()
|
|
437
|
+
);
|
|
438
|
+
unsafe {
|
|
439
|
+
CloseHandle(token);
|
|
440
|
+
}
|
|
441
|
+
let token_user = unsafe { &*storage.as_ptr().cast::<TOKEN_USER>() };
|
|
442
|
+
SidBuffer {
|
|
443
|
+
sid: token_user.User.Sid,
|
|
444
|
+
_storage: storage,
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
fn local_system_sid() -> SidBuffer {
|
|
449
|
+
let mut required = 0_u32;
|
|
450
|
+
unsafe {
|
|
451
|
+
CreateWellKnownSid(
|
|
452
|
+
WinLocalSystemSid,
|
|
453
|
+
std::ptr::null_mut(),
|
|
454
|
+
std::ptr::null_mut(),
|
|
455
|
+
&mut required,
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
assert!(required > 0);
|
|
459
|
+
let mut storage = aligned_buffer(required);
|
|
460
|
+
let sid = storage.as_mut_ptr().cast();
|
|
461
|
+
assert_ne!(
|
|
462
|
+
unsafe {
|
|
463
|
+
CreateWellKnownSid(WinLocalSystemSid, std::ptr::null_mut(), sid, &mut required)
|
|
464
|
+
},
|
|
465
|
+
0,
|
|
466
|
+
"{}",
|
|
467
|
+
std::io::Error::last_os_error()
|
|
468
|
+
);
|
|
469
|
+
SidBuffer {
|
|
470
|
+
_storage: storage,
|
|
471
|
+
sid,
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
fn security_descriptor(path: &Path) -> Vec<usize> {
|
|
476
|
+
let wide = wide_path(path);
|
|
477
|
+
let mut required = 0_u32;
|
|
478
|
+
unsafe {
|
|
479
|
+
GetFileSecurityW(
|
|
480
|
+
wide.as_ptr(),
|
|
481
|
+
DACL_SECURITY_INFORMATION,
|
|
482
|
+
std::ptr::null_mut(),
|
|
483
|
+
0,
|
|
484
|
+
&mut required,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
assert!(required > 0, "{}", std::io::Error::last_os_error());
|
|
488
|
+
let mut descriptor = aligned_buffer(required);
|
|
489
|
+
assert_ne!(
|
|
490
|
+
unsafe {
|
|
491
|
+
GetFileSecurityW(
|
|
492
|
+
wide.as_ptr(),
|
|
493
|
+
DACL_SECURITY_INFORMATION,
|
|
494
|
+
descriptor.as_mut_ptr().cast(),
|
|
495
|
+
required,
|
|
496
|
+
&mut required,
|
|
497
|
+
)
|
|
498
|
+
},
|
|
499
|
+
0,
|
|
500
|
+
"{}",
|
|
501
|
+
std::io::Error::last_os_error()
|
|
502
|
+
);
|
|
503
|
+
descriptor
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
fn dacl_from_descriptor(descriptor: &[usize]) -> *mut ACL {
|
|
507
|
+
let mut present = 0;
|
|
508
|
+
let mut defaulted = 0;
|
|
509
|
+
let mut dacl = std::ptr::null_mut();
|
|
510
|
+
assert_ne!(
|
|
511
|
+
unsafe {
|
|
512
|
+
GetSecurityDescriptorDacl(
|
|
513
|
+
descriptor.as_ptr().cast_mut().cast(),
|
|
514
|
+
&mut present,
|
|
515
|
+
&mut dacl,
|
|
516
|
+
&mut defaulted,
|
|
517
|
+
)
|
|
518
|
+
},
|
|
519
|
+
0,
|
|
520
|
+
"{}",
|
|
521
|
+
std::io::Error::last_os_error()
|
|
522
|
+
);
|
|
523
|
+
assert_ne!(present, 0);
|
|
524
|
+
assert!(!dacl.is_null());
|
|
525
|
+
dacl
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
fn is_dacl_protected(path: &Path) -> bool {
|
|
529
|
+
let descriptor = security_descriptor(path);
|
|
530
|
+
let mut control = 0_u16;
|
|
531
|
+
let mut revision = 0_u32;
|
|
532
|
+
assert_ne!(
|
|
533
|
+
unsafe {
|
|
534
|
+
GetSecurityDescriptorControl(
|
|
535
|
+
descriptor.as_ptr().cast_mut().cast(),
|
|
536
|
+
&mut control,
|
|
537
|
+
&mut revision,
|
|
538
|
+
)
|
|
539
|
+
},
|
|
540
|
+
0,
|
|
541
|
+
"{}",
|
|
542
|
+
std::io::Error::last_os_error()
|
|
543
|
+
);
|
|
544
|
+
control & SE_DACL_PROTECTED != 0
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
fn assert_private_acl(path: &Path) {
|
|
548
|
+
let descriptor = security_descriptor(path);
|
|
549
|
+
let mut control = 0_u16;
|
|
550
|
+
let mut revision = 0_u32;
|
|
551
|
+
assert_ne!(
|
|
552
|
+
unsafe {
|
|
553
|
+
GetSecurityDescriptorControl(
|
|
554
|
+
descriptor.as_ptr().cast_mut().cast(),
|
|
555
|
+
&mut control,
|
|
556
|
+
&mut revision,
|
|
557
|
+
)
|
|
558
|
+
},
|
|
559
|
+
0
|
|
560
|
+
);
|
|
561
|
+
assert_ne!(control & SE_DACL_PROTECTED, 0);
|
|
562
|
+
|
|
563
|
+
let dacl = dacl_from_descriptor(&descriptor);
|
|
564
|
+
let mut information = ACL_SIZE_INFORMATION::default();
|
|
565
|
+
assert_ne!(
|
|
566
|
+
unsafe {
|
|
567
|
+
GetAclInformation(
|
|
568
|
+
dacl,
|
|
569
|
+
(&mut information as *mut ACL_SIZE_INFORMATION).cast(),
|
|
570
|
+
size_of::<ACL_SIZE_INFORMATION>() as u32,
|
|
571
|
+
AclSizeInformation,
|
|
572
|
+
)
|
|
573
|
+
},
|
|
574
|
+
0
|
|
575
|
+
);
|
|
576
|
+
let user = current_user_sid();
|
|
577
|
+
let system = local_system_sid();
|
|
578
|
+
let user_is_system = unsafe { EqualSid(user.sid, system.sid) } != 0;
|
|
579
|
+
assert_eq!(information.AceCount, if user_is_system { 1 } else { 2 });
|
|
580
|
+
let mut saw_user = false;
|
|
581
|
+
let mut saw_system = false;
|
|
582
|
+
for index in 0..information.AceCount {
|
|
583
|
+
let mut ace = std::ptr::null_mut();
|
|
584
|
+
assert_ne!(unsafe { GetAce(dacl, index, &mut ace) }, 0);
|
|
585
|
+
let header = unsafe { std::ptr::read_unaligned(ace.cast::<ACE_HEADER>()) };
|
|
586
|
+
assert_eq!(header.AceType, ACCESS_ALLOWED_ACE_TYPE_VALUE);
|
|
587
|
+
assert_eq!(
|
|
588
|
+
header.AceFlags,
|
|
589
|
+
(OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE) as u8
|
|
590
|
+
);
|
|
591
|
+
assert!(header.AceSize as usize >= size_of::<ACCESS_ALLOWED_ACE>());
|
|
592
|
+
let allowed = ace.cast::<ACCESS_ALLOWED_ACE>();
|
|
593
|
+
let mask = unsafe { std::ptr::addr_of!((*allowed).Mask).read_unaligned() };
|
|
594
|
+
assert_eq!(mask, FILE_ALL_ACCESS);
|
|
595
|
+
let sid = unsafe { std::ptr::addr_of!((*allowed).SidStart).cast_mut().cast() };
|
|
596
|
+
let matches_user = unsafe { EqualSid(sid, user.sid) } != 0;
|
|
597
|
+
let matches_system = unsafe { EqualSid(sid, system.sid) } != 0;
|
|
598
|
+
if user_is_system {
|
|
599
|
+
assert!(matches_user && matches_system, "unexpected ACE SID");
|
|
600
|
+
} else {
|
|
601
|
+
assert_ne!(matches_user, matches_system, "unexpected ACE SID");
|
|
602
|
+
}
|
|
603
|
+
assert!(!(matches_user && saw_user), "duplicate TokenUser ACE");
|
|
604
|
+
assert!(!(matches_system && saw_system), "duplicate LocalSystem ACE");
|
|
605
|
+
saw_user |= matches_user;
|
|
606
|
+
saw_system |= matches_system;
|
|
607
|
+
}
|
|
608
|
+
assert!(saw_user && saw_system);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
fn apply_legacy_owner_rights_acl(path: &Path) {
|
|
612
|
+
let descriptor_text = "D:P(A;OICI;FA;;;OW)(A;OICI;FA;;;SY)"
|
|
613
|
+
.encode_utf16()
|
|
614
|
+
.chain(Some(0))
|
|
615
|
+
.collect::<Vec<_>>();
|
|
616
|
+
let mut descriptor: PSECURITY_DESCRIPTOR = std::ptr::null_mut();
|
|
617
|
+
assert_ne!(
|
|
618
|
+
unsafe {
|
|
619
|
+
ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
|
620
|
+
descriptor_text.as_ptr(),
|
|
621
|
+
SDDL_REVISION_1,
|
|
622
|
+
&mut descriptor,
|
|
623
|
+
std::ptr::null_mut(),
|
|
624
|
+
)
|
|
625
|
+
},
|
|
626
|
+
0
|
|
627
|
+
);
|
|
628
|
+
let wide = wide_path(path);
|
|
629
|
+
assert_ne!(
|
|
630
|
+
unsafe {
|
|
631
|
+
SetFileSecurityW(
|
|
632
|
+
wide.as_ptr(),
|
|
633
|
+
DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
|
|
634
|
+
descriptor,
|
|
635
|
+
)
|
|
636
|
+
},
|
|
637
|
+
0,
|
|
638
|
+
"{}",
|
|
639
|
+
std::io::Error::last_os_error()
|
|
640
|
+
);
|
|
641
|
+
unsafe {
|
|
642
|
+
LocalFree(descriptor);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
fn create_junction(target: &Path, link: &Path) {
|
|
647
|
+
use std::os::windows::process::CommandExt;
|
|
648
|
+
use windows_sys::Win32::System::Threading::CREATE_NO_WINDOW;
|
|
649
|
+
|
|
650
|
+
let command = std::env::var_os("COMSPEC").unwrap_or_else(|| "cmd.exe".into());
|
|
651
|
+
let output = std::process::Command::new(command)
|
|
652
|
+
.args(["/d", "/c", "mklink", "/J"])
|
|
653
|
+
.arg(link)
|
|
654
|
+
.arg(target)
|
|
655
|
+
.creation_flags(CREATE_NO_WINDOW)
|
|
656
|
+
.output()
|
|
657
|
+
.unwrap();
|
|
658
|
+
assert!(
|
|
659
|
+
output.status.success(),
|
|
660
|
+
"cannot create test junction: stdout={} stderr={}",
|
|
661
|
+
String::from_utf8_lossy(&output.stdout),
|
|
662
|
+
String::from_utf8_lossy(&output.stderr)
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
#[test]
|
|
667
|
+
fn private_acl_is_protected_and_contains_only_token_user_and_system() {
|
|
668
|
+
let temp = tempfile::tempdir().unwrap();
|
|
669
|
+
let directory = temp.path().join("fastctx/runtime/edit-locks");
|
|
670
|
+
ensure_private_directory(&directory, "edit-lock").unwrap();
|
|
671
|
+
for managed in managed_chain(&directory) {
|
|
672
|
+
assert_private_acl(&managed);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
#[test]
|
|
677
|
+
fn generic_private_storage_creates_missing_parent_chain_privately() {
|
|
678
|
+
let temp = tempfile::tempdir().unwrap();
|
|
679
|
+
let parent = temp.path().join(".fastctx");
|
|
680
|
+
let directory = parent.join("jobs");
|
|
681
|
+
ensure_private_directory(&directory, "background job registry").unwrap();
|
|
682
|
+
assert_private_acl(&parent);
|
|
683
|
+
assert_private_acl(&directory);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
#[test]
|
|
687
|
+
fn managed_chain_rejects_a_reparse_anchor_before_creating_children() {
|
|
688
|
+
let temp = tempfile::tempdir().unwrap();
|
|
689
|
+
let home = temp.path().join("home");
|
|
690
|
+
let outside = temp.path().join("outside");
|
|
691
|
+
std::fs::create_dir(&home).unwrap();
|
|
692
|
+
std::fs::create_dir(&outside).unwrap();
|
|
693
|
+
let anchor = home.join(".fastctx");
|
|
694
|
+
create_junction(&outside, &anchor);
|
|
695
|
+
|
|
696
|
+
let directory = anchor.join("jobs");
|
|
697
|
+
let error = ensure_private_directory(&directory, "background job registry").unwrap_err();
|
|
698
|
+
assert!(error.contains("not a private directory"), "{error}");
|
|
699
|
+
assert!(!outside.join("jobs").exists());
|
|
700
|
+
std::fs::remove_dir(anchor).unwrap();
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
#[test]
|
|
704
|
+
fn legacy_owner_rights_acl_is_replaced_in_place() {
|
|
705
|
+
let temp = tempfile::tempdir().unwrap();
|
|
706
|
+
let directory = temp.path().join("fastctx/runtime/edit-locks");
|
|
707
|
+
std::fs::create_dir_all(&directory).unwrap();
|
|
708
|
+
let sentinel = directory.join("existing.lock");
|
|
709
|
+
std::fs::write(&sentinel, b"keep").unwrap();
|
|
710
|
+
for managed in managed_chain(&directory).into_iter().rev() {
|
|
711
|
+
apply_legacy_owner_rights_acl(&managed);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
ensure_private_directory(&directory, "edit-lock").unwrap();
|
|
715
|
+
|
|
716
|
+
assert_eq!(std::fs::read(sentinel).unwrap(), b"keep");
|
|
717
|
+
for managed in managed_chain(&directory) {
|
|
718
|
+
assert_private_acl(&managed);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
#[test]
|
|
723
|
+
fn managed_chain_ignores_an_earlier_profile_component_named_fastctx() {
|
|
724
|
+
let path = PathBuf::from(r"C:\fastctx\profile\AppData\Local\fastctx\runtime\edit-locks");
|
|
725
|
+
assert_eq!(
|
|
726
|
+
managed_chain(&path),
|
|
727
|
+
vec![
|
|
728
|
+
PathBuf::from(r"C:\fastctx\profile\AppData\Local\fastctx"),
|
|
729
|
+
PathBuf::from(r"C:\fastctx\profile\AppData\Local\fastctx\runtime"),
|
|
730
|
+
path,
|
|
731
|
+
]
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
#[test]
|
|
736
|
+
fn permission_denied_error_has_an_in_place_recovery_path() {
|
|
737
|
+
let message = directory_io_error(
|
|
738
|
+
"open",
|
|
739
|
+
Path::new(r"C:\Users\tester\AppData\Local\fastctx"),
|
|
740
|
+
"edit-lock",
|
|
741
|
+
std::io::Error::from_raw_os_error(5),
|
|
742
|
+
);
|
|
743
|
+
assert!(message.contains("owner-only Windows ACL"), "{message}");
|
|
744
|
+
assert!(message.contains("elevated process under the same Windows account"));
|
|
745
|
+
assert!(message.contains("repair that ACL in place"));
|
|
746
|
+
assert!(message.contains("never deletes the directory"));
|
|
747
|
+
assert!(message.contains("If access is still denied"));
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
#[test]
|
|
751
|
+
#[ignore]
|
|
752
|
+
fn issue10_acl_transition_probe() {
|
|
753
|
+
let Some(path) = std::env::var_os("FASTCTX_ISSUE10_PROBE_PATH") else {
|
|
754
|
+
return;
|
|
755
|
+
};
|
|
756
|
+
let output = std::env::var_os("FASTCTX_ISSUE10_PROBE_OUTPUT").unwrap();
|
|
757
|
+
let path = Path::new(&path);
|
|
758
|
+
let result: Result<(), String> = (|| {
|
|
759
|
+
for iteration in 0..64 {
|
|
760
|
+
ensure_private_directory(path, "edit-lock")
|
|
761
|
+
.map_err(|error| format!("iteration {iteration}: {error}"))?;
|
|
762
|
+
let lock_path = path.join("transition-probe.lock");
|
|
763
|
+
super::super::open_lock_file(&lock_path, "edit lock")
|
|
764
|
+
.map_err(|error| format!("iteration {iteration}: {error}"))?;
|
|
765
|
+
}
|
|
766
|
+
Ok(())
|
|
767
|
+
})();
|
|
768
|
+
let message = match &result {
|
|
769
|
+
Ok(()) => "ok".to_string(),
|
|
770
|
+
Err(error) => format!("error: {error}"),
|
|
771
|
+
};
|
|
772
|
+
std::fs::write(output, message).unwrap();
|
|
773
|
+
result.unwrap();
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
#[test]
|
|
777
|
+
fn directory_reparse_point_is_rejected_without_touching_its_target() {
|
|
778
|
+
let temp = tempfile::tempdir().unwrap();
|
|
779
|
+
let target = temp.path().join("outside");
|
|
780
|
+
std::fs::create_dir(&target).unwrap();
|
|
781
|
+
let sentinel = target.join("sentinel");
|
|
782
|
+
std::fs::write(&sentinel, b"outside").unwrap();
|
|
783
|
+
let protected_before = is_dacl_protected(&target);
|
|
784
|
+
let link = temp.path().join("edit-locks");
|
|
785
|
+
create_junction(&target, &link);
|
|
786
|
+
|
|
787
|
+
let error = ensure_private_directory(&link, "edit-lock").unwrap_err();
|
|
788
|
+
assert!(error.contains("not a private directory"), "{error}");
|
|
789
|
+
assert_eq!(std::fs::read(sentinel).unwrap(), b"outside");
|
|
790
|
+
assert_eq!(is_dacl_protected(&target), protected_before);
|
|
791
|
+
std::fs::remove_dir(link).unwrap();
|
|
792
|
+
}
|
|
793
|
+
}
|