1use crate::error::{Error, WireKind};
19
20use super::{HttpRequest, Transport};
21
22fn encode(value: &str) -> String {
29 use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, utf8_percent_encode};
30 const ESCAPED: &AsciiSet = &NON_ALPHANUMERIC
31 .remove(b'-')
32 .remove(b'_')
33 .remove(b'.')
34 .remove(b'~');
35 utf8_percent_encode(value, ESCAPED).to_string()
36}
37
38fn file_path(path: &str, mode: Option<&str>) -> String {
40 match mode {
41 Some(mode) => format!("/v1/fs/file?path={}&mode={}", encode(path), encode(mode)),
42 None => format!("/v1/fs/file?path={}", encode(path)),
43 }
44}
45
46fn tar_path(path: &str) -> String {
48 format!("/v1/fs/tar?path={}", encode(path))
49}
50
51pub(crate) async fn upload_file(
53 transport: &Transport,
54 path: &str,
55 data: &[u8],
56 mode: Option<&str>,
57) -> Result<(), Error> {
58 let mut request = HttpRequest::new("PUT", file_path(path, mode));
59 request
60 .headers
61 .push(("content-type".into(), "application/octet-stream".into()));
62 request.body = data.to_vec();
63 transport.send(request).await?;
64 Ok(())
65}
66
67pub(crate) async fn download_file(transport: &Transport, path: &str) -> Result<Vec<u8>, Error> {
69 let response = transport
70 .send(HttpRequest::new("GET", file_path(path, None)))
71 .await?;
72 Ok(response.body)
73}
74
75pub(crate) async fn file_exists(transport: &Transport, path: &str) -> Result<bool, Error> {
81 match download_file(transport, path).await {
82 Ok(_) => Ok(true),
83 Err(err) if err.wire_kind() == Some(WireKind::NotFound) => Ok(false),
84 Err(err) => Err(err),
85 }
86}
87
88pub(crate) async fn upload_tar(
95 transport: &Transport,
96 remote: &str,
97 archive: &[u8],
98) -> Result<(), Error> {
99 let mut request = HttpRequest::new("PUT", tar_path(remote));
100 request
101 .headers
102 .push(("content-type".into(), "application/x-tar".into()));
103 request.body = archive.to_vec();
104 transport.send(request).await?;
105 Ok(())
106}
107
108pub(crate) async fn download_tar(transport: &Transport, remote: &str) -> Result<Vec<u8>, Error> {
117 let response = transport
118 .send(HttpRequest::new("GET", tar_path(remote)))
119 .await?;
120 Ok(response.body)
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::session::testing::{Recorder, Reply, session_with};
127 use std::sync::Arc;
128
129 #[tokio::test]
135 async fn an_upload_carries_its_octal_mode_as_a_string_only_when_given() {
136 let recorder = Recorder::with([Reply::Body(200, Vec::new()), Reply::Body(200, Vec::new())]);
137 let (session, _, _) = session_with(Arc::clone(&recorder));
138
139 session
140 .upload_file("/tmp/f", b"body", Some("0644"))
141 .await
142 .expect("uploads");
143 let request = recorder.last();
144 assert_eq!(request.method, "PUT");
145 assert!(request.path.contains("mode=0644"), "{}", request.path);
146 assert_eq!(request.body, b"body");
147
148 session
149 .upload_file("/tmp/g", b"", None)
150 .await
151 .expect("uploads");
152 assert!(
153 !recorder.last().path.contains("mode="),
154 "an unset mode must not become a default one: {}",
155 recorder.last().path
156 );
157 }
158
159 #[tokio::test]
164 async fn a_path_with_query_metacharacters_is_percent_encoded() {
165 let recorder = Recorder::with([Reply::Body(200, Vec::new())]);
166 let (session, _, _) = session_with(Arc::clone(&recorder));
167
168 session
169 .upload_file("/tmp/a&b c#d", b"x", None)
170 .await
171 .expect("uploads");
172 let path = recorder.last().path;
173 assert_eq!(path, "/v1/fs/file?path=%2Ftmp%2Fa%26b%20c%23d");
174 assert!(!path.contains("&b"), "the path truncated the query: {path}");
175 }
176
177 #[tokio::test]
179 async fn a_download_returns_raw_bytes_including_invalid_utf8() {
180 let bytes = vec![0xff, 0x00, 0xfe, b'a'];
181 let recorder = Recorder::with([Reply::Body(200, bytes.clone())]);
182 let (session, _, _) = session_with(Arc::clone(&recorder));
183
184 let got = session.download_file("/tmp/bin").await.expect("downloads");
185 assert_eq!(got, bytes);
186 assert_eq!(recorder.last().method, "GET");
187 }
188
189 #[tokio::test]
193 async fn file_exists_distinguishes_an_absent_path_from_a_malformed_request() {
194 let recorder = Recorder::with([
195 Reply::Body(200, b"content".to_vec()),
196 Reply::Body(404, b"not found".to_vec()),
197 Reply::Body(400, b"path is a directory".to_vec()),
198 ]);
199 let (session, _, _) = session_with(recorder);
200
201 assert!(session.file_exists("/tmp/there").await.expect("exists"));
202 assert!(!session.file_exists("/tmp/absent").await.expect("absent"));
203
204 let err = session
205 .file_exists("/tmp/dir")
206 .await
207 .expect_err("a 400 is not an absence");
208 assert_eq!(err.wire_kind(), Some(WireKind::ProtocolError));
209 }
210
211 #[tokio::test]
213 async fn tar_transfer_uses_the_tar_route_and_does_not_touch_the_archive() {
214 let archive = vec![b'u'; 1024];
215 let recorder = Recorder::with([
216 Reply::Body(200, Vec::new()),
217 Reply::Body(200, archive.clone()),
218 ]);
219 let (session, _, _) = session_with(Arc::clone(&recorder));
220
221 session
222 .upload_tar("/workspace", &archive)
223 .await
224 .expect("uploads");
225 let request = recorder.last();
226 assert_eq!(request.path, "/v1/fs/tar?path=%2Fworkspace");
227 assert_eq!(request.body, archive);
228
229 let got = session.download_tar("/workspace").await.expect("downloads");
230 assert_eq!(got, archive);
231 }
232
233 #[tokio::test]
236 async fn an_over_cap_upload_surfaces_as_too_large() {
237 let recorder = Recorder::with([Reply::Body(413, b"body exceeds max_body_bytes".to_vec())]);
238 let (session, _, _) = session_with(recorder);
239
240 let err = session
241 .upload_file("/tmp/huge", b"...", None)
242 .await
243 .expect_err("413");
244 assert_eq!(err.wire_kind(), Some(WireKind::TooLarge));
245 }
246
247 #[test]
250 fn unreserved_characters_pass_through_the_encoder() {
251 assert_eq!(encode("abcXYZ019-_.~"), "abcXYZ019-_.~");
252 assert_eq!(encode("/"), "%2F");
253 }
254}