eggserve_core/path/
policy.rs1use super::rejected::PathRejection;
2
3#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
12pub enum DotfilePolicy {
13 #[default]
14 Denied,
15 Allow,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct PathPolicy {
23 pub dotfiles: DotfilePolicy,
24 pub reject_backslash: bool,
25}
26
27impl Default for PathPolicy {
28 fn default() -> Self {
29 Self {
30 dotfiles: DotfilePolicy::Denied,
31 reject_backslash: true,
32 }
33 }
34}
35
36impl PathPolicy {
37 #[allow(dead_code)]
38 pub fn check_dotfile(&self, component: &str) -> Result<(), PathRejection> {
39 if self.dotfiles == DotfilePolicy::Denied && component.starts_with('.') {
40 return Err(PathRejection::DotfileDenied);
41 }
42 Ok(())
43 }
44
45 #[allow(dead_code)]
46 pub fn check_backslash(&self, component: &str) -> Result<(), PathRejection> {
47 if self.reject_backslash && component.contains('\\') {
48 return Err(PathRejection::SeparatorAmbiguity);
49 }
50 Ok(())
51 }
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57
58 #[test]
59 fn default_policy_denies_dotfiles() {
60 let policy = PathPolicy::default();
61 assert_eq!(policy.dotfiles, DotfilePolicy::Denied);
62 }
63
64 #[test]
65 fn default_policy_rejects_backslash() {
66 let policy = PathPolicy::default();
67 assert!(policy.reject_backslash);
68 }
69
70 #[test]
71 fn check_dotfile_denied() {
72 let policy = PathPolicy::default();
73 assert_eq!(
74 policy.check_dotfile(".env").unwrap_err(),
75 PathRejection::DotfileDenied
76 );
77 }
78
79 #[test]
80 fn check_dotfile_allowed() {
81 let policy = PathPolicy {
82 dotfiles: DotfilePolicy::Allow,
83 ..PathPolicy::default()
84 };
85 assert!(policy.check_dotfile(".env").is_ok());
86 }
87
88 #[test]
89 fn check_backslash_rejected() {
90 let policy = PathPolicy::default();
91 assert_eq!(
92 policy.check_backslash("foo\\bar").unwrap_err(),
93 PathRejection::SeparatorAmbiguity
94 );
95 }
96
97 #[test]
98 fn check_backslash_allowed() {
99 let policy = PathPolicy {
100 reject_backslash: false,
101 ..PathPolicy::default()
102 };
103 assert!(policy.check_backslash("foo\\bar").is_ok());
104 }
105
106 #[test]
107 fn dotfile_policy_deny_rejects_hidden_component() {
108 let policy = PathPolicy::default();
109 assert_eq!(
110 policy.check_dotfile(".hidden").unwrap_err(),
111 PathRejection::DotfileDenied
112 );
113 }
114
115 #[test]
116 fn dotfile_policy_allow_permits_hidden_component() {
117 let policy = PathPolicy {
118 dotfiles: DotfilePolicy::Allow,
119 ..PathPolicy::default()
120 };
121 assert!(policy.check_dotfile(".hidden").is_ok());
122 }
123
124 #[test]
125 fn dotfile_policy_deny_rejects_nested_dotfile() {
126 let policy = PathPolicy::default();
127 assert_eq!(
128 policy.check_dotfile(".env").unwrap_err(),
129 PathRejection::DotfileDenied
130 );
131 assert_eq!(
132 policy.check_dotfile(".gitconfig").unwrap_err(),
133 PathRejection::DotfileDenied
134 );
135 }
136
137 #[test]
138 fn dotfile_policy_allow_permits_all_dotfiles() {
139 let policy = PathPolicy {
140 dotfiles: DotfilePolicy::Allow,
141 ..PathPolicy::default()
142 };
143 assert!(policy.check_dotfile(".env").is_ok());
144 assert!(policy.check_dotfile(".hidden").is_ok());
145 assert!(policy.check_dotfile(".gitconfig").is_ok());
146 }
147
148 #[test]
149 fn backslash_rejected_by_default() {
150 let policy = PathPolicy::default();
151 assert_eq!(
152 policy.check_backslash("foo\\bar").unwrap_err(),
153 PathRejection::SeparatorAmbiguity
154 );
155 assert_eq!(
156 policy.check_backslash("\\").unwrap_err(),
157 PathRejection::SeparatorAmbiguity
158 );
159 assert_eq!(
160 policy.check_backslash("a\\b\\c").unwrap_err(),
161 PathRejection::SeparatorAmbiguity
162 );
163 }
164
165 #[test]
166 fn backslash_allowed_when_policy_permits() {
167 let policy = PathPolicy {
168 reject_backslash: false,
169 ..PathPolicy::default()
170 };
171 assert!(policy.check_backslash("foo\\bar").is_ok());
172 assert!(policy.check_backslash("\\").is_ok());
173 assert!(policy.check_backslash("a\\b\\c").is_ok());
174 }
175
176 #[test]
177 fn check_dotfile_no_dot_prefix_allowed() {
178 let policy = PathPolicy::default();
179 assert!(policy.check_dotfile("file.txt").is_ok());
180 assert!(policy.check_dotfile("normaldir").is_ok());
181 }
182}