1use std::collections::BTreeMap;
4use std::ffi::{OsStr, OsString};
5use std::fs::{self, File, OpenOptions};
6use std::io::{self, Read, Write};
7use std::path::{Component, Path, PathBuf};
8use std::time::{Duration, SystemTime, UNIX_EPOCH};
9
10use anyhow::{Context, Result, anyhow, bail};
11use fs2::FileExt;
12
13use crate::paths::validate_no_escaping_symlink_ancestors;
14
15#[path = "../vtcode_paths_migration.rs"]
16mod migration;
17pub use migration::{
18 LegacyMigrator, MigrationEntry, MigrationFailure, MigrationReport, MigrationSkip, MigrationSkipReason,
19};
20
21const APP: &str = "vtcode";
22const MARKER: &str = "legacy-v1.complete";
23const PRIVATE_FILE_LOCK_ATTEMPTS: usize = 100;
24const PRIVATE_FILE_LOCK_DELAY: Duration = Duration::from_millis(10);
25
26struct NativeRoots {
27 config_dir: PathBuf,
28 data_dir: PathBuf,
29 state_dir: PathBuf,
30 cache_dir: PathBuf,
31 runtime_dir: Option<PathBuf>,
32 executable_dir: PathBuf,
33}
34
35fn native_roots(
36 #[cfg_attr(
37 target_os = "windows",
38 allow(
39 unused_variables,
40 reason = "home_dir is only consumed by macOS and XDG root resolution; Windows uses known-folder APIs"
41 )
42 )]
43 home_dir: &Path,
44) -> Result<NativeRoots> {
45 #[cfg(target_os = "macos")]
46 {
47 let root = dirs::data_local_dir()
48 .ok_or_else(|| anyhow!("could not determine the macOS application support directory"))?
49 .join("com.vinhnx.vtcode");
50 Ok(NativeRoots {
51 config_dir: root.clone(),
52 data_dir: root.clone(),
53 state_dir: root.join("state"),
54 cache_dir: dirs::cache_dir()
55 .ok_or_else(|| anyhow!("could not determine the macOS cache directory"))?
56 .join("com.vinhnx.vtcode"),
57 runtime_dir: None,
58 executable_dir: home_dir.join(".local/bin"),
59 })
60 }
61 #[cfg(target_os = "windows")]
62 {
63 let root = dirs::data_dir()
64 .ok_or_else(|| anyhow!("could not determine the Windows application data directory"))?
65 .join("vinhnx")
66 .join(APP);
67 Ok(NativeRoots {
68 config_dir: root.join("config"),
69 data_dir: root.join("data"),
70 state_dir: root.join("state"),
71 cache_dir: root.join("cache"),
72 runtime_dir: None,
73 executable_dir: root.join("bin"),
74 })
75 }
76 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
77 {
78 Ok(NativeRoots {
79 config_dir: home_dir.join(".config").join(APP),
80 data_dir: home_dir.join(".local/share").join(APP),
81 state_dir: home_dir.join(".local/state").join(APP),
82 cache_dir: home_dir.join(".cache").join(APP),
83 runtime_dir: None,
84 executable_dir: home_dir.join(".local/bin"),
85 })
86 }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct VtCodePaths {
92 config_dir: PathBuf,
93 data_dir: PathBuf,
94 state_dir: PathBuf,
95 cache_dir: PathBuf,
96 runtime_dir: PathBuf,
97 executable_dir: PathBuf,
98 system_config_dirs: Vec<PathBuf>,
99 system_data_dirs: Vec<PathBuf>,
100 legacy_home_dir: PathBuf,
101}
102
103impl VtCodePaths {
104 pub fn from_env() -> Result<Self> {
106 Self::from_environment_os(&std::env::vars_os().collect())
107 }
108
109 pub fn resolve() -> Result<Self> {
111 Self::from_env()
112 }
113
114 pub fn from_environment(environment: &[(&str, &str)]) -> Result<Self> {
116 Self::from_environment_os(
117 &environment
118 .iter()
119 .map(|(key, value)| (OsString::from(key), OsString::from(value)))
120 .collect(),
121 )
122 }
123
124 fn from_environment_os(environment: &BTreeMap<OsString, OsString>) -> Result<Self> {
125 let home_dir = environment
126 .get(OsStr::new("HOME"))
127 .filter(|value| !value.is_empty())
128 .map(PathBuf::from)
129 .filter(|path| path.is_absolute())
130 .or_else(dirs::home_dir)
131 .ok_or_else(|| anyhow!("could not determine the user home directory"))?;
132 let native = native_roots(&home_dir)?;
133 let home_override = env_path(environment, "VTCODE_HOME");
134 if let Some(path) = &home_override {
135 validate_absolute("VTCODE_HOME", path)?;
136 }
137 let legacy_home_dir = home_override.clone().unwrap_or_else(|| home_dir.join(".vtcode"));
138
139 let config_dir = match env_path(environment, "VTCODE_CONFIG") {
140 Some(path) => {
141 validate_absolute("VTCODE_CONFIG", &path)?;
142 path
143 }
144 None => xdg_app_dir(environment, "XDG_CONFIG_HOME", &native.config_dir)?,
145 };
146 let data_dir = match env_path(environment, "VTCODE_DATA") {
147 Some(path) => {
148 validate_absolute("VTCODE_DATA", &path)?;
149 path
150 }
151 None => xdg_app_dir(environment, "XDG_DATA_HOME", &native.data_dir)?,
152 };
153 let state_dir = xdg_app_dir(environment, "XDG_STATE_HOME", &native.state_dir)?;
154 let cache_dir = xdg_app_dir(environment, "XDG_CACHE_HOME", &native.cache_dir)?;
155 let runtime_dir = match () {
156 _ if is_xdg_platform() => match env_path(environment, "XDG_RUNTIME_DIR") {
157 Some(path) if path.is_absolute() => path.join(APP),
158 None => state_dir.join("runtime"),
159 Some(_) => state_dir.join("runtime"),
160 },
161 _ => native.runtime_dir.unwrap_or_else(|| state_dir.join("runtime")),
162 };
163 for (name, path) in [
164 ("configuration directory", &config_dir),
165 ("data directory", &data_dir),
166 ("state directory", &state_dir),
167 ("cache directory", &cache_dir),
168 ("runtime directory", &runtime_dir),
169 ] {
170 validate_absolute(name, path)?;
171 }
172 Ok(Self {
173 config_dir,
174 data_dir,
175 state_dir,
176 cache_dir,
177 runtime_dir,
178 executable_dir: executable_dir(environment, native.executable_dir)?,
179 system_config_dirs: system_config_dirs(environment)?,
180 system_data_dirs: system_data_dirs(environment)?,
181 legacy_home_dir,
182 })
183 }
184
185 pub fn config_dir(&self) -> &Path {
187 &self.config_dir
188 }
189 pub fn data_dir(&self) -> &Path {
191 &self.data_dir
192 }
193 pub fn state_dir(&self) -> &Path {
195 &self.state_dir
196 }
197 pub fn cache_dir(&self) -> &Path {
199 &self.cache_dir
200 }
201 pub fn runtime_dir(&self) -> &Path {
203 &self.runtime_dir
204 }
205 pub fn executable_dir(&self) -> &Path {
207 &self.executable_dir
208 }
209 pub fn system_config_dirs(&self) -> &[PathBuf] {
211 &self.system_config_dirs
212 }
213 pub fn system_data_dirs(&self) -> &[PathBuf] {
215 &self.system_data_dirs
216 }
217 pub fn system_config_paths(&self, relative: impl AsRef<Path>) -> Result<Vec<PathBuf>> {
221 let relative = relative.as_ref();
222 validate_relative_path("system configuration path", relative)?;
223 let mut paths = if cfg!(unix) {
224 vec![PathBuf::from("/etc/vtcode").join(relative)]
225 } else {
226 Vec::new()
227 };
228 paths.extend(self.system_config_dirs.iter().rev().map(|base| base.join(APP).join(relative)));
229 paths.dedup();
230 Ok(paths)
231 }
232 pub fn system_data_paths(&self, relative: impl AsRef<Path>) -> Result<Vec<PathBuf>> {
234 let relative = relative.as_ref();
235 validate_relative_path("system data path", relative)?;
236 Ok(self.system_data_dirs.iter().map(|base| base.join(APP).join(relative)).collect())
237 }
238 pub fn legacy_home_dir(&self) -> &Path {
240 &self.legacy_home_dir
241 }
242
243 pub fn legacy_dir(&self) -> &Path {
245 self.legacy_home_dir()
246 }
247
248 pub fn config_path(&self, relative: impl AsRef<Path>) -> Result<PathBuf> {
250 child_path(&self.config_dir, relative.as_ref(), "configuration")
251 }
252
253 pub fn data_path(&self, relative: impl AsRef<Path>) -> Result<PathBuf> {
255 child_path(&self.data_dir, relative.as_ref(), "data")
256 }
257
258 pub fn state_path(&self, relative: impl AsRef<Path>) -> Result<PathBuf> {
260 child_path(&self.state_dir, relative.as_ref(), "state")
261 }
262
263 pub fn cache_path(&self, relative: impl AsRef<Path>) -> Result<PathBuf> {
265 child_path(&self.cache_dir, relative.as_ref(), "cache")
266 }
267
268 pub fn runtime_path(&self, relative: impl AsRef<Path>) -> Result<PathBuf> {
270 child_path(&self.runtime_dir, relative.as_ref(), "runtime")
271 }
272
273 pub fn executable_path(&self, relative: impl AsRef<Path>) -> Result<PathBuf> {
275 child_path(&self.executable_dir, relative.as_ref(), "executable")
276 }
277
278 pub fn config_file(&self) -> PathBuf {
280 self.config_dir.join("vtcode.toml")
281 }
282 pub fn skills_dir(&self) -> PathBuf {
284 self.data_dir.join("skills")
285 }
286 pub fn plugins_dir(&self) -> PathBuf {
288 self.data_dir.join("plugins")
289 }
290 pub fn auth_dir(&self) -> PathBuf {
292 self.config_dir.join("auth")
293 }
294 pub fn auth_file(&self) -> PathBuf {
296 self.auth_dir().join("auth.json")
297 }
298 pub fn logs_dir(&self) -> PathBuf {
300 self.state_dir.join("logs")
301 }
302 pub fn sessions_dir(&self) -> PathBuf {
304 self.state_dir.join("sessions")
305 }
306 pub fn telemetry_dir(&self) -> PathBuf {
308 self.state_dir.join("telemetry")
309 }
310 pub fn migration_marker_path(&self) -> PathBuf {
312 self.state_dir.join("migration").join(MARKER)
313 }
314
315 pub fn migration_report_path(&self) -> PathBuf {
317 self.state_dir.join("migration").join("legacy-v1.json")
318 }
319
320 pub fn ensure_runtime_dir(&self) -> Result<&Path> {
322 ensure_private_dir(&self.runtime_dir).context("could not create VT Code runtime directory")?;
323 Ok(&self.runtime_dir)
324 }
325
326 pub fn ensure_config_dir(&self) -> Result<&Path> {
329 ensure_user_dir(&self.config_dir).context("could not create VT Code configuration directory")?;
330 Ok(&self.config_dir)
331 }
332
333 pub fn ensure_data_dir(&self) -> Result<&Path> {
336 ensure_user_dir(&self.data_dir).context("could not create VT Code data directory")?;
337 Ok(&self.data_dir)
338 }
339
340 pub fn ensure_state_dir(&self) -> Result<&Path> {
343 ensure_user_dir(&self.state_dir).context("could not create VT Code state directory")?;
344 Ok(&self.state_dir)
345 }
346
347 pub fn ensure_cache_dir(&self) -> Result<&Path> {
350 ensure_user_dir(&self.cache_dir).context("could not create VT Code cache directory")?;
351 Ok(&self.cache_dir)
352 }
353
354 pub fn ensure_executable_dir(&self) -> Result<&Path> {
357 ensure_user_dir(&self.executable_dir).context("could not create VT Code executable directory")?;
358 Ok(&self.executable_dir)
359 }
360
361 pub fn ensure_user_dir(path: impl AsRef<Path>) -> Result<PathBuf> {
365 let path = path.as_ref();
366 ensure_user_dir(path).with_context(|| format!("could not create user directory {}", path.display()))?;
367 Ok(path.to_path_buf())
368 }
369
370 pub fn create_private_file(path: impl AsRef<Path>) -> Result<File> {
376 let path = path.as_ref();
377 ensure_file_parent(path)?;
378 create_private_new_file(path).with_context(|| format!("could not create private file {}", path.display()))
379 }
380
381 pub fn open_private_append_file(path: impl AsRef<Path>) -> Result<File> {
383 let path = path.as_ref();
384 ensure_file_parent(path)?;
385 open_private_append(path).with_context(|| format!("could not open private file {}", path.display()))
386 }
387
388 pub fn read_file_no_follow(path: impl AsRef<Path>) -> Result<Vec<u8>> {
390 let path = path.as_ref();
391 validate_no_escaping_symlink_ancestors(path, false)
392 .with_context(|| format!("could not validate file path {}", path.display()))?;
393 let mut file = open_no_follow(path).with_context(|| format!("could not open file {}", path.display()))?;
394 let metadata = file
395 .metadata()
396 .with_context(|| format!("could not inspect file {}", path.display()))?;
397 if !metadata.is_file() {
398 bail!("{} is not a regular file", path.display());
399 }
400 let mut contents = Vec::new();
401 let _bytes_read = file
402 .read_to_end(&mut contents)
403 .with_context(|| format!("could not read file {}", path.display()))?;
404 Ok(contents)
405 }
406
407 pub fn write_private_file_atomic(path: impl AsRef<Path>, contents: &[u8]) -> Result<()> {
413 let destination = path.as_ref();
414 ensure_file_parent(destination)?;
415 validate_file_destination(destination)?;
416 let parent = destination
417 .parent()
418 .ok_or_else(|| anyhow!("private file {} has no parent", destination.display()))?;
419 let stem = destination.file_name().unwrap_or_else(|| OsStr::new("file"));
420 let (temporary, mut file) = unique_private_file(parent, stem)?;
421 let result: io::Result<()> = (|| {
422 file.write_all(contents)?;
423 file.sync_all()?;
424 drop(file);
425 fs::rename(&temporary, destination)
428 })();
429 if result.is_err() {
430 remove_temporary_file(&temporary);
431 }
432 result.with_context(|| format!("could not atomically write {}", destination.display()))
433 }
434
435 pub fn write_private_file_atomic_if_absent(path: impl AsRef<Path>, contents: &[u8]) -> Result<bool> {
442 let destination = path.as_ref();
443 ensure_file_parent(destination)?;
444 validate_file_destination(destination)?;
445 if fs::symlink_metadata(destination).is_ok() {
446 return Ok(false);
447 }
448
449 let parent = destination
450 .parent()
451 .ok_or_else(|| anyhow!("private file {} has no parent", destination.display()))?;
452 let stem = destination.file_name().unwrap_or_else(|| OsStr::new("file"));
453 let (temporary, mut file) = unique_private_file(parent, stem)?;
454 let result: io::Result<bool> = (|| {
455 file.write_all(contents)?;
456 file.sync_all()?;
457 drop(file);
458 match fs::hard_link(&temporary, destination) {
459 Ok(()) => Ok(true),
460 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => Ok(false),
461 Err(error) => Err(error),
462 }
463 })();
464 remove_temporary_file(&temporary);
465 result.with_context(|| format!("could not atomically create {}", destination.display()))
466 }
467
468 pub fn with_private_file_lock<T>(path: impl AsRef<Path>, operation: impl FnOnce() -> Result<T>) -> Result<T> {
474 let destination = path.as_ref();
475 ensure_file_parent(destination)?;
476 let parent = destination
477 .parent()
478 .ok_or_else(|| anyhow!("private file {} has no parent", destination.display()))?;
479 let stem = destination.file_name().unwrap_or_else(|| OsStr::new("file"));
480 let lock_path = parent.join(format!(".{}.lock", stem.to_string_lossy()));
481 let _lock = acquire_private_file_lock(&lock_path)?;
482 operation()
483 }
484
485 pub fn ensure_runtime_child_dir(&self, relative: impl AsRef<Path>) -> Result<PathBuf> {
487 let path = self.runtime_path(relative)?;
488 ensure_private_dir(&path).context("could not create VT Code runtime child directory")?;
489 Ok(path)
490 }
491
492 pub fn ensure_config_child_dir(&self, relative: impl AsRef<Path>) -> Result<PathBuf> {
494 let path = self.config_path(relative)?;
495 ensure_user_dir(&path).context("could not create VT Code configuration child directory")?;
496 Ok(path)
497 }
498
499 pub fn ensure_data_child_dir(&self, relative: impl AsRef<Path>) -> Result<PathBuf> {
501 let path = self.data_path(relative)?;
502 ensure_user_dir(&path).context("could not create VT Code data child directory")?;
503 Ok(path)
504 }
505
506 pub fn ensure_state_child_dir(&self, relative: impl AsRef<Path>) -> Result<PathBuf> {
508 let path = self.state_path(relative)?;
509 ensure_user_dir(&path).context("could not create VT Code state child directory")?;
510 Ok(path)
511 }
512
513 pub fn ensure_cache_child_dir(&self, relative: impl AsRef<Path>) -> Result<PathBuf> {
515 let path = self.cache_path(relative)?;
516 ensure_user_dir(&path).context("could not create VT Code cache child directory")?;
517 Ok(path)
518 }
519
520 pub fn ensure_executable_child_dir(&self, relative: impl AsRef<Path>) -> Result<PathBuf> {
522 let path = self.executable_path(relative)?;
523 ensure_user_dir(&path).context("could not create VT Code executable child directory")?;
524 Ok(path)
525 }
526
527 pub fn ensure_auth_dir(&self) -> Result<PathBuf> {
529 let path = self.auth_dir();
530 ensure_private_dir(&path).context("could not create VT Code authentication directory")?;
531 Ok(path)
532 }
533
534 pub fn create_auth_file(&self, name: impl AsRef<str>) -> Result<PathBuf> {
536 let name = name.as_ref();
537 if !is_safe_file_name(name) {
538 bail!("authentication file name '{name}' must be one normal path component");
539 }
540 let path = self.ensure_auth_dir()?.join(name);
541 let _file = create_private_new_file(&path)
542 .with_context(|| format!("could not create authentication file {}", path.display()))?;
543 Ok(path)
544 }
545
546 pub fn migrate_legacy(&self) -> Result<MigrationReport> {
548 LegacyMigrator::new(self.clone()).run()
549 }
550}
551
552fn env_path(environment: &BTreeMap<OsString, OsString>, name: &str) -> Option<PathBuf> {
553 environment
554 .get(OsStr::new(name))
555 .filter(|value| !value.is_empty())
556 .and_then(|value| {
557 if let Some(text) = value.to_str() {
558 let trimmed = text.trim();
559 (!trimmed.is_empty()).then(|| PathBuf::from(trimmed))
560 } else {
561 Some(PathBuf::from(value))
562 }
563 })
564}
565fn xdg_app_dir(environment: &BTreeMap<OsString, OsString>, name: &str, native: &Path) -> Result<PathBuf> {
566 if is_xdg_platform()
567 && let Some(path) = env_path(environment, name)
568 && path.is_absolute()
569 {
570 return Ok(path.join(APP));
571 }
572 Ok(native.to_path_buf())
573}
574fn executable_dir(environment: &BTreeMap<OsString, OsString>, native: PathBuf) -> Result<PathBuf> {
575 if is_xdg_platform()
576 && let Some(path) = env_path(environment, "XDG_BIN_HOME")
577 && path.is_absolute()
578 {
579 return Ok(path);
580 }
581 Ok(native)
582}
583fn system_config_dirs(environment: &BTreeMap<OsString, OsString>) -> Result<Vec<PathBuf>> {
584 if !is_xdg_platform() {
585 return Ok(Vec::new());
586 }
587 let configured = environment.get(OsStr::new("XDG_CONFIG_DIRS")).map(OsString::as_os_str);
588 let mut paths = configured
589 .into_iter()
590 .flat_map(std::env::split_paths)
591 .filter(|path| path.is_absolute())
592 .collect::<Vec<_>>();
593 if paths.is_empty() {
594 paths.push(PathBuf::from("/etc/xdg"));
595 }
596 Ok(paths)
597}
598fn system_data_dirs(environment: &BTreeMap<OsString, OsString>) -> Result<Vec<PathBuf>> {
599 if !is_xdg_platform() {
600 return Ok(Vec::new());
601 }
602 let configured = environment.get(OsStr::new("XDG_DATA_DIRS")).map(OsString::as_os_str);
603 let mut paths = configured
604 .into_iter()
605 .flat_map(std::env::split_paths)
606 .filter(|path| path.is_absolute())
607 .collect::<Vec<_>>();
608 if paths.is_empty() {
609 paths.extend([PathBuf::from("/usr/local/share"), PathBuf::from("/usr/share")]);
610 }
611 Ok(paths)
612}
613fn validate_absolute(name: &str, path: &Path) -> Result<()> {
614 if path.is_absolute() {
615 Ok(())
616 } else {
617 bail!("{name} must be an absolute path, got '{}'", path.display())
618 }
619}
620fn validate_relative_path(name: &str, path: &Path) -> Result<()> {
621 if !path.as_os_str().is_empty() && path.components().all(|component| matches!(component, Component::Normal(_))) {
622 Ok(())
623 } else {
624 bail!("{name} must be a non-empty relative path without traversal, got '{}'", path.display())
625 }
626}
627
628fn child_path(root: &Path, relative: &Path, category: &str) -> Result<PathBuf> {
629 validate_relative_path(&format!("{category} child path"), relative)?;
630 Ok(root.join(relative))
631}
632const fn is_xdg_platform() -> bool {
633 cfg!(any(
634 target_os = "linux",
635 target_os = "freebsd",
636 target_os = "netbsd",
637 target_os = "openbsd",
638 target_os = "dragonfly"
639 ))
640}
641fn is_safe_file_name(name: &str) -> bool {
642 let mut parts = Path::new(name).components();
643 matches!(parts.next(), Some(Component::Normal(_))) && parts.next().is_none()
644}
645fn ensure_private_dir(path: &Path) -> io::Result<()> {
646 match fs::symlink_metadata(path) {
647 Ok(metadata) if metadata.file_type().is_symlink() => {
648 return Err(io::Error::other(format!("refusing symlink directory {}", path.display())));
649 }
650 Ok(metadata) if !metadata.is_dir() => {
651 return Err(io::Error::other(format!("{} is not a directory", path.display())));
652 }
653 Ok(_) => {}
654 Err(error) if error.kind() == io::ErrorKind::NotFound => {
655 ensure_private_parent_dir(path)?;
656 create_private_dir(path)?;
657 }
658 Err(error) => return Err(error),
659 }
660 set_private_permissions(path)
661}
662
663fn ensure_user_dir(path: &Path) -> io::Result<()> {
667 validate_no_escaping_symlink_ancestors(path, true)?;
668 match fs::symlink_metadata(path) {
669 Ok(metadata) if metadata.file_type().is_symlink() => {
670 return Err(io::Error::other(format!("refusing symlink directory {}", path.display())));
671 }
672 Ok(metadata) if !metadata.is_dir() => {
673 return Err(io::Error::other(format!("{} is not a directory", path.display())));
674 }
675 Ok(_) => return Ok(()),
676 Err(error) if error.kind() == io::ErrorKind::NotFound => {}
677 Err(error) => return Err(error),
678 }
679
680 let Some(parent) = path.parent() else {
681 return Err(io::Error::other(format!("{} has no parent directory", path.display())));
682 };
683 if parent != path {
684 ensure_user_dir(parent)?;
685 }
686 match fs::symlink_metadata(path) {
687 Ok(metadata) if metadata.file_type().is_symlink() => {
688 Err(io::Error::other(format!("refusing symlink directory {}", path.display())))
689 }
690 Ok(metadata) if !metadata.is_dir() => Err(io::Error::other(format!("{} is not a directory", path.display()))),
691 Ok(_) => Ok(()),
692 Err(error) if error.kind() == io::ErrorKind::NotFound => create_user_dir(path),
693 Err(error) => Err(error),
694 }
695}
696
697fn ensure_private_parent_dir(path: &Path) -> io::Result<()> {
698 validate_no_escaping_symlink_ancestors(path, true)?;
699 let Some(parent) = path.parent() else {
700 return Ok(());
701 };
702 if parent == path {
703 return Ok(());
704 }
705 match fs::symlink_metadata(parent) {
706 Ok(metadata) if metadata.file_type().is_symlink() => {
707 Err(io::Error::other(format!("refusing symlink directory {}", parent.display())))
708 }
709 Ok(metadata) if !metadata.is_dir() => Err(io::Error::other(format!("{} is not a directory", parent.display()))),
710 Ok(_) => Ok(()),
711 Err(error) if error.kind() == io::ErrorKind::NotFound => {
712 ensure_private_parent_dir(parent)?;
713 create_user_dir(parent)
714 }
715 Err(error) => Err(error),
716 }
717}
718
719fn ensure_migration_dir(path: &Path) -> io::Result<()> {
722 ensure_user_dir(path)
723}
724
725fn create_user_dir(path: &Path) -> io::Result<()> {
726 match fs::create_dir(path) {
727 Ok(()) => set_private_permissions(path),
728 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => match fs::symlink_metadata(path) {
729 Ok(metadata) if metadata.file_type().is_symlink() => {
730 Err(io::Error::other(format!("refusing symlink directory {}", path.display())))
731 }
732 Ok(metadata) if !metadata.is_dir() => {
733 Err(io::Error::other(format!("{} is not a directory", path.display())))
734 }
735 Ok(_) => Ok(()),
736 Err(error) => Err(error),
737 },
738 Err(error) => Err(error),
739 }
740}
741
742fn create_private_dir(path: &Path) -> io::Result<()> {
743 match fs::create_dir(path) {
744 Ok(()) => Ok(()),
745 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => match fs::symlink_metadata(path) {
746 Ok(metadata) if metadata.file_type().is_symlink() => {
747 Err(io::Error::other(format!("refusing symlink directory {}", path.display())))
748 }
749 Ok(metadata) if !metadata.is_dir() => {
750 Err(io::Error::other(format!("{} is not a directory", path.display())))
751 }
752 Ok(_) => Ok(()),
753 Err(error) => Err(error),
754 },
755 Err(error) => Err(error),
756 }
757}
758
759fn create_private_new_file(path: &Path) -> io::Result<File> {
760 let mut options = OpenOptions::new();
761 let _ = options.write(true).create_new(true);
762 #[cfg(unix)]
763 {
764 use std::os::unix::fs::OpenOptionsExt;
765 let _ = options.mode(0o600).custom_flags(libc::O_NOFOLLOW);
766 }
767 options.open(path)
768}
769fn open_no_follow(path: &Path) -> io::Result<File> {
770 let mut options = OpenOptions::new();
771 let _ = options.read(true);
772 #[cfg(unix)]
773 {
774 use std::os::unix::fs::OpenOptionsExt;
775 let _ = options.custom_flags(libc::O_NOFOLLOW);
776 }
777 options.open(path)
778}
779
780fn open_private_append(path: &Path) -> io::Result<File> {
781 let mut options = OpenOptions::new();
782 let _ = options.create(true).append(true).read(true).write(true);
783 #[cfg(unix)]
784 {
785 use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
786 let _ = options.mode(0o600).custom_flags(libc::O_NOFOLLOW);
787 let file = options.open(path)?;
788 file.set_permissions(fs::Permissions::from_mode(0o600))?;
789 Ok(file)
790 }
791 #[cfg(not(unix))]
792 options.open(path)
793}
794
795fn ensure_file_parent(path: &Path) -> Result<()> {
796 let parent = path
797 .parent()
798 .ok_or_else(|| anyhow!("file {} has no parent directory", path.display()))?;
799 let parent = if parent.as_os_str().is_empty() {
800 Path::new(".")
801 } else {
802 parent
803 };
804 ensure_user_dir(parent).with_context(|| format!("could not create file parent {}", parent.display()))?;
805 Ok(())
806}
807
808fn validate_file_destination(path: &Path) -> Result<()> {
809 match fs::symlink_metadata(path) {
810 Ok(metadata) if metadata.file_type().is_symlink() => {
811 bail!("refusing to replace symlinked file {}", path.display())
812 }
813 Ok(metadata) if !metadata.is_file() => bail!("{} is not a regular file", path.display()),
814 Ok(_) => Ok(()),
815 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
816 Err(error) => Err(error).with_context(|| format!("could not inspect {}", path.display())),
817 }
818}
819
820fn unique_private_file(parent: &Path, stem: &OsStr) -> Result<(PathBuf, File)> {
821 let timestamp = SystemTime::now()
822 .duration_since(UNIX_EPOCH)
823 .map(|duration| duration.as_nanos())
824 .unwrap_or_default();
825 let stem = stem.to_string_lossy();
826 for attempt in 0..32u8 {
827 let temporary = parent.join(format!(".{stem}.{}.{}.{}.tmp", std::process::id(), timestamp, attempt));
828 match create_private_new_file(&temporary) {
829 Ok(file) => return Ok((temporary, file)),
830 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
831 Err(error) => return Err(error).with_context(|| format!("could not create {}", temporary.display())),
832 }
833 }
834 bail!("could not allocate a unique private temporary file in {}", parent.display())
835}
836
837struct PrivateFileLock {
838 _file: File,
839}
840
841fn acquire_private_file_lock(path: &Path) -> Result<PrivateFileLock> {
842 let file = open_private_lock_file(path)?;
843 for attempt in 0..PRIVATE_FILE_LOCK_ATTEMPTS {
844 match file.try_lock_exclusive() {
845 Ok(()) => return Ok(PrivateFileLock { _file: file }),
846 Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
847 if attempt + 1 < PRIVATE_FILE_LOCK_ATTEMPTS {
848 std::thread::sleep(PRIVATE_FILE_LOCK_DELAY);
849 }
850 }
851 Err(error) => return Err(error).with_context(|| format!("could not lock {}", path.display())),
852 }
853 }
854 bail!("timed out waiting for private file lock {}", path.display())
855}
856
857fn open_private_lock_file(path: &Path) -> Result<File> {
858 match create_private_lock_file(path) {
859 Ok(file) => Ok(file),
860 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
861 validate_file_destination(path)?;
862 let mut options = OpenOptions::new();
863 let _ = options.read(true).write(true);
864 #[cfg(unix)]
865 {
866 use std::os::unix::fs::OpenOptionsExt;
867 let _ = options.custom_flags(libc::O_NOFOLLOW);
868 }
869 options
870 .open(path)
871 .with_context(|| format!("could not open private lock {}", path.display()))
872 }
873 Err(error) => Err(error).with_context(|| format!("could not create lock {}", path.display())),
874 }
875}
876
877fn create_private_lock_file(path: &Path) -> io::Result<File> {
878 let mut options = OpenOptions::new();
879 let _ = options.read(true).write(true).create_new(true);
880 #[cfg(unix)]
881 {
882 use std::os::unix::fs::OpenOptionsExt;
883 let _ = options.mode(0o600).custom_flags(libc::O_NOFOLLOW);
884 }
885 options.open(path)
886}
887
888impl Drop for PrivateFileLock {
889 fn drop(&mut self) {
890 if let Err(error) = self._file.unlock() {
891 tracing::debug!(%error, "failed to release private file lock");
892 }
893 }
894}
895
896fn remove_temporary_file(path: &Path) {
897 if let Err(error) = fs::remove_file(path)
898 && error.kind() != io::ErrorKind::NotFound
899 {
900 tracing::debug!(path = %path.display(), %error, "failed to remove private temporary file");
901 }
902}
903#[cfg_attr(
904 not(unix),
905 allow(unused_variables, reason = "only the Unix permission block consumes the path")
906)]
907fn set_private_permissions(path: &Path) -> io::Result<()> {
908 #[cfg(unix)]
909 {
910 use std::os::unix::fs::PermissionsExt;
911 fs::set_permissions(path, fs::Permissions::from_mode(0o700))?;
912 }
913 Ok(())
914}
915
916#[cfg(test)]
917mod tests;