xet_runtime/file_utils/
privilege_context.rs1use std::fs::File;
2#[cfg(unix)]
3use std::os::unix::fs::MetadataExt;
4use std::path::Path;
5use std::sync::LazyLock;
6
7#[cfg(unix)]
8use colored::Colorize;
9use tracing::error;
10#[cfg(windows)]
11use winapi::um::{
12 processthreadsapi::GetCurrentProcess,
13 processthreadsapi::OpenProcessToken,
14 securitybaseapi::GetTokenInformation,
15 winnt::{HANDLE, TOKEN_ELEVATION, TOKEN_QUERY, TokenElevation},
16};
17
18#[cfg(test)]
19static mut WARNING_PRINTED: bool = false;
20
21fn is_elevated_impl() -> bool {
23 #[cfg(unix)]
26 {
27 unsafe { libc::geteuid() == 0 }
28 }
29
30 #[cfg(windows)]
31 {
32 let mut token: HANDLE = std::ptr::null_mut();
33 if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) } == 0 {
34 return false;
35 }
36
37 let mut elevation: TOKEN_ELEVATION = unsafe { std::mem::zeroed() };
38 let mut return_length = 0;
39 let success = unsafe {
40 GetTokenInformation(
41 token,
42 TokenElevation,
43 &mut elevation as *mut _ as *mut _,
44 std::mem::size_of::<TOKEN_ELEVATION>() as u32,
45 &mut return_length,
46 )
47 };
48
49 if success == 0 {
50 false
51 } else {
52 elevation.TokenIsElevated != 0
53 }
54 }
55
56 #[cfg(not(any(unix, windows)))]
57 {
58 false
60 }
61}
62
63static IS_ELEVATED: LazyLock<bool> = LazyLock::new(is_elevated_impl);
64
65pub fn is_elevated() -> bool {
66 *IS_ELEVATED
67}
68
69#[derive(Debug, Clone, Copy)]
82pub enum PrivilegedExecutionContext {
83 Regular,
84 Elevated,
85}
86
87impl PrivilegedExecutionContext {
88 pub fn current() -> PrivilegedExecutionContext {
89 match is_elevated() {
90 false => PrivilegedExecutionContext::Regular,
91 true => PrivilegedExecutionContext::Elevated,
92 }
93 }
94
95 pub fn is_elevated(&self) -> bool {
96 match self {
97 PrivilegedExecutionContext::Regular => false,
98 PrivilegedExecutionContext::Elevated => true,
99 }
100 }
101
102 pub fn create_dir_all(&self, path: impl AsRef<Path>) -> std::io::Result<()> {
106 let path = std::env::current_dir()?.join(path);
108 let path = path.as_path();
109
110 let mut root = path;
112 while !root.exists() {
113 let Some(pparent) = root.parent() else {
114 return Err(std::io::Error::new(
115 std::io::ErrorKind::InvalidInput,
116 format!("Path {root:?} has no parent."),
117 ));
118 };
119
120 root = pparent;
121 }
122
123 std::fs::create_dir_all(path).inspect_err(|err| {
125 if err.kind() == std::io::ErrorKind::PermissionDenied {
126 permission_warning(root, true);
127 }
128 })?;
129
130 #[cfg(unix)]
134 if self.is_elevated() {
135 let root_meta = std::fs::metadata(root)?;
136 let mut path = path;
137 while path != root {
138 std::os::unix::fs::chown(path, Some(root_meta.uid()), Some(root_meta.gid()))?;
139 let Some(pparent) = path.parent() else {
140 return Err(std::io::Error::new(
141 std::io::ErrorKind::InvalidInput,
142 format!("Path {path:?} has no parent."),
143 ));
144 };
145 path = pparent;
146 }
147 }
148
149 Ok(())
150 }
151
152 pub fn create_file(&self, path: impl AsRef<Path>) -> std::io::Result<File> {
156 let path = std::env::current_dir()?.join(path);
158 let path = path.as_path();
159
160 let Some(pparent) = path.parent() else {
161 return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("Path {path:?} has no parent.")));
162 };
163
164 self.create_dir_all(pparent)?;
165
166 #[allow(unused_variables)]
167 let parent_meta = std::fs::metadata(pparent)?;
168
169 #[cfg(unix)]
170 let exist = path.exists();
171
172 let create = || {
173 std::fs::OpenOptions::new()
174 .create(true)
175 .truncate(false)
176 .write(true)
177 .open(path)
178 .inspect_err(|err| {
179 if err.kind() == std::io::ErrorKind::PermissionDenied {
180 permission_warning(path, false);
181 }
182 })
183 };
184
185 create()?;
187
188 #[cfg(unix)]
192 if !exist && self.is_elevated() {
193 std::os::unix::fs::chown(path, Some(parent_meta.uid()), Some(parent_meta.gid()))?;
195 }
196
197 create()
199 }
200}
201
202pub fn create_dir_all(path: impl AsRef<Path>) -> std::io::Result<()> {
203 PrivilegedExecutionContext::current().create_dir_all(path)
204}
205
206pub fn create_file(path: impl AsRef<Path>) -> std::io::Result<File> {
207 PrivilegedExecutionContext::current().create_file(path)
208}
209
210#[allow(unused_variables)]
211fn permission_warning(path: &Path, recursive: bool) {
212 #[cfg(unix)]
213 {
214 let username = whoami::username().unwrap_or_else(|_| "unknown".to_string());
215 let message = format!(
216 "The process doesn't have correct read-write permission into path {path:?}, please resets
217 ownership by 'sudo chown{}{} {path:?}'.",
218 if recursive { " -R " } else { " " },
219 username
220 );
221
222 eprintln!("{}", message.bright_blue());
223 }
224
225 #[cfg(windows)]
226 eprintln!(
227 "The process doesn't have correct read-write permission into path {path:?}, please resets
228 permission in the Properties dialog box under the Security tab."
229 );
230
231 error!("Permission denied for path {path:?}");
232
233 #[cfg(test)]
234 unsafe {
235 WARNING_PRINTED = true
236 };
237}
238
239#[cfg(all(test, unix))]
240mod test {
241 use std::os::unix::fs::MetadataExt;
242 use std::path::Path;
243
244 use anyhow::Result;
245
246 use super::{PrivilegedExecutionContext, WARNING_PRINTED};
247
248 #[test]
249 #[ignore = "run manually"]
250 fn test_create_dir_all() -> Result<()> {
251 let test_path = std::env::var("HF_XET_TEST_PATH")?;
263 std::env::set_current_dir(test_path)?;
264 let permission = PrivilegedExecutionContext::current();
265
266 let test = Path::new("rootdir/regdir1/regdir2");
267
268 assert!(permission.create_dir_all(test).is_err());
269 unsafe { assert!(WARNING_PRINTED) };
270
271 Ok(())
272 }
273
274 #[test]
275 #[ignore = "run manually"]
276 fn test_create_dir_all_sudo() -> Result<()> {
277 let test_path = std::env::var("HF_XET_TEST_PATH")?;
290 std::env::set_current_dir(test_path)?;
291 let permission = PrivilegedExecutionContext::current();
292
293 let test = Path::new("regdir/regdir1/regdir2");
294
295 permission.create_dir_all(test)?;
296
297 assert!(test.exists());
298
299 assert!(std::fs::metadata(test)?.uid() != 0);
301
302 let parent = test.parent().unwrap();
303
304 assert!(std::fs::metadata(parent)?.uid() != 0);
306
307 Ok(())
308 }
309
310 #[test]
311 #[ignore = "run manually"]
312 fn test_create_file() -> Result<()> {
313 let test_path = std::env::var("HF_XET_TEST_PATH")?;
327 std::env::set_current_dir(test_path)?;
328 let permission = PrivilegedExecutionContext::current();
329
330 let test1 = Path::new("rootdir/regdir1/regdir2/file");
331
332 assert!(permission.create_file(test1).is_err());
333 unsafe { assert!(WARNING_PRINTED) };
334
335 unsafe { WARNING_PRINTED = false };
336
337 let test2 = Path::new("rootdir/file");
338 assert!(permission.create_file(test2).is_err());
339 unsafe { assert!(WARNING_PRINTED) };
340
341 Ok(())
342 }
343
344 #[test]
345 #[ignore = "run manually"]
346 fn test_create_file_sudo() -> Result<()> {
347 let test_path = std::env::var("HF_XET_TEST_PATH")?;
359 std::env::set_current_dir(test_path)?;
360
361 let test = Path::new("regdir/regdir1/regdir2/file");
362
363 let permission = PrivilegedExecutionContext::current();
364 permission.create_file(test)?;
365
366 assert!(test.exists());
367
368 assert!(std::fs::metadata(test)?.uid() != 0);
370
371 let parent = test.parent().unwrap();
372
373 assert!(std::fs::metadata(parent)?.uid() != 0);
375
376 Ok(())
377 }
378}