dk_protocol/
validation.rs1use tonic::Status;
4
5pub fn validate_file_path(path: &str) -> Result<(), Status> {
11 if path.is_empty() {
12 return Err(Status::invalid_argument("file path cannot be empty"));
13 }
14 if path.starts_with('/') || path.starts_with('\\') {
15 return Err(Status::invalid_argument("file path must be relative"));
16 }
17 if path.contains('\0') {
18 return Err(Status::invalid_argument("file path contains null byte"));
19 }
20 for component in path.split('/') {
22 if component == ".." {
23 return Err(Status::invalid_argument(
24 "file path contains '..' traversal",
25 ));
26 }
27 }
28 Ok(())
29}
30
31pub const MAX_FILE_SIZE: usize = 50 * 1024 * 1024;
33
34#[cfg(test)]
35mod tests {
36 use super::*;
37
38 #[test]
39 fn accepts_valid_relative_paths() {
40 assert!(validate_file_path("src/main.rs").is_ok());
41 assert!(validate_file_path("README.md").is_ok());
42 assert!(validate_file_path("a/b/c/d.txt").is_ok());
43 assert!(validate_file_path(".hidden").is_ok());
44 }
45
46 #[test]
47 fn rejects_empty_path() {
48 let err = validate_file_path("").unwrap_err();
49 assert_eq!(err.code(), tonic::Code::InvalidArgument);
50 }
51
52 #[test]
53 fn rejects_absolute_paths() {
54 assert!(validate_file_path("/etc/passwd").is_err());
55 assert!(validate_file_path("\\windows\\system32").is_err());
56 }
57
58 #[test]
59 fn rejects_null_byte() {
60 assert!(validate_file_path("src/\0evil.rs").is_err());
61 }
62
63 #[test]
64 fn rejects_traversal() {
65 assert!(validate_file_path("../etc/passwd").is_err());
66 assert!(validate_file_path("src/../../etc/passwd").is_err());
67 assert!(validate_file_path("foo/..").is_err());
68 }
69
70 #[test]
71 fn allows_dots_that_are_not_traversal() {
72 assert!(validate_file_path("src/.env").is_ok());
73 assert!(validate_file_path("src/...").is_ok());
74 assert!(validate_file_path(".gitignore").is_ok());
75 }
76}