kagi_sync/domain/
project_state.rs1use 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}