Skip to main content

kagi_sync/domain/
project_state.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Serialize, Deserialize, Debug, Clone)]
4pub struct ProjectState {
5    pub project_id: String,
6    pub revision: i64,
7    pub kagi_json: String,
8    pub access_json: String,
9    pub files: Vec<ProjectFile>,
10}
11
12#[derive(Serialize, Deserialize, Debug, Clone)]
13pub struct ProjectFile {
14    pub path: String,
15    pub content: String,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub sha256: Option<String>,
18}
19
20#[cfg(feature = "server")]
21pub fn validate_file_path(path: &str) -> Result<(), &'static str> {
22    if path.starts_with('/') || path.contains("\\") || path.contains("..") {
23        return Err("absolute or parent-relative path");
24    }
25    for part in path.split('/') {
26        if part.is_empty() || part == "." || part == ".." {
27            return Err("invalid path segment");
28        }
29    }
30    if !path.starts_with("secrets/") || !path.ends_with(".enc") {
31        return Err("path must start with secrets/ and end with .enc");
32    }
33    Ok(())
34}
35
36#[cfg(all(test, feature = "server"))]
37mod tests {
38    use super::*;
39
40    #[test]
41    fn test_validate_file_path_valid() {
42        assert!(validate_file_path("secrets/api/development.enc").is_ok());
43        assert!(validate_file_path("secrets/web/production.enc").is_ok());
44    }
45
46    #[test]
47    fn test_validate_file_path_rejects_absolute() {
48        assert_eq!(
49            validate_file_path("/etc/passwd"),
50            Err("absolute or parent-relative path")
51        );
52    }
53
54    #[test]
55    fn test_validate_file_path_rejects_backslash() {
56        assert_eq!(
57            validate_file_path("secrets\\windows.enc"),
58            Err("absolute or parent-relative path")
59        );
60    }
61
62    #[test]
63    fn test_validate_file_path_rejects_parent_relative() {
64        assert_eq!(
65            validate_file_path("secrets/../other.env"),
66            Err("absolute or parent-relative path")
67        );
68    }
69
70    #[test]
71    fn test_validate_file_path_rejects_dot_segment() {
72        assert_eq!(
73            validate_file_path("secrets/./development.enc"),
74            Err("invalid path segment")
75        );
76    }
77
78    #[test]
79    fn test_validate_file_path_rejects_empty_segment() {
80        assert_eq!(
81            validate_file_path("secrets//development.enc"),
82            Err("invalid path segment")
83        );
84    }
85
86    #[test]
87    fn test_validate_file_path_rejects_wrong_prefix() {
88        assert_eq!(
89            validate_file_path("config/development.enc"),
90            Err("path must start with secrets/ and end with .enc")
91        );
92    }
93
94    #[test]
95    fn test_validate_file_path_rejects_wrong_suffix() {
96        assert_eq!(
97            validate_file_path("secrets/api/development.txt"),
98            Err("path must start with secrets/ and end with .enc")
99        );
100    }
101}