sley_submodule/
path_safety.rs1use sley_core::{GitError, Result};
4use std::io;
5use std::path::{Component, Path};
6use std::{fs, iter::Peekable};
7
8pub fn submodule_path_has_symlink_parent(
16 worktree_root: &Path,
17 relative_path: &Path,
18) -> Result<bool> {
19 if relative_path.is_absolute()
20 || relative_path
21 .components()
22 .any(|component| matches!(component, Component::ParentDir | Component::RootDir))
23 {
24 return Err(GitError::InvalidPath(format!(
25 "invalid submodule worktree path {}",
26 relative_path.display()
27 )));
28 }
29 let mut components: Peekable<_> = relative_path.components().peekable();
30 let mut current = worktree_root.to_path_buf();
31 while let Some(component) = components.next() {
32 let Component::Normal(name) = component else {
33 continue;
34 };
35 if components.peek().is_none() {
36 break;
37 }
38 current.push(name);
39 match fs::symlink_metadata(¤t) {
40 Ok(metadata) if metadata.file_type().is_symlink() => return Ok(true),
41 Ok(_) => {}
42 Err(err)
43 if matches!(
44 err.kind(),
45 io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
46 ) =>
47 {
48 return Ok(false);
49 }
50 Err(err) => return Err(err.into()),
51 }
52 }
53 Ok(false)
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59
60 #[cfg(unix)]
61 #[test]
62 fn allows_final_symlink_but_rejects_symlinked_parent() {
63 let root = std::env::temp_dir().join(format!(
64 "sley-submodule-path-safety-{}-{}",
65 std::process::id(),
66 line!()
67 ));
68 let _ = fs::remove_dir_all(&root);
69 fs::create_dir_all(root.join("target")).expect("create symlink target");
70 std::os::unix::fs::symlink("target", root.join("final")).expect("create final symlink");
71 std::os::unix::fs::symlink("target", root.join("parent")).expect("create parent symlink");
72
73 assert!(
74 !submodule_path_has_symlink_parent(&root, Path::new("final"))
75 .expect("inspect final symlink")
76 );
77 assert!(
78 submodule_path_has_symlink_parent(&root, Path::new("parent/submodule"))
79 .expect("inspect symlinked parent")
80 );
81
82 fs::remove_dir_all(root).expect("clean fixture");
83 }
84}