1use std::sync::Arc;
14
15use anyhow::{Context, Result};
16use async_trait::async_trait;
17use base64::engine::general_purpose::STANDARD as BASE64;
18use base64::Engine as _;
19use serde::{Deserialize, Serialize};
20
21use agentos_sidecar_client::wire::{
22 self, GuestFilesystemCallRequest, GuestFilesystemOperation, GuestFilesystemResultResponse,
23 GuestFilesystemStat, RootFilesystemEntry, RootFilesystemEntryEncoding, RootFilesystemEntryKind,
24};
25
26use crate::agent_os::AgentOs;
27use crate::error::ClientError;
28
29#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum FileContent {
36 Text(String),
37 Bytes(Vec<u8>),
38}
39
40impl From<String> for FileContent {
41 fn from(value: String) -> Self {
42 FileContent::Text(value)
43 }
44}
45
46impl From<&str> for FileContent {
47 fn from(value: &str) -> Self {
48 FileContent::Text(value.to_string())
49 }
50}
51
52impl From<Vec<u8>> for FileContent {
53 fn from(value: Vec<u8>) -> Self {
54 FileContent::Bytes(value)
55 }
56}
57
58impl From<&[u8]> for FileContent {
59 fn from(value: &[u8]) -> Self {
60 FileContent::Bytes(value.to_vec())
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66pub struct DirEntry {
67 pub path: String,
68 #[serde(rename = "type")]
69 pub entry_type: DirEntryType,
70 pub size: u64,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(rename_all = "lowercase")]
76pub enum DirEntryType {
77 File,
78 Directory,
79 Symlink,
80}
81
82#[derive(Debug, Clone, Default, PartialEq, Eq)]
85pub struct ReaddirRecursiveOptions {
86 pub max_depth: Option<u32>,
87 pub exclude: Vec<String>,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct BatchWriteEntry {
93 pub path: String,
94 pub content: FileContent,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct BatchWriteResult {
100 pub path: String,
101 pub success: bool,
102 pub error: Option<String>,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct BatchReadResult {
108 pub path: String,
109 pub content: Option<Vec<u8>>,
110 pub error: Option<String>,
111}
112
113#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
115pub struct MkdirOptions {
116 pub recursive: bool,
117}
118
119#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
121pub struct DeleteOptions {
122 pub recursive: bool,
123}
124
125#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
127pub struct MountFsOptions {
128 pub read_only: bool,
129}
130
131#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
133pub struct VirtualStat {
134 pub mode: u32,
135 pub size: u64,
136 pub blocks: u64,
137 pub dev: u64,
138 pub rdev: u64,
139 #[serde(rename = "isDirectory")]
140 pub is_directory: bool,
141 #[serde(rename = "isSymbolicLink")]
142 pub is_symbolic_link: bool,
143 #[serde(rename = "atimeMs")]
144 pub atime_ms: f64,
145 #[serde(rename = "mtimeMs")]
146 pub mtime_ms: f64,
147 #[serde(rename = "ctimeMs")]
148 pub ctime_ms: f64,
149 #[serde(rename = "birthtimeMs")]
150 pub birthtime_ms: f64,
151 pub ino: u64,
152 pub nlink: u64,
153 pub uid: u32,
154 pub gid: u32,
155}
156
157#[derive(Clone)]
162pub struct MountedFs {
163 pub driver: Arc<dyn VirtualFileSystem>,
164 pub read_only: bool,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct VirtualDirEntry {
170 pub name: String,
171 pub is_directory: bool,
172 pub is_symbolic_link: bool,
173}
174
175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181pub struct RootSnapshotExport {
182 pub kind: SnapshotExportKind,
183 pub source: FilesystemSnapshotExport,
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
188pub enum SnapshotExportKind {
189 #[serde(rename = "snapshot-export")]
190 SnapshotExport,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195pub struct FilesystemSnapshotExport {
196 pub format: String,
197 pub filesystem: FilesystemSnapshotEntries,
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
202pub struct FilesystemSnapshotEntries {
203 pub entries: Vec<FilesystemEntry>,
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
208pub struct FilesystemEntry {
209 pub path: String,
210 #[serde(rename = "type")]
211 pub entry_type: DirEntryType,
212 pub mode: String,
213 pub uid: u32,
214 pub gid: u32,
215 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub content: Option<String>,
217 #[serde(default, skip_serializing_if = "Option::is_none")]
218 pub encoding: Option<FilesystemEntryEncoding>,
219 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub target: Option<String>,
221}
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
225#[serde(rename_all = "lowercase")]
226pub enum FilesystemEntryEncoding {
227 Utf8,
228 Base64,
229}
230
231#[async_trait]
240pub trait VirtualFileSystem: Send + Sync {
241 async fn read_file(&self, path: &str) -> Result<Vec<u8>>;
242 async fn read_text_file(&self, path: &str) -> Result<String>;
243 async fn read_dir(&self, path: &str) -> Result<Vec<String>>;
244 async fn read_dir_with_types(&self, path: &str) -> Result<Vec<VirtualDirEntry>>;
245 async fn write_file(&self, path: &str, content: &[u8]) -> Result<()>;
246 async fn create_dir(&self, path: &str) -> Result<()>;
247 async fn mkdir(&self, path: &str, recursive: bool) -> Result<()>;
248 async fn exists(&self, path: &str) -> Result<bool>;
249 async fn stat(&self, path: &str) -> Result<VirtualStat>;
250 async fn lstat(&self, path: &str) -> Result<VirtualStat>;
251 async fn remove_file(&self, path: &str) -> Result<()>;
252 async fn remove_dir(&self, path: &str) -> Result<()>;
253 async fn rename(&self, from: &str, to: &str) -> Result<()>;
254 async fn realpath(&self, path: &str) -> Result<String>;
255 async fn symlink(&self, target: &str, path: &str) -> Result<()>;
256 async fn readlink(&self, path: &str) -> Result<String>;
257 async fn link(&self, existing: &str, new_path: &str) -> Result<()>;
258 async fn chmod(&self, path: &str, mode: u32) -> Result<()>;
259 async fn chown(&self, path: &str, uid: u32, gid: u32) -> Result<()>;
260 async fn utimes(&self, path: &str, atime_ms: f64, mtime_ms: f64) -> Result<()>;
261 async fn truncate(&self, path: &str, len: u64) -> Result<()>;
262 async fn pread(&self, path: &str, offset: u64, length: u64) -> Result<Vec<u8>>;
263 async fn pwrite(&self, path: &str, offset: u64, data: &[u8]) -> Result<u64>;
264}
265
266impl AgentOs {
271 pub(crate) fn posix_normalize(path: &str) -> String {
278 if path.is_empty() {
279 return String::from(".");
280 }
281
282 let is_absolute = path.starts_with('/');
283 let trailing_slash = path.ends_with('/');
284
285 let mut segments: Vec<&str> = Vec::new();
286 for part in path.split('/') {
287 match part {
288 "" | "." => {}
289 ".." => {
290 match segments.last().copied() {
291 Some(last) if last != ".." => {
292 segments.pop();
293 }
294 Some(_) | None => {
295 if !is_absolute {
298 segments.push("..");
299 }
300 }
301 }
302 }
303 other => segments.push(other),
304 }
305 }
306
307 let mut joined = segments.join("/");
308 if joined.is_empty() {
309 if is_absolute {
310 return String::from("/");
311 }
312 return String::from(".");
313 }
314
315 if trailing_slash {
316 joined.push('/');
317 }
318 if is_absolute {
319 let mut absolute = String::from("/");
320 absolute.push_str(&joined);
321 absolute
322 } else {
323 joined
324 }
325 }
326
327 pub(crate) fn assert_safe_absolute_path(path: &str) -> std::result::Result<(), ClientError> {
329 if !path.starts_with('/') {
330 return Err(ClientError::PathNotAbsolute(path.to_string()));
331 }
332 if Self::posix_normalize(path) != path {
333 return Err(ClientError::PathNotNormalized(path.to_string()));
334 }
335 Ok(())
336 }
337
338 pub(crate) fn assert_writable_absolute_path(
340 path: &str,
341 ) -> std::result::Result<(), ClientError> {
342 Self::assert_safe_absolute_path(path)?;
343 if path == "/proc"
344 || path.starts_with("/proc/")
345 || path == "/etc/agentos"
346 || path.starts_with("/etc/agentos/")
347 {
348 return Err(ClientError::PathReadOnly(path.to_string()));
349 }
350 Ok(())
351 }
352}
353
354impl AgentOs {
359 fn batch_error_message(err: &anyhow::Error) -> String {
365 match err.downcast_ref::<ClientError>() {
366 Some(client_error) => client_error.batch_message(),
367 None => err.to_string(),
368 }
369 }
370
371 fn fs_vm_scope(&self) -> wire::OwnershipScope {
373 wire::OwnershipScope::VmOwnership(wire::VmOwnership {
374 connection_id: self.connection_id().to_string(),
375 session_id: self.wire_session_id().to_string(),
376 vm_id: self.vm_id().to_string(),
377 })
378 }
379
380 fn join_child(dir: &str, child: &str) -> String {
383 if dir == "/" {
384 format!("/{child}")
385 } else {
386 format!("{dir}/{child}")
387 }
388 }
389
390 async fn guest_fs_call(
393 &self,
394 request: GuestFilesystemCallRequest,
395 ) -> Result<GuestFilesystemResultResponse> {
396 let scope = self.fs_vm_scope();
397 let response = self
398 .transport()
399 .request_wire(
400 scope,
401 wire::RequestPayload::GuestFilesystemCallRequest(request),
402 )
403 .await
404 .context("guest filesystem call failed")?;
405 match response {
406 wire::ResponsePayload::GuestFilesystemResultResponse(result) => Ok(result),
407 wire::ResponsePayload::RejectedResponse(wire::RejectedResponse { code, message }) => {
408 Err(ClientError::Kernel { code, message }.into())
409 }
410 other => Err(anyhow::anyhow!(
411 "unexpected response to guest filesystem call: {other:?}"
412 )),
413 }
414 }
415
416 fn fs_request(
418 operation: GuestFilesystemOperation,
419 path: impl Into<String>,
420 ) -> GuestFilesystemCallRequest {
421 GuestFilesystemCallRequest {
422 operation,
423 path: path.into(),
424 destination_path: None,
425 target: None,
426 content: None,
427 encoding: None,
428 recursive: false,
429 max_depth: None,
430 mode: None,
431 uid: None,
432 gid: None,
433 atime_ms: None,
434 mtime_ms: None,
435 len: None,
436 offset: None,
437 }
438 }
439
440 fn virtual_stat_from(stat: GuestFilesystemStat) -> VirtualStat {
443 VirtualStat {
444 mode: stat.mode,
445 size: stat.size,
446 blocks: stat.blocks,
447 dev: stat.dev,
448 rdev: stat.rdev,
449 is_directory: stat.is_directory,
450 is_symbolic_link: stat.is_symbolic_link,
451 atime_ms: stat.atime_ms as f64,
452 mtime_ms: stat.mtime_ms as f64,
453 ctime_ms: stat.ctime_ms as f64,
454 birthtime_ms: stat.birthtime_ms as f64,
455 ino: stat.ino,
456 nlink: stat.nlink,
457 uid: stat.uid,
458 gid: stat.gid,
459 }
460 }
461
462 async fn kernel_read_file(&self, path: &str) -> Result<Vec<u8>> {
468 let result = self
469 .guest_fs_call(Self::fs_request(GuestFilesystemOperation::ReadFile, path))
470 .await?;
471 let content = result
472 .content
473 .with_context(|| format!("sidecar returned no file content for {path}"))?;
474 match result.encoding {
475 Some(RootFilesystemEntryEncoding::Base64) => BASE64
476 .decode(content.as_bytes())
477 .context("decoding base64 file content"),
478 Some(RootFilesystemEntryEncoding::Utf8) | None => Ok(content.into_bytes()),
479 }
480 }
481
482 async fn kernel_write_file(&self, path: &str, content: &FileContent) -> Result<()> {
486 let (encoded, encoding) = match content {
487 FileContent::Text(text) => (text.clone(), None),
488 FileContent::Bytes(bytes) => (
489 BASE64.encode(bytes),
490 Some(RootFilesystemEntryEncoding::Base64),
491 ),
492 };
493 let mut request = Self::fs_request(GuestFilesystemOperation::WriteFile, path);
494 request.content = Some(encoded);
495 request.encoding = encoding;
496 self.guest_fs_call(request).await?;
497 Ok(())
498 }
499
500 async fn kernel_mkdir(&self, path: &str) -> Result<()> {
506 self.guest_fs_call(Self::fs_request(GuestFilesystemOperation::CreateDir, path))
507 .await?;
508 Ok(())
509 }
510
511 async fn kernel_exists(&self, path: &str) -> Result<bool> {
512 let result = self
513 .guest_fs_call(Self::fs_request(GuestFilesystemOperation::Exists, path))
514 .await?;
515 Ok(result.exists.unwrap_or(false))
516 }
517
518 async fn kernel_readdir(&self, path: &str) -> Result<Vec<String>> {
519 let result = self
520 .guest_fs_call(Self::fs_request(GuestFilesystemOperation::ReadDir, path))
521 .await?;
522 Ok(result
527 .entries
528 .unwrap_or_default()
529 .into_iter()
530 .map(|entry| entry.name)
531 .collect())
532 }
533
534 async fn kernel_readdir_recursive(
535 &self,
536 path: &str,
537 max_depth: Option<u32>,
538 ) -> Result<Vec<wire::GuestDirEntry>> {
539 let mut request = Self::fs_request(GuestFilesystemOperation::ReadDirRecursive, path);
540 request.max_depth = max_depth;
541 let result = self.guest_fs_call(request).await?;
542 Ok(result.entries.unwrap_or_default())
543 }
544
545 async fn kernel_stat(&self, path: &str) -> Result<VirtualStat> {
546 let result = self
547 .guest_fs_call(Self::fs_request(GuestFilesystemOperation::Stat, path))
548 .await?;
549 let stat = result.stat.context("stat response missing stat payload")?;
550 Ok(Self::virtual_stat_from(stat))
551 }
552
553 async fn kernel_lstat(&self, path: &str) -> Result<VirtualStat> {
554 let result = self
555 .guest_fs_call(Self::fs_request(GuestFilesystemOperation::Lstat, path))
556 .await?;
557 let stat = result.stat.context("lstat response missing stat payload")?;
558 Ok(Self::virtual_stat_from(stat))
559 }
560
561 async fn kernel_remove_path(&self, path: &str, recursive: bool) -> Result<()> {
562 let mut request = Self::fs_request(GuestFilesystemOperation::Remove, path);
563 request.recursive = recursive;
564 self.guest_fs_call(request).await?;
565 Ok(())
566 }
567
568 async fn kernel_move_path(&self, from: &str, to: &str) -> Result<()> {
569 let mut request = Self::fs_request(GuestFilesystemOperation::Move, from);
570 request.destination_path = Some(to.to_string());
571 request.recursive = true;
572 self.guest_fs_call(request).await?;
573 Ok(())
574 }
575
576 async fn mkdirp(&self, path: &str) -> Result<()> {
579 Self::assert_writable_absolute_path(path)?;
580 let mut current = String::new();
581 for part in path.split('/').filter(|p| !p.is_empty()) {
582 current.push('/');
583 current.push_str(part);
584 if !self.kernel_exists(¤t).await? {
585 self.kernel_mkdir(¤t).await?;
586 }
587 }
588 Ok(())
589 }
590}
591
592impl AgentOs {
597 pub async fn read_file(&self, path: &str) -> Result<Vec<u8>> {
599 Self::assert_safe_absolute_path(path)?;
600 self.kernel_read_file(path).await
601 }
602
603 pub async fn write_file(&self, path: &str, content: impl Into<FileContent>) -> Result<()> {
605 Self::assert_writable_absolute_path(path)?;
606 let content = content.into();
607 self.kernel_write_file(path, &content).await
608 }
609
610 pub async fn write_files(&self, entries: Vec<BatchWriteEntry>) -> Vec<BatchWriteResult> {
612 let mut results = Vec::with_capacity(entries.len());
613 for entry in entries {
614 let outcome: Result<()> = async {
615 Self::assert_writable_absolute_path(&entry.path)?;
616 if let Some(idx) = entry.path.rfind('/') {
619 let parent = &entry.path[..idx];
620 if !parent.is_empty() {
621 self.mkdirp(parent).await?;
622 }
623 }
624 self.kernel_write_file(&entry.path, &entry.content).await?;
625 Ok(())
626 }
627 .await;
628 match outcome {
629 Ok(()) => results.push(BatchWriteResult {
630 path: entry.path,
631 success: true,
632 error: None,
633 }),
634 Err(err) => results.push(BatchWriteResult {
635 path: entry.path,
636 success: false,
637 error: Some(Self::batch_error_message(&err)),
638 }),
639 }
640 }
641 results
642 }
643
644 pub async fn read_files(&self, paths: Vec<String>) -> Vec<BatchReadResult> {
646 let mut results = Vec::with_capacity(paths.len());
647 for path in paths {
648 let outcome: Result<Vec<u8>> = async {
649 Self::assert_safe_absolute_path(&path)?;
650 self.kernel_read_file(&path).await
651 }
652 .await;
653 match outcome {
654 Ok(content) => results.push(BatchReadResult {
655 path,
656 content: Some(content),
657 error: None,
658 }),
659 Err(err) => results.push(BatchReadResult {
660 path,
661 content: None,
662 error: Some(Self::batch_error_message(&err)),
663 }),
664 }
665 }
666 results
667 }
668
669 pub async fn mkdir(&self, path: &str, options: MkdirOptions) -> Result<()> {
672 if options.recursive {
673 return self.mkdirp(path).await;
674 }
675 Self::assert_writable_absolute_path(path)?;
676 self.kernel_mkdir(path).await
677 }
678
679 pub async fn readdir(&self, path: &str) -> Result<Vec<String>> {
681 Self::assert_safe_absolute_path(path)?;
682 self.kernel_readdir(path).await
683 }
684
685 pub(crate) async fn acp_read_dir_with_types(&self, path: &str) -> Result<Vec<VirtualDirEntry>> {
689 Self::assert_safe_absolute_path(path)?;
690 let names = self.kernel_readdir(path).await?;
691 let mut entries = Vec::with_capacity(names.len());
692 for name in names {
693 if name == "." || name == ".." {
694 continue;
695 }
696 let full_path = Self::join_child(path, &name);
697 let stat = self.kernel_lstat(&full_path).await?;
698 entries.push(VirtualDirEntry {
699 name,
700 is_directory: stat.is_directory,
701 is_symbolic_link: stat.is_symbolic_link,
702 });
703 }
704 Ok(entries)
705 }
706
707 pub async fn read_dir_with_types(&self, path: &str) -> Result<Vec<VirtualDirEntry>> {
712 self.acp_read_dir_with_types(path).await
713 }
714
715 pub async fn readdir_recursive(
717 &self,
718 path: &str,
719 options: ReaddirRecursiveOptions,
720 ) -> Result<Vec<DirEntry>> {
721 Self::assert_safe_absolute_path(path)?;
722 let exclude: std::collections::HashSet<&str> =
723 options.exclude.iter().map(String::as_str).collect();
724 let entries = self
725 .kernel_readdir_recursive(path, options.max_depth)
726 .await?;
727 let mut excluded_prefixes: Vec<String> = Vec::new();
728 let mut results: Vec<DirEntry> = Vec::new();
729
730 for entry in entries {
731 if excluded_prefixes.iter().any(|prefix| {
732 entry.path == *prefix || entry.path.starts_with(&format!("{prefix}/"))
733 }) {
734 continue;
735 }
736 if exclude.contains(entry.name.as_str()) {
737 if entry.is_directory && !entry.is_symbolic_link {
738 excluded_prefixes.push(entry.path);
739 }
740 continue;
741 }
742
743 let entry_type = if entry.is_symbolic_link {
744 DirEntryType::Symlink
745 } else if entry.is_directory {
746 DirEntryType::Directory
747 } else {
748 DirEntryType::File
749 };
750 results.push(DirEntry {
751 path: entry.path,
752 entry_type,
753 size: entry.size,
754 });
755 }
756
757 Ok(results)
758 }
759
760 pub async fn stat(&self, path: &str) -> Result<VirtualStat> {
762 Self::assert_safe_absolute_path(path)?;
763 self.kernel_stat(path).await
764 }
765
766 pub async fn exists(&self, path: &str) -> Result<bool> {
768 Self::assert_safe_absolute_path(path)?;
769 self.kernel_exists(path).await
770 }
771
772 pub async fn snapshot_root_filesystem(&self) -> Result<RootSnapshotExport> {
774 let scope = self.fs_vm_scope();
775 let response = self
776 .transport()
777 .request_wire(scope, wire::RequestPayload::SnapshotRootFilesystemRequest)
778 .await
779 .context("snapshot root filesystem failed")?;
780 let snapshot = match response {
781 wire::ResponsePayload::RootFilesystemSnapshotResponse(snapshot) => snapshot,
782 wire::ResponsePayload::RejectedResponse(wire::RejectedResponse { code, message }) => {
783 return Err(ClientError::Kernel { code, message }.into());
784 }
785 other => {
786 return Err(anyhow::anyhow!(
787 "unexpected response to snapshot root filesystem: {other:?}"
788 ));
789 }
790 };
791
792 let entries = snapshot
793 .entries
794 .into_iter()
795 .map(Self::snapshot_entry_from)
796 .collect::<Result<Vec<_>>>()?;
797
798 Ok(RootSnapshotExport {
799 kind: SnapshotExportKind::SnapshotExport,
800 source: FilesystemSnapshotExport {
801 format: String::from("agentos-filesystem-snapshot-v1"),
802 filesystem: FilesystemSnapshotEntries { entries },
803 },
804 })
805 }
806
807 pub fn mount_fs(
812 &self,
813 path: &str,
814 driver: Arc<dyn VirtualFileSystem>,
815 options: MountFsOptions,
816 ) -> std::result::Result<(), ClientError> {
817 Self::assert_safe_absolute_path(path)?;
818 let _ = self.inner().in_process_mounts.insert(
819 path.to_string(),
820 MountedFs {
821 driver,
822 read_only: options.read_only,
823 },
824 );
825 Ok(())
826 }
827
828 pub fn unmount_fs(&self, path: &str) -> std::result::Result<(), ClientError> {
830 Self::assert_safe_absolute_path(path)?;
831 self.inner().in_process_mounts.remove(path);
832 Ok(())
833 }
834
835 pub async fn move_path(&self, from: &str, to: &str) -> Result<()> {
838 Self::assert_writable_absolute_path(from)?;
839 Self::assert_writable_absolute_path(to)?;
840 self.kernel_move_path(from, to).await
841 }
842
843 pub async fn delete(&self, path: &str, options: DeleteOptions) -> Result<()> {
846 Self::assert_writable_absolute_path(path)?;
847 self.kernel_remove_path(path, options.recursive).await
848 }
849
850 fn snapshot_entry_from(entry: RootFilesystemEntry) -> Result<FilesystemEntry> {
860 let entry_type = match entry.kind {
861 RootFilesystemEntryKind::File => DirEntryType::File,
862 RootFilesystemEntryKind::Directory => DirEntryType::Directory,
863 RootFilesystemEntryKind::Symlink => DirEntryType::Symlink,
864 };
865 let fallback_mode = match entry.kind {
868 RootFilesystemEntryKind::Directory => 0o755,
869 RootFilesystemEntryKind::Symlink => 0o777,
870 RootFilesystemEntryKind::File => 0o644,
871 };
872 let mode = format!("0{:o}", entry.mode.unwrap_or(fallback_mode) & 0o7777);
873 let uid = entry.uid.unwrap_or(0);
874 let gid = entry.gid.unwrap_or(0);
875
876 match entry.kind {
877 RootFilesystemEntryKind::File => {
878 let encoding = match entry.encoding {
879 Some(RootFilesystemEntryEncoding::Utf8) | None => FilesystemEntryEncoding::Utf8,
880 Some(RootFilesystemEntryEncoding::Base64) => FilesystemEntryEncoding::Base64,
881 };
882 Ok(FilesystemEntry {
883 path: entry.path,
884 entry_type,
885 mode,
886 uid,
887 gid,
888 content: Some(entry.content.unwrap_or_default()),
889 encoding: Some(encoding),
890 target: None,
891 })
892 }
893 RootFilesystemEntryKind::Symlink => {
894 let target = entry.target.with_context(|| {
895 format!(
896 "sidecar root snapshot for {} is missing a symlink target",
897 entry.path
898 )
899 })?;
900 Ok(FilesystemEntry {
901 path: entry.path,
902 entry_type,
903 mode,
904 uid,
905 gid,
906 content: None,
907 encoding: None,
908 target: Some(target),
909 })
910 }
911 RootFilesystemEntryKind::Directory => Ok(FilesystemEntry {
912 path: entry.path,
913 entry_type,
914 mode,
915 uid,
916 gid,
917 content: None,
918 encoding: None,
919 target: None,
920 }),
921 }
922 }
923}