1use crate::error::{Error, WireKind};
19
20use super::{HttpRequest, Transport};
21
22fn encode(value: &str) -> String {
30 let mut out = String::with_capacity(value.len());
31 for byte in value.as_bytes() {
32 match byte {
33 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
34 out.push(*byte as char);
35 }
36 other => out.push_str(&format!("%{other:02X}")),
37 }
38 }
39 out
40}
41
42fn file_path(path: &str, mode: Option<&str>) -> String {
44 match mode {
45 Some(mode) => format!("/v1/fs/file?path={}&mode={}", encode(path), encode(mode)),
46 None => format!("/v1/fs/file?path={}", encode(path)),
47 }
48}
49
50fn tar_path(path: &str) -> String {
52 format!("/v1/fs/tar?path={}", encode(path))
53}
54
55pub(crate) async fn upload_file(
57 transport: &Transport,
58 path: &str,
59 data: &[u8],
60 mode: Option<&str>,
61) -> Result<(), Error> {
62 let mut request = HttpRequest::new("PUT", file_path(path, mode));
63 request
64 .headers
65 .push(("content-type".into(), "application/octet-stream".into()));
66 request.body = data.to_vec();
67 transport.send(request).await?;
68 Ok(())
69}
70
71pub(crate) async fn download_file(transport: &Transport, path: &str) -> Result<Vec<u8>, Error> {
73 let response = transport
74 .send(HttpRequest::new("GET", file_path(path, None)))
75 .await?;
76 Ok(response.body)
77}
78
79pub(crate) async fn file_exists(transport: &Transport, path: &str) -> Result<bool, Error> {
85 match download_file(transport, path).await {
86 Ok(_) => Ok(true),
87 Err(err) if err.wire_kind() == Some(WireKind::NotFound) => Ok(false),
88 Err(err) => Err(err),
89 }
90}
91
92pub(crate) async fn upload_tar(
99 transport: &Transport,
100 remote: &str,
101 archive: &[u8],
102) -> Result<(), Error> {
103 let mut request = HttpRequest::new("PUT", tar_path(remote));
104 request
105 .headers
106 .push(("content-type".into(), "application/x-tar".into()));
107 request.body = archive.to_vec();
108 transport.send(request).await?;
109 Ok(())
110}
111
112pub(crate) async fn download_tar(transport: &Transport, remote: &str) -> Result<Vec<u8>, Error> {
121 let response = transport
122 .send(HttpRequest::new("GET", tar_path(remote)))
123 .await?;
124 Ok(response.body)
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::session::testing::{Recorder, Reply, session_with};
131 use std::sync::Arc;
132
133 #[tokio::test]
139 async fn an_upload_carries_its_octal_mode_as_a_string_only_when_given() {
140 let recorder = Recorder::with([Reply::Body(200, Vec::new()), Reply::Body(200, Vec::new())]);
141 let (session, _, _) = session_with(Arc::clone(&recorder));
142
143 session
144 .upload_file("/tmp/f", b"body", Some("0644"))
145 .await
146 .expect("uploads");
147 let request = recorder.last();
148 assert_eq!(request.method, "PUT");
149 assert!(request.path.contains("mode=0644"), "{}", request.path);
150 assert_eq!(request.body, b"body");
151
152 session
153 .upload_file("/tmp/g", b"", None)
154 .await
155 .expect("uploads");
156 assert!(
157 !recorder.last().path.contains("mode="),
158 "an unset mode must not become a default one: {}",
159 recorder.last().path
160 );
161 }
162
163 #[tokio::test]
168 async fn a_path_with_query_metacharacters_is_percent_encoded() {
169 let recorder = Recorder::with([Reply::Body(200, Vec::new())]);
170 let (session, _, _) = session_with(Arc::clone(&recorder));
171
172 session
173 .upload_file("/tmp/a&b c#d", b"x", None)
174 .await
175 .expect("uploads");
176 let path = recorder.last().path;
177 assert_eq!(path, "/v1/fs/file?path=%2Ftmp%2Fa%26b%20c%23d");
178 assert!(!path.contains("&b"), "the path truncated the query: {path}");
179 }
180
181 #[tokio::test]
183 async fn a_download_returns_raw_bytes_including_invalid_utf8() {
184 let bytes = vec![0xff, 0x00, 0xfe, b'a'];
185 let recorder = Recorder::with([Reply::Body(200, bytes.clone())]);
186 let (session, _, _) = session_with(Arc::clone(&recorder));
187
188 let got = session.download_file("/tmp/bin").await.expect("downloads");
189 assert_eq!(got, bytes);
190 assert_eq!(recorder.last().method, "GET");
191 }
192
193 #[tokio::test]
197 async fn file_exists_distinguishes_an_absent_path_from_a_malformed_request() {
198 let recorder = Recorder::with([
199 Reply::Body(200, b"content".to_vec()),
200 Reply::Body(404, b"not found".to_vec()),
201 Reply::Body(400, b"path is a directory".to_vec()),
202 ]);
203 let (session, _, _) = session_with(recorder);
204
205 assert!(session.file_exists("/tmp/there").await.expect("exists"));
206 assert!(!session.file_exists("/tmp/absent").await.expect("absent"));
207
208 let err = session
209 .file_exists("/tmp/dir")
210 .await
211 .expect_err("a 400 is not an absence");
212 assert_eq!(err.wire_kind(), Some(WireKind::ProtocolError));
213 }
214
215 #[tokio::test]
217 async fn tar_transfer_uses_the_tar_route_and_does_not_touch_the_archive() {
218 let archive = vec![b'u'; 1024];
219 let recorder = Recorder::with([
220 Reply::Body(200, Vec::new()),
221 Reply::Body(200, archive.clone()),
222 ]);
223 let (session, _, _) = session_with(Arc::clone(&recorder));
224
225 session
226 .upload_tar("/workspace", &archive)
227 .await
228 .expect("uploads");
229 let request = recorder.last();
230 assert_eq!(request.path, "/v1/fs/tar?path=%2Fworkspace");
231 assert_eq!(request.body, archive);
232
233 let got = session.download_tar("/workspace").await.expect("downloads");
234 assert_eq!(got, archive);
235 }
236
237 #[tokio::test]
240 async fn an_over_cap_upload_surfaces_as_too_large() {
241 let recorder = Recorder::with([Reply::Body(413, b"body exceeds max_body_bytes".to_vec())]);
242 let (session, _, _) = session_with(recorder);
243
244 let err = session
245 .upload_file("/tmp/huge", b"...", None)
246 .await
247 .expect_err("413");
248 assert_eq!(err.wire_kind(), Some(WireKind::TooLarge));
249 }
250
251 #[test]
254 fn unreserved_characters_pass_through_the_encoder() {
255 assert_eq!(encode("abcXYZ019-_.~"), "abcXYZ019-_.~");
256 assert_eq!(encode("/"), "%2F");
257 }
258}