1use std::{
37 collections::HashSet,
38 ffi::OsStr,
39 fs::{self, File},
40 io::{self, Write},
41 path::{Component, Path, PathBuf},
42};
43
44use crate::fs_atomic::enrich_fs_error;
45
46fn components_beneath<'a>(root: &Path, path: &'a Path) -> io::Result<Vec<&'a OsStr>> {
50 let relative = path.strip_prefix(root).map_err(|_| {
51 io::Error::new(
52 io::ErrorKind::InvalidInput,
53 format!(
54 "worktree path {} is outside checkout {}",
55 path.display(),
56 root.display()
57 ),
58 )
59 })?;
60 let mut names = Vec::new();
61 for component in relative.components() {
62 match component {
63 Component::Normal(name) => names.push(name),
64 Component::CurDir => {}
65 Component::ParentDir | Component::RootDir | Component::Prefix(_) => {
66 return Err(io::Error::new(
67 io::ErrorKind::InvalidInput,
68 format!(
69 "worktree path {} escapes checkout {}",
70 path.display(),
71 root.display()
72 ),
73 ));
74 }
75 }
76 }
77 Ok(names)
78}
79
80pub struct NoFollowDirectories<'a> {
88 root: &'a Path,
89 verified: HashSet<PathBuf>,
90}
91
92impl<'a> NoFollowDirectories<'a> {
93 pub fn new(root: &'a Path) -> Self {
94 Self {
95 root,
96 verified: HashSet::new(),
97 }
98 }
99
100 pub fn ensure(&mut self, dir: &Path) -> io::Result<()> {
104 let mut current = self.root.to_path_buf();
105 for name in components_beneath(self.root, dir)? {
106 current.push(name);
107 if self.verified.contains(¤t) {
108 continue;
109 }
110 ensure_real_directory(¤t)?;
111 self.verified.insert(current.clone());
112 }
113 Ok(())
114 }
115
116 pub fn ensure_parent(&mut self, path: &Path) -> io::Result<()> {
118 let names = components_beneath(self.root, path)?;
119 if names.len() <= 1 {
120 return Ok(());
121 }
122 match path.parent() {
123 Some(parent) => self.ensure(parent),
124 None => Ok(()),
125 }
126 }
127
128 pub fn parent_is_real_directory(&mut self, path: &Path) -> io::Result<bool> {
132 let names = components_beneath(self.root, path)?;
133 let Some((_, parents)) = names.split_last() else {
134 return Ok(true);
135 };
136 let mut current = self.root.to_path_buf();
137 for name in parents {
138 current.push(name);
139 if self.verified.contains(¤t) {
140 continue;
141 }
142 match fs::symlink_metadata(¤t) {
143 Ok(metadata) if metadata.is_dir() => {
144 self.verified.insert(current.clone());
145 }
146 Ok(_) => return Ok(false),
147 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(false),
148 Err(error) => return Err(enrich_fs_error(¤t, "inspecting", error)),
149 }
150 }
151 Ok(true)
152 }
153}
154
155pub fn refuse_symlinked_parent(root: &Path, path: &Path) -> io::Result<()> {
158 let names = components_beneath(root, path)?;
159 let Some((_, parents)) = names.split_last() else {
160 return Ok(());
161 };
162 let mut current = root.to_path_buf();
163 for name in parents {
164 current.push(name);
165 let metadata = fs::symlink_metadata(¤t)
166 .map_err(|error| enrich_fs_error(¤t, "inspecting", error))?;
167 if metadata.file_type().is_symlink() {
168 return Err(io::Error::other(format!(
169 "refusing to write {} through symlink {}: worktree writes never follow symlinks",
170 path.strip_prefix(root).unwrap_or(path).display(),
171 current.strip_prefix(root).unwrap_or(¤t).display(),
172 )));
173 }
174 }
175 Ok(())
176}
177
178fn ensure_real_directory(path: &Path) -> io::Result<()> {
179 match fs::symlink_metadata(path) {
180 Ok(metadata) if metadata.is_dir() => Ok(()),
181 Ok(metadata) if metadata.file_type().is_symlink() => {
182 fs::remove_file(path)
185 .or_else(|_| fs::remove_dir(path))
186 .map_err(|error| enrich_fs_error(path, "removing", error))?;
187 create_directory(path)
188 }
189 Ok(_) => Err(enrich_fs_error(
190 path,
191 "creating",
192 io::Error::new(
193 io::ErrorKind::AlreadyExists,
194 "a file occupies a path that must be a directory",
195 ),
196 )),
197 Err(error) if error.kind() == io::ErrorKind::NotFound => create_directory(path),
198 Err(error) => Err(enrich_fs_error(path, "inspecting", error)),
199 }
200}
201
202fn create_directory(path: &Path) -> io::Result<()> {
203 match fs::create_dir(path) {
204 Ok(()) => Ok(()),
205 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
207 match fs::symlink_metadata(path) {
208 Ok(metadata) if metadata.is_dir() => Ok(()),
209 _ => Err(enrich_fs_error(path, "creating", error)),
210 }
211 }
212 Err(error) => Err(enrich_fs_error(path, "creating", error)),
213 }
214}
215
216fn unlink_leaf(path: &Path) -> io::Result<()> {
219 match fs::symlink_metadata(path) {
220 Ok(metadata) if !metadata.is_dir() => fs::remove_file(path)
221 .or_else(|error| {
222 if metadata.file_type().is_symlink() {
224 fs::remove_dir(path)
225 } else {
226 Err(error)
227 }
228 })
229 .map_err(|error| enrich_fs_error(path, "removing", error)),
230 Ok(_) => Ok(()),
231 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
232 Err(error) => Err(enrich_fs_error(path, "inspecting", error)),
233 }
234}
235
236pub fn create_new_nofollow(path: &Path) -> io::Result<File> {
239 let mut options = fs::OpenOptions::new();
240 options.write(true).create_new(true);
241 #[cfg(unix)]
242 {
243 use std::os::unix::fs::OpenOptionsExt;
244 options.custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC);
245 }
246 options.open(path)
247}
248
249pub fn open_existing_nofollow(path: &Path) -> io::Result<File> {
252 let mut options = fs::OpenOptions::new();
253 options.read(true);
254 #[cfg(unix)]
255 {
256 use std::os::unix::fs::OpenOptionsExt;
257 options.custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC);
258 }
259 options.open(path)
260}
261
262pub fn create_replacing_leaf(path: &Path) -> io::Result<File> {
265 unlink_leaf(path)?;
266 create_new_nofollow(path).map_err(|error| enrich_fs_error(path, "writing", error))
267}
268
269pub fn write_file_beneath(root: &Path, path: &Path, bytes: &[u8]) -> io::Result<File> {
272 NoFollowDirectories::new(root).ensure_parent(path)?;
273 let mut file = create_replacing_leaf(path)?;
274 file.write_all(bytes)
275 .map_err(|error| enrich_fs_error(path, "writing", error))?;
276 Ok(file)
277}
278
279#[cfg(unix)]
283pub fn write_symlink_beneath(root: &Path, path: &Path, target: &OsStr) -> io::Result<()> {
284 NoFollowDirectories::new(root).ensure_parent(path)?;
285 unlink_leaf(path)?;
286 std::os::unix::fs::symlink(target, path)
287 .map_err(|error| enrich_fs_error(path, "creating symlink", error))
288}
289
290pub fn remove_leaf_beneath(root: &Path, path: &Path) -> io::Result<bool> {
294 if !NoFollowDirectories::new(root).parent_is_real_directory(path)? {
295 return Ok(false);
296 }
297 match fs::symlink_metadata(path) {
298 Ok(metadata) if metadata.is_dir() => Ok(false),
299 Ok(_) => {
300 unlink_leaf(path)?;
301 Ok(true)
302 }
303 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
304 Err(error) => Err(enrich_fs_error(path, "inspecting", error)),
305 }
306}
307
308pub fn remove_path_beneath(root: &Path, path: &Path) -> io::Result<bool> {
312 if !NoFollowDirectories::new(root).parent_is_real_directory(path)? {
313 return Ok(false);
314 }
315 match fs::symlink_metadata(path) {
316 Ok(_) => {
317 crate::fs_ops::remove_path_recursively(path)?;
318 Ok(true)
319 }
320 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
321 Err(error) => Err(enrich_fs_error(path, "inspecting", error)),
322 }
323}
324
325pub const MAX_IN_TREE_CONTROL_FILE_BYTES: u64 = 1 << 20;
329
330#[derive(Debug, PartialEq, Eq)]
332pub enum InTreeControlFile {
333 Absent,
334 Contents(Vec<u8>),
335 Symlink,
339 NotRegular,
342 TooLarge(u64),
343}
344
345pub fn read_in_tree_control_file(path: &Path, max_bytes: u64) -> io::Result<InTreeControlFile> {
349 let metadata = match fs::symlink_metadata(path) {
350 Ok(metadata) => metadata,
351 Err(error) if error.kind() == io::ErrorKind::NotFound => {
352 return Ok(InTreeControlFile::Absent);
353 }
354 Err(error) => return Err(enrich_fs_error(path, "inspecting", error)),
355 };
356 if metadata.file_type().is_symlink() {
357 return Ok(InTreeControlFile::Symlink);
358 }
359 if !metadata.is_file() {
360 return Ok(InTreeControlFile::NotRegular);
361 }
362 if metadata.len() > max_bytes {
363 return Ok(InTreeControlFile::TooLarge(metadata.len()));
364 }
365 let mut options = fs::OpenOptions::new();
366 options.read(true);
367 #[cfg(unix)]
368 {
369 use std::os::unix::fs::OpenOptionsExt;
370 options.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK | libc::O_CLOEXEC);
373 }
374 let file = match options.open(path) {
375 Ok(file) => file,
376 Err(error) if error.kind() == io::ErrorKind::NotFound => {
377 return Ok(InTreeControlFile::Absent);
378 }
379 Err(error) => return Err(enrich_fs_error(path, "reading", error)),
380 };
381 if !file.metadata()?.is_file() {
382 return Ok(InTreeControlFile::NotRegular);
383 }
384 let mut contents = Vec::new();
385 io::Read::read_to_end(&mut io::Read::take(file, max_bytes + 1), &mut contents)
386 .map_err(|error| enrich_fs_error(path, "reading", error))?;
387 if contents.len() as u64 > max_bytes {
388 return Ok(InTreeControlFile::TooLarge(contents.len() as u64));
389 }
390 Ok(InTreeControlFile::Contents(contents))
391}
392
393pub fn prepare_regular_file_beneath(root: &Path, path: &Path) -> io::Result<()> {
400 let names = components_beneath(root, path)?;
401 let Some((_, parents)) = names.split_last() else {
402 return Ok(());
403 };
404 let mut current = root.to_path_buf();
405 for name in parents {
406 current.push(name);
407 match fs::symlink_metadata(¤t) {
408 Ok(metadata) if metadata.is_dir() => {}
409 Ok(metadata) if metadata.file_type().is_symlink() => {
410 return Err(io::Error::other(format!(
411 "refusing to write {} through symlink {}",
412 path.strip_prefix(root).unwrap_or(path).display(),
413 current.strip_prefix(root).unwrap_or(¤t).display(),
414 )));
415 }
416 Ok(_) => {
417 return Err(enrich_fs_error(
418 ¤t,
419 "creating",
420 io::Error::new(
421 io::ErrorKind::AlreadyExists,
422 "a file occupies a path that must be a directory",
423 ),
424 ));
425 }
426 Err(error) if error.kind() == io::ErrorKind::NotFound => create_directory(¤t)?,
427 Err(error) => return Err(enrich_fs_error(¤t, "inspecting", error)),
428 }
429 }
430 match fs::symlink_metadata(path) {
431 Ok(metadata) if metadata.is_file() => Ok(()),
432 Ok(metadata) if metadata.file_type().is_symlink() => Err(io::Error::other(format!(
433 "refusing to write {}: it is a symlink",
434 path.strip_prefix(root).unwrap_or(path).display(),
435 ))),
436 Ok(_) => Err(io::Error::other(format!(
437 "refusing to write {}: it is not a regular file",
438 path.strip_prefix(root).unwrap_or(path).display(),
439 ))),
440 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
441 Err(error) => Err(enrich_fs_error(path, "inspecting", error)),
442 }
443}
444
445#[cfg(all(test, unix))]
446mod tests {
447 use std::{fs, os::unix::fs::symlink};
448
449 use super::*;
450
451 #[test]
452 fn ensure_replaces_symlinked_parent_with_directory() {
453 let root = tempfile::tempdir().unwrap();
454 let outside = tempfile::tempdir().unwrap();
455 symlink(outside.path(), root.path().join("link")).unwrap();
456
457 write_file_beneath(root.path(), &root.path().join("link/sub/file"), b"x").unwrap();
458
459 assert!(
460 fs::symlink_metadata(root.path().join("link"))
461 .unwrap()
462 .is_dir()
463 );
464 assert_eq!(fs::read(root.path().join("link/sub/file")).unwrap(), b"x");
465 assert_eq!(fs::read_dir(outside.path()).unwrap().count(), 0);
466 }
467
468 #[test]
469 fn write_replaces_leaf_symlink_instead_of_following_it() {
470 let root = tempfile::tempdir().unwrap();
471 let outside = tempfile::tempdir().unwrap();
472 let victim = outside.path().join("victim");
473 fs::write(&victim, b"keep").unwrap();
474 symlink(&victim, root.path().join("leaf")).unwrap();
475
476 write_file_beneath(root.path(), &root.path().join("leaf"), b"new").unwrap();
477
478 assert_eq!(fs::read(&victim).unwrap(), b"keep");
479 assert!(
480 fs::symlink_metadata(root.path().join("leaf"))
481 .unwrap()
482 .is_file()
483 );
484 }
485
486 #[test]
487 fn removals_never_reach_through_a_symlinked_parent() {
488 let root = tempfile::tempdir().unwrap();
489 let outside = tempfile::tempdir().unwrap();
490 fs::write(outside.path().join("file"), b"keep").unwrap();
491 fs::create_dir(outside.path().join("dir")).unwrap();
492 symlink(outside.path(), root.path().join("link")).unwrap();
493
494 assert!(!remove_leaf_beneath(root.path(), &root.path().join("link/file")).unwrap());
495 assert!(!remove_path_beneath(root.path(), &root.path().join("link/dir")).unwrap());
496
497 assert_eq!(fs::read(outside.path().join("file")).unwrap(), b"keep");
498 assert!(outside.path().join("dir").is_dir());
499 }
500
501 #[test]
502 fn refuse_symlinked_parent_names_the_symlink() {
503 let root = tempfile::tempdir().unwrap();
504 let outside = tempfile::tempdir().unwrap();
505 symlink(outside.path(), root.path().join("link")).unwrap();
506
507 let error = refuse_symlinked_parent(root.path(), &root.path().join("link/leaf"))
508 .unwrap_err()
509 .to_string();
510
511 assert!(error.contains("through symlink link"), "{error}");
512 }
513
514 #[test]
515 fn paths_outside_or_escaping_the_root_are_rejected() {
516 let root = tempfile::tempdir().unwrap();
517 assert!(write_file_beneath(root.path(), Path::new("/elsewhere/file"), b"x").is_err());
518 assert!(write_file_beneath(root.path(), &root.path().join("../escape"), b"x").is_err());
519 }
520
521 #[test]
522 fn symlink_beneath_writes_arbitrary_targets_exactly() {
523 let root = tempfile::tempdir().unwrap();
524 for (name, target) in [("abs", "/usr/share/dict/words"), ("rel", "../../outside")] {
525 write_symlink_beneath(
526 root.path(),
527 &root.path().join("d").join(name),
528 target.as_ref(),
529 )
530 .unwrap();
531 assert_eq!(
532 fs::read_link(root.path().join("d").join(name)).unwrap(),
533 Path::new(target)
534 );
535 }
536 }
537}