1use std::collections::HashMap;
7use std::ffi::CString;
8use std::io::IoSlice;
9use std::os::fd::AsRawFd;
10use std::os::unix::ffi::OsStrExt;
11use std::os::unix::fs::{MetadataExt, PermissionsExt};
12use std::path::Path;
13use std::sync::Arc;
14
15use bytes::Bytes;
16use microsandbox_protocol::bulk::{
17 BULK_FLOW_MASK_GUEST_TO_HOST, BULK_FLOW_MASK_HOST_TO_GUEST, BULK_PROTOCOL_VERSION,
18 BulkAccepted, BulkCredit, BulkFinish, BulkFlow, BulkKind, BulkOffer, BulkReceiveState,
19 BulkRecord, BulkSendState, DEFAULT_BULK_WINDOW, DEFAULT_FILESYSTEM_BULK_RECORD_PAYLOAD,
20};
21use microsandbox_protocol::codec;
22use microsandbox_protocol::fs::{
23 FS_CHUNK_SIZE, FsData, FsEntryInfo, FsOp, FsOpenOptions, FsRequest, FsResponse, FsResponseData,
24 FsSetAttrs,
25};
26use microsandbox_protocol::message::{Message, MessageType};
27use microsandbox_protocol::transport::relay_client_slot;
28use serde::Serialize;
29use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
30use tokio::sync::{Mutex, watch};
31use tokio::task::JoinHandle;
32
33use crate::session::{
34 BulkSessionOutput, RawActivity, RawSessionCompletion, RawSessionOutput, SessionOutput,
35 SessionOutputSender,
36};
37
38const DEFAULT_READ_DIR_LIMIT: u32 = 128;
44
45const MAX_OPEN_HANDLES_PER_OWNER: usize = 1024;
47
48#[derive(Default)]
54pub struct FsState {
55 next_handle: u64,
56 handles: HashMap<u64, FsHandleEntry>,
57}
58
59pub struct FsWriteSession {
61 owner_id: u32,
62 handle: u64,
63 file: Arc<Mutex<tokio::fs::File>>,
64 offset: u64,
65 append: bool,
66 expected_len: Option<u64>,
67 written: u64,
68 bulk: Option<BulkReceiveState>,
69}
70
71pub struct FsReadSession {
73 owner_id: u32,
74 handle: u64,
75 task: JoinHandle<()>,
76 credit_tx: Option<watch::Sender<Option<BulkCredit>>>,
77}
78
79pub enum FsStreamSession {
81 Read(FsReadSession),
83
84 Write(FsWriteSession),
86}
87
88enum FsHandleEntry {
89 File {
90 owner_id: u32,
91 file: Arc<Mutex<tokio::fs::File>>,
92 read: bool,
93 write: bool,
94 append: bool,
95 path: String,
96 },
97 Dir {
98 owner_id: u32,
99 dir: Arc<Mutex<tokio::fs::ReadDir>>,
100 path: String,
101 },
102}
103
104impl FsState {
109 pub fn close_owner_range(&mut self, id_start: u32, id_end_exclusive: u32) {
111 self.handles.retain(|_, handle| {
112 let owner_id = handle.owner_id();
113 owner_id < id_start || owner_id >= id_end_exclusive
114 });
115 }
116
117 pub fn clear(&mut self) {
119 self.handles.clear();
120 }
121
122 fn insert_file(
123 &mut self,
124 owner_id: u32,
125 file: tokio::fs::File,
126 read: bool,
127 write: bool,
128 append: bool,
129 path: String,
130 ) -> Result<u64, String> {
131 self.enforce_owner_limit(owner_id)?;
132 let handle = self.alloc_handle();
133 self.handles.insert(
134 handle,
135 FsHandleEntry::File {
136 owner_id,
137 file: Arc::new(Mutex::new(file)),
138 read,
139 write,
140 append,
141 path,
142 },
143 );
144 Ok(handle)
145 }
146
147 fn insert_dir(
148 &mut self,
149 owner_id: u32,
150 dir: tokio::fs::ReadDir,
151 path: String,
152 ) -> Result<u64, String> {
153 self.enforce_owner_limit(owner_id)?;
154 let handle = self.alloc_handle();
155 self.handles.insert(
156 handle,
157 FsHandleEntry::Dir {
158 owner_id,
159 dir: Arc::new(Mutex::new(dir)),
160 path,
161 },
162 );
163 Ok(handle)
164 }
165
166 fn close_handle(&mut self, caller_id: u32, handle: u64) -> Result<FsHandleEntry, String> {
167 if let Some(entry) = self.handles.get(&handle) {
168 entry.ensure_owner(handle, caller_id)?;
169 }
170 self.handles
171 .remove(&handle)
172 .ok_or_else(|| format!("invalid handle: {handle}"))
173 }
174
175 fn file(
176 &self,
177 caller_id: u32,
178 handle: u64,
179 need_read: bool,
180 need_write: bool,
181 ) -> Result<(Arc<Mutex<tokio::fs::File>>, bool, String), String> {
182 match self.handles.get(&handle) {
183 Some(FsHandleEntry::File {
184 file,
185 read,
186 write,
187 append,
188 path,
189 ..
190 }) => {
191 self.handles
192 .get(&handle)
193 .expect("entry just matched")
194 .ensure_owner(handle, caller_id)?;
195 if need_read && !read {
196 return Err(format!("handle {handle} is not open for reading"));
197 }
198 if need_write && !write && !append {
199 return Err(format!("handle {handle} is not open for writing"));
200 }
201 Ok((Arc::clone(file), *append, path.clone()))
202 }
203 Some(FsHandleEntry::Dir { .. }) => Err(format!("handle {handle} is a directory")),
204 None => Err(format!("invalid handle: {handle}")),
205 }
206 }
207
208 fn dir(
209 &self,
210 caller_id: u32,
211 handle: u64,
212 ) -> Result<(Arc<Mutex<tokio::fs::ReadDir>>, String), String> {
213 match self.handles.get(&handle) {
214 Some(FsHandleEntry::Dir { dir, path, .. }) => {
215 self.handles
216 .get(&handle)
217 .expect("entry just matched")
218 .ensure_owner(handle, caller_id)?;
219 Ok((Arc::clone(dir), path.clone()))
220 }
221 Some(FsHandleEntry::File { .. }) => Err(format!("handle {handle} is a file")),
222 None => Err(format!("invalid handle: {handle}")),
223 }
224 }
225
226 fn alloc_handle(&mut self) -> u64 {
227 self.next_handle = self.next_handle.wrapping_add(1).max(1);
228 while self.handles.contains_key(&self.next_handle) {
229 self.next_handle = self.next_handle.wrapping_add(1).max(1);
230 }
231 self.next_handle
232 }
233
234 fn enforce_owner_limit(&self, owner_id: u32) -> Result<(), String> {
235 let count = self
236 .handles
237 .values()
238 .filter(|entry| same_relay_client(entry.owner_id(), owner_id))
239 .count();
240 if count >= MAX_OPEN_HANDLES_PER_OWNER {
241 return Err(format!(
242 "too many open filesystem handles for relay client: {count}"
243 ));
244 }
245 Ok(())
246 }
247}
248
249impl FsHandleEntry {
250 fn owner_id(&self) -> u32 {
251 match self {
252 Self::File { owner_id, .. } | Self::Dir { owner_id, .. } => *owner_id,
253 }
254 }
255
256 fn ensure_owner(&self, handle: u64, caller_id: u32) -> Result<(), String> {
257 if same_relay_client(self.owner_id(), caller_id) {
258 Ok(())
259 } else {
260 Err(format!(
261 "handle {handle} is owned by a different relay client"
262 ))
263 }
264 }
265}
266
267impl FsReadSession {
268 pub fn owner_id(&self) -> u32 {
270 self.owner_id
271 }
272
273 pub fn handle(&self) -> u64 {
275 self.handle
276 }
277
278 pub fn abort(self) {
280 self.task.abort();
281 }
282
283 pub fn apply_credit(&self, credit: BulkCredit) -> Result<(), String> {
285 let Some(tx) = &self.credit_tx else {
286 return Err("filesystem read session is not using raw bulk".into());
287 };
288 if credit.kind != BulkKind::Filesystem || credit.flow != BulkFlow::GuestToHost {
289 return Err("filesystem read received credit for another kind or flow".into());
290 }
291 tx.send_replace(Some(credit));
292 Ok(())
293 }
294
295 pub fn is_bulk(&self) -> bool {
297 self.credit_tx.is_some()
298 }
299}
300
301impl FsWriteSession {
302 pub fn owner_id(&self) -> u32 {
304 self.owner_id
305 }
306
307 pub fn handle(&self) -> u64 {
309 self.handle
310 }
311
312 pub fn is_bulk(&self) -> bool {
314 self.bulk.is_some()
315 }
316}
317
318fn same_relay_client(left: u32, right: u32) -> bool {
323 relay_client_slot(left).is_some_and(|left| Some(left) == relay_client_slot(right))
324}
325
326pub async fn handle_fs_request(
328 id: u32,
329 protocol_version: u8,
330 req: FsRequest,
331 state: &mut FsState,
332 out_buf: &mut Vec<u8>,
333 session_tx: &SessionOutputSender,
334) -> Result<Option<FsStreamSession>, String> {
335 let FsRequest { op, bulk } = req;
336 if bulk.is_some() && protocol_version < BULK_PROTOCOL_VERSION {
337 encode_response(
338 id,
339 error_response(format!(
340 "raw bulk offer requires protocol generation {BULK_PROTOCOL_VERSION}"
341 )),
342 out_buf,
343 )?;
344 return Ok(None);
345 }
346 if bulk.is_some() && !matches!(&op, FsOp::Read { .. } | FsOp::Write { .. }) {
347 encode_response(
348 id,
349 error_response("raw bulk is only valid for streaming reads and writes".into()),
350 out_buf,
351 )?;
352 return Ok(None);
353 }
354
355 match op {
356 FsOp::RealPath { path } => {
357 let resp = handle_realpath(&path).await;
358 encode_response(id, resp, out_buf)?;
359 Ok(None)
360 }
361 FsOp::Stat {
362 path,
363 follow_symlink,
364 } => {
365 let resp = handle_stat(&path, follow_symlink).await;
366 encode_response(id, resp, out_buf)?;
367 Ok(None)
368 }
369 FsOp::SetStat {
370 path,
371 follow_symlink,
372 attrs,
373 } => {
374 let resp = handle_setstat(&path, follow_symlink, attrs).await;
375 encode_response(id, resp, out_buf)?;
376 Ok(None)
377 }
378 FsOp::List { path } => {
379 let resp = handle_list(&path).await;
380 encode_response(id, resp, out_buf)?;
381 Ok(None)
382 }
383 FsOp::ReadLink { path } => {
384 let resp = handle_readlink(&path).await;
385 encode_response(id, resp, out_buf)?;
386 Ok(None)
387 }
388 FsOp::Symlink { target, link_path } => {
389 let resp = handle_symlink(&target, &link_path).await;
390 encode_response(id, resp, out_buf)?;
391 Ok(None)
392 }
393 FsOp::Mkdir { path, mode } => {
394 let resp = handle_mkdir(&path, mode).await;
395 encode_response(id, resp, out_buf)?;
396 Ok(None)
397 }
398 FsOp::Remove { path } => {
399 let resp = handle_remove(&path).await;
400 encode_response(id, resp, out_buf)?;
401 Ok(None)
402 }
403 FsOp::RemoveDir { path, recursive } => {
404 let resp = handle_remove_dir(&path, recursive).await;
405 encode_response(id, resp, out_buf)?;
406 Ok(None)
407 }
408 FsOp::Copy { src, dst } => {
409 let resp = handle_copy(&src, &dst).await;
410 encode_response(id, resp, out_buf)?;
411 Ok(None)
412 }
413 FsOp::Rename { src, dst } => {
414 let resp = handle_rename(&src, &dst).await;
415 encode_response(id, resp, out_buf)?;
416 Ok(None)
417 }
418 FsOp::OpenFile { path, options } => {
419 let resp = handle_open_file(id, state, &path, options).await;
420 encode_response(id, resp, out_buf)?;
421 Ok(None)
422 }
423 FsOp::OpenDir { path } => {
424 let resp = handle_open_dir(id, state, &path).await;
425 encode_response(id, resp, out_buf)?;
426 Ok(None)
427 }
428 FsOp::CloseHandle { handle } => {
429 let resp = handle_close_handle(id, state, handle).await;
430 encode_response(id, resp, out_buf)?;
431 Ok(None)
432 }
433 FsOp::Read {
434 handle,
435 offset,
436 len,
437 } => match state.file(id, handle, true, false) {
438 Ok((file, _, _)) => {
439 let tx = session_tx.clone();
440 let (task, credit_tx) = match bulk {
441 Some(offer) => {
442 let accepted = accept_fs_read_offer(offer)?;
443 encode_control(MessageType::BulkAccepted, id, &accepted, out_buf)?;
444 let sender = BulkSendState::new(
445 BulkKind::Filesystem,
446 BulkFlow::GuestToHost,
447 accepted.max_record_payload,
448 accepted.guest_to_host_credit_limit,
449 )
450 .map_err(|error| format!("accept read bulk state: {error}"))?;
451 let (credit_tx, credit_rx) = watch::channel(None);
452 let task = tokio::spawn(async move {
453 handle_bulk_read_stream(id, file, offset, len, sender, credit_rx, &tx)
454 .await;
455 });
456 (task, Some(credit_tx))
457 }
458 None => {
459 let task = tokio::spawn(async move {
460 handle_read_stream(id, file, offset, len, &tx).await;
461 });
462 (task, None)
463 }
464 };
465 Ok(Some(FsStreamSession::Read(FsReadSession {
466 owner_id: id,
467 handle,
468 task,
469 credit_tx,
470 })))
471 }
472 Err(e) => {
473 encode_response(id, error_response(format!("read: {e}")), out_buf)?;
474 Ok(None)
475 }
476 },
477 FsOp::Write {
478 handle,
479 offset,
480 len,
481 } => match state.file(id, handle, false, true) {
482 Ok((file, append, _)) => {
483 let bulk = match bulk {
484 Some(offer) => {
485 let accepted = accept_fs_write_offer(offer)?;
486 encode_control(MessageType::BulkAccepted, id, &accepted, out_buf)?;
487 Some(
488 BulkReceiveState::new(
489 BulkKind::Filesystem,
490 BulkFlow::HostToGuest,
491 accepted.max_record_payload,
492 accepted.host_to_guest_credit_limit,
493 DEFAULT_BULK_WINDOW,
494 )
495 .map_err(|error| format!("accept write bulk state: {error}"))?,
496 )
497 }
498 None => None,
499 };
500 Ok(Some(FsStreamSession::Write(FsWriteSession {
501 owner_id: id,
502 handle,
503 file,
504 offset,
505 append,
506 expected_len: len,
507 written: 0,
508 bulk,
509 })))
510 }
511 Err(e) => {
512 encode_response(id, error_response(format!("write: {e}")), out_buf)?;
513 Ok(None)
514 }
515 },
516 FsOp::ReadDir { handle, limit } => {
517 let resp = handle_read_dir(id, state, handle, limit).await;
518 encode_response(id, resp, out_buf)?;
519 Ok(None)
520 }
521 FsOp::FStat { handle } => {
522 let resp = handle_fstat(id, state, handle).await;
523 encode_response(id, resp, out_buf)?;
524 Ok(None)
525 }
526 FsOp::FSetStat { handle, attrs } => {
527 let resp = handle_fsetstat(id, state, handle, attrs).await;
528 encode_response(id, resp, out_buf)?;
529 Ok(None)
530 }
531 }
532}
533
534pub async fn handle_fs_data(
539 id: u32,
540 data: FsData,
541 session: &mut FsWriteSession,
542 out_buf: &mut Vec<u8>,
543) -> Result<bool, String> {
544 if session.bulk.is_some() {
545 encode_response(
546 id,
547 error_response("CBOR filesystem data is invalid after raw bulk acceptance".into()),
548 out_buf,
549 )?;
550 return Ok(true);
551 }
552 if data.data.is_empty() {
553 if let Some(expected) = session.expected_len
554 && session.written != expected
555 {
556 let resp = error_response(format!(
557 "write length mismatch: expected {expected}, wrote {}",
558 session.written
559 ));
560 encode_response(id, resp, out_buf)?;
561 return Ok(true);
562 }
563
564 if let Some(expected) = session.expected_len {
565 let next_written = session.written.saturating_add(data.data.len() as u64);
566 if next_written > expected {
567 let resp = error_response(format!(
568 "write length mismatch: expected {expected}, received at least {next_written}"
569 ));
570 encode_response(id, resp, out_buf)?;
571 return Ok(true);
572 }
573 }
574
575 let mut file = session.file.lock().await;
576 if let Err(e) = file.flush().await {
577 encode_response(id, error_response(format!("flush: {e}")), out_buf)?;
578 return Ok(true);
579 }
580
581 encode_response(id, ok_response(None), out_buf)?;
582 Ok(true)
583 } else {
584 let mut file = session.file.lock().await;
585 if !session.append
586 && let Err(e) = file.seek(std::io::SeekFrom::Start(session.offset)).await
587 {
588 encode_response(id, error_response(format!("seek: {e}")), out_buf)?;
589 return Ok(true);
590 }
591 if let Err(e) = file.write_all(&data.data).await {
592 encode_response(id, error_response(format!("write: {e}")), out_buf)?;
593 return Ok(true);
594 }
595 session.offset = session.offset.saturating_add(data.data.len() as u64);
596 session.written = session.written.saturating_add(data.data.len() as u64);
597 Ok(false)
598 }
599}
600
601pub async fn handle_fs_bulk_record(
603 id: u32,
604 record: &BulkRecord,
605 session: &mut FsWriteSession,
606 out_buf: &mut Vec<u8>,
607) -> Result<bool, String> {
608 handle_fs_bulk_records(id, std::slice::from_ref(record), session, out_buf).await
609}
610
611pub async fn handle_fs_bulk_records(
617 id: u32,
618 records: &[BulkRecord],
619 session: &mut FsWriteSession,
620 out_buf: &mut Vec<u8>,
621) -> Result<bool, String> {
622 if records.is_empty() {
623 return Err("filesystem bulk record batch is empty".into());
624 }
625
626 let mut final_end = session.written;
627 let mut payload_bytes = 0usize;
628 {
629 let Some(receiver) = session.bulk.as_mut() else {
630 return Err("raw bulk record sent to a generation-6 filesystem write".into());
631 };
632 for record in records {
633 final_end = receiver
634 .accept_record(record)
635 .map_err(|error| format!("invalid filesystem bulk record: {error}"))?;
636 payload_bytes = payload_bytes
637 .checked_add(record.payload.len())
638 .ok_or_else(|| "filesystem bulk batch byte count overflowed usize".to_string())?;
639
640 if let Some(expected) = session.expected_len
641 && final_end > expected
642 {
643 encode_response(
644 id,
645 error_response(format!(
646 "write length mismatch: expected {expected}, received at least {final_end}"
647 )),
648 out_buf,
649 )?;
650 return Ok(true);
651 }
652 }
653 }
654
655 let mut file = session.file.lock().await;
656 if !session.append
657 && let Err(error) = file.seek(std::io::SeekFrom::Start(session.offset)).await
658 {
659 encode_response(id, error_response(format!("seek: {error}")), out_buf)?;
660 return Ok(true);
661 }
662 if let Err(error) = write_bulk_payloads_vectored(&mut file, records).await {
663 encode_response(id, error_response(format!("write: {error}")), out_buf)?;
664 return Ok(true);
665 }
666 drop(file);
667
668 session.offset = session.offset.saturating_add(payload_bytes as u64);
669 session.written = final_end;
670 let receiver = session
671 .bulk
672 .as_mut()
673 .expect("bulk receiver was validated before the filesystem write");
674 if let Some(credit) = receiver
675 .consume(final_end)
676 .map_err(|error| format!("advance filesystem bulk credit: {error}"))?
677 {
678 encode_control(MessageType::BulkCredit, id, &credit, out_buf)?;
679 }
680 Ok(false)
681}
682
683async fn write_bulk_payloads_vectored(
685 file: &mut tokio::fs::File,
686 records: &[BulkRecord],
687) -> std::io::Result<()> {
688 if records.len() == 1 {
689 return file.write_all(&records[0].payload).await;
690 }
691
692 let mut record_index = 0usize;
693 let mut record_offset = 0usize;
694
695 while record_index < records.len() {
696 let mut slices = Vec::with_capacity(records.len() - record_index);
697 slices.push(IoSlice::new(
698 &records[record_index].payload[record_offset..],
699 ));
700 slices.extend(
701 records[record_index + 1..]
702 .iter()
703 .map(|record| IoSlice::new(&record.payload)),
704 );
705
706 let written = file.write_vectored(&slices).await?;
707 if written == 0 {
708 return Err(std::io::Error::new(
709 std::io::ErrorKind::WriteZero,
710 "failed to write filesystem bulk batch",
711 ));
712 }
713
714 let mut remaining = written;
715 while record_index < records.len() {
716 let record_remaining = records[record_index].payload.len() - record_offset;
717 if remaining < record_remaining {
718 record_offset += remaining;
719 break;
720 }
721 remaining -= record_remaining;
722 record_index += 1;
723 record_offset = 0;
724 if remaining == 0 {
725 break;
726 }
727 }
728 }
729 Ok(())
730}
731
732pub async fn handle_fs_bulk_finish(
734 id: u32,
735 finish: BulkFinish,
736 session: &mut FsWriteSession,
737 out_buf: &mut Vec<u8>,
738) -> Result<bool, String> {
739 let Some(receiver) = session.bulk.as_mut() else {
740 return Err("bulk finish sent to a generation-6 filesystem write".into());
741 };
742 receiver
743 .accept_finish(finish)
744 .map_err(|error| format!("invalid filesystem bulk finish: {error}"))?;
745
746 if let Some(expected) = session.expected_len
747 && session.written != expected
748 {
749 encode_response(
750 id,
751 error_response(format!(
752 "write length mismatch: expected {expected}, wrote {}",
753 session.written
754 )),
755 out_buf,
756 )?;
757 return Ok(true);
758 }
759
760 let mut file = session.file.lock().await;
761 if let Err(error) = file.flush().await {
762 encode_response(id, error_response(format!("flush: {error}")), out_buf)?;
763 return Ok(true);
764 }
765 encode_response(id, ok_response(None), out_buf)?;
766 Ok(true)
767}
768
769async fn handle_realpath(path: &str) -> FsResponse {
774 match realpath(path).await {
775 Ok(path) => ok_response(Some(FsResponseData::Path(path))),
776 Err(e) => error_response(format!("realpath: {e}")),
777 }
778}
779
780async fn handle_stat(path: &str, follow_symlink: bool) -> FsResponse {
781 let result = if follow_symlink {
782 tokio::fs::metadata(path).await
783 } else {
784 tokio::fs::symlink_metadata(path).await
785 };
786
787 match result {
788 Ok(meta) => ok_response(Some(FsResponseData::Stat(metadata_to_entry_info(
789 path, &meta,
790 )))),
791 Err(e) => error_response(format!("stat: {e}")),
792 }
793}
794
795async fn handle_setstat(path: &str, follow_symlink: bool, attrs: FsSetAttrs) -> FsResponse {
796 match apply_path_attrs(path, follow_symlink, attrs).await {
797 Ok(()) => ok_response(None),
798 Err(e) => error_response(format!("setstat: {e}")),
799 }
800}
801
802async fn handle_list(path: &str) -> FsResponse {
803 match read_all_dir(path).await {
804 Ok(entries) => ok_response(Some(FsResponseData::List(entries))),
805 Err(e) => error_response(format!("readdir: {e}")),
806 }
807}
808
809async fn handle_readlink(path: &str) -> FsResponse {
810 match tokio::fs::read_link(path).await {
811 Ok(target) => ok_response(Some(FsResponseData::Path(
812 target.to_string_lossy().to_string(),
813 ))),
814 Err(e) => error_response(format!("readlink: {e}")),
815 }
816}
817
818async fn handle_symlink(target: &str, link_path: &str) -> FsResponse {
819 let target = target.to_string();
820 let link_path = link_path.to_string();
821 match tokio::task::spawn_blocking(move || std::os::unix::fs::symlink(target, link_path)).await {
822 Ok(Ok(())) => ok_response(None),
823 Ok(Err(e)) => error_response(format!("symlink: {e}")),
824 Err(e) => error_response(format!("symlink task: {e}")),
825 }
826}
827
828async fn handle_open_file(
829 id: u32,
830 state: &mut FsState,
831 path: &str,
832 options: FsOpenOptions,
833) -> FsResponse {
834 let mut open_options = tokio::fs::OpenOptions::new();
835 open_options
836 .read(options.read)
837 .write(options.write)
838 .append(options.append)
839 .create(options.create)
840 .truncate(options.truncate)
841 .create_new(options.create_new);
842 if let Some(mode) = options.mode {
843 open_options.mode(mode);
844 }
845
846 match open_options.open(path).await {
847 Ok(file) => match state.insert_file(
848 id,
849 file,
850 options.read,
851 options.write,
852 options.append,
853 path.to_string(),
854 ) {
855 Ok(handle) => ok_response(Some(FsResponseData::Handle(handle))),
856 Err(e) => error_response(format!("open: {e}")),
857 },
858 Err(e) => error_response(format!("open: {e}")),
859 }
860}
861
862async fn handle_open_dir(id: u32, state: &mut FsState, path: &str) -> FsResponse {
863 match tokio::fs::read_dir(path).await {
864 Ok(dir) => match state.insert_dir(id, dir, path.to_string()) {
865 Ok(handle) => ok_response(Some(FsResponseData::Handle(handle))),
866 Err(e) => error_response(format!("opendir: {e}")),
867 },
868 Err(e) => error_response(format!("opendir: {e}")),
869 }
870}
871
872async fn handle_close_handle(id: u32, state: &mut FsState, handle: u64) -> FsResponse {
873 match state.close_handle(id, handle) {
874 Ok(FsHandleEntry::File { file, .. }) => {
875 let mut file = file.lock().await;
876 match file.flush().await {
877 Ok(()) => ok_response(None),
878 Err(e) => error_response(format!("close: {e}")),
879 }
880 }
881 Ok(FsHandleEntry::Dir { .. }) => ok_response(None),
882 Err(e) => error_response(format!("close: {e}")),
883 }
884}
885
886async fn handle_read_dir(id: u32, state: &FsState, handle: u64, limit: Option<u32>) -> FsResponse {
887 let (dir, path) = match state.dir(id, handle) {
888 Ok(v) => v,
889 Err(e) => return error_response(format!("readdir: {e}")),
890 };
891
892 let limit = limit.unwrap_or(DEFAULT_READ_DIR_LIMIT).max(1);
893 let mut dir = dir.lock().await;
894 let mut entries = Vec::new();
895
896 for _ in 0..limit {
897 match dir.next_entry().await {
898 Ok(Some(entry)) => {
899 let entry_path = entry.path();
900 let path_str = entry_path.to_string_lossy().to_string();
901 match tokio::fs::symlink_metadata(&entry_path).await {
902 Ok(meta) => entries.push(metadata_to_entry_info(&path_str, &meta)),
903 Err(_) => entries.push(unknown_entry_info(&path_str)),
904 }
905 }
906 Ok(None) => break,
907 Err(e) => return error_response(format!("readdir {path}: {e}")),
908 }
909 }
910
911 ok_response(Some(FsResponseData::List(entries)))
912}
913
914async fn handle_fstat(id: u32, state: &FsState, handle: u64) -> FsResponse {
915 match state.handles.get(&handle) {
916 Some(FsHandleEntry::File { file, path, .. }) => {
917 if let Err(e) = state
918 .handles
919 .get(&handle)
920 .expect("entry just matched")
921 .ensure_owner(handle, id)
922 {
923 return error_response(format!("fstat: {e}"));
924 }
925 let file = file.lock().await;
926 match file.metadata().await {
927 Ok(meta) => ok_response(Some(FsResponseData::Stat(metadata_to_entry_info(
928 path, &meta,
929 )))),
930 Err(e) => error_response(format!("fstat: {e}")),
931 }
932 }
933 Some(FsHandleEntry::Dir { path, .. }) => {
934 if let Err(e) = state
935 .handles
936 .get(&handle)
937 .expect("entry just matched")
938 .ensure_owner(handle, id)
939 {
940 return error_response(format!("fstat: {e}"));
941 }
942 match tokio::fs::metadata(path).await {
943 Ok(meta) => ok_response(Some(FsResponseData::Stat(metadata_to_entry_info(
944 path, &meta,
945 )))),
946 Err(e) => error_response(format!("fstat: {e}")),
947 }
948 }
949 None => error_response(format!("fstat: invalid handle: {handle}")),
950 }
951}
952
953async fn handle_fsetstat(id: u32, state: &FsState, handle: u64, attrs: FsSetAttrs) -> FsResponse {
954 let (file, _, path) = match state.file(id, handle, false, false) {
955 Ok(v) => v,
956 Err(e) => return error_response(format!("fsetstat: {e}")),
957 };
958
959 let mut file = file.lock().await;
960 match apply_file_attrs(&mut file, &path, attrs).await {
961 Ok(()) => ok_response(None),
962 Err(e) => error_response(format!("fsetstat: {e}")),
963 }
964}
965
966async fn handle_mkdir(path: &str, mode: Option<u32>) -> FsResponse {
967 match tokio::fs::create_dir_all(path).await {
968 Ok(()) => {
969 if let Some(mode) = mode
970 && let Err(e) =
971 tokio::fs::set_permissions(path, std::fs::Permissions::from_mode(mode)).await
972 {
973 return error_response(format!("chmod: {e}"));
974 }
975 ok_response(None)
976 }
977 Err(e) => error_response(format!("mkdir: {e}")),
978 }
979}
980
981async fn handle_remove(path: &str) -> FsResponse {
982 match tokio::fs::remove_file(path).await {
983 Ok(()) => ok_response(None),
984 Err(e) => error_response(format!("remove: {e}")),
985 }
986}
987
988async fn handle_remove_dir(path: &str, recursive: bool) -> FsResponse {
989 let result = if recursive {
990 tokio::fs::remove_dir_all(path).await
991 } else {
992 tokio::fs::remove_dir(path).await
993 };
994 match result {
995 Ok(()) => ok_response(None),
996 Err(e) => error_response(format!("remove_dir: {e}")),
997 }
998}
999
1000async fn handle_copy(src: &str, dst: &str) -> FsResponse {
1001 match tokio::fs::copy(src, dst).await {
1002 Ok(_) => ok_response(None),
1003 Err(e) => error_response(format!("copy: {e}")),
1004 }
1005}
1006
1007async fn handle_rename(src: &str, dst: &str) -> FsResponse {
1008 match tokio::fs::rename(src, dst).await {
1009 Ok(()) => ok_response(None),
1010 Err(e) => error_response(format!("rename: {e}")),
1011 }
1012}
1013
1014fn accept_fs_read_offer(offer: BulkOffer) -> Result<BulkAccepted, String> {
1015 let offer = offer
1016 .validate()
1017 .map_err(|error| format!("invalid filesystem read bulk offer: {error}"))?;
1018 if offer.guest_to_host_credit_limit == 0 {
1019 return Err("filesystem read bulk offer must grant guest-to-host credit".into());
1020 }
1021 Ok(BulkAccepted {
1022 kind: BulkKind::Filesystem,
1023 flows: BULK_FLOW_MASK_GUEST_TO_HOST,
1024 format: offer.format,
1025 max_record_payload: offer
1026 .max_record_payload
1027 .min(DEFAULT_FILESYSTEM_BULK_RECORD_PAYLOAD),
1028 host_to_guest_credit_limit: 0,
1029 guest_to_host_credit_limit: offer.guest_to_host_credit_limit,
1030 })
1031}
1032
1033fn accept_fs_write_offer(offer: BulkOffer) -> Result<BulkAccepted, String> {
1034 let offer = offer
1035 .validate()
1036 .map_err(|error| format!("invalid filesystem write bulk offer: {error}"))?;
1037 if offer.guest_to_host_credit_limit != 0 {
1038 return Err("filesystem write bulk offer must not grant guest-to-host credit".into());
1039 }
1040 Ok(BulkAccepted {
1041 kind: BulkKind::Filesystem,
1042 flows: BULK_FLOW_MASK_HOST_TO_GUEST,
1043 format: offer.format,
1044 max_record_payload: offer
1045 .max_record_payload
1046 .min(DEFAULT_FILESYSTEM_BULK_RECORD_PAYLOAD),
1047 host_to_guest_credit_limit: DEFAULT_BULK_WINDOW,
1048 guest_to_host_credit_limit: 0,
1049 })
1050}
1051
1052async fn handle_bulk_read_stream(
1053 id: u32,
1054 file: Arc<Mutex<tokio::fs::File>>,
1055 offset: u64,
1056 len: Option<u64>,
1057 mut sender: BulkSendState,
1058 mut credit_rx: watch::Receiver<Option<BulkCredit>>,
1059 tx: &SessionOutputSender,
1060) {
1061 let mut file = file.lock().await;
1062 if let Err(error) = file.seek(std::io::SeekFrom::Start(offset)).await {
1063 send_raw_response(id, false, Some(format!("seek: {error}")), None, tx).await;
1064 return;
1065 }
1066
1067 let mut remaining = len;
1068 loop {
1069 if remaining == Some(0) {
1070 break;
1071 }
1072 while sender.available_credit() == 0 {
1073 if credit_rx.changed().await.is_err() {
1074 return;
1075 }
1076 let Some(credit) = *credit_rx.borrow_and_update() else {
1077 continue;
1078 };
1079 if let Err(error) = sender.apply_credit(credit) {
1080 send_raw_response(
1081 id,
1082 false,
1083 Some(format!("invalid filesystem bulk credit: {error}")),
1084 None,
1085 tx,
1086 )
1087 .await;
1088 return;
1089 }
1090 }
1091
1092 let read_len = sender
1093 .available_credit()
1094 .min(sender.max_record_payload() as u64)
1095 .min(remaining.unwrap_or(u64::MAX)) as usize;
1096 let Some(permit) = tx.reserve_bulk(read_len).await else {
1097 return;
1098 };
1099 let mut payload = vec![0u8; read_len];
1100 match file.read(&mut payload).await {
1101 Ok(0) => break,
1102 Ok(read) => {
1103 payload.truncate(read);
1104 if let Some(remaining) = &mut remaining {
1105 *remaining = remaining.saturating_sub(read as u64);
1106 }
1107 let record_offset = match sender.admit(read) {
1108 Ok(offset) => offset,
1109 Err(error) => {
1110 send_raw_response(
1111 id,
1112 false,
1113 Some(format!("admit filesystem bulk record: {error}")),
1114 None,
1115 tx,
1116 )
1117 .await;
1118 return;
1119 }
1120 };
1121 let record = BulkRecord {
1122 id,
1123 kind: BulkKind::Filesystem,
1124 flow: BulkFlow::GuestToHost,
1125 offset: record_offset,
1126 payload: Bytes::from(payload),
1127 };
1128 let output = BulkSessionOutput::new(record, RawActivity::fs_bytes(read));
1129 if !tx
1130 .send_reserved(id, SessionOutput::Bulk(output), permit)
1131 .await
1132 {
1133 return;
1134 }
1135 }
1136 Err(error) => {
1137 send_raw_response(id, false, Some(format!("read: {error}")), None, tx).await;
1138 return;
1139 }
1140 }
1141 }
1142
1143 let finish = match sender.finish() {
1144 Ok(finish) => finish,
1145 Err(error) => {
1146 send_raw_response(
1147 id,
1148 false,
1149 Some(format!("finish filesystem bulk read: {error}")),
1150 None,
1151 tx,
1152 )
1153 .await;
1154 return;
1155 }
1156 };
1157 if !send_raw_control(id, MessageType::BulkFinish, &finish, None, tx).await {
1158 return;
1159 }
1160 send_raw_response(id, true, None, None, tx).await;
1161}
1162
1163async fn handle_read_stream(
1164 id: u32,
1165 file: Arc<Mutex<tokio::fs::File>>,
1166 offset: u64,
1167 len: Option<u64>,
1168 tx: &SessionOutputSender,
1169) {
1170 let mut file = file.lock().await;
1171 if let Err(e) = file.seek(std::io::SeekFrom::Start(offset)).await {
1172 send_raw_response(id, false, Some(format!("seek: {e}")), None, tx).await;
1173 return;
1174 }
1175
1176 let mut remaining = len;
1177 let mut chunk = vec![0u8; FS_CHUNK_SIZE];
1178 let mut buf = Vec::new();
1179
1180 loop {
1181 let Some(permit) = tx.reserve(codec::MAX_FRAME_SIZE as usize + 4).await else {
1184 return;
1185 };
1186 let read_len = match remaining {
1187 Some(0) => break,
1188 Some(n) => chunk.len().min(n as usize),
1189 None => chunk.len(),
1190 };
1191
1192 match file.read(&mut chunk[..read_len]).await {
1193 Ok(0) => break,
1194 Ok(n) => {
1195 if let Some(ref mut remaining) = remaining {
1196 *remaining = remaining.saturating_sub(n as u64);
1197 }
1198 let data = FsData {
1199 data: chunk[..n].to_vec(),
1200 };
1201 let msg = match Message::with_payload(MessageType::FsData, id, &data) {
1202 Ok(msg) => msg,
1203 Err(e) => {
1204 send_raw_response(id, false, Some(format!("encode chunk: {e}")), None, tx)
1205 .await;
1206 return;
1207 }
1208 };
1209 buf.clear();
1210 if let Err(e) = codec::encode_to_buf(&msg, &mut buf) {
1211 send_raw_response(
1212 id,
1213 false,
1214 Some(format!("encode chunk frame: {e}")),
1215 None,
1216 tx,
1217 )
1218 .await;
1219 return;
1220 }
1221 let output =
1222 RawSessionOutput::new(std::mem::take(&mut buf), RawActivity::fs_bytes(n), None);
1223 if !tx
1224 .send_reserved(id, SessionOutput::Raw(output), permit)
1225 .await
1226 {
1227 return;
1228 }
1229 }
1230 Err(e) => {
1231 send_raw_response(id, false, Some(format!("read: {e}")), None, tx).await;
1232 return;
1233 }
1234 }
1235 }
1236
1237 send_raw_response(id, true, None, None, tx).await;
1238}
1239
1240async fn apply_path_attrs(
1245 path: &str,
1246 follow_symlink: bool,
1247 attrs: FsSetAttrs,
1248) -> Result<(), String> {
1249 if let Some(size) = attrs.size {
1250 let file = tokio::fs::OpenOptions::new()
1251 .write(true)
1252 .open(path)
1253 .await
1254 .map_err(|e| format!("open for truncate: {e}"))?;
1255 file.set_len(size)
1256 .await
1257 .map_err(|e| format!("set_len: {e}"))?;
1258 }
1259
1260 if let Some(mode) = attrs.mode {
1261 if !follow_symlink
1262 && tokio::fs::symlink_metadata(path)
1263 .await
1264 .map_err(|e| format!("lstat before chmod: {e}"))?
1265 .file_type()
1266 .is_symlink()
1267 {
1268 return Err("chmod on symlink without following is not supported".into());
1269 }
1270 tokio::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
1271 .await
1272 .map_err(|e| format!("chmod: {e}"))?;
1273 }
1274
1275 if attrs.uid.is_some() || attrs.gid.is_some() {
1276 chown_path(path, follow_symlink, attrs.uid, attrs.gid)?;
1277 }
1278
1279 if attrs.atime.is_some() || attrs.mtime.is_some() {
1280 set_times_path(path, follow_symlink, attrs.atime, attrs.mtime).await?;
1281 }
1282
1283 Ok(())
1284}
1285
1286async fn apply_file_attrs(
1287 file: &mut tokio::fs::File,
1288 path: &str,
1289 attrs: FsSetAttrs,
1290) -> Result<(), String> {
1291 if let Some(size) = attrs.size {
1292 file.set_len(size)
1293 .await
1294 .map_err(|e| format!("set_len: {e}"))?;
1295 }
1296
1297 if let Some(mode) = attrs.mode {
1298 file.set_permissions(std::fs::Permissions::from_mode(mode))
1299 .await
1300 .map_err(|e| format!("chmod: {e}"))?;
1301 }
1302
1303 if attrs.uid.is_some() || attrs.gid.is_some() {
1304 let uid = attrs.uid.map(|v| v as libc::uid_t).unwrap_or(!0);
1305 let gid = attrs.gid.map(|v| v as libc::gid_t).unwrap_or(!0);
1306 let rc = unsafe { libc::fchown(file.as_raw_fd(), uid, gid) };
1307 if rc != 0 {
1308 return Err(format!("fchown: {}", std::io::Error::last_os_error()));
1309 }
1310 }
1311
1312 if attrs.atime.is_some() || attrs.mtime.is_some() {
1313 set_times_fd(file.as_raw_fd(), path, attrs.atime, attrs.mtime).await?;
1314 }
1315
1316 Ok(())
1317}
1318
1319fn chown_path(
1320 path: &str,
1321 follow_symlink: bool,
1322 uid: Option<u32>,
1323 gid: Option<u32>,
1324) -> Result<(), String> {
1325 let c_path = cstring_path(path)?;
1326 let uid = uid.map(|v| v as libc::uid_t).unwrap_or(!0);
1327 let gid = gid.map(|v| v as libc::gid_t).unwrap_or(!0);
1328 let rc = unsafe {
1329 if follow_symlink {
1330 libc::chown(c_path.as_ptr(), uid, gid)
1331 } else {
1332 libc::lchown(c_path.as_ptr(), uid, gid)
1333 }
1334 };
1335 if rc != 0 {
1336 return Err(format!("chown: {}", std::io::Error::last_os_error()));
1337 }
1338 Ok(())
1339}
1340
1341async fn set_times_path(
1342 path: &str,
1343 follow_symlink: bool,
1344 atime: Option<i64>,
1345 mtime: Option<i64>,
1346) -> Result<(), String> {
1347 let meta = if follow_symlink {
1348 tokio::fs::metadata(path).await
1349 } else {
1350 tokio::fs::symlink_metadata(path).await
1351 }
1352 .map_err(|e| format!("stat before utimensat: {e}"))?;
1353 let times = timespecs(atime.unwrap_or(meta.atime()), mtime.unwrap_or(meta.mtime()));
1354 let c_path = cstring_path(path)?;
1355 let flags = if follow_symlink {
1356 0
1357 } else {
1358 libc::AT_SYMLINK_NOFOLLOW
1359 };
1360 let rc = unsafe { libc::utimensat(libc::AT_FDCWD, c_path.as_ptr(), times.as_ptr(), flags) };
1361 if rc != 0 {
1362 return Err(format!("utimensat: {}", std::io::Error::last_os_error()));
1363 }
1364 Ok(())
1365}
1366
1367async fn set_times_fd(
1368 fd: std::os::fd::RawFd,
1369 path: &str,
1370 atime: Option<i64>,
1371 mtime: Option<i64>,
1372) -> Result<(), String> {
1373 let meta = tokio::fs::metadata(path)
1374 .await
1375 .map_err(|e| format!("stat before futimens: {e}"))?;
1376 let times = timespecs(atime.unwrap_or(meta.atime()), mtime.unwrap_or(meta.mtime()));
1377 let rc = unsafe { libc::futimens(fd, times.as_ptr()) };
1378 if rc != 0 {
1379 return Err(format!("futimens: {}", std::io::Error::last_os_error()));
1380 }
1381 Ok(())
1382}
1383
1384fn timespecs(atime: i64, mtime: i64) -> [libc::timespec; 2] {
1385 [
1386 libc::timespec {
1387 tv_sec: atime as _,
1388 tv_nsec: 0,
1389 },
1390 libc::timespec {
1391 tv_sec: mtime as _,
1392 tv_nsec: 0,
1393 },
1394 ]
1395}
1396
1397fn encode_response(id: u32, resp: FsResponse, out_buf: &mut Vec<u8>) -> Result<(), String> {
1402 let msg = Message::with_payload(MessageType::FsResponse, id, &resp)
1403 .map_err(|e| format!("encode fs response: {e}"))?;
1404 codec::encode_to_buf(&msg, out_buf).map_err(|e| format!("encode fs response frame: {e}"))?;
1405 Ok(())
1406}
1407
1408fn encode_control<T: Serialize>(
1409 message_type: MessageType,
1410 id: u32,
1411 payload: &T,
1412 out_buf: &mut Vec<u8>,
1413) -> Result<(), String> {
1414 let message = Message::with_payload(message_type, id, payload)
1415 .map_err(|error| format!("encode {}: {error}", message_type.as_str()))?;
1416 codec::encode_to_buf(&message, out_buf)
1417 .map_err(|error| format!("encode {} frame: {error}", message_type.as_str()))
1418}
1419
1420async fn send_raw_control<T: Serialize>(
1421 id: u32,
1422 message_type: MessageType,
1423 payload: &T,
1424 completion: Option<RawSessionCompletion>,
1425 tx: &SessionOutputSender,
1426) -> bool {
1427 let mut frame = Vec::new();
1428 if let Err(error) = encode_control(message_type, id, payload, &mut frame) {
1429 eprintln!("failed to {error}");
1430 return false;
1431 }
1432 tx.send(
1433 id,
1434 SessionOutput::Raw(RawSessionOutput::new(
1435 frame,
1436 RawActivity::guest_message(),
1437 completion,
1438 )),
1439 )
1440 .await
1441}
1442
1443async fn send_raw_response(
1444 id: u32,
1445 ok: bool,
1446 error: Option<String>,
1447 data: Option<FsResponseData>,
1448 tx: &SessionOutputSender,
1449) {
1450 let resp = FsResponse { ok, error, data };
1451 match Message::with_payload(MessageType::FsResponse, id, &resp) {
1452 Ok(msg) => {
1453 let mut buf = Vec::new();
1454 match codec::encode_to_buf(&msg, &mut buf) {
1455 Ok(()) => {
1456 let output = RawSessionOutput::new(
1457 buf,
1458 RawActivity::guest_message(),
1459 Some(RawSessionCompletion::FsRead),
1460 );
1461 let _ = tx.send(id, SessionOutput::Raw(output)).await;
1462 }
1463 Err(e) => {
1464 eprintln!("failed to encode fs response frame for {id}: {e}");
1465 }
1466 }
1467 }
1468 Err(e) => {
1469 eprintln!("failed to encode fs response for {id}: {e}");
1470 }
1471 }
1472}
1473
1474async fn realpath(path: &str) -> Result<String, String> {
1475 match tokio::fs::canonicalize(path).await {
1476 Ok(path) => Ok(path.to_string_lossy().to_string()),
1477 Err(original_error) => {
1478 let path = Path::new(path);
1479 let Some(parent) = path.parent() else {
1480 return Err(original_error.to_string());
1481 };
1482 let parent = tokio::fs::canonicalize(parent)
1483 .await
1484 .map_err(|_| original_error.to_string())?;
1485 let resolved = match path.file_name() {
1486 Some(name) => parent.join(name),
1487 None => parent,
1488 };
1489 Ok(resolved.to_string_lossy().to_string())
1490 }
1491 }
1492}
1493
1494async fn read_all_dir(path: &str) -> Result<Vec<FsEntryInfo>, String> {
1495 let mut dir = tokio::fs::read_dir(path)
1496 .await
1497 .map_err(|e| format!("opendir: {e}"))?;
1498 let mut entries = Vec::new();
1499
1500 loop {
1501 match dir.next_entry().await {
1502 Ok(Some(entry)) => {
1503 let entry_path = entry.path();
1504 let path_str = entry_path.to_string_lossy().to_string();
1505 match tokio::fs::symlink_metadata(&entry_path).await {
1506 Ok(meta) => entries.push(metadata_to_entry_info(&path_str, &meta)),
1507 Err(_) => entries.push(unknown_entry_info(&path_str)),
1508 }
1509 }
1510 Ok(None) => break,
1511 Err(e) => return Err(e.to_string()),
1512 }
1513 }
1514
1515 Ok(entries)
1516}
1517
1518fn ok_response(data: Option<FsResponseData>) -> FsResponse {
1519 FsResponse {
1520 ok: true,
1521 error: None,
1522 data,
1523 }
1524}
1525
1526fn error_response(error: String) -> FsResponse {
1527 FsResponse {
1528 ok: false,
1529 error: Some(error),
1530 data: None,
1531 }
1532}
1533
1534fn metadata_to_entry_info(path: &str, meta: &std::fs::Metadata) -> FsEntryInfo {
1535 let kind = if meta.is_file() {
1536 "file"
1537 } else if meta.is_dir() {
1538 "dir"
1539 } else if meta.is_symlink() {
1540 "symlink"
1541 } else {
1542 "other"
1543 };
1544
1545 let mtime = Some(meta.mtime());
1546 let atime = Some(meta.atime());
1547
1548 FsEntryInfo {
1549 path: path.to_string(),
1550 kind: kind.to_string(),
1551 size: meta.len(),
1552 mode: meta.mode(),
1553 modified: mtime,
1554 uid: meta.uid(),
1555 gid: meta.gid(),
1556 atime,
1557 mtime,
1558 }
1559}
1560
1561fn unknown_entry_info(path: &str) -> FsEntryInfo {
1562 FsEntryInfo {
1563 path: path.to_string(),
1564 kind: "other".to_string(),
1565 size: 0,
1566 mode: 0,
1567 modified: None,
1568 uid: 0,
1569 gid: 0,
1570 atime: None,
1571 mtime: None,
1572 }
1573}
1574
1575fn cstring_path(path: impl AsRef<Path>) -> Result<CString, String> {
1576 CString::new(path.as_ref().as_os_str().as_bytes())
1577 .map_err(|e| format!("path contains NUL: {e}"))
1578}
1579
1580#[cfg(test)]
1585mod tests {
1586 use std::time::{SystemTime, UNIX_EPOCH};
1587
1588 use super::*;
1589
1590 #[test]
1591 fn filesystem_offer_preserves_an_older_hosts_smaller_record_limit() {
1592 assert_eq!(
1593 DEFAULT_FILESYSTEM_BULK_RECORD_PAYLOAD as usize,
1594 FS_CHUNK_SIZE
1595 );
1596 let old_offer = BulkOffer {
1597 max_record_payload: microsandbox_protocol::bulk::DEFAULT_BULK_RECORD_PAYLOAD,
1598 ..BulkOffer::filesystem_write()
1599 };
1600
1601 let accepted = accept_fs_write_offer(old_offer).unwrap();
1602 assert_eq!(
1603 accepted.max_record_payload,
1604 microsandbox_protocol::bulk::DEFAULT_BULK_RECORD_PAYLOAD
1605 );
1606 }
1607
1608 #[tokio::test]
1609 async fn raw_bulk_write_requires_exact_offsets_and_finish_length() {
1610 let path = test_path("bulk-write");
1611 let file = tokio::fs::File::create(&path).await.unwrap();
1612 let mut session = FsWriteSession {
1613 owner_id: 1,
1614 handle: 1,
1615 file: Arc::new(Mutex::new(file)),
1616 offset: 0,
1617 append: false,
1618 expected_len: Some(7),
1619 written: 0,
1620 bulk: Some(
1621 BulkReceiveState::new(
1622 BulkKind::Filesystem,
1623 BulkFlow::HostToGuest,
1624 microsandbox_protocol::bulk::DEFAULT_BULK_RECORD_PAYLOAD,
1625 DEFAULT_BULK_WINDOW,
1626 DEFAULT_BULK_WINDOW,
1627 )
1628 .unwrap(),
1629 ),
1630 };
1631 let mut out = Vec::new();
1632
1633 let wrong_offset = BulkRecord {
1634 id: 1,
1635 kind: BulkKind::Filesystem,
1636 flow: BulkFlow::HostToGuest,
1637 offset: 1,
1638 payload: Bytes::from_static(b"ignored"),
1639 };
1640 assert!(
1641 handle_fs_bulk_record(1, &wrong_offset, &mut session, &mut out)
1642 .await
1643 .unwrap_err()
1644 .contains("does not match expected")
1645 );
1646
1647 let first = BulkRecord {
1648 offset: 0,
1649 payload: Bytes::from_static(b"ign"),
1650 ..wrong_offset
1651 };
1652 let second = BulkRecord {
1653 offset: 3,
1654 payload: Bytes::from_static(b"ored"),
1655 ..first.clone()
1656 };
1657 assert!(
1658 !handle_fs_bulk_records(1, &[first, second], &mut session, &mut out)
1659 .await
1660 .unwrap()
1661 );
1662 assert!(
1663 handle_fs_bulk_finish(
1664 1,
1665 BulkFinish {
1666 kind: BulkKind::Filesystem,
1667 flow: BulkFlow::HostToGuest,
1668 final_offset: 7,
1669 },
1670 &mut session,
1671 &mut out,
1672 )
1673 .await
1674 .unwrap()
1675 );
1676 drop(session);
1677
1678 let response = codec::try_decode_from_buf(&mut out).unwrap().unwrap();
1679 assert_eq!(response.t, MessageType::FsResponse);
1680 assert!(response.payload::<FsResponse>().unwrap().ok);
1681 assert_eq!(tokio::fs::read(&path).await.unwrap(), b"ignored");
1682 tokio::fs::remove_file(path).await.unwrap();
1683 }
1684
1685 #[tokio::test]
1686 async fn raw_bulk_read_emits_payload_exact_finish_then_terminal_response() {
1687 let path = test_path("bulk-read");
1688 tokio::fs::write(&path, b"raw-read-payload").await.unwrap();
1689 let file = tokio::fs::File::open(&path).await.unwrap();
1690 let sender = BulkSendState::new(
1691 BulkKind::Filesystem,
1692 BulkFlow::GuestToHost,
1693 microsandbox_protocol::bulk::DEFAULT_BULK_RECORD_PAYLOAD,
1694 DEFAULT_BULK_WINDOW,
1695 )
1696 .unwrap();
1697 let (_credit_tx, credit_rx) = watch::channel(None);
1698 let (session_tx, mut session_rx) = SessionOutputSender::channel();
1699
1700 handle_bulk_read_stream(
1701 2,
1702 Arc::new(Mutex::new(file)),
1703 0,
1704 None,
1705 sender,
1706 credit_rx,
1707 &session_tx,
1708 )
1709 .await;
1710
1711 let first = session_rx.recv().await.unwrap();
1712 let SessionOutput::Bulk(first) = first.output else {
1713 panic!("expected raw filesystem record");
1714 };
1715 assert_eq!(first.record.offset, 0);
1716 assert_eq!(
1717 first.record.payload,
1718 Bytes::from_static(b"raw-read-payload")
1719 );
1720
1721 let finish = decode_raw_output(session_rx.recv().await.unwrap().output);
1722 assert_eq!(finish.t, MessageType::BulkFinish);
1723 let finish: BulkFinish = finish.payload().unwrap();
1724 assert_eq!(finish.final_offset, b"raw-read-payload".len() as u64);
1725 let response = decode_raw_output(session_rx.recv().await.unwrap().output);
1726 assert_eq!(response.t, MessageType::FsResponse);
1727 assert!(response.payload::<FsResponse>().unwrap().ok);
1728 tokio::fs::remove_file(path).await.unwrap();
1729 }
1730
1731 fn decode_raw_output(output: SessionOutput) -> Message {
1732 let SessionOutput::Raw(mut output) = output else {
1733 panic!("expected raw control frame");
1734 };
1735 codec::try_decode_from_buf(&mut output.frame)
1736 .unwrap()
1737 .unwrap()
1738 }
1739
1740 fn test_path(name: &str) -> std::path::PathBuf {
1741 let unique = SystemTime::now()
1742 .duration_since(UNIX_EPOCH)
1743 .unwrap()
1744 .as_nanos();
1745 std::env::temp_dir().join(format!("msb-agentd-{name}-{}-{unique}", std::process::id()))
1746 }
1747}