Skip to main content

dk_protocol/
validation.rs

1//! Shared validation helpers for gRPC request fields.
2
3use tonic::Status;
4
5/// Validate a client-supplied file path.
6///
7/// Rejects empty paths, absolute paths, null bytes, and `..` traversal
8/// components. All gRPC handlers that accept a file path from the client
9/// should call this before any further processing.
10pub 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    // Check for path traversal
21    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
31/// Maximum allowed file content size (50 MB).
32pub 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}