1use std::os::unix::fs::{MetadataExt, PermissionsExt};
7
8use microsandbox_protocol::{
9 codec::encode_to_buf,
10 fs::{FS_CHUNK_SIZE, FsData, FsEntryInfo, FsOp, FsRequest, FsResponse, FsResponseData},
11 message::{Message, MessageType},
12};
13use tokio::{
14 io::{AsyncReadExt, AsyncWriteExt},
15 sync::mpsc,
16};
17
18use crate::session::SessionOutput;
19
20pub struct FsWriteSession {
26 file: tokio::fs::File,
27}
28
29pub async fn handle_fs_request(
44 id: u32,
45 req: FsRequest,
46 out_buf: &mut Vec<u8>,
47 session_tx: &mpsc::UnboundedSender<(u32, SessionOutput)>,
48) -> Result<Option<FsWriteSession>, String> {
49 match req.op {
50 FsOp::Stat { path } => {
51 let resp = handle_stat(&path).await;
52 encode_response(id, resp, out_buf)?;
53 Ok(None)
54 }
55 FsOp::List { path } => {
56 let resp = handle_list(&path).await;
57 encode_response(id, resp, out_buf)?;
58 Ok(None)
59 }
60 FsOp::Read { path } => {
61 let tx = session_tx.clone();
62 tokio::spawn(async move {
63 handle_read_stream(id, &path, &tx).await;
64 });
65 Ok(None)
66 }
67 FsOp::Write { path, mode } => match handle_write_open(&path, mode).await {
68 Ok(session) => Ok(Some(session)),
69 Err(e) => {
70 let resp = FsResponse {
71 ok: false,
72 error: Some(e),
73 data: None,
74 };
75 encode_response(id, resp, out_buf)?;
76 Ok(None)
77 }
78 },
79 FsOp::Mkdir { path } => {
80 let resp = handle_mkdir(&path).await;
81 encode_response(id, resp, out_buf)?;
82 Ok(None)
83 }
84 FsOp::Remove { path } => {
85 let resp = handle_remove(&path).await;
86 encode_response(id, resp, out_buf)?;
87 Ok(None)
88 }
89 FsOp::RemoveDir { path } => {
90 let resp = handle_remove_dir(&path).await;
91 encode_response(id, resp, out_buf)?;
92 Ok(None)
93 }
94 FsOp::Copy { src, dst } => {
95 let resp = handle_copy(&src, &dst).await;
96 encode_response(id, resp, out_buf)?;
97 Ok(None)
98 }
99 FsOp::Rename { src, dst } => {
100 let resp = handle_rename(&src, &dst).await;
101 encode_response(id, resp, out_buf)?;
102 Ok(None)
103 }
104 }
105}
106
107pub async fn handle_fs_data(
112 id: u32,
113 data: FsData,
114 session: &mut FsWriteSession,
115 out_buf: &mut Vec<u8>,
116) -> Result<bool, String> {
117 if data.data.is_empty() {
118 if let Err(e) = session.file.flush().await {
120 let resp = FsResponse {
121 ok: false,
122 error: Some(format!("flush: {e}")),
123 data: None,
124 };
125 encode_response(id, resp, out_buf)?;
126 return Ok(true);
127 }
128
129 let resp = FsResponse {
130 ok: true,
131 error: None,
132 data: None,
133 };
134 encode_response(id, resp, out_buf)?;
135 Ok(true)
136 } else {
137 if let Err(e) = session.file.write_all(&data.data).await {
139 let resp = FsResponse {
140 ok: false,
141 error: Some(format!("write: {e}")),
142 data: None,
143 };
144 encode_response(id, resp, out_buf)?;
145 return Ok(true);
146 }
147 Ok(false)
148 }
149}
150
151fn encode_response(id: u32, resp: FsResponse, out_buf: &mut Vec<u8>) -> Result<(), String> {
157 let msg = Message::with_payload(MessageType::FsResponse, id, &resp)
158 .map_err(|e| format!("encode fs response: {e}"))?;
159 encode_to_buf(&msg, out_buf).map_err(|e| format!("encode fs response frame: {e}"))?;
160 Ok(())
161}
162
163async fn handle_stat(path: &str) -> FsResponse {
165 match tokio::fs::symlink_metadata(path).await {
166 Ok(meta) => FsResponse {
167 ok: true,
168 error: None,
169 data: Some(FsResponseData::Stat(metadata_to_entry_info(path, &meta))),
170 },
171 Err(e) => FsResponse {
172 ok: false,
173 error: Some(format!("stat: {e}")),
174 data: None,
175 },
176 }
177}
178
179async fn handle_list(path: &str) -> FsResponse {
181 match tokio::fs::read_dir(path).await {
182 Ok(mut dir) => {
183 let mut entries = Vec::new();
184 loop {
185 match dir.next_entry().await {
186 Ok(Some(entry)) => {
187 let entry_path = entry.path();
188 let path_str = entry_path.to_string_lossy().to_string();
189 match tokio::fs::symlink_metadata(&entry_path).await {
190 Ok(meta) => {
191 entries.push(metadata_to_entry_info(&path_str, &meta));
192 }
193 Err(_) => {
194 entries.push(FsEntryInfo {
196 path: path_str,
197 kind: "other".to_string(),
198 size: 0,
199 mode: 0,
200 modified: None,
201 });
202 }
203 }
204 }
205 Ok(None) => break,
206 Err(e) => {
207 return FsResponse {
208 ok: false,
209 error: Some(format!("readdir: {e}")),
210 data: None,
211 };
212 }
213 }
214 }
215 FsResponse {
216 ok: true,
217 error: None,
218 data: Some(FsResponseData::List(entries)),
219 }
220 }
221 Err(e) => FsResponse {
222 ok: false,
223 error: Some(format!("opendir: {e}")),
224 data: None,
225 },
226 }
227}
228
229async fn handle_read_stream(id: u32, path: &str, tx: &mpsc::UnboundedSender<(u32, SessionOutput)>) {
231 let file = match tokio::fs::File::open(path).await {
232 Ok(f) => f,
233 Err(e) => {
234 send_raw_response(id, false, Some(format!("open: {e}")), None, tx);
235 return;
236 }
237 };
238
239 let mut reader = tokio::io::BufReader::new(file);
240 let mut chunk = vec![0u8; FS_CHUNK_SIZE];
241 let mut buf = Vec::new();
242
243 loop {
244 match reader.read(&mut chunk).await {
245 Ok(0) => break,
246 Ok(n) => {
247 let data = FsData {
248 data: chunk[..n].to_vec(),
249 };
250 let msg = match Message::with_payload(MessageType::FsData, id, &data) {
251 Ok(msg) => msg,
252 Err(e) => {
253 send_raw_response(id, false, Some(format!("encode chunk: {e}")), None, tx);
254 return;
255 }
256 };
257 buf.clear();
258 if let Err(e) = encode_to_buf(&msg, &mut buf) {
259 send_raw_response(
260 id,
261 false,
262 Some(format!("encode chunk frame: {e}")),
263 None,
264 tx,
265 );
266 return;
267 }
268 if tx.send((id, SessionOutput::Raw(buf.clone()))).is_err() {
269 return;
270 }
271 }
272 Err(e) => {
273 send_raw_response(id, false, Some(format!("read: {e}")), None, tx);
274 return;
275 }
276 }
277 }
278
279 send_raw_response(id, true, None, None, tx);
281}
282
283fn send_raw_response(
285 id: u32,
286 ok: bool,
287 error: Option<String>,
288 data: Option<FsResponseData>,
289 tx: &mpsc::UnboundedSender<(u32, SessionOutput)>,
290) {
291 let resp = FsResponse { ok, error, data };
292 match Message::with_payload(MessageType::FsResponse, id, &resp) {
293 Ok(msg) => {
294 let mut buf = Vec::new();
295 match encode_to_buf(&msg, &mut buf) {
296 Ok(()) => {
297 let _ = tx.send((id, SessionOutput::Raw(buf)));
298 }
299 Err(e) => {
300 eprintln!("failed to encode fs response frame for {id}: {e}");
301 }
302 }
303 }
304 Err(e) => {
305 eprintln!("failed to encode fs response for {id}: {e}");
306 }
307 }
308}
309
310async fn handle_write_open(path: &str, mode: Option<u32>) -> Result<FsWriteSession, String> {
312 if let Some(parent) = std::path::Path::new(path).parent()
314 && !parent.as_os_str().is_empty()
315 {
316 tokio::fs::create_dir_all(parent)
317 .await
318 .map_err(|e| format!("mkdir parent: {e}"))?;
319 }
320
321 let file = tokio::fs::OpenOptions::new()
322 .write(true)
323 .create(true)
324 .truncate(true)
325 .open(path)
326 .await
327 .map_err(|e| format!("open for write: {e}"))?;
328
329 if let Some(mode) = mode {
331 let perms = std::fs::Permissions::from_mode(mode);
332 file.set_permissions(perms)
333 .await
334 .map_err(|e| format!("set permissions: {e}"))?;
335 }
336
337 Ok(FsWriteSession { file })
338}
339
340async fn handle_mkdir(path: &str) -> FsResponse {
342 match tokio::fs::create_dir_all(path).await {
343 Ok(()) => FsResponse {
344 ok: true,
345 error: None,
346 data: None,
347 },
348 Err(e) => FsResponse {
349 ok: false,
350 error: Some(format!("mkdir: {e}")),
351 data: None,
352 },
353 }
354}
355
356async fn handle_remove(path: &str) -> FsResponse {
358 match tokio::fs::remove_file(path).await {
359 Ok(()) => FsResponse {
360 ok: true,
361 error: None,
362 data: None,
363 },
364 Err(e) => FsResponse {
365 ok: false,
366 error: Some(format!("remove: {e}")),
367 data: None,
368 },
369 }
370}
371
372async fn handle_remove_dir(path: &str) -> FsResponse {
374 match tokio::fs::remove_dir_all(path).await {
375 Ok(()) => FsResponse {
376 ok: true,
377 error: None,
378 data: None,
379 },
380 Err(e) => FsResponse {
381 ok: false,
382 error: Some(format!("remove_dir: {e}")),
383 data: None,
384 },
385 }
386}
387
388async fn handle_copy(src: &str, dst: &str) -> FsResponse {
390 if let Some(parent) = std::path::Path::new(dst).parent()
392 && !parent.as_os_str().is_empty()
393 && let Err(e) = tokio::fs::create_dir_all(parent).await
394 {
395 return FsResponse {
396 ok: false,
397 error: Some(format!("mkdir parent: {e}")),
398 data: None,
399 };
400 }
401
402 match tokio::fs::copy(src, dst).await {
403 Ok(_) => FsResponse {
404 ok: true,
405 error: None,
406 data: None,
407 },
408 Err(e) => FsResponse {
409 ok: false,
410 error: Some(format!("copy: {e}")),
411 data: None,
412 },
413 }
414}
415
416async fn handle_rename(src: &str, dst: &str) -> FsResponse {
418 if let Some(parent) = std::path::Path::new(dst).parent()
420 && !parent.as_os_str().is_empty()
421 && let Err(e) = tokio::fs::create_dir_all(parent).await
422 {
423 return FsResponse {
424 ok: false,
425 error: Some(format!("mkdir parent: {e}")),
426 data: None,
427 };
428 }
429
430 match tokio::fs::rename(src, dst).await {
431 Ok(()) => FsResponse {
432 ok: true,
433 error: None,
434 data: None,
435 },
436 Err(e) => FsResponse {
437 ok: false,
438 error: Some(format!("rename: {e}")),
439 data: None,
440 },
441 }
442}
443
444fn metadata_to_entry_info(path: &str, meta: &std::fs::Metadata) -> FsEntryInfo {
446 let kind = if meta.is_file() {
447 "file"
448 } else if meta.is_dir() {
449 "dir"
450 } else if meta.is_symlink() {
451 "symlink"
452 } else {
453 "other"
454 };
455
456 let modified = meta
457 .modified()
458 .ok()
459 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
460 .map(|d| d.as_secs() as i64);
461
462 FsEntryInfo {
463 path: path.to_string(),
464 kind: kind.to_string(),
465 size: meta.len(),
466 mode: meta.mode(),
467 modified,
468 }
469}