1use std::collections::HashMap;
7use std::ffi::CString;
8use std::os::fd::AsRawFd;
9use std::os::unix::ffi::OsStrExt;
10use std::os::unix::fs::{MetadataExt, PermissionsExt};
11use std::path::Path;
12use std::sync::Arc;
13
14use microsandbox_protocol::AGENT_RELAY_ID_RANGE_STEP;
15use microsandbox_protocol::codec;
16use microsandbox_protocol::fs::{
17 FS_CHUNK_SIZE, FsData, FsEntryInfo, FsOp, FsOpenOptions, FsRequest, FsResponse, FsResponseData,
18 FsSetAttrs,
19};
20use microsandbox_protocol::message::{Message, MessageType};
21use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
22use tokio::sync::{Mutex, mpsc};
23use tokio::task::JoinHandle;
24
25use crate::session::{RawActivity, RawSessionCompletion, RawSessionOutput, SessionOutput};
26
27const DEFAULT_READ_DIR_LIMIT: u32 = 128;
33
34const MAX_OPEN_HANDLES_PER_OWNER: usize = 1024;
36
37#[derive(Default)]
43pub struct FsState {
44 next_handle: u64,
45 handles: HashMap<u64, FsHandleEntry>,
46}
47
48pub struct FsWriteSession {
50 owner_id: u32,
51 handle: u64,
52 file: Arc<Mutex<tokio::fs::File>>,
53 offset: u64,
54 append: bool,
55 expected_len: Option<u64>,
56 written: u64,
57}
58
59pub struct FsReadSession {
61 owner_id: u32,
62 handle: u64,
63 task: JoinHandle<()>,
64}
65
66pub enum FsStreamSession {
68 Read(FsReadSession),
70
71 Write(FsWriteSession),
73}
74
75enum FsHandleEntry {
76 File {
77 owner_id: u32,
78 file: Arc<Mutex<tokio::fs::File>>,
79 read: bool,
80 write: bool,
81 append: bool,
82 path: String,
83 },
84 Dir {
85 owner_id: u32,
86 dir: Arc<Mutex<tokio::fs::ReadDir>>,
87 path: String,
88 },
89}
90
91impl FsState {
96 pub fn close_owner_range(&mut self, id_start: u32, id_end_exclusive: u32) {
98 self.handles.retain(|_, handle| {
99 let owner_id = handle.owner_id();
100 owner_id < id_start || owner_id >= id_end_exclusive
101 });
102 }
103
104 pub fn clear(&mut self) {
106 self.handles.clear();
107 }
108
109 fn insert_file(
110 &mut self,
111 owner_id: u32,
112 file: tokio::fs::File,
113 read: bool,
114 write: bool,
115 append: bool,
116 path: String,
117 ) -> Result<u64, String> {
118 self.enforce_owner_limit(owner_id)?;
119 let handle = self.alloc_handle();
120 self.handles.insert(
121 handle,
122 FsHandleEntry::File {
123 owner_id,
124 file: Arc::new(Mutex::new(file)),
125 read,
126 write,
127 append,
128 path,
129 },
130 );
131 Ok(handle)
132 }
133
134 fn insert_dir(
135 &mut self,
136 owner_id: u32,
137 dir: tokio::fs::ReadDir,
138 path: String,
139 ) -> Result<u64, String> {
140 self.enforce_owner_limit(owner_id)?;
141 let handle = self.alloc_handle();
142 self.handles.insert(
143 handle,
144 FsHandleEntry::Dir {
145 owner_id,
146 dir: Arc::new(Mutex::new(dir)),
147 path,
148 },
149 );
150 Ok(handle)
151 }
152
153 fn close_handle(&mut self, caller_id: u32, handle: u64) -> Result<FsHandleEntry, String> {
154 if let Some(entry) = self.handles.get(&handle) {
155 entry.ensure_owner(handle, caller_id)?;
156 }
157 self.handles
158 .remove(&handle)
159 .ok_or_else(|| format!("invalid handle: {handle}"))
160 }
161
162 fn file(
163 &self,
164 caller_id: u32,
165 handle: u64,
166 need_read: bool,
167 need_write: bool,
168 ) -> Result<(Arc<Mutex<tokio::fs::File>>, bool, String), String> {
169 match self.handles.get(&handle) {
170 Some(FsHandleEntry::File {
171 file,
172 read,
173 write,
174 append,
175 path,
176 ..
177 }) => {
178 self.handles
179 .get(&handle)
180 .expect("entry just matched")
181 .ensure_owner(handle, caller_id)?;
182 if need_read && !read {
183 return Err(format!("handle {handle} is not open for reading"));
184 }
185 if need_write && !write && !append {
186 return Err(format!("handle {handle} is not open for writing"));
187 }
188 Ok((Arc::clone(file), *append, path.clone()))
189 }
190 Some(FsHandleEntry::Dir { .. }) => Err(format!("handle {handle} is a directory")),
191 None => Err(format!("invalid handle: {handle}")),
192 }
193 }
194
195 fn dir(
196 &self,
197 caller_id: u32,
198 handle: u64,
199 ) -> Result<(Arc<Mutex<tokio::fs::ReadDir>>, String), String> {
200 match self.handles.get(&handle) {
201 Some(FsHandleEntry::Dir { dir, path, .. }) => {
202 self.handles
203 .get(&handle)
204 .expect("entry just matched")
205 .ensure_owner(handle, caller_id)?;
206 Ok((Arc::clone(dir), path.clone()))
207 }
208 Some(FsHandleEntry::File { .. }) => Err(format!("handle {handle} is a file")),
209 None => Err(format!("invalid handle: {handle}")),
210 }
211 }
212
213 fn alloc_handle(&mut self) -> u64 {
214 self.next_handle = self.next_handle.wrapping_add(1).max(1);
215 while self.handles.contains_key(&self.next_handle) {
216 self.next_handle = self.next_handle.wrapping_add(1).max(1);
217 }
218 self.next_handle
219 }
220
221 fn enforce_owner_limit(&self, owner_id: u32) -> Result<(), String> {
222 let count = self
223 .handles
224 .values()
225 .filter(|entry| same_relay_client(entry.owner_id(), owner_id))
226 .count();
227 if count >= MAX_OPEN_HANDLES_PER_OWNER {
228 return Err(format!(
229 "too many open filesystem handles for relay client: {count}"
230 ));
231 }
232 Ok(())
233 }
234}
235
236impl FsHandleEntry {
237 fn owner_id(&self) -> u32 {
238 match self {
239 Self::File { owner_id, .. } | Self::Dir { owner_id, .. } => *owner_id,
240 }
241 }
242
243 fn ensure_owner(&self, handle: u64, caller_id: u32) -> Result<(), String> {
244 if same_relay_client(self.owner_id(), caller_id) {
245 Ok(())
246 } else {
247 Err(format!(
248 "handle {handle} is owned by a different relay client"
249 ))
250 }
251 }
252}
253
254impl FsReadSession {
255 pub fn owner_id(&self) -> u32 {
257 self.owner_id
258 }
259
260 pub fn handle(&self) -> u64 {
262 self.handle
263 }
264
265 pub fn abort(self) {
267 self.task.abort();
268 }
269}
270
271impl FsWriteSession {
272 pub fn owner_id(&self) -> u32 {
274 self.owner_id
275 }
276
277 pub fn handle(&self) -> u64 {
279 self.handle
280 }
281}
282
283fn relay_client_slot(id: u32) -> Option<u32> {
288 if id == 0 {
289 None
290 } else {
291 Some((id - 1) / AGENT_RELAY_ID_RANGE_STEP)
292 }
293}
294
295fn same_relay_client(left: u32, right: u32) -> bool {
296 relay_client_slot(left).is_some_and(|left| Some(left) == relay_client_slot(right))
297}
298
299pub async fn handle_fs_request(
301 id: u32,
302 req: FsRequest,
303 state: &mut FsState,
304 out_buf: &mut Vec<u8>,
305 session_tx: &mpsc::UnboundedSender<(u32, SessionOutput)>,
306) -> Result<Option<FsStreamSession>, String> {
307 match req.op {
308 FsOp::RealPath { path } => {
309 let resp = handle_realpath(&path).await;
310 encode_response(id, resp, out_buf)?;
311 Ok(None)
312 }
313 FsOp::Stat {
314 path,
315 follow_symlink,
316 } => {
317 let resp = handle_stat(&path, follow_symlink).await;
318 encode_response(id, resp, out_buf)?;
319 Ok(None)
320 }
321 FsOp::SetStat {
322 path,
323 follow_symlink,
324 attrs,
325 } => {
326 let resp = handle_setstat(&path, follow_symlink, attrs).await;
327 encode_response(id, resp, out_buf)?;
328 Ok(None)
329 }
330 FsOp::List { path } => {
331 let resp = handle_list(&path).await;
332 encode_response(id, resp, out_buf)?;
333 Ok(None)
334 }
335 FsOp::ReadLink { path } => {
336 let resp = handle_readlink(&path).await;
337 encode_response(id, resp, out_buf)?;
338 Ok(None)
339 }
340 FsOp::Symlink { target, link_path } => {
341 let resp = handle_symlink(&target, &link_path).await;
342 encode_response(id, resp, out_buf)?;
343 Ok(None)
344 }
345 FsOp::Mkdir { path, mode } => {
346 let resp = handle_mkdir(&path, mode).await;
347 encode_response(id, resp, out_buf)?;
348 Ok(None)
349 }
350 FsOp::Remove { path } => {
351 let resp = handle_remove(&path).await;
352 encode_response(id, resp, out_buf)?;
353 Ok(None)
354 }
355 FsOp::RemoveDir { path, recursive } => {
356 let resp = handle_remove_dir(&path, recursive).await;
357 encode_response(id, resp, out_buf)?;
358 Ok(None)
359 }
360 FsOp::Copy { src, dst } => {
361 let resp = handle_copy(&src, &dst).await;
362 encode_response(id, resp, out_buf)?;
363 Ok(None)
364 }
365 FsOp::Rename { src, dst } => {
366 let resp = handle_rename(&src, &dst).await;
367 encode_response(id, resp, out_buf)?;
368 Ok(None)
369 }
370 FsOp::OpenFile { path, options } => {
371 let resp = handle_open_file(id, state, &path, options).await;
372 encode_response(id, resp, out_buf)?;
373 Ok(None)
374 }
375 FsOp::OpenDir { path } => {
376 let resp = handle_open_dir(id, state, &path).await;
377 encode_response(id, resp, out_buf)?;
378 Ok(None)
379 }
380 FsOp::CloseHandle { handle } => {
381 let resp = handle_close_handle(id, state, handle).await;
382 encode_response(id, resp, out_buf)?;
383 Ok(None)
384 }
385 FsOp::Read {
386 handle,
387 offset,
388 len,
389 } => match state.file(id, handle, true, false) {
390 Ok((file, _, _)) => {
391 let tx = session_tx.clone();
392 let task = tokio::spawn(async move {
393 handle_read_stream(id, file, offset, len, &tx).await;
394 });
395 Ok(Some(FsStreamSession::Read(FsReadSession {
396 owner_id: id,
397 handle,
398 task,
399 })))
400 }
401 Err(e) => {
402 encode_response(id, error_response(format!("read: {e}")), out_buf)?;
403 Ok(None)
404 }
405 },
406 FsOp::Write {
407 handle,
408 offset,
409 len,
410 } => match state.file(id, handle, false, true) {
411 Ok((file, append, _)) => Ok(Some(FsStreamSession::Write(FsWriteSession {
412 owner_id: id,
413 handle,
414 file,
415 offset,
416 append,
417 expected_len: len,
418 written: 0,
419 }))),
420 Err(e) => {
421 encode_response(id, error_response(format!("write: {e}")), out_buf)?;
422 Ok(None)
423 }
424 },
425 FsOp::ReadDir { handle, limit } => {
426 let resp = handle_read_dir(id, state, handle, limit).await;
427 encode_response(id, resp, out_buf)?;
428 Ok(None)
429 }
430 FsOp::FStat { handle } => {
431 let resp = handle_fstat(id, state, handle).await;
432 encode_response(id, resp, out_buf)?;
433 Ok(None)
434 }
435 FsOp::FSetStat { handle, attrs } => {
436 let resp = handle_fsetstat(id, state, handle, attrs).await;
437 encode_response(id, resp, out_buf)?;
438 Ok(None)
439 }
440 }
441}
442
443pub async fn handle_fs_data(
448 id: u32,
449 data: FsData,
450 session: &mut FsWriteSession,
451 out_buf: &mut Vec<u8>,
452) -> Result<bool, String> {
453 if data.data.is_empty() {
454 if let Some(expected) = session.expected_len
455 && session.written != expected
456 {
457 let resp = error_response(format!(
458 "write length mismatch: expected {expected}, wrote {}",
459 session.written
460 ));
461 encode_response(id, resp, out_buf)?;
462 return Ok(true);
463 }
464
465 if let Some(expected) = session.expected_len {
466 let next_written = session.written.saturating_add(data.data.len() as u64);
467 if next_written > expected {
468 let resp = error_response(format!(
469 "write length mismatch: expected {expected}, received at least {next_written}"
470 ));
471 encode_response(id, resp, out_buf)?;
472 return Ok(true);
473 }
474 }
475
476 let mut file = session.file.lock().await;
477 if let Err(e) = file.flush().await {
478 encode_response(id, error_response(format!("flush: {e}")), out_buf)?;
479 return Ok(true);
480 }
481
482 encode_response(id, ok_response(None), out_buf)?;
483 Ok(true)
484 } else {
485 let mut file = session.file.lock().await;
486 if !session.append
487 && let Err(e) = file.seek(std::io::SeekFrom::Start(session.offset)).await
488 {
489 encode_response(id, error_response(format!("seek: {e}")), out_buf)?;
490 return Ok(true);
491 }
492 if let Err(e) = file.write_all(&data.data).await {
493 encode_response(id, error_response(format!("write: {e}")), out_buf)?;
494 return Ok(true);
495 }
496 session.offset = session.offset.saturating_add(data.data.len() as u64);
497 session.written = session.written.saturating_add(data.data.len() as u64);
498 Ok(false)
499 }
500}
501
502async fn handle_realpath(path: &str) -> FsResponse {
507 match realpath(path).await {
508 Ok(path) => ok_response(Some(FsResponseData::Path(path))),
509 Err(e) => error_response(format!("realpath: {e}")),
510 }
511}
512
513async fn handle_stat(path: &str, follow_symlink: bool) -> FsResponse {
514 let result = if follow_symlink {
515 tokio::fs::metadata(path).await
516 } else {
517 tokio::fs::symlink_metadata(path).await
518 };
519
520 match result {
521 Ok(meta) => ok_response(Some(FsResponseData::Stat(metadata_to_entry_info(
522 path, &meta,
523 )))),
524 Err(e) => error_response(format!("stat: {e}")),
525 }
526}
527
528async fn handle_setstat(path: &str, follow_symlink: bool, attrs: FsSetAttrs) -> FsResponse {
529 match apply_path_attrs(path, follow_symlink, attrs).await {
530 Ok(()) => ok_response(None),
531 Err(e) => error_response(format!("setstat: {e}")),
532 }
533}
534
535async fn handle_list(path: &str) -> FsResponse {
536 match read_all_dir(path).await {
537 Ok(entries) => ok_response(Some(FsResponseData::List(entries))),
538 Err(e) => error_response(format!("readdir: {e}")),
539 }
540}
541
542async fn handle_readlink(path: &str) -> FsResponse {
543 match tokio::fs::read_link(path).await {
544 Ok(target) => ok_response(Some(FsResponseData::Path(
545 target.to_string_lossy().to_string(),
546 ))),
547 Err(e) => error_response(format!("readlink: {e}")),
548 }
549}
550
551async fn handle_symlink(target: &str, link_path: &str) -> FsResponse {
552 let target = target.to_string();
553 let link_path = link_path.to_string();
554 match tokio::task::spawn_blocking(move || std::os::unix::fs::symlink(target, link_path)).await {
555 Ok(Ok(())) => ok_response(None),
556 Ok(Err(e)) => error_response(format!("symlink: {e}")),
557 Err(e) => error_response(format!("symlink task: {e}")),
558 }
559}
560
561async fn handle_open_file(
562 id: u32,
563 state: &mut FsState,
564 path: &str,
565 options: FsOpenOptions,
566) -> FsResponse {
567 let mut open_options = tokio::fs::OpenOptions::new();
568 open_options
569 .read(options.read)
570 .write(options.write)
571 .append(options.append)
572 .create(options.create)
573 .truncate(options.truncate)
574 .create_new(options.create_new);
575 if let Some(mode) = options.mode {
576 open_options.mode(mode);
577 }
578
579 match open_options.open(path).await {
580 Ok(file) => match state.insert_file(
581 id,
582 file,
583 options.read,
584 options.write,
585 options.append,
586 path.to_string(),
587 ) {
588 Ok(handle) => ok_response(Some(FsResponseData::Handle(handle))),
589 Err(e) => error_response(format!("open: {e}")),
590 },
591 Err(e) => error_response(format!("open: {e}")),
592 }
593}
594
595async fn handle_open_dir(id: u32, state: &mut FsState, path: &str) -> FsResponse {
596 match tokio::fs::read_dir(path).await {
597 Ok(dir) => match state.insert_dir(id, dir, path.to_string()) {
598 Ok(handle) => ok_response(Some(FsResponseData::Handle(handle))),
599 Err(e) => error_response(format!("opendir: {e}")),
600 },
601 Err(e) => error_response(format!("opendir: {e}")),
602 }
603}
604
605async fn handle_close_handle(id: u32, state: &mut FsState, handle: u64) -> FsResponse {
606 match state.close_handle(id, handle) {
607 Ok(FsHandleEntry::File { file, .. }) => {
608 let mut file = file.lock().await;
609 match file.flush().await {
610 Ok(()) => ok_response(None),
611 Err(e) => error_response(format!("close: {e}")),
612 }
613 }
614 Ok(FsHandleEntry::Dir { .. }) => ok_response(None),
615 Err(e) => error_response(format!("close: {e}")),
616 }
617}
618
619async fn handle_read_dir(id: u32, state: &FsState, handle: u64, limit: Option<u32>) -> FsResponse {
620 let (dir, path) = match state.dir(id, handle) {
621 Ok(v) => v,
622 Err(e) => return error_response(format!("readdir: {e}")),
623 };
624
625 let limit = limit.unwrap_or(DEFAULT_READ_DIR_LIMIT).max(1);
626 let mut dir = dir.lock().await;
627 let mut entries = Vec::new();
628
629 for _ in 0..limit {
630 match dir.next_entry().await {
631 Ok(Some(entry)) => {
632 let entry_path = entry.path();
633 let path_str = entry_path.to_string_lossy().to_string();
634 match tokio::fs::symlink_metadata(&entry_path).await {
635 Ok(meta) => entries.push(metadata_to_entry_info(&path_str, &meta)),
636 Err(_) => entries.push(unknown_entry_info(&path_str)),
637 }
638 }
639 Ok(None) => break,
640 Err(e) => return error_response(format!("readdir {path}: {e}")),
641 }
642 }
643
644 ok_response(Some(FsResponseData::List(entries)))
645}
646
647async fn handle_fstat(id: u32, state: &FsState, handle: u64) -> FsResponse {
648 match state.handles.get(&handle) {
649 Some(FsHandleEntry::File { file, path, .. }) => {
650 if let Err(e) = state
651 .handles
652 .get(&handle)
653 .expect("entry just matched")
654 .ensure_owner(handle, id)
655 {
656 return error_response(format!("fstat: {e}"));
657 }
658 let file = file.lock().await;
659 match file.metadata().await {
660 Ok(meta) => ok_response(Some(FsResponseData::Stat(metadata_to_entry_info(
661 path, &meta,
662 )))),
663 Err(e) => error_response(format!("fstat: {e}")),
664 }
665 }
666 Some(FsHandleEntry::Dir { path, .. }) => {
667 if let Err(e) = state
668 .handles
669 .get(&handle)
670 .expect("entry just matched")
671 .ensure_owner(handle, id)
672 {
673 return error_response(format!("fstat: {e}"));
674 }
675 match tokio::fs::metadata(path).await {
676 Ok(meta) => ok_response(Some(FsResponseData::Stat(metadata_to_entry_info(
677 path, &meta,
678 )))),
679 Err(e) => error_response(format!("fstat: {e}")),
680 }
681 }
682 None => error_response(format!("fstat: invalid handle: {handle}")),
683 }
684}
685
686async fn handle_fsetstat(id: u32, state: &FsState, handle: u64, attrs: FsSetAttrs) -> FsResponse {
687 let (file, _, path) = match state.file(id, handle, false, false) {
688 Ok(v) => v,
689 Err(e) => return error_response(format!("fsetstat: {e}")),
690 };
691
692 let mut file = file.lock().await;
693 match apply_file_attrs(&mut file, &path, attrs).await {
694 Ok(()) => ok_response(None),
695 Err(e) => error_response(format!("fsetstat: {e}")),
696 }
697}
698
699async fn handle_mkdir(path: &str, mode: Option<u32>) -> FsResponse {
700 match tokio::fs::create_dir_all(path).await {
701 Ok(()) => {
702 if let Some(mode) = mode
703 && let Err(e) =
704 tokio::fs::set_permissions(path, std::fs::Permissions::from_mode(mode)).await
705 {
706 return error_response(format!("chmod: {e}"));
707 }
708 ok_response(None)
709 }
710 Err(e) => error_response(format!("mkdir: {e}")),
711 }
712}
713
714async fn handle_remove(path: &str) -> FsResponse {
715 match tokio::fs::remove_file(path).await {
716 Ok(()) => ok_response(None),
717 Err(e) => error_response(format!("remove: {e}")),
718 }
719}
720
721async fn handle_remove_dir(path: &str, recursive: bool) -> FsResponse {
722 let result = if recursive {
723 tokio::fs::remove_dir_all(path).await
724 } else {
725 tokio::fs::remove_dir(path).await
726 };
727 match result {
728 Ok(()) => ok_response(None),
729 Err(e) => error_response(format!("remove_dir: {e}")),
730 }
731}
732
733async fn handle_copy(src: &str, dst: &str) -> FsResponse {
734 match tokio::fs::copy(src, dst).await {
735 Ok(_) => ok_response(None),
736 Err(e) => error_response(format!("copy: {e}")),
737 }
738}
739
740async fn handle_rename(src: &str, dst: &str) -> FsResponse {
741 match tokio::fs::rename(src, dst).await {
742 Ok(()) => ok_response(None),
743 Err(e) => error_response(format!("rename: {e}")),
744 }
745}
746
747async fn handle_read_stream(
748 id: u32,
749 file: Arc<Mutex<tokio::fs::File>>,
750 offset: u64,
751 len: Option<u64>,
752 tx: &mpsc::UnboundedSender<(u32, SessionOutput)>,
753) {
754 let mut file = file.lock().await;
755 if let Err(e) = file.seek(std::io::SeekFrom::Start(offset)).await {
756 send_raw_response(id, false, Some(format!("seek: {e}")), None, tx);
757 return;
758 }
759
760 let mut remaining = len;
761 let mut chunk = vec![0u8; FS_CHUNK_SIZE];
762 let mut buf = Vec::new();
763
764 loop {
765 let read_len = match remaining {
766 Some(0) => break,
767 Some(n) => chunk.len().min(n as usize),
768 None => chunk.len(),
769 };
770
771 match file.read(&mut chunk[..read_len]).await {
772 Ok(0) => break,
773 Ok(n) => {
774 if let Some(ref mut remaining) = remaining {
775 *remaining = remaining.saturating_sub(n as u64);
776 }
777 let data = FsData {
778 data: chunk[..n].to_vec(),
779 };
780 let msg = match Message::with_payload(MessageType::FsData, id, &data) {
781 Ok(msg) => msg,
782 Err(e) => {
783 send_raw_response(id, false, Some(format!("encode chunk: {e}")), None, tx);
784 return;
785 }
786 };
787 buf.clear();
788 if let Err(e) = codec::encode_to_buf(&msg, &mut buf) {
789 send_raw_response(
790 id,
791 false,
792 Some(format!("encode chunk frame: {e}")),
793 None,
794 tx,
795 );
796 return;
797 }
798 let output = RawSessionOutput::new(buf.clone(), RawActivity::fs_bytes(n), None);
799 if tx.send((id, SessionOutput::Raw(output))).is_err() {
800 return;
801 }
802 }
803 Err(e) => {
804 send_raw_response(id, false, Some(format!("read: {e}")), None, tx);
805 return;
806 }
807 }
808 }
809
810 send_raw_response(id, true, None, None, tx);
811}
812
813async fn apply_path_attrs(
818 path: &str,
819 follow_symlink: bool,
820 attrs: FsSetAttrs,
821) -> Result<(), String> {
822 if let Some(size) = attrs.size {
823 let file = tokio::fs::OpenOptions::new()
824 .write(true)
825 .open(path)
826 .await
827 .map_err(|e| format!("open for truncate: {e}"))?;
828 file.set_len(size)
829 .await
830 .map_err(|e| format!("set_len: {e}"))?;
831 }
832
833 if let Some(mode) = attrs.mode {
834 if !follow_symlink
835 && tokio::fs::symlink_metadata(path)
836 .await
837 .map_err(|e| format!("lstat before chmod: {e}"))?
838 .file_type()
839 .is_symlink()
840 {
841 return Err("chmod on symlink without following is not supported".into());
842 }
843 tokio::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
844 .await
845 .map_err(|e| format!("chmod: {e}"))?;
846 }
847
848 if attrs.uid.is_some() || attrs.gid.is_some() {
849 chown_path(path, follow_symlink, attrs.uid, attrs.gid)?;
850 }
851
852 if attrs.atime.is_some() || attrs.mtime.is_some() {
853 set_times_path(path, follow_symlink, attrs.atime, attrs.mtime).await?;
854 }
855
856 Ok(())
857}
858
859async fn apply_file_attrs(
860 file: &mut tokio::fs::File,
861 path: &str,
862 attrs: FsSetAttrs,
863) -> Result<(), String> {
864 if let Some(size) = attrs.size {
865 file.set_len(size)
866 .await
867 .map_err(|e| format!("set_len: {e}"))?;
868 }
869
870 if let Some(mode) = attrs.mode {
871 file.set_permissions(std::fs::Permissions::from_mode(mode))
872 .await
873 .map_err(|e| format!("chmod: {e}"))?;
874 }
875
876 if attrs.uid.is_some() || attrs.gid.is_some() {
877 let uid = attrs.uid.map(|v| v as libc::uid_t).unwrap_or(!0);
878 let gid = attrs.gid.map(|v| v as libc::gid_t).unwrap_or(!0);
879 let rc = unsafe { libc::fchown(file.as_raw_fd(), uid, gid) };
880 if rc != 0 {
881 return Err(format!("fchown: {}", std::io::Error::last_os_error()));
882 }
883 }
884
885 if attrs.atime.is_some() || attrs.mtime.is_some() {
886 set_times_fd(file.as_raw_fd(), path, attrs.atime, attrs.mtime).await?;
887 }
888
889 Ok(())
890}
891
892fn chown_path(
893 path: &str,
894 follow_symlink: bool,
895 uid: Option<u32>,
896 gid: Option<u32>,
897) -> Result<(), String> {
898 let c_path = cstring_path(path)?;
899 let uid = uid.map(|v| v as libc::uid_t).unwrap_or(!0);
900 let gid = gid.map(|v| v as libc::gid_t).unwrap_or(!0);
901 let rc = unsafe {
902 if follow_symlink {
903 libc::chown(c_path.as_ptr(), uid, gid)
904 } else {
905 libc::lchown(c_path.as_ptr(), uid, gid)
906 }
907 };
908 if rc != 0 {
909 return Err(format!("chown: {}", std::io::Error::last_os_error()));
910 }
911 Ok(())
912}
913
914async fn set_times_path(
915 path: &str,
916 follow_symlink: bool,
917 atime: Option<i64>,
918 mtime: Option<i64>,
919) -> Result<(), String> {
920 let meta = if follow_symlink {
921 tokio::fs::metadata(path).await
922 } else {
923 tokio::fs::symlink_metadata(path).await
924 }
925 .map_err(|e| format!("stat before utimensat: {e}"))?;
926 let times = timespecs(atime.unwrap_or(meta.atime()), mtime.unwrap_or(meta.mtime()));
927 let c_path = cstring_path(path)?;
928 let flags = if follow_symlink {
929 0
930 } else {
931 libc::AT_SYMLINK_NOFOLLOW
932 };
933 let rc = unsafe { libc::utimensat(libc::AT_FDCWD, c_path.as_ptr(), times.as_ptr(), flags) };
934 if rc != 0 {
935 return Err(format!("utimensat: {}", std::io::Error::last_os_error()));
936 }
937 Ok(())
938}
939
940async fn set_times_fd(
941 fd: std::os::fd::RawFd,
942 path: &str,
943 atime: Option<i64>,
944 mtime: Option<i64>,
945) -> Result<(), String> {
946 let meta = tokio::fs::metadata(path)
947 .await
948 .map_err(|e| format!("stat before futimens: {e}"))?;
949 let times = timespecs(atime.unwrap_or(meta.atime()), mtime.unwrap_or(meta.mtime()));
950 let rc = unsafe { libc::futimens(fd, times.as_ptr()) };
951 if rc != 0 {
952 return Err(format!("futimens: {}", std::io::Error::last_os_error()));
953 }
954 Ok(())
955}
956
957fn timespecs(atime: i64, mtime: i64) -> [libc::timespec; 2] {
958 [
959 libc::timespec {
960 tv_sec: atime as _,
961 tv_nsec: 0,
962 },
963 libc::timespec {
964 tv_sec: mtime as _,
965 tv_nsec: 0,
966 },
967 ]
968}
969
970fn encode_response(id: u32, resp: FsResponse, out_buf: &mut Vec<u8>) -> Result<(), String> {
975 let msg = Message::with_payload(MessageType::FsResponse, id, &resp)
976 .map_err(|e| format!("encode fs response: {e}"))?;
977 codec::encode_to_buf(&msg, out_buf).map_err(|e| format!("encode fs response frame: {e}"))?;
978 Ok(())
979}
980
981fn send_raw_response(
982 id: u32,
983 ok: bool,
984 error: Option<String>,
985 data: Option<FsResponseData>,
986 tx: &mpsc::UnboundedSender<(u32, SessionOutput)>,
987) {
988 let resp = FsResponse { ok, error, data };
989 match Message::with_payload(MessageType::FsResponse, id, &resp) {
990 Ok(msg) => {
991 let mut buf = Vec::new();
992 match codec::encode_to_buf(&msg, &mut buf) {
993 Ok(()) => {
994 let output = RawSessionOutput::new(
995 buf,
996 RawActivity::guest_message(),
997 Some(RawSessionCompletion::FsRead),
998 );
999 let _ = tx.send((id, SessionOutput::Raw(output)));
1000 }
1001 Err(e) => {
1002 eprintln!("failed to encode fs response frame for {id}: {e}");
1003 }
1004 }
1005 }
1006 Err(e) => {
1007 eprintln!("failed to encode fs response for {id}: {e}");
1008 }
1009 }
1010}
1011
1012async fn realpath(path: &str) -> Result<String, String> {
1013 match tokio::fs::canonicalize(path).await {
1014 Ok(path) => Ok(path.to_string_lossy().to_string()),
1015 Err(original_error) => {
1016 let path = Path::new(path);
1017 let Some(parent) = path.parent() else {
1018 return Err(original_error.to_string());
1019 };
1020 let parent = tokio::fs::canonicalize(parent)
1021 .await
1022 .map_err(|_| original_error.to_string())?;
1023 let resolved = match path.file_name() {
1024 Some(name) => parent.join(name),
1025 None => parent,
1026 };
1027 Ok(resolved.to_string_lossy().to_string())
1028 }
1029 }
1030}
1031
1032async fn read_all_dir(path: &str) -> Result<Vec<FsEntryInfo>, String> {
1033 let mut dir = tokio::fs::read_dir(path)
1034 .await
1035 .map_err(|e| format!("opendir: {e}"))?;
1036 let mut entries = Vec::new();
1037
1038 loop {
1039 match dir.next_entry().await {
1040 Ok(Some(entry)) => {
1041 let entry_path = entry.path();
1042 let path_str = entry_path.to_string_lossy().to_string();
1043 match tokio::fs::symlink_metadata(&entry_path).await {
1044 Ok(meta) => entries.push(metadata_to_entry_info(&path_str, &meta)),
1045 Err(_) => entries.push(unknown_entry_info(&path_str)),
1046 }
1047 }
1048 Ok(None) => break,
1049 Err(e) => return Err(e.to_string()),
1050 }
1051 }
1052
1053 Ok(entries)
1054}
1055
1056fn ok_response(data: Option<FsResponseData>) -> FsResponse {
1057 FsResponse {
1058 ok: true,
1059 error: None,
1060 data,
1061 }
1062}
1063
1064fn error_response(error: String) -> FsResponse {
1065 FsResponse {
1066 ok: false,
1067 error: Some(error),
1068 data: None,
1069 }
1070}
1071
1072fn metadata_to_entry_info(path: &str, meta: &std::fs::Metadata) -> FsEntryInfo {
1073 let kind = if meta.is_file() {
1074 "file"
1075 } else if meta.is_dir() {
1076 "dir"
1077 } else if meta.is_symlink() {
1078 "symlink"
1079 } else {
1080 "other"
1081 };
1082
1083 let mtime = Some(meta.mtime());
1084 let atime = Some(meta.atime());
1085
1086 FsEntryInfo {
1087 path: path.to_string(),
1088 kind: kind.to_string(),
1089 size: meta.len(),
1090 mode: meta.mode(),
1091 modified: mtime,
1092 uid: meta.uid(),
1093 gid: meta.gid(),
1094 atime,
1095 mtime,
1096 }
1097}
1098
1099fn unknown_entry_info(path: &str) -> FsEntryInfo {
1100 FsEntryInfo {
1101 path: path.to_string(),
1102 kind: "other".to_string(),
1103 size: 0,
1104 mode: 0,
1105 modified: None,
1106 uid: 0,
1107 gid: 0,
1108 atime: None,
1109 mtime: None,
1110 }
1111}
1112
1113fn cstring_path(path: impl AsRef<Path>) -> Result<CString, String> {
1114 CString::new(path.as_ref().as_os_str().as_bytes())
1115 .map_err(|e| format!("path contains NUL: {e}"))
1116}