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    let is_secret = path.starts_with("secrets/") && path.ends_with(".enc");
31    let is_file_artifact =
32        path == "files/index.enc" || (path.starts_with("files/kgf_") && path.ends_with(".enc"));
33    if !is_secret && !is_file_artifact {
34        return Err("path must be an encrypted secrets or files artifact");
35    }
36    Ok(())
37}
38
39#[cfg(all(test, feature = "server"))]
40mod tests {
41    use super::*;
42
43    #[test]
44    fn test_validate_file_path_valid() {
45        assert!(validate_file_path("secrets/api/development.enc").is_ok());
46        assert!(validate_file_path("secrets/web/production.enc").is_ok());
47        assert!(validate_file_path("files/index.enc").is_ok());
48        assert!(validate_file_path("files/kgf_abc123.enc").is_ok());
49    }
50
51    #[test]
52    fn test_validate_file_path_rejects_absolute() {
53        assert_eq!(
54            validate_file_path("/etc/passwd"),
55            Err("absolute or parent-relative path")
56        );
57    }
58
59    #[test]
60    fn test_validate_file_path_rejects_backslash() {
61        assert_eq!(
62            validate_file_path("secrets\\windows.enc"),
63            Err("absolute or parent-relative path")
64        );
65    }
66
67    #[test]
68    fn test_validate_file_path_rejects_parent_relative() {
69        assert_eq!(
70            validate_file_path("secrets/../other.env"),
71            Err("absolute or parent-relative path")
72        );
73    }
74
75    #[test]
76    fn test_validate_file_path_rejects_dot_segment() {
77        assert_eq!(
78            validate_file_path("secrets/./development.enc"),
79            Err("invalid path segment")
80        );
81    }
82
83    #[test]
84    fn test_validate_file_path_rejects_empty_segment() {
85        assert_eq!(
86            validate_file_path("secrets//development.enc"),
87            Err("invalid path segment")
88        );
89    }
90
91    #[test]
92    fn test_validate_file_path_rejects_wrong_prefix() {
93        assert_eq!(
94            validate_file_path("config/development.enc"),
95            Err("path must be an encrypted secrets or files artifact")
96        );
97    }
98
99    #[test]
100    fn test_validate_file_path_rejects_wrong_suffix() {
101        assert_eq!(
102            validate_file_path("secrets/api/development.txt"),
103            Err("path must be an encrypted secrets or files artifact")
104        );
105    }
106
107    #[test]
108    fn test_validate_file_path_rejects_raw_file_artifact_name() {
109        assert_eq!(
110            validate_file_path("files/service-account.json.enc"),
111            Err("path must be an encrypted secrets or files artifact")
112        );
113    }
114}