diff --git a/.gitignore b/.gitignore index d793ad2..35f2e6e 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ bin/test.rs # Local configuration files *.local.conf *.env + + +TODO.md \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index d584e89..13ef50b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -163,6 +173,41 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + [[package]] name = "clap" version = "4.6.1" @@ -275,6 +320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core", "typenum", ] @@ -370,6 +416,27 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + [[package]] name = "document-features" version = "0.2.12" @@ -471,6 +538,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -536,10 +614,12 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hypr-mount" -version = "0.1.1" +version = "0.1.5" dependencies = [ + "chacha20poly1305", "clap", "crossterm", + "dirs", "ratatui", "serde", "serde_json", @@ -579,6 +659,15 @@ dependencies = [ "rustversion", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "instability" version = "0.3.10" @@ -658,6 +747,15 @@ version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "libc", +] + [[package]] name = "line-clipping" version = "0.3.5" @@ -822,6 +920,18 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "ordered-float" version = "4.6.0" @@ -949,6 +1059,17 @@ dependencies = [ "siphasher", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -1015,6 +1136,9 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] [[package]] name = "ratatui" @@ -1110,6 +1234,17 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + [[package]] name = "regex" version = "1.12.3" @@ -1314,6 +1449,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.109" @@ -1507,6 +1648,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "utf8parse" version = "0.2.2" @@ -1846,6 +1997,12 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 53f025a..ab25a6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hypr-mount" -version = "0.1.1" +version = "0.1.5" edition = "2024" [dependencies] @@ -10,6 +10,8 @@ serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" thiserror = "2.0.18" clap = { version = "4.6.1", features = ["derive"] } +dirs = "6.0.0" +chacha20poly1305 = "0.10" [[bin]] name = "hypr-mount" diff --git a/src/app/events.rs b/src/app/events.rs index c3c003f..dd4064c 100644 --- a/src/app/events.rs +++ b/src/app/events.rs @@ -1,4 +1,7 @@ -use crate::app::{AppMode, MountApp, SelectedRow}; +use crate::{ + app::{AppMode, MountApp, SelectedRow}, + core::drive_handle, +}; use crossterm::event::{KeyCode, KeyEventKind}; use ratatui::DefaultTerminal; use std::io; @@ -15,6 +18,7 @@ impl MountApp { AppMode::ScriptPreview => self.script_view_key_event(key_event)?, AppMode::ScriptPopup => self.script_popup_key_event(key_event)?, AppMode::MessagePopup => self.message_popup_key_event(key_event)?, + AppMode::PasswordInput => self.password_input_key_event(key_event)?, } } } @@ -58,6 +62,27 @@ impl MountApp { KeyCode::Char('g') => { self.mode = AppMode::ScriptPreview; } + KeyCode::Char('r') => match drive_handle::list_drives() { + Ok(new_drives) => { + // Preserve selection state: keep selected drive names, remap to new indices + let selected_names: Vec = self + .selected_rows + .iter() + .filter_map(|idx| self.drives.get(*idx).map(|d| d.name.clone())) + .collect(); + + self.drives = new_drives; + + // Remap selections by matching drive names + self.selected_rows.clear(); + for (new_idx, drive) in self.drives.iter().enumerate() { + if selected_names.contains(&drive.name) { + self.selected_rows.insert(new_idx); + } + } + } + Err(err) => self.status_message = err.to_string(), + }, _ => {} } @@ -165,4 +190,51 @@ impl MountApp { } Ok(()) } + + fn password_input_key_event( + &mut self, + key_event: crossterm::event::KeyEvent, + ) -> io::Result<()> { + if key_event.kind != KeyEventKind::Press { + return Ok(()); + } + + match key_event.code { + KeyCode::Esc => { + self.password_input.clear(); + self.password_error.clear(); + self.luks_uuid = None; + self.mode = AppMode::MainTable; + } + KeyCode::Enter => { + if let Some(uuid) = self.luks_uuid.clone() { + if self.password_input.is_empty() { + self.password_error = "Password cannot be empty".to_string(); + } else { + // TODO: implement crypto::unlock_and_mount(uuid, password) + self.status_message = format!( + "LUKS unlock attempted for UUID: {} (password: {} chars)", + uuid, + self.password_input.len() + ); + self.password_input.clear(); + self.password_error.clear(); + self.luks_uuid = None; + self.mode = AppMode::MainTable; + } + } else { + self.password_error = "No drive selected".to_string(); + } + } + KeyCode::Backspace => { + self.password_input.pop(); + } + KeyCode::Char(c) => { + self.password_input.push(c); + } + _ => {} + } + + Ok(()) + } } diff --git a/src/app/mod.rs b/src/app/mod.rs index 472d04a..f43d205 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -24,6 +24,11 @@ pub struct MountApp { pub script_view: ScriptView, pub mode: AppMode, pub args: CliArgs, + // Password input state for LUKS unlock + pub password_input: String, + pub password_error: String, + pub password_scroll: u16, + pub luks_uuid: Option, } impl MountApp { @@ -43,6 +48,11 @@ impl MountApp { }, mode: AppMode::MainTable, args, + // Password input state for LUKS unlock + password_input: String::new(), + password_error: String::new(), + password_scroll: 0, + luks_uuid: None, } } } @@ -54,6 +64,7 @@ pub enum AppMode { ScriptPreview, ScriptPopup, MessagePopup, + PasswordInput, // LUKS password prompt } pub struct SelectedRow { @@ -73,7 +84,7 @@ struct PopupPar<'a> { #[derive(Parser, Debug)] #[command(name = "Hypr-Mount")] -#[command(version = "b1.0.1")] +#[command(version = "b1.0.5")] #[command(about = "A TUI drive mounter", long_about = None)] pub struct CliArgs { #[arg(long, group = "mode")] @@ -119,6 +130,14 @@ impl MountApp { && let Some(uuid) = &drive.uuid { if !drive.is_mounted { + if drive.is_luks { + // TODO: implement crypto::unlock_and_mount(uuid, password) + self.luks_uuid = Some(uuid.to_string()); + self.password_input.clear(); + self.password_error.clear(); + self.mode = AppMode::PasswordInput; + return; + } match drive_handle::mount_drive(uuid) { Ok(()) => { drive.is_mounted = true; @@ -142,54 +161,64 @@ impl MountApp { } fn mount_unmount_selected_drives(&mut self) { let mut success_count = 0; - let mut error_count = 0; - let mut error_msg = String::new(); + let mut skip_count = 0; + let mut errors: Vec = Vec::new(); for (idx, drive) in self.drives.iter_mut().enumerate() { if !self.selected_rows.contains(&idx) { - continue; // if drives not selected just skip + continue; } - if let Some(ref uuid) = drive.uuid { - if drive.is_mounted { - match drive_handle::unmount_drive(uuid) { - Ok(()) => { - drive.is_mounted = !drive.is_mounted; - self.selected_rows.remove(&idx); - success_count += 1; - } - Err(err) => { - error_msg = err.to_string(); - error_count += 1; + match drive.uuid.as_deref() { + Some(uuid) => { + if drive.is_mounted { + match drive_handle::unmount_drive(uuid) { + Ok(()) => { + drive.is_mounted = false; + self.selected_rows.remove(&idx); + success_count += 1; + } + Err(err) => { + errors.push(format!("{}: {}", drive.name, err)); + } } + } else if drive.is_luks { + // TODO: implement crypto::unlock_and_mount(uuid, password) + self.luks_uuid = Some(uuid.to_string()); + self.password_input.clear(); + self.password_error.clear(); + self.mode = AppMode::PasswordInput; + return; } - } else { match drive_handle::mount_drive(uuid) { Ok(()) => { - drive.is_mounted = !drive.is_mounted; + drive.is_mounted = true; self.selected_rows.remove(&idx); success_count += 1; } Err(err) => { - error_msg = err.to_string(); - error_count += 1; + errors.push(format!("{}: {}", drive.name, err)); } } } - } else { - error_msg = String::from("Drive has no UUID"); - error_count += 1; + None => { + errors.push(format!("{}: skipped (no UUID)", drive.name)); + skip_count += 1; + } } } - if success_count > 0 && error_count == 0 { + // success_count, skip_count, and errors collected above + if success_count > 0 && errors.is_empty() { self.status_message = format!("Successfully processed {} drive(s)", success_count); - } else if error_count > 0 { + } else if !errors.is_empty() { + let error_list = errors.join(", "); self.status_message = format!( - "Error processing {} of {} drive(s): {}", - error_count, - success_count + error_count, - error_msg + "Processed {} succeeded, {} skipped, {} failed: {}", + success_count, + skip_count, + errors.len(), + error_list ); } else if success_count == 0 { self.status_message = String::from("No drives selected for mounting/unmounting"); diff --git a/src/app/ui.rs b/src/app/ui.rs index 96d0fd3..752bf65 100644 --- a/src/app/ui.rs +++ b/src/app/ui.rs @@ -49,8 +49,12 @@ impl MountApp { } else { Style::default() }), - Cell::from(item.device_path.as_str()), - Cell::from(item.fstype.as_str()), + Cell::from(item.mount_point.as_str()), + Cell::from(if item.is_luks { + "[LUKS]".to_string() + } else { + item.fstype.clone() + }), Cell::from(item.size.as_str()), Cell::from(status_text), Cell::from(uuid_res), @@ -131,6 +135,11 @@ impl MountApp { frame.render_widget(Clear, popup.area); frame.render_widget(popup.pop_block, popup.area); } + AppMode::PasswordInput => { + let popup = self.password_popup(area); + frame.render_widget(Clear, popup.area); + frame.render_widget(popup.pop_block, popup.area); + } _ => {} } } @@ -149,15 +158,75 @@ impl MountApp { } } + fn password_popup(&mut self, area: Rect) -> PopupPar<'_> { + let popup_area = app::popup_area(area, 50, 25); + + let content_width = 30; + let pad_size = (popup_area.width as usize).saturating_sub(content_width) / 2; + let indent = " ".repeat(pad_size); + + let masked = "*".repeat(self.password_input.chars().count()); + + let display = if masked.len() > content_width { + let start = masked.len() - content_width; + masked.chars().skip(start).collect::() + } else { + masked + }; + + let mut lines: Vec = vec![ + Line::from(vec![ + Span::raw(indent.clone()), + Span::raw("Enter password for LUKS drive:"), + ]), + Line::from(""), + ]; + + let pw_style = Style::new().fg(Color::Yellow); + lines.push(Line::from(vec![ + Span::raw(indent.clone()), + Span::styled(display, pw_style), + ])); + + if !self.password_error.is_empty() { + lines.push(Line::from("")); + lines.push(Line::from(vec![ + Span::raw(indent.clone()), + Span::styled( + format!("Error: {}", self.password_error), + Style::new().fg(Color::Red), + ), + ])); + } + + lines.push(Line::from("")); + lines.push(Line::from(vec![ + Span::raw(indent), + Span::raw("[Enter] Submit [Esc] Cancel"), + ])); + + let block = Block::bordered() + .title(Line::from(" LUKS Password ").bold()) + .border_style(Style::new().fg(Color::Cyan)); + let text = Paragraph::new(lines) + .style(Style::new().fg(Color::Yellow)) + .block(block); + + PopupPar { + pop_block: text, + area: popup_area, + } + } + fn create_table_widget<'a>( &self, rows: &'a [ratatui::widgets::Row<'a>], ) -> ratatui::widgets::Table<'a> { let widths = [ Constraint::Length(5), // " Sel " - Constraint::Length(14), // "/dev/nvme0n1p1" + Constraint::Length(16), // "/dev/nvme0n1p1" Constraint::Fill(1), // Mount Point - Constraint::Length(10), // "fstype" + Constraint::Length(12), // "fstype" Constraint::Length(10), // "100.5G" Constraint::Length(12), // "UNMOUNTED" Constraint::Fill(1), // UUID @@ -229,7 +298,7 @@ impl MountApp { let drive_string = format!( "Drive: {}\nMounted as: {}\nType: {}\nSize: {}\nUUID: {}", drive.name, - drive.device_path, + drive.mount_point, drive.fstype, drive.size, drive.uuid.as_ref()?, diff --git a/src/core/drive_handle.rs b/src/core/drive_handle.rs index cd69e4e..41db70e 100644 --- a/src/core/drive_handle.rs +++ b/src/core/drive_handle.rs @@ -9,31 +9,45 @@ struct LsblkData { } #[derive(Serialize, Deserialize, Debug)] -struct Drives { - name: String, - size: String, - uuid: Option, - children: Option>, +pub(crate) struct Drives { + pub(crate) name: String, + pub(crate) size: String, + pub(crate) uuid: Option, + pub(crate) children: Option>, } #[derive(Serialize, Deserialize, Debug)] -struct Partition { - name: String, - size: String, - uuid: Option, - mountpoints: Vec, - fstype: Option, +pub(crate) struct Partition { + pub(crate) name: String, + pub(crate) size: String, + pub(crate) uuid: Option, + pub(crate) mountpoints: Vec, + pub(crate) fstype: Option, } impl Partition { - fn is_system_drive(&self) -> bool { - self.fstype - .as_deref() - .unwrap_or("None") - .to_lowercase() - .contains("swap") - || self.get_mountpoint().to_lowercase().contains("boot") + pub(crate) fn is_system_drive(&self) -> bool { + let fstype = self.fstype.as_deref().unwrap_or(""); + if fstype == "crypto_LUKS" { + return false; + } + fstype.to_lowercase().contains("swap") + || fstype.is_empty() + || fstype == "squashfs" || self.name.to_lowercase().contains("loop") + || self.name.to_lowercase().contains("dm-") + || self.mountpoints.iter().any(|mp| { + mp == "/proc" + || mp.starts_with("/sys/") + || (mp == "/run" || mp.starts_with("/run/")) && !mp.starts_with("/run/media") + || mp.starts_with("/boot") + || (mp.to_lowercase().contains("efi") && fstype == "vfat") + || mp.contains("cgroup") + }) + } + + pub(crate) fn is_luks(&self) -> bool { + self.fstype.as_deref() == Some("crypto_LUKS") } fn get_mountpoint(&self) -> String { self.mountpoints @@ -61,13 +75,15 @@ pub fn list_drives() -> Result, HyprMountError> { if part.is_system_drive() { continue; } + let part_is_luks = part.is_luks(); drives_list.push(DriveItem { name: format!("/dev/{}", part.name), - device_path: part.get_mountpoint(), + mount_point: part.get_mountpoint(), size: part.size, uuid: part.uuid, is_mounted: !part.mountpoints.is_empty(), - fstype: part.fstype.unwrap_or("None".to_string()), + fstype: part.fstype.clone().unwrap_or_else(|| "None".to_string()), + is_luks: part_is_luks, }); } } @@ -103,10 +119,8 @@ fn run_udisk_command(action: &str, uuid: &str) -> Result<(), HyprMountError> { } pub fn clean_udisk_error(stderr: &str) -> String { if stderr.contains("GDBus.Error") { - let parts: Vec<&str> = stderr.split(": ").collect(); - - if let Some(part) = parts.last() { - return part.trim().to_string(); + if let Some((_, last)) = stderr.rsplit_once(": ") { + return last.trim().to_string(); } } stderr.trim().to_string() diff --git a/src/core/error.rs b/src/core/error.rs index a349919..b046912 100644 --- a/src/core/error.rs +++ b/src/core/error.rs @@ -1,7 +1,7 @@ +use std::io; use std::path::PathBuf; use std::str::Utf8Error; use std::string::FromUtf8Error; -use std::{env, io}; use thiserror::Error; #[derive(Error, Debug)] @@ -30,8 +30,8 @@ pub enum HyprMountError { #[error("UDisksCtl error: {err_msg}")] UDiskCtlError { err_msg: String }, - #[error("Could not find home directory: {0}")] - HomePath(#[from] env::VarError), + #[error("Could not determine home directory")] + HomeDir, #[error("Failed to convert executable path to string (probably invalid UTF-8)")] ExePath(), diff --git a/src/core/mod.rs b/src/core/mod.rs index 0593789..2783337 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -7,15 +7,16 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone)] pub struct DriveItem { pub name: String, - pub device_path: String, + pub mount_point: String, pub size: String, pub uuid: Option, pub is_mounted: bool, pub fstype: String, + pub is_luks: bool, } #[derive(Serialize, Deserialize, Debug)] pub struct DriveConfig { pub(crate) name: String, - pub(crate) device_path: String, + pub(crate) mount_point: String, pub(crate) uuid: String, } diff --git a/src/core/mount.rs b/src/core/mount.rs index df8bfcb..9ebaa58 100644 --- a/src/core/mount.rs +++ b/src/core/mount.rs @@ -9,7 +9,7 @@ use std::path::PathBuf; impl From<&DriveItem> for DriveConfig { fn from(drive: &DriveItem) -> Self { DriveConfig { - device_path: drive.device_path.to_owned(), + mount_point: drive.mount_point.to_owned(), name: drive.name.to_owned(), uuid: drive.uuid.as_deref().unwrap_or("").to_string(), } @@ -17,7 +17,7 @@ impl From<&DriveItem> for DriveConfig { } pub fn get_config_path() -> Result { - let home_dir = std::env::var("HOME")?; + let home_dir = dirs::home_dir().ok_or(HyprMountError::HomeDir)?; let conf_path = PathBuf::from(home_dir) .join(".config") .join("hypr-mount") @@ -99,12 +99,11 @@ pub fn driveconf_script_gen(conf: Vec) -> Result<(), HyprMountError } pub fn automount_drives_service() -> Result<(), HyprMountError> { - let home = std::env::var("HOME")?; + let home = dirs::home_dir().ok_or(HyprMountError::HomeDir)?; let config_name = "hypr-mount.service"; let exec_path = std::env::current_exe()?; - let exec_path_str = exec_path.to_str() - .ok_or(HyprMountError::ExePath())?; + let exec_path_str = exec_path.to_str().ok_or(HyprMountError::ExePath())?; let dir = PathBuf::from(home) .join(".config") @@ -122,8 +121,7 @@ pub fn automount_drives_service() -> Result<(), HyprMountError> { \n\ [Install]\n\ WantedBy=default.target", - "hypr-mount service", - exec_path_str + "hypr-mount service", exec_path_str ); fs::create_dir_all(&dir)?; diff --git a/src/unit-test.rs b/src/unit-test.rs index 670afa7..550bdf9 100644 --- a/src/unit-test.rs +++ b/src/unit-test.rs @@ -9,29 +9,32 @@ mod tests { fn test_to_automount_conf() { let drive1 = DriveItem { name: "/dev/sda1".to_string(), - device_path: "/dev/sda1".to_string(), + mount_point: "/run/media/user/MyBook".to_string(), size: "500 GiB".to_string(), uuid: Some("AABBCCDD-1122-3344-5566-778899AABBCC".to_string()), is_mounted: true, fstype: "ext4".to_string(), + is_luks: false, }; let drive2 = DriveItem { name: "/dev/sdb1".to_string(), - device_path: "/dev/sdb1".to_string(), + mount_point: "/run/media/user/Windows".to_string(), size: "2 TiB".to_string(), uuid: Some("FEEFF00D-0000-0000-0000-000000000000".to_string()), is_mounted: false, fstype: "ntfs".to_string(), + is_luks: false, }; let drive3 = DriveItem { name: "/dev/sdc".to_string(), - device_path: "/dev/sdc".to_string(), + mount_point: String::new(), size: "16 GiB".to_string(), uuid: None, is_mounted: false, fstype: "None".to_string(), + is_luks: false, }; let drives = vec![drive1, drive2, drive3]; @@ -43,10 +46,10 @@ mod tests { assert_eq!(result.len(), 2); assert_eq!(result[0].name, "/dev/sda1"); - assert_eq!(result[0].device_path, "/dev/sda1"); + assert_eq!(result[0].mount_point, "/run/media/user/MyBook"); assert_eq!(result[0].uuid, "AABBCCDD-1122-3344-5566-778899AABBCC"); assert_eq!(result[1].name, "/dev/sdc"); - assert_eq!(result[1].device_path, "/dev/sdc"); + assert_eq!(result[1].mount_point, ""); assert_eq!(result[1].uuid, ""); } @@ -58,25 +61,60 @@ mod tests { assert!(cleaned.contains("NotAuthorizedCanObtain")); } + #[test] + fn test_clean_udisks_error_multi_colon() { + // Test the fix: messages with multiple ": " should only split on the last one + let error_msg = "GDBus.Error(org.freedesktop.UDisks2.error): Failed to mount: device busy"; + let cleaned = clean_udisk_error(error_msg); + + assert_eq!(cleaned, "device busy"); + } + + #[test] + fn test_clean_udisks_error_no_gdbus() { + // Non-GDBus errors should pass through unchanged + let error_msg = "Some random error message"; + let cleaned = clean_udisk_error(error_msg); + + assert_eq!(cleaned, "Some random error message"); + } + + #[test] + fn test_drive_item_mount_point_field() { + let drive = DriveItem { + name: "/dev/sda1".to_string(), + mount_point: "/mnt/data".to_string(), + size: "1TB".to_string(), + uuid: Some("abc-123".to_string()), + is_mounted: true, + fstype: "ext4".to_string(), + is_luks: false, + }; + + assert_eq!(drive.mount_point, "/mnt/data"); + assert_eq!(drive.name, "/dev/sda1"); + } + #[test] fn test_drive_config_creation() { let drive_item = DriveItem { name: "/dev/sda1".to_string(), - device_path: "/mnt/data".to_string(), + mount_point: "/run/media/user/MyBook".to_string(), size: "1 TB".to_string(), uuid: Some("12345678-1234-1234-1234-123456789abc".to_string()), is_mounted: false, fstype: "ext4".to_string(), + is_luks: false, }; let config = DriveConfig { name: drive_item.name.clone(), - device_path: drive_item.device_path.clone(), + mount_point: drive_item.mount_point.clone(), uuid: drive_item.uuid.clone().unwrap_or_default(), }; assert_eq!(config.name, "/dev/sda1"); - assert_eq!(config.device_path, "/mnt/data"); + assert_eq!(config.mount_point, "/run/media/user/MyBook"); assert_eq!(config.uuid, "12345678-1234-1234-1234-123456789abc"); } @@ -84,11 +122,12 @@ mod tests { fn test_drive_item_properties() { let drive_item = DriveItem { name: "/dev/sda1".to_string(), - device_path: "/mnt/data".to_string(), + mount_point: "/run/media/user/MyBook".to_string(), size: "1 TB".to_string(), uuid: Some("12345678-1234-1234-1234-123456789abc".to_string()), is_mounted: true, fstype: "ext4".to_string(), + is_luks: false, }; assert_eq!(drive_item.name, "/dev/sda1"); @@ -100,5 +139,60 @@ mod tests { Some("12345678-1234-1234-1234-123456789abc".to_string()) ); } -} + #[test] + fn test_luks_partition_detection() { + let luks_partition = Partition { + name: "sda3".to_string(), + size: "100 GiB".to_string(), + uuid: Some("luks-12345678-1234-1234-1234-123456789abc".to_string()), + mountpoints: Vec::new(), + fstype: Some("crypto_LUKS".to_string()), + }; + + assert!( + !luks_partition.is_system_drive(), + "LUKS partition should not be filtered as system drive" + ); + assert!( + luks_partition.is_luks(), + "Partition with fstype crypto_LUKS should be detected as LUKS" + ); + + // Test that non-LUKS partitions still work correctly + let normal_partition = Partition { + name: "sda1".to_string(), + size: "500 MiB".to_string(), + uuid: Some("abcd-1234".to_string()), + mountpoints: vec!["/boot".to_string()], + fstype: Some("ext4".to_string()), + }; + + assert!( + normal_partition.is_system_drive(), + "/boot partition should be filtered as system drive" + ); + assert!( + !normal_partition.is_luks(), + "ext4 partition should not be detected as LUKS" + ); + + // Test that swap partitions are still filtered + let swap_partition = Partition { + name: "sda2".to_string(), + size: "8 GiB".to_string(), + uuid: Some("swap-1234".to_string()), + mountpoints: Vec::new(), + fstype: Some("swap".to_string()), + }; + + assert!( + swap_partition.is_system_drive(), + "swap partition should be filtered as system drive" + ); + assert!( + !swap_partition.is_luks(), + "swap partition should not be detected as LUKS" + ); + } +}