files_sdk/
utils.rs

1//! Utility functions for the Files.com SDK
2//!
3//! This module provides common utility functions used throughout the SDK,
4//! including path encoding, URL construction, and other helpers.
5
6/// Encodes a file path for safe use in URLs
7///
8/// Files.com paths may contain special characters (spaces, brackets, unicode, etc.)
9/// that need to be properly URL-encoded. This function:
10/// - Splits the path by `/` to preserve directory structure
11/// - Percent-encodes each path segment individually (using %20 for spaces, not +)
12/// - Rejoins with `/` separators
13///
14/// # Arguments
15///
16/// * `path` - The file or folder path to encode
17///
18/// # Returns
19///
20/// A URL-safe encoded path string
21///
22/// # Examples
23///
24/// ```
25/// use files_sdk::utils::encode_path;
26///
27/// assert_eq!(encode_path("/my folder/file.txt"), "/my%20folder/file.txt");
28/// assert_eq!(encode_path("/data/file[2024].txt"), "/data/file%5B2024%5D.txt");
29/// assert_eq!(encode_path("/文档/файл.txt"), "/%E6%96%87%E6%A1%A3/%D1%84%D0%B0%D0%B9%D0%BB.txt");
30/// ```
31pub fn encode_path(path: &str) -> String {
32    // Handle empty or root path
33    if path.is_empty() || path == "/" {
34        return path.to_string();
35    }
36
37    // Split by '/', encode each segment, then rejoin
38    let segments: Vec<String> = path
39        .split('/')
40        .map(|segment| {
41            if segment.is_empty() {
42                // Preserve empty segments (leading/trailing slashes)
43                segment.to_string()
44            } else {
45                // Percent-encode the segment
46                // We use a simple manual approach to ensure %20 for spaces (not +)
47                percent_encode(segment)
48            }
49        })
50        .collect();
51
52    segments.join("/")
53}
54
55/// Percent-encodes a string for use in URL paths
56///
57/// Unlike form encoding which uses + for spaces, this uses %20 for spaces
58/// and encodes all non-alphanumeric characters except: - _ . ~
59///
60/// This follows RFC 3986 unreserved characters
61fn percent_encode(s: &str) -> String {
62    let mut encoded = String::new();
63
64    for byte in s.bytes() {
65        match byte {
66            // Unreserved characters (RFC 3986)
67            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
68                encoded.push(byte as char);
69            }
70            // Everything else gets percent-encoded
71            _ => {
72                encoded.push_str(&format!("%{:02X}", byte));
73            }
74        }
75    }
76
77    encoded
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn test_encode_simple_path() {
86        assert_eq!(encode_path("/simple/path.txt"), "/simple/path.txt");
87    }
88
89    #[test]
90    fn test_encode_path_with_spaces() {
91        assert_eq!(
92            encode_path("/my folder/my file.txt"),
93            "/my%20folder/my%20file.txt"
94        );
95    }
96
97    #[test]
98    fn test_encode_path_with_brackets() {
99        assert_eq!(
100            encode_path("/data/file[2024].txt"),
101            "/data/file%5B2024%5D.txt"
102        );
103    }
104
105    #[test]
106    fn test_encode_path_with_unicode() {
107        // Chinese characters
108        assert_eq!(
109            encode_path("/文档/测试.txt"),
110            "/%E6%96%87%E6%A1%A3/%E6%B5%8B%E8%AF%95.txt"
111        );
112
113        // Cyrillic characters
114        assert_eq!(
115            encode_path("/папка/файл.txt"),
116            "/%D0%BF%D0%B0%D0%BF%D0%BA%D0%B0/%D1%84%D0%B0%D0%B9%D0%BB.txt"
117        );
118    }
119
120    #[test]
121    fn test_encode_path_with_quotes() {
122        assert_eq!(
123            encode_path("/\"quoted\"/file.txt"),
124            "/%22quoted%22/file.txt"
125        );
126    }
127
128    #[test]
129    fn test_encode_path_with_special_chars() {
130        assert_eq!(encode_path("/data/file@#$.txt"), "/data/file%40%23%24.txt");
131    }
132
133    #[test]
134    fn test_encode_empty_path() {
135        assert_eq!(encode_path(""), "");
136    }
137
138    #[test]
139    fn test_encode_root_path() {
140        assert_eq!(encode_path("/"), "/");
141    }
142
143    #[test]
144    fn test_encode_path_preserves_leading_slash() {
145        assert_eq!(encode_path("/folder/file"), "/folder/file");
146    }
147
148    #[test]
149    fn test_encode_path_without_leading_slash() {
150        assert_eq!(encode_path("folder/file"), "folder/file");
151    }
152
153    #[test]
154    fn test_encode_path_with_trailing_slash() {
155        assert_eq!(encode_path("/folder/"), "/folder/");
156    }
157
158    #[test]
159    fn test_encode_complex_path() {
160        // Combination of spaces, brackets, and unicode
161        assert_eq!(
162            encode_path("/my folder/data [2024]/文档.txt"),
163            "/my%20folder/data%20%5B2024%5D/%E6%96%87%E6%A1%A3.txt"
164        );
165    }
166}