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 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}