1use crate::Result;
10use anyhow::{bail, Context};
11use std::path::Path;
12
13pub fn prepare_log_dir(dir: &Path) -> Result<()> {
15 #[cfg(unix)]
16 {
17 use std::fs::DirBuilder;
18 use std::os::unix::fs::{DirBuilderExt, PermissionsExt};
19
20 DirBuilder::new()
21 .recursive(true)
22 .mode(0o700)
23 .create(dir)
24 .with_context(|| format!("Failed to create log directory {}", dir.display()))?;
25
26 let meta = std::fs::symlink_metadata(dir)
27 .with_context(|| format!("Failed to inspect log directory {}", dir.display()))?;
28 if meta.file_type().is_symlink() {
29 bail!(
30 "Log directory {} is a symlink; refusing to use it",
31 dir.display()
32 );
33 }
34 if !meta.is_dir() {
35 bail!("Log directory {} is not a directory", dir.display());
36 }
37 std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700)).with_context(
38 || {
39 format!(
40 "Log directory {} is not owned by the current user",
41 dir.display()
42 )
43 },
44 )?;
45 }
46 #[cfg(not(unix))]
47 {
48 std::fs::create_dir_all(dir)
49 .with_context(|| format!("Failed to create log directory {}", dir.display()))?;
50 }
51 Ok(())
52}
53
54#[cfg(all(test, unix))]
55mod tests {
56 use super::*;
57 use std::os::unix::fs::PermissionsExt;
58
59 fn scratch() -> std::path::PathBuf {
60 let dir =
61 std::env::temp_dir().join(format!("mcp-airlock-logtest-{}", uuid::Uuid::new_v4()));
62 std::fs::create_dir_all(&dir).unwrap();
63 dir
64 }
65
66 #[test]
67 fn test_creates_private_directory() {
68 let base = scratch();
69 let dir = base.join("a/b/logs");
70 prepare_log_dir(&dir).unwrap();
71 let mode = std::fs::metadata(&dir).unwrap().permissions().mode() & 0o777;
72 assert_eq!(mode, 0o700);
73 std::fs::remove_dir_all(base).unwrap();
74 }
75
76 #[test]
77 fn test_tightens_existing_directory() {
78 let base = scratch();
79 let dir = base.join("logs");
80 std::fs::create_dir(&dir).unwrap();
81 std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o777)).unwrap();
82 prepare_log_dir(&dir).unwrap();
83 let mode = std::fs::metadata(&dir).unwrap().permissions().mode() & 0o777;
84 assert_eq!(mode, 0o700);
85 std::fs::remove_dir_all(base).unwrap();
86 }
87
88 #[test]
89 fn test_rejects_symlink() {
90 let base = scratch();
91 let target = base.join("elsewhere");
92 std::fs::create_dir(&target).unwrap();
93 let link = base.join("logs");
94 std::os::unix::fs::symlink(&target, &link).unwrap();
95 let err = prepare_log_dir(&link).unwrap_err().to_string();
96 assert!(err.contains("symlink"), "{err}");
97 std::fs::remove_dir_all(base).unwrap();
98 }
99
100 #[test]
101 fn test_rejects_file() {
102 let base = scratch();
103 let file = base.join("logs");
104 std::fs::write(&file, b"").unwrap();
105 assert!(prepare_log_dir(&file).is_err());
106 std::fs::remove_dir_all(base).unwrap();
107 }
108}