lean_ctx/core/
pathjail_write.rs1use std::path::{Path, PathBuf};
12
13const PROJECT_DATA_DIR: &str = ".lean-ctx";
14
15pub fn rebuild_below(root: &Path, rel: &Path) -> Result<PathBuf, String> {
18 lean_ctx_fs::confined_write::rebuild_below(root, rel).map_err(|e| e.to_string())
19}
20
21pub fn write_below(root: &Path, rel: &Path, content: &[u8], mode: u32) -> Result<PathBuf, String> {
27 lean_ctx_fs::confined_write::write_below(root, rel, content, mode).map_err(|e| e.to_string())
28}
29
30fn project_data_rel(project_root: &Path, rel: &Path) -> Result<PathBuf, String> {
33 crate::core::pathutil::safe_project_data_dir(project_root)?;
34 Ok(Path::new(PROJECT_DATA_DIR).join(rel))
35}
36
37pub fn project_data_destination(project_root: &Path, rel: &Path) -> Result<PathBuf, String> {
39 rebuild_below(project_root, &project_data_rel(project_root, rel)?)
40}
41
42pub fn write_project_data(
45 project_root: &Path,
46 rel: &Path,
47 content: &[u8],
48) -> Result<PathBuf, String> {
49 write_below(
50 project_root,
51 &project_data_rel(project_root, rel)?,
52 content,
53 0o600,
54 )
55}
56
57pub fn relative_below(root: &Path, path: &Path) -> Result<PathBuf, String> {
61 let plain = |rel: &Path| {
62 rel.components()
63 .all(|c| matches!(c, std::path::Component::Normal(_)))
64 && rel.components().next().is_some()
65 };
66 if let Ok(rel) = path.strip_prefix(root)
67 && plain(rel)
68 {
69 return Ok(rel.to_path_buf());
70 }
71 let canonical_root = crate::core::pathutil::canonicalize_secure_or_self(root);
72 match path.strip_prefix(&canonical_root) {
73 Ok(rel) if plain(rel) => Ok(rel.to_path_buf()),
74 _ => Err(format!(
75 "{} is not below {}",
76 path.display(),
77 root.display()
78 )),
79 }
80}
81
82pub fn write_requested_below(
90 root: &Path,
91 requested: &Path,
92 jailed: &Path,
93 content: &[u8],
94) -> Result<PathBuf, String> {
95 write_requested_below_with_mode(root, requested, jailed, content, 0o600)
96}
97
98pub fn write_requested_below_with_mode(
101 root: &Path,
102 requested: &Path,
103 jailed: &Path,
104 content: &[u8],
105 mode: u32,
106) -> Result<PathBuf, String> {
107 if let Ok(rel) = relative_below(root, requested).or_else(|_| relative_below(root, jailed)) {
108 return write_below(root, &rel, content, mode);
109 }
110 let (Some(parent), Some(name)) = (jailed.parent(), jailed.file_name()) else {
111 return Err(format!("invalid destination: {}", jailed.display()));
112 };
113 std::fs::create_dir_all(parent).map_err(|e| format!("{}: {e}", parent.display()))?;
114 write_below(parent, Path::new(name), content, mode)
115}
116
117#[cfg(all(test, unix))]
118mod tests {
119 use super::*;
120 use std::os::unix::fs::symlink;
121
122 #[test]
123 fn project_data_writes_land_in_the_project() {
124 let project = tempfile::tempdir().unwrap();
125 let path = write_project_data(project.path(), Path::new("proofs/a.json"), b"{}").unwrap();
126 assert_eq!(path, project.path().join(".lean-ctx/proofs/a.json"));
127 assert_eq!(std::fs::read(&path).unwrap(), b"{}");
128 }
129
130 #[test]
131 fn a_symlinked_data_dir_pointing_outside_is_refused() {
132 let tmp = tempfile::tempdir().unwrap();
133 let project = tmp.path().join("project");
134 let outside = tmp.path().join("outside");
135 std::fs::create_dir_all(&project).unwrap();
136 std::fs::create_dir_all(&outside).unwrap();
137 symlink(&outside, project.join(".lean-ctx")).unwrap();
138 let err = write_project_data(&project, Path::new("proofs/a.json"), b"x").unwrap_err();
139 assert!(err.contains("symlink"), "{err}");
140 assert!(!outside.join("proofs").exists());
141 }
142
143 #[test]
144 fn relative_below_keeps_links_visible() {
145 let tmp = tempfile::tempdir().unwrap();
146 let root = tmp.path().join("r");
147 std::fs::create_dir_all(&root).unwrap();
148 assert_eq!(
149 relative_below(&root, &root.join("a/b")).unwrap(),
150 PathBuf::from("a/b")
151 );
152 assert!(relative_below(&root, &tmp.path().join("x")).is_err());
153 }
154}