agentmux_srv\backend/
schema.rs

1#![allow(dead_code)]
2// Copyright 2025-2026, AgentMux Corp.
3// SPDX-License-Identifier: Apache-2.0
4
5//! Schema path resolution with `.json` extension fallback.
6//! Port of Go's `pkg/schema/schema.go`.
7//!
8//! Provides file path resolution for JSON schema files, trying the
9//! exact path first, then falling back to appending `.json`.
10
11
12use std::path::{Path, PathBuf};
13
14/// The content type for JSON schema responses.
15pub const SCHEMA_CONTENT_TYPE: &str = "application/schema+json";
16
17/// Resolve a schema file path, trying the exact path first, then with `.json` extension.
18///
19/// Returns `Some(path)` if a file exists at the exact path or with `.json` appended.
20/// Returns `None` if neither exists.
21pub fn resolve_schema_path(base_dir: &Path, name: &str) -> Option<PathBuf> {
22    // Prevent directory traversal
23    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    // Try with .json extension
33    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
41/// Get the schema directory path from the app path.
42pub fn get_schema_dir(app_path: &Path) -> PathBuf {
43    app_path.join("schema")
44}
45
46/// Check if a schema directory exists and is valid.
47pub fn schema_dir_exists(app_path: &Path) -> bool {
48    let schema_dir = get_schema_dir(app_path);
49    schema_dir.is_dir()
50}
51
52/// Normalize a request path for schema lookup.
53/// Strips leading slashes and ensures no directory traversal.
54pub 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}