hexz_cli/cmd/data/
mount.rs1use 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 ¶ms.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#[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 uid = unsafe { libc::getuid() };
140 }
141 if gid == 0 {
142 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 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}