Skip to main content

hexz_cli/cmd/data/
mount.rs

1//! Mount Hexz archives as FUSE filesystems.
2
3use anyhow::{Context, Result};
4use daemonize::Daemonize;
5use hexz_common::constants::DEFAULT_ZSTD_LEVEL;
6use hexz_core::Archive;
7use hexz_core::algo::compression::{Compressor, lz4::Lz4Compressor, zstd::ZstdCompressor};
8use hexz_core::algo::encryption::aes_gcm::AesGcmEncryptor;
9use hexz_core::format::header::{CompressionType, Header};
10use hexz_core::format::magic::HEADER_SIZE;
11use hexz_fuse::fuse::Hexz;
12use hexz_store::StorageBackend;
13use hexz_store::local::MmapBackend;
14use std::path::{Path, PathBuf};
15use std::sync::Arc;
16use colored::Colorize;
17
18pub(crate) fn parse_size(s: &str) -> Result<usize> {
19    let s = s.trim();
20    let (num, suffix) = if let Some(idx) = s.find(|c: char| !c.is_numeric() && c != '.') {
21        (&s[..idx], &s[idx..])
22    } else {
23        (s, "")
24    };
25
26    let n: f64 = num.parse()?;
27    let multiplier = match suffix.to_lowercase().as_str() {
28        "k" | "kb" => 1024.0,
29        "m" | "mb" => 1024.0 * 1024.0,
30        "g" | "gb" => 1024.0 * 1024.0 * 1024.0,
31        "t" | "tb" => 1024.0 * 1024.0 * 1024.0 * 1024.0,
32        "" => 1.0,
33        _ => return Err(anyhow::anyhow!("Invalid size suffix: {suffix}")),
34    };
35
36    Ok((n * multiplier) as usize)
37}
38
39pub(crate) fn open_archive(
40    hexz_path: &str,
41    cache_size: Option<&str>,
42    prefetch: Option<u32>,
43) -> Result<Arc<Archive>> {
44    let abs_hexz_path = std::fs::canonicalize(hexz_path)
45        .context(format!("Failed to resolve archive path: {hexz_path}"))?;
46
47    let (header, password) = {
48        let backend = MmapBackend::new(&abs_hexz_path)?;
49        let header_bytes = backend.read_exact(0, HEADER_SIZE)?;
50        let header: Header = bincode::deserialize(&header_bytes)?;
51
52        let password = if header.encryption.is_some() {
53            Some(rpassword::prompt_password("Enter encryption password: ")?)
54        } else {
55            None
56        };
57        (header, password)
58    };
59
60    let backend = Arc::new(MmapBackend::new(&abs_hexz_path)?);
61
62    let dictionary = if let (Some(offset), Some(length)) =
63        (header.dictionary_offset, header.dictionary_length)
64    {
65        Some(backend.read_exact(offset, length as usize)?.to_vec())
66    } else {
67        None
68    };
69
70    let compressor: Box<dyn Compressor> = match header.compression {
71        CompressionType::Lz4 => Box::new(Lz4Compressor::new()),
72        CompressionType::Zstd => Box::new(ZstdCompressor::new(DEFAULT_ZSTD_LEVEL, dictionary.as_deref())),
73    };
74
75    let encryptor = if let (Some(params), Some(pass)) = (header.encryption, password) {
76        Some(Box::new(AesGcmEncryptor::new(
77            pass.as_bytes(),
78            &params.salt,
79            params.iterations,
80        )?)
81            as Box<dyn hexz_core::algo::encryption::Encryptor>)
82    } else {
83        None
84    };
85
86    let cache_capacity = if let Some(s) = cache_size {
87        Some(parse_size(s)?)
88    } else {
89        None
90    };
91
92    let cache_size_owned: Option<String> = cache_size.map(String::from);
93    let abs_hexz_path_clone = abs_hexz_path;
94
95    let parent_loader: hexz_core::api::file::ParentLoader = Box::new(move |parent_path: &str| {
96        let parent_full_path = abs_hexz_path_clone
97            .parent()
98            .ok_or_else(|| hexz_common::Error::Io(std::io::Error::other("archive path has no parent directory")))?
99            .join(parent_path);
100        let path_str = parent_full_path
101            .to_str()
102            .ok_or_else(|| hexz_common::Error::Io(std::io::Error::other("parent path is not valid UTF-8")))?;
103        open_archive(path_str, cache_size_owned.as_deref(), prefetch)
104            .map_err(|e| hexz_common::Error::Io(std::io::Error::other(e.to_string())))
105    });
106
107    Ok(Archive::with_cache_and_loader(
108        backend,
109        compressor,
110        encryptor,
111        cache_capacity,
112        prefetch,
113        Some(&parent_loader),
114    )?)
115}
116
117/// Execute the `hexz mount` command to mount an archive as a FUSE filesystem.
118#[allow(clippy::too_many_arguments, unsafe_code)]
119pub fn run(
120    hexz_path: &str,
121    mountpoint: &Path,
122    daemon: bool,
123    cache_size: Option<&str>,
124    mut uid: u32,
125    mut gid: u32,
126    overlay: Option<PathBuf>,
127    editable: bool,
128    metadata_dir: Option<&Path>,
129) -> Result<()> {
130    if uid == 0 {
131        // SAFETY: getuid() is always safe to call
132        uid = unsafe { libc::getuid() };
133    }
134    if gid == 0 {
135        // SAFETY: getgid() is always safe to call
136        gid = unsafe { libc::getgid() };
137    }
138
139    let abs_mountpoint = if mountpoint.exists() {
140        std::fs::canonicalize(mountpoint)
141            .context(format!("Failed to resolve mountpoint: {}", mountpoint.display()))?
142    } else {
143        mountpoint.to_path_buf()
144    };
145
146    let snap = open_archive(hexz_path, cache_size, None)?;
147
148    // Handle --editable / --overlay
149    let overlay = if let Some(o) = overlay {
150        std::fs::create_dir_all(&o)?;
151        Some(o)
152    } else if editable {
153        let temp_overlay = std::env::temp_dir().join(format!(
154            "hexz_overlay_{}",
155            std::time::SystemTime::now()
156                .duration_since(std::time::UNIX_EPOCH)
157                .unwrap_or_default()
158                .as_secs()
159        ));
160        std::fs::create_dir_all(&temp_overlay)?;
161        if !daemon {
162            println!("  {} Editable mode enabled. Overlay: {}", "→".yellow(), temp_overlay.display().to_string().bright_black());
163        }
164        Some(temp_overlay)
165    } else {
166        None
167    };
168
169    if daemon {
170        let log_dir = std::env::var("XDG_RUNTIME_DIR")
171            .or_else(|_| std::env::var("TMPDIR"))
172            .unwrap_or_else(|_| "/tmp".to_string());
173        let stdout = std::fs::File::create(format!("{log_dir}/hexz.log"))
174            .or_else(|_| std::fs::File::create("/dev/null"))
175            .context("Failed to create log file")?;
176        let stderr = std::fs::File::create(format!("{log_dir}/hexz.err"))
177            .or_else(|_| std::fs::File::create("/dev/null"))
178            .context("Failed to create error log file")?;
179
180        Daemonize::new()
181            .working_directory("/")
182            .stdout(stdout)
183            .stderr(stderr)
184            .start()?;
185    }
186
187    let mut options = vec![
188        fuser::MountOption::FSName("hexz".to_string()),
189        fuser::MountOption::DefaultPermissions,
190    ];
191
192    if overlay.is_none() {
193        options.push(fuser::MountOption::RO);
194    }
195
196    let fs = Hexz::new(snap, uid, gid, overlay, metadata_dir)?;
197
198    if daemon {
199        eprintln!("  {} Mounting at {} (daemonized)", "✓".green(), abs_mountpoint.display().to_string().cyan());
200    }
201
202    fuser::mount2(fs, abs_mountpoint, &options)?;
203
204    Ok(())
205}