Skip to main content

eggserve_core/path/
policy.rs

1use super::rejected::PathRejection;
2
3/// Controls whether dotfile paths are accepted during [`ConfinedPath`](super::ConfinedPath) parsing.
4///
5/// Default: `Denied`. Paths containing a component starting with `.` are
6/// rejected before filesystem resolution.
7///
8/// This is distinct from [`crate::policy::DotfilePolicy`], which controls
9/// whether dotfiles are served in the final response. Both must allow dotfiles
10/// for them to be served.
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
12pub enum DotfilePolicy {
13    #[default]
14    Denied,
15    Allow,
16}
17
18/// Configuration for path validation during [`ConfinedPath`](super::ConfinedPath) parsing.
19///
20/// Default: dotfiles denied, backslash rejected.
21#[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}