Skip to content

Commit 948104c

Browse files
committed
Release v0.1.1
Security: - Enable Content Security Policy (CSP) - Disable devtools in production builds Features: - Auto-refresh disk list on volume mount/unmount - CLI path discovery (PATH, env var, common locations) - Config validation for RAM, vCPUs, log level - Watcher thread shutdown mechanism Improvements: - Increase mount timeout from 2.5s to 10s - Reduce tokio features for smaller binary - Cross-platform build script (Node.js) Fixes: - Compiler warnings (unused imports/variables) - Unused CSS selectors in MountStatus - Disk list race condition on eject
1 parent 5dc9251 commit 948104c

17 files changed

Lines changed: 318 additions & 58 deletions

File tree

CHANGELOG.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
## [0.1.1] - 2025-01-27
6+
7+
### Security
8+
- Enabled Content Security Policy (CSP) to protect against script injection
9+
- Disabled devtools in production builds
10+
11+
### Added
12+
- Auto-refresh disk list when volumes are mounted/unmounted
13+
- CLI path discovery (searches PATH, ANYLINUXFS_PATH env var, common locations)
14+
- Config validation for RAM, vCPUs, and log level settings
15+
- Watcher thread shutdown mechanism to prevent thread accumulation
16+
17+
### Changed
18+
- Increased mount verification timeout from 2.5s to 10s
19+
- Reduced tokio dependency features (smaller binary)
20+
- Build script now cross-platform (Node.js instead of macOS-specific sed)
21+
22+
### Fixed
23+
- Compiler warnings (unused imports and variables)
24+
- Unused CSS selectors in MountStatus component
25+
- Disk list race condition on eject (1.5s settle time)
26+
27+
## [0.1.0] - 2025-01-24
28+
29+
### Added
30+
- Initial release
31+
- Mount/unmount Linux filesystems (ext4, btrfs, XFS, etc.) on macOS
32+
- Support for encrypted drives (LUKS/BitLocker)
33+
- Real-time mount status monitoring
34+
- Log viewer with file watching
35+
- VM configuration (RAM, vCPUs, log level)
36+
- Admin mode for enhanced disk detection
37+
- Force cleanup for orphaned VM instances

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
22
"name": "anylinuxfs-gui",
3-
"version": "0.1.0",
3+
"version": "0.1.1",
44
"private": true,
55
"type": "module",
66
"scripts": {
77
"dev": "vite dev",
88
"build": "vite build && npm run fix-paths",
9-
"fix-paths": "for f in build/*.html; do sed -i '' 's|href=\"/|href=\"./|g; s|src=\"/|src=\"./|g; s|import(\"/|import(\"./|g' \"$f\"; done",
9+
"fix-paths": "node scripts/fix-paths.js",
1010
"preview": "vite preview",
1111
"tauri": "tauri",
1212
"tauri:dev": "tauri dev",

scripts/fix-paths.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env node
2+
// Cross-platform script to fix absolute paths in HTML files for Tauri
3+
import { readFileSync, writeFileSync, readdirSync } from 'fs';
4+
import { join } from 'path';
5+
6+
const buildDir = 'build';
7+
8+
// Find all HTML files in build directory
9+
const htmlFiles = readdirSync(buildDir).filter(f => f.endsWith('.html'));
10+
11+
for (const file of htmlFiles) {
12+
const filePath = join(buildDir, file);
13+
let content = readFileSync(filePath, 'utf-8');
14+
15+
// Replace absolute paths with relative paths
16+
content = content
17+
.replace(/href="\//g, 'href="./')
18+
.replace(/src="\//g, 'src="./')
19+
.replace(/import\("\//g, 'import("./');
20+
21+
writeFileSync(filePath, content);
22+
console.log(`Fixed paths in ${file}`);
23+
}

src-tauri/Cargo.lock

Lines changed: 1 addition & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "anylinuxfs-gui"
3-
version = "0.0.1"
3+
version = "0.1.1"
44
edition = "2021"
55

66
[lib]
@@ -15,7 +15,7 @@ tauri = { version = "2", features = [] }
1515
tauri-plugin-shell = "2"
1616
serde = { version = "1", features = ["derive"] }
1717
serde_json = "1"
18-
tokio = { version = "1", features = ["full"] }
18+
tokio = { version = "1", features = ["rt", "rt-multi-thread"] }
1919
notify = "6"
2020
toml = "0.8"
2121
dirs = "5"

src-tauri/src/cli.rs

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,68 @@
11
use std::process::{Command, Stdio};
2-
use std::io::{Write, Read};
2+
use std::io::Read;
33
use std::fs;
44
use std::os::unix::fs::PermissionsExt;
5-
use std::path::Path;
5+
use std::path::{Path, PathBuf};
66
use std::time::{Duration, Instant};
7+
use std::sync::OnceLock;
8+
9+
/// Common locations to search for anylinuxfs
10+
const SEARCH_PATHS: &[&str] = &[
11+
"/opt/homebrew/bin/anylinuxfs",
12+
"/usr/local/bin/anylinuxfs",
13+
"/usr/bin/anylinuxfs",
14+
];
15+
16+
/// Cached path to anylinuxfs binary
17+
static ANYLINUXFS_PATH: OnceLock<Option<PathBuf>> = OnceLock::new();
18+
19+
/// Find anylinuxfs in PATH or common locations
20+
fn find_anylinuxfs() -> Option<PathBuf> {
21+
// First check ANYLINUXFS_PATH environment variable
22+
if let Ok(env_path) = std::env::var("ANYLINUXFS_PATH") {
23+
let path = PathBuf::from(&env_path);
24+
if path.exists() {
25+
return Some(path);
26+
}
27+
}
728

8-
const ANYLINUXFS_PATH: &str = "/opt/homebrew/bin/anylinuxfs";
29+
// Search in PATH using `which`
30+
if let Ok(output) = Command::new("which").arg("anylinuxfs").output() {
31+
if output.status.success() {
32+
let path_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
33+
if !path_str.is_empty() {
34+
let path = PathBuf::from(&path_str);
35+
if path.exists() {
36+
return Some(path);
37+
}
38+
}
39+
}
40+
}
41+
42+
// Fall back to common locations
43+
for search_path in SEARCH_PATHS {
44+
let path = Path::new(search_path);
45+
if path.exists() {
46+
return Some(path.to_path_buf());
47+
}
48+
}
49+
50+
None
51+
}
52+
53+
/// Get the cached path to anylinuxfs, finding it if needed
54+
fn get_anylinuxfs_path() -> Option<&'static PathBuf> {
55+
ANYLINUXFS_PATH.get_or_init(find_anylinuxfs).as_ref()
56+
}
957

1058
/// Check if the anylinuxfs CLI is available
1159
pub fn is_available() -> bool {
12-
Path::new(ANYLINUXFS_PATH).exists()
60+
get_anylinuxfs_path().is_some()
1361
}
1462

1563
/// Get the path to the anylinuxfs CLI
16-
pub fn get_path() -> &'static str {
17-
ANYLINUXFS_PATH
64+
pub fn get_path() -> Option<&'static Path> {
65+
get_anylinuxfs_path().map(|p| p.as_path())
1866
}
1967

2068
/// Execute an anylinuxfs command with optional sudo elevation
@@ -27,7 +75,10 @@ pub fn execute_command(args: &[&str], needs_sudo: bool, passphrase: Option<&str>
2775
}
2876

2977
fn execute_direct(args: &[&str], passphrase: Option<&str>) -> Result<String, String> {
30-
let mut cmd = Command::new(ANYLINUXFS_PATH);
78+
let cli_path = get_anylinuxfs_path()
79+
.ok_or_else(|| "anylinuxfs CLI not found in PATH or standard locations".to_string())?;
80+
81+
let mut cmd = Command::new(cli_path);
3182
cmd.args(args);
3283
cmd.stdin(Stdio::null());
3384

@@ -47,12 +98,16 @@ fn execute_direct(args: &[&str], passphrase: Option<&str>) -> Result<String, Str
4798
}
4899

49100
fn execute_with_sudo(args: &[&str], passphrase: Option<&str>) -> Result<String, String> {
101+
let cli_path = get_anylinuxfs_path()
102+
.ok_or_else(|| "anylinuxfs CLI not found in PATH or standard locations".to_string())?;
103+
50104
// Create a temporary askpass script that uses osascript
51105
// This way the password never passes through our code
52106
let askpass_script = create_askpass_script()?;
53107

54108
// Build the command arguments for sudo with SUDO_ASKPASS
55-
let mut sudo_args = vec!["-A", "--", ANYLINUXFS_PATH];
109+
let cli_path_str = cli_path.to_string_lossy();
110+
let mut sudo_args: Vec<&str> = vec!["-A", "--", &cli_path_str];
56111
sudo_args.extend(args.iter().copied());
57112

58113
let mut cmd = Command::new("sudo");

src-tauri/src/commands/config.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,32 @@ pub fn get_config() -> Result<AppConfig, String> {
7979
})
8080
}
8181

82+
// Valid configuration values (must match frontend options)
83+
const VALID_RAM_OPTIONS: &[u32] = &[512, 1024, 2048, 4096, 8192, 16384];
84+
const VALID_VCPU_OPTIONS: &[u32] = &[1, 2, 4, 8, 16];
85+
const VALID_LOG_LEVELS: &[&str] = &["off", "error", "warn", "info", "debug", "trace"];
86+
8287
#[tauri::command]
8388
pub async fn update_config(ram_mb: Option<u32>, vcpus: Option<u32>, log_level: Option<String>) -> Result<(), String> {
89+
// Validate inputs before running commands
90+
if let Some(ram) = ram_mb {
91+
if !VALID_RAM_OPTIONS.contains(&ram) {
92+
return Err(format!("Invalid RAM value: {}MB. Valid options: {:?}", ram, VALID_RAM_OPTIONS));
93+
}
94+
}
95+
96+
if let Some(cpus) = vcpus {
97+
if !VALID_VCPU_OPTIONS.contains(&cpus) {
98+
return Err(format!("Invalid vCPU value: {}. Valid options: {:?}", cpus, VALID_VCPU_OPTIONS));
99+
}
100+
}
101+
102+
if let Some(ref level) = log_level {
103+
if !VALID_LOG_LEVELS.contains(&level.as_str()) {
104+
return Err(format!("Invalid log level: '{}'. Valid options: {:?}", level, VALID_LOG_LEVELS));
105+
}
106+
}
107+
84108
// Run in blocking task to avoid freezing UI
85109
tokio::task::spawn_blocking(move || {
86110
// Use the CLI to update config values

src-tauri/src/commands/disk.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ fn parse_disk_list_output(output: &str) -> Result<DiskListResult, String> {
324324
})
325325
}
326326

327-
fn parse_partition_line(line: &str, disk_device: &str, partition_num: u32) -> Option<Partition> {
327+
fn parse_partition_line(line: &str, _disk_device: &str, _partition_num: u32) -> Option<Partition> {
328328
// Format: "Microsoft Basic Data NO NAME 47.2 GB disk6s1"
329329
// Or: "ext4 linuxrootfs 7.5 GB disk6s5"
330330
// The identifier is always at the end, size is before it
@@ -415,7 +415,8 @@ pub async fn mount_disk(device: String, passphrase: Option<String>) -> Result<St
415415
let result = execute_command(&["mount", &device], true, pass_ref);
416416

417417
// Give a moment for mount to complete, then verify with retries
418-
for _ in 0..5 {
418+
// 20 retries × 500ms = 10 seconds total timeout
419+
for _ in 0..20 {
419420
thread::sleep(Duration::from_millis(500));
420421
if check_nfs_mount_exists() {
421422
return Ok(result.unwrap_or_else(|_| "Mounted successfully".to_string()));

0 commit comments

Comments
 (0)