convergio_types/
platform_paths.rs1use std::path::PathBuf;
9
10pub fn convergio_data_dir() -> PathBuf {
12 if let Some(data) = dirs::data_dir() {
13 return data.join("Convergio");
14 }
15 dirs::home_dir()
16 .unwrap_or_else(|| PathBuf::from("."))
17 .join(".convergio")
18}
19
20pub fn project_output_dir(project_name: &str) -> Result<PathBuf, String> {
23 validate_path_components(std::path::Path::new(project_name))?;
24 if project_name.is_empty() {
25 return Err("project name must not be empty".into());
26 }
27 Ok(convergio_data_dir()
28 .join("projects")
29 .join(project_name)
30 .join("output"))
31}
32
33pub fn sanitize_path(
36 path: &std::path::Path,
37 allowed_base: &std::path::Path,
38) -> Result<PathBuf, String> {
39 let canonical = path
40 .canonicalize()
41 .map_err(|e| format!("invalid path {}: {e}", path.display()))?;
42 let base = allowed_base
43 .canonicalize()
44 .map_err(|e| format!("invalid base {}: {e}", allowed_base.display()))?;
45 if canonical.starts_with(&base) {
46 Ok(canonical)
47 } else {
48 Err(format!(
49 "path {} is outside allowed directory {}",
50 canonical.display(),
51 base.display()
52 ))
53 }
54}
55
56pub fn validate_path_components(path: &std::path::Path) -> Result<(), String> {
60 for component in path.components() {
61 match component {
62 std::path::Component::ParentDir => {
63 return Err(format!("path traversal '..' in {}", path.display()));
64 }
65 std::path::Component::RootDir | std::path::Component::Prefix(_) => {
66 return Err(format!("absolute path not allowed: {}", path.display()));
67 }
68 std::path::Component::Normal(s) => {
69 let s = s.to_string_lossy();
70 if s.starts_with('.') && s.len() > 1 && !s.starts_with("..") {
71 }
73 if s.contains('\0') {
74 return Err("null byte in path component".into());
75 }
76 }
77 _ => {}
78 }
79 }
80 Ok(())
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 #[test]
88 fn data_dir_is_absolute_or_fallback() {
89 let dir = convergio_data_dir();
90 let name = dir.file_name().unwrap().to_str().unwrap();
91 assert!(
92 name == "Convergio" || name == ".convergio",
93 "unexpected dir name: {name}"
94 );
95 }
96
97 #[test]
98 fn project_output_dir_structure() {
99 let out = project_output_dir("my-app").unwrap();
100 assert!(out.ends_with("projects/my-app/output"));
101 }
102
103 #[test]
104 fn project_output_dir_rejects_traversal() {
105 assert!(project_output_dir("../etc/passwd").is_err());
106 assert!(project_output_dir("").is_err());
107 }
108
109 #[test]
110 fn validate_rejects_absolute_path() {
111 assert!(validate_path_components(std::path::Path::new("/etc/passwd")).is_err());
112 }
113}