Skip to main content

mur_common/muragent/
reader.rs

1//! `.muragent` reader — extract and inspect a signed agent package.
2
3use crate::muragent::MuragentError;
4use flate2::read::GzDecoder;
5use std::collections::BTreeMap;
6use std::io::Read;
7use std::path::Path;
8use tar::Archive;
9
10pub struct MuragentArchive {
11    /// All files in the tarball keyed by path → raw bytes.
12    pub files: BTreeMap<String, Vec<u8>>,
13}
14
15impl MuragentArchive {
16    /// Read and extract all files from a `.muragent` tar.gz.
17    pub fn read(path: &Path) -> Result<Self, MuragentError> {
18        let file = std::fs::File::open(path).map_err(MuragentError::Io)?;
19        let gz = GzDecoder::new(file);
20        let mut archive = Archive::new(gz);
21        let mut files = BTreeMap::new();
22
23        for entry in archive
24            .entries()
25            .map_err(|e| MuragentError::Other(format!("tar entries: {e}")))?
26        {
27            let mut entry = entry.map_err(|e| MuragentError::Other(format!("tar entry: {e}")))?;
28
29            let entry_path = entry
30                .path()
31                .map_err(|e| MuragentError::Other(format!("entry path: {e}")))?
32                .to_str()
33                .ok_or_else(|| MuragentError::Other("non-UTF-8 path in tarball".into()))?
34                .to_string();
35
36            let entry_type = entry.header().entry_type();
37            if entry_type == tar::EntryType::Symlink || entry_type == tar::EntryType::Link {
38                return Err(MuragentError::ExecutableContent(format!(
39                    "symlinks not allowed in .muragent: {entry_path}"
40                )));
41            }
42
43            if entry_type != tar::EntryType::Regular
44                && entry_type != tar::EntryType::Directory
45                && entry_type != tar::EntryType::GNULongName
46                && entry_type != tar::EntryType::GNULongLink
47            {
48                return Err(MuragentError::ExecutableContent(format!(
49                    "tar entry type {:?} not allowed: {entry_path}",
50                    entry_type
51                )));
52            }
53
54            // Skip directories — we don't need them in the map
55            if entry_type == tar::EntryType::Directory {
56                continue;
57            }
58
59            crate::muragent::jcs_canonical::validate_tarball_path(&entry_path)
60                .map_err(|e| MuragentError::Other(e.to_string()))?;
61
62            // Check mode bits — regular files must not be executable
63            let mode = entry.header().mode().unwrap_or(0o644);
64            crate::muragent::executable_ban::check_mode_bits(mode, false)
65                .map_err(MuragentError::ExecutableContent)?;
66
67            let mut data = Vec::new();
68            entry.read_to_end(&mut data).map_err(MuragentError::Io)?;
69
70            files.insert(entry_path, data);
71        }
72
73        Ok(Self { files })
74    }
75
76    pub fn get(&self, path: &str) -> Option<&[u8]> {
77        self.files.get(path).map(|v| v.as_slice())
78    }
79
80    pub fn get_str(&self, path: &str) -> Result<&str, MuragentError> {
81        let bytes = self
82            .get(path)
83            .ok_or_else(|| MuragentError::Other(format!("file not found: {path}")))?;
84        std::str::from_utf8(bytes)
85            .map_err(|e| MuragentError::Other(format!("{path} is not valid UTF-8: {e}")))
86    }
87
88    pub fn files_as_vec(&self) -> Vec<(String, Vec<u8>)> {
89        self.files
90            .iter()
91            .map(|(k, v)| (k.clone(), v.clone()))
92            .collect()
93    }
94}