Skip to main content

hexz_cli/cmd/data/
mount.rs

1//! Mount Hexz archives as FUSE filesystems.
2
3use anyhow::{Context, Result};
4use colored::Colorize;
5use daemonize::Daemonize;
6use hexz_common::constants::DEFAULT_ZSTD_LEVEL;
7use hexz_core::Archive;
8use hexz_core::algo::compression::{Compressor, lz4::Lz4Compressor, zstd::ZstdCompressor};
9use hexz_core::algo::encryption::aes_gcm::AesGcmEncryptor;
10use hexz_core::format::header::{CompressionType, Header};
11use hexz_core::format::magic::HEADER_SIZE;
12use hexz_fuse::fuse::Hexz;
13use hexz_store::StorageBackend;
14use hexz_store::local::MmapBackend;
15use std::path::{Path, PathBuf};
16use std::sync::Arc;
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(
73            DEFAULT_ZSTD_LEVEL,
74            dictionary.as_deref(),
75        )),
76    };
77
78    let encryptor = if let (Some(params), Some(pass)) = (header.encryption, password) {
79        Some(Box::new(AesGcmEncryptor::new(
80            pass.as_bytes(),
81            &params.salt,
82            params.iterations,
83        )?)
84            as Box<dyn hexz_core::algo::encryption::Encryptor>)
85    } else {
86        None
87    };
88
89    let cache_capacity = if let Some(s) = cache_size {
90        Some(parse_size(s)?)
91    } else {
92        None
93    };
94
95    let cache_size_owned: Option<String> = cache_size.map(String::from);
96    let abs_hexz_path_clone = abs_hexz_path;
97
98    let parent_loader: hexz_core::api::file::ParentLoader = Box::new(move |parent_path: &str| {
99        let parent_full_path = abs_hexz_path_clone
100            .parent()
101            .ok_or_else(|| {
102                hexz_common::Error::Io(std::io::Error::other(
103                    "archive path has no parent directory",
104                ))
105            })?
106            .join(parent_path);
107        let path_str = parent_full_path.to_str().ok_or_else(|| {
108            hexz_common::Error::Io(std::io::Error::other("parent path is not valid UTF-8"))
109        })?;
110        open_archive(path_str, cache_size_owned.as_deref(), prefetch)
111            .map_err(|e| hexz_common::Error::Io(std::io::Error::other(e.to_string())))
112    });
113
114    Ok(Archive::with_cache_and_loader(
115        backend,
116        compressor,
117        encryptor,
118        cache_capacity,
119        prefetch,
120        Some(&parent_loader),
121    )?)
122}
123
124/// Execute the `hexz mount` command to mount an archive as a FUSE filesystem.
125#[allow(clippy::too_many_arguments, unsafe_code)]
126pub fn run(
127    hexz_path: &str,
128    mountpoint: &Path,
129    daemon: bool,
130    cache_size: Option<&str>,
131    mut uid: u32,
132    mut gid: u32,
133    overlay: Option<PathBuf>,
134    editable: bool,
135    metadata_dir: Option<&Path>,
136) -> Result<()> {
137    if uid == 0 {
138        // SAFETY: getuid() is always safe to call
139        uid = unsafe { libc::getuid() };
140    }
141    if gid == 0 {
142        // SAFETY: getgid() is always safe to call
143        gid = unsafe { libc::getgid() };
144    }
145
146    let abs_mountpoint = if mountpoint.exists() {
147        std::fs::canonicalize(mountpoint).context(format!(
148            "Failed to resolve mountpoint: {}",
149            mountpoint.display()
150        ))?
151    } else {
152        mountpoint.to_path_buf()
153    };
154
155    let snap = open_archive(hexz_path, cache_size, None)?;
156
157    // Handle --editable / --overlay
158    let overlay = if let Some(o) = overlay {
159        std::fs::create_dir_all(&o)?;
160        Some(o)
161    } else if editable {
162        let temp_overlay = std::env::temp_dir().join(format!(
163            "hexz_overlay_{}",
164            std::time::SystemTime::now()
165                .duration_since(std::time::UNIX_EPOCH)
166                .unwrap_or_default()
167                .as_secs()
168        ));
169        std::fs::create_dir_all(&temp_overlay)?;
170        if !daemon {
171            println!(
172                "  {} Editable mode enabled. Overlay: {}",
173                "→".yellow(),
174                temp_overlay.display().to_string().bright_black()
175            );
176        }
177        Some(temp_overlay)
178    } else {
179        None
180    };
181
182    if daemon {
183        let log_dir = std::env::var("XDG_RUNTIME_DIR")
184            .or_else(|_| std::env::var("TMPDIR"))
185            .unwrap_or_else(|_| "/tmp".to_string());
186        let stdout = std::fs::File::create(format!("{log_dir}/hexz.log"))
187            .or_else(|_| std::fs::File::create("/dev/null"))
188            .context("Failed to create log file")?;
189        let stderr = std::fs::File::create(format!("{log_dir}/hexz.err"))
190            .or_else(|_| std::fs::File::create("/dev/null"))
191            .context("Failed to create error log file")?;
192
193        Daemonize::new()
194            .working_directory("/")
195            .stdout(stdout)
196            .stderr(stderr)
197            .start()?;
198    }
199
200    let mut options = vec![
201        fuser::MountOption::FSName("hexz".to_string()),
202        fuser::MountOption::DefaultPermissions,
203    ];
204
205    if overlay.is_none() {
206        options.push(fuser::MountOption::RO);
207    }
208
209    let fs = Hexz::new(snap, uid, gid, overlay, metadata_dir)?;
210
211    if daemon {
212        eprintln!(
213            "  {} Mounting at {} (daemonized)",
214            "✓".green(),
215            abs_mountpoint.display().to_string().cyan()
216        );
217    }
218
219    fuser::mount2(fs, abs_mountpoint, &options)?;
220
221    Ok(())
222}