agentmux_srv/backend/
schema.rs1#![allow(dead_code)]
2use std::path::{Path, PathBuf};
14
15pub const SCHEMA_CONTENT_TYPE: &str = "application/schema+json";
17
18pub fn resolve_schema_path(base_dir: &Path, name: &str) -> Option<PathBuf> {
23 if name.contains("..") {
25 return None;
26 }
27
28 let exact = base_dir.join(name);
29 if exact.is_file() {
30 return Some(exact);
31 }
32
33 let with_ext = base_dir.join(format!("{}.json", name));
35 if with_ext.is_file() {
36 return Some(with_ext);
37 }
38
39 None
40}
41
42pub fn get_schema_dir(app_path: &Path) -> PathBuf {
44 app_path.join("schema")
45}
46
47pub fn schema_dir_exists(app_path: &Path) -> bool {
49 let schema_dir = get_schema_dir(app_path);
50 schema_dir.is_dir()
51}
52
53pub fn normalize_schema_request(path: &str) -> Option<String> {
56 let stripped = path.trim_start_matches('/');
57 if stripped.contains("..") {
58 return None;
59 }
60 if stripped.is_empty() {
61 return None;
62 }
63 Some(stripped.to_string())
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use std::fs;
70
71 fn setup_test_dir() -> PathBuf {
72 let dir = std::env::temp_dir().join(format!(
73 "schema_test_{}_{:?}",
74 std::process::id(),
75 std::thread::current().id()
76 ));
77 let schema_dir = dir.join("schema");
78 let _ = fs::remove_dir_all(&dir);
79 fs::create_dir_all(&schema_dir).unwrap();
80 schema_dir
81 }
82
83 fn cleanup(dir: &Path) {
84 let _ = fs::remove_dir_all(dir.parent().unwrap_or(dir));
85 }
86
87 #[test]
88 fn test_resolve_exact_path() {
89 let dir = setup_test_dir();
90 fs::write(dir.join("test.json"), "{}").unwrap();
91
92 let result = resolve_schema_path(&dir, "test.json");
93 assert!(result.is_some());
94 assert!(result.unwrap().ends_with("test.json"));
95
96 cleanup(&dir);
97 }
98
99 #[test]
100 fn test_resolve_with_json_fallback() {
101 let dir = setup_test_dir();
102 fs::write(dir.join("test.json"), "{}").unwrap();
103
104 let result = resolve_schema_path(&dir, "test");
105 assert!(result.is_some());
106 assert!(result.unwrap().ends_with("test.json"));
107
108 cleanup(&dir);
109 }
110
111 #[test]
112 fn test_resolve_not_found() {
113 let dir = setup_test_dir();
114
115 let result = resolve_schema_path(&dir, "missing");
116 assert!(result.is_none());
117
118 cleanup(&dir);
119 }
120
121 #[test]
122 fn test_resolve_traversal_rejected() {
123 let dir = setup_test_dir();
124
125 let result = resolve_schema_path(&dir, "../etc/passwd");
126 assert!(result.is_none());
127
128 cleanup(&dir);
129 }
130
131 #[test]
132 fn test_normalize_schema_request() {
133 assert_eq!(normalize_schema_request("/foo/bar"), Some("foo/bar".into()));
134 assert_eq!(normalize_schema_request("foo"), Some("foo".into()));
135 assert_eq!(normalize_schema_request("/"), None);
136 assert_eq!(normalize_schema_request(""), None);
137 assert_eq!(normalize_schema_request("/../bad"), None);
138 }
139
140 #[test]
141 fn test_get_schema_dir() {
142 let app_path = Path::new("/opt/agentmux");
143 assert_eq!(get_schema_dir(app_path), PathBuf::from("/opt/agentmux/schema"));
144 }
145
146 #[test]
147 fn test_schema_dir_exists() {
148 let dir = setup_test_dir();
149 let app_path = dir.parent().unwrap();
150 assert!(schema_dir_exists(app_path));
151 cleanup(&dir);
152 }
153
154 #[test]
155 fn test_schema_dir_not_exists() {
156 let app_path = Path::new("/nonexistent/path");
157 assert!(!schema_dir_exists(app_path));
158 }
159
160 #[test]
161 fn test_schema_content_type() {
162 assert_eq!(SCHEMA_CONTENT_TYPE, "application/schema+json");
163 }
164}