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