Skip to main content

microsandbox_agentd/
fs.rs

1//! Guest-side filesystem operation handlers.
2//!
3//! Handles `core.fs.*` protocol messages by performing filesystem operations
4//! using `std::fs` and `tokio::fs`, then sending responses back to the host.
5
6use 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
38//--------------------------------------------------------------------------------------------------
39// Constants
40//--------------------------------------------------------------------------------------------------
41
42/// Default maximum number of entries returned by one `ReadDir` request.
43const DEFAULT_READ_DIR_LIMIT: u32 = 128;
44
45/// Maximum number of open filesystem handles owned by one relay client.
46const MAX_OPEN_HANDLES_PER_OWNER: usize = 1024;
47
48//--------------------------------------------------------------------------------------------------
49// Types
50//--------------------------------------------------------------------------------------------------
51
52/// Mutable filesystem protocol state held by agentd.
53#[derive(Default)]
54pub struct FsState {
55    next_handle: u64,
56    handles: HashMap<u64, FsHandleEntry>,
57}
58
59/// Tracks an in-progress streaming write operation.
60pub 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
71/// Tracks an in-progress streaming read operation.
72pub struct FsReadSession {
73    owner_id: u32,
74    handle: u64,
75    task: JoinHandle<()>,
76    credit_tx: Option<watch::Sender<Option<BulkCredit>>>,
77}
78
79/// A filesystem stream session started by a request.
80pub enum FsStreamSession {
81    /// Read stream task.
82    Read(FsReadSession),
83
84    /// Write stream awaiting `FsData` chunks.
85    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
104//--------------------------------------------------------------------------------------------------
105// Methods
106//--------------------------------------------------------------------------------------------------
107
108impl FsState {
109    /// Close handles opened by a disconnected relay client.
110    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    /// Close all handles.
118    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    /// Correlation ID whose relay client owns this read stream.
269    pub fn owner_id(&self) -> u32 {
270        self.owner_id
271    }
272
273    /// Filesystem handle being read.
274    pub fn handle(&self) -> u64 {
275        self.handle
276    }
277
278    /// Abort the background read task.
279    pub fn abort(self) {
280        self.task.abort();
281    }
282
283    /// Delivers the latest absolute credit update to a generation-8 read task.
284    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    /// Whether this read session negotiated generation-8 raw bulk.
296    pub fn is_bulk(&self) -> bool {
297        self.credit_tx.is_some()
298    }
299}
300
301impl FsWriteSession {
302    /// Correlation ID whose relay client owns this write stream.
303    pub fn owner_id(&self) -> u32 {
304        self.owner_id
305    }
306
307    /// Filesystem handle being written.
308    pub fn handle(&self) -> u64 {
309        self.handle
310    }
311
312    /// Whether this write session negotiated generation-8 raw bulk.
313    pub fn is_bulk(&self) -> bool {
314        self.bulk.is_some()
315    }
316}
317
318//--------------------------------------------------------------------------------------------------
319// Functions
320//--------------------------------------------------------------------------------------------------
321
322fn 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
326/// Handles an incoming `FsRequest` message.
327pub 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
534/// Handles an incoming `FsData` message for a streaming write session.
535///
536/// If `data` is empty, the file is flushed and a terminal `FsResponse` is sent.
537/// Returns `true` if the session should be removed (EOF received).
538pub 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
601/// Handles one generation-8 raw record for a filesystem write correlation.
602pub 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
611/// Handles a bounded contiguous batch of generation-8 filesystem records.
612///
613/// The protocol still validates and accounts for each wire record independently. Coalescing only
614/// changes the local filesystem effect: one lock, one seek and a vectored write replace multiple
615/// blocking-file tasks when a peer negotiates records below the current filesystem default.
616pub 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
683/// Completes a vectored Tokio file write even when the kernel accepts only a prefix of the batch.
684async 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
732/// Handles the exact end marker for a generation-8 filesystem write.
733pub 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
769//--------------------------------------------------------------------------------------------------
770// Functions: Handlers
771//--------------------------------------------------------------------------------------------------
772
773async 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        // Reserve before the file read and CBOR materialization so concurrent streams cannot
1182        // each create an uncharged maximum-sized frame while aggregate output is saturated.
1183        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
1240//--------------------------------------------------------------------------------------------------
1241// Functions: Attribute Helpers
1242//--------------------------------------------------------------------------------------------------
1243
1244async 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
1397//--------------------------------------------------------------------------------------------------
1398// Functions: Helpers
1399//--------------------------------------------------------------------------------------------------
1400
1401fn 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//--------------------------------------------------------------------------------------------------
1581// Tests
1582//--------------------------------------------------------------------------------------------------
1583
1584#[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}