nextcloud_uploads/
path.rs1use nextcloud_dav::DavPath;
4use nextcloud_sdk_core::{ApiPath, UrlError};
5
6pub fn validate_user(user_id: &str) -> Result<&str, UrlError> {
8 let trimmed = user_id.trim();
9 if trimmed.is_empty() || trimmed == "." || trimmed == ".." || trimmed.contains('/') {
10 return Err(UrlError::InvalidSegment(user_id.to_owned()));
11 }
12 check_characters(trimmed)?;
13 Ok(trimmed)
14}
15
16pub fn validate_upload_id(upload_id: &str) -> Result<&str, UrlError> {
18 let trimmed = upload_id.trim();
19 if trimmed.is_empty() || trimmed == "." || trimmed == ".." || trimmed.contains('/') {
20 return Err(UrlError::InvalidSegment(upload_id.to_owned()));
21 }
22 check_characters(trimmed)?;
23 Ok(trimmed)
24}
25
26pub fn validate_chunk_index(chunk_index: u32) -> Result<u32, UrlError> {
30 if chunk_index == 0 {
31 return Err(UrlError::InvalidSegment(
32 "chunk index must be greater than zero".to_owned(),
33 ));
34 }
35 Ok(chunk_index)
36}
37
38pub fn destination_header_value(user_id: &str, destination_path: &str) -> Result<String, UrlError> {
46 let trimmed = destination_path.trim();
47 if trimmed.is_empty() {
48 return Err(UrlError::InvalidSegment(
49 "destination path cannot be empty".to_owned(),
50 ));
51 }
52
53 if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
54 check_characters(trimmed)?;
55 return Ok(trimmed.to_owned());
56 }
57
58 if trimmed.starts_with("/remote.php/dav/") {
59 check_characters(trimmed)?;
60 return Ok(trimmed.to_owned());
61 }
62
63 let relative = trimmed.trim_start_matches('/');
64 if relative.is_empty() {
65 return Err(UrlError::InvalidSegment(
66 "destination path cannot be a bare slash".to_owned(),
67 ));
68 }
69
70 let dav_path = DavPath::new(user_id, relative)?;
71 Ok(dav_path.as_str().to_owned())
72}
73
74fn check_characters(segment: &str) -> Result<(), UrlError> {
76 let rejected = segment
77 .chars()
78 .any(|c| c.is_control() || matches!(c, '?' | '#' | '\\'));
79 if rejected {
80 return Err(UrlError::InvalidSegment(segment.to_owned()));
81 }
82 Ok(())
83}
84
85pub fn upload_collection_path(user_id: &str, upload_id: &str) -> Result<ApiPath, UrlError> {
89 let user = validate_user(user_id)?;
90 let upload = validate_upload_id(upload_id)?;
91 ApiPath::dav(format!("uploads/{user}/{upload}"))
92}
93
94pub fn chunk_path(user_id: &str, upload_id: &str, chunk_index: u32) -> Result<ApiPath, UrlError> {
99 let user = validate_user(user_id)?;
100 let upload = validate_upload_id(upload_id)?;
101 let valid_index = validate_chunk_index(chunk_index)?;
102 let chunk_segment = format!("{valid_index:05}");
103 ApiPath::dav(format!("uploads/{user}/{upload}/{chunk_segment}"))
104}
105
106pub fn assembly_path(user_id: &str, upload_id: &str) -> Result<ApiPath, UrlError> {
110 let user = validate_user(user_id)?;
111 let upload = validate_upload_id(upload_id)?;
112 ApiPath::dav(format!("uploads/{user}/{upload}/.file"))
113}
114
115pub fn abort_path(user_id: &str, upload_id: &str) -> Result<ApiPath, UrlError> {
119 let user = validate_user(user_id)?;
120 let upload = validate_upload_id(upload_id)?;
121 ApiPath::dav(format!("uploads/{user}/{upload}/"))
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn valid_paths_construction() {
130 let collection = upload_collection_path("alice", "up-123").unwrap();
131 assert_eq!(collection.as_str(), "/remote.php/dav/uploads/alice/up-123");
132
133 let chunk1 = chunk_path("alice", "up-123", 1).unwrap();
134 assert_eq!(
135 chunk1.as_str(),
136 "/remote.php/dav/uploads/alice/up-123/00001"
137 );
138
139 let chunk42 = chunk_path("alice", "up-123", 42).unwrap();
140 assert_eq!(
141 chunk42.as_str(),
142 "/remote.php/dav/uploads/alice/up-123/00042"
143 );
144
145 let assembly = assembly_path("alice", "up-123").unwrap();
146 assert_eq!(
147 assembly.as_str(),
148 "/remote.php/dav/uploads/alice/up-123/.file"
149 );
150
151 let abort = abort_path("alice", "up-123").unwrap();
152 assert_eq!(abort.as_str(), "/remote.php/dav/uploads/alice/up-123/");
153 }
154
155 #[test]
156 fn destination_header_resolution() {
157 let rel = destination_header_value("alice", "Photos/trip.jpg").unwrap();
158 assert_eq!(rel, "/remote.php/dav/files/alice/Photos/trip.jpg");
159
160 let leading_slash_rel = destination_header_value("alice", "/Photos/trip.jpg").unwrap();
161 assert_eq!(
162 leading_slash_rel,
163 "/remote.php/dav/files/alice/Photos/trip.jpg"
164 );
165
166 let full_dav =
167 destination_header_value("alice", "/remote.php/dav/files/alice/Photos/trip.jpg")
168 .unwrap();
169 assert_eq!(full_dav, "/remote.php/dav/files/alice/Photos/trip.jpg");
170
171 let full_url = destination_header_value(
172 "alice",
173 "https://cloud.example.com/remote.php/dav/files/alice/Photos/trip.jpg",
174 )
175 .unwrap();
176 assert_eq!(
177 full_url,
178 "https://cloud.example.com/remote.php/dav/files/alice/Photos/trip.jpg"
179 );
180 }
181
182 #[test]
183 fn rejects_invalid_segments() {
184 assert!(validate_user("").is_err());
185 assert!(validate_user("..").is_err());
186 assert!(validate_user("alice/bob").is_err());
187 assert!(validate_user("alice?x").is_err());
188
189 assert!(validate_upload_id("").is_err());
190 assert!(validate_upload_id("..").is_err());
191 assert!(validate_upload_id("id/nested").is_err());
192 assert!(validate_upload_id("id#bad").is_err());
193
194 assert!(validate_chunk_index(0).is_err());
195 assert!(validate_chunk_index(1).is_ok());
196
197 assert!(destination_header_value("alice", "").is_err());
198 assert!(destination_header_value("alice", "/").is_err());
199 assert!(destination_header_value("alice", "../traversal").is_err());
200 assert!(destination_header_value("alice", "file?bad").is_err());
201 }
202}