mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-27 04:19:19 +00:00
1da0fa7223
Preserve pending pairing tokens until credential-store migration succeeds, defer keychain access until frontend hydration, reject symlink and malformed ownership paths, and restrict metadata credentials to exact origins. Refs #15 Refs #16
212 lines
6.2 KiB
Rust
212 lines
6.2 KiB
Rust
use std::path::{Path, PathBuf};
|
|
use tauri::{AppHandle, Manager, Runtime};
|
|
|
|
pub const PORTABLE_MARKER: &str = "portable.flag";
|
|
const PORTABLE_DATA_DIR: &str = "data";
|
|
const PORTABLE_LOG_DIR: &str = "logs";
|
|
const PORTABLE_WEBVIEW_DIR: &str = "webview";
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum StorageMode {
|
|
Standard,
|
|
Portable { root: PathBuf },
|
|
}
|
|
|
|
impl StorageMode {
|
|
pub fn detect() -> Self {
|
|
let Some(executable) = std::env::current_exe().ok() else {
|
|
return Self::Standard;
|
|
};
|
|
let Some(root) = executable.parent() else {
|
|
return Self::Standard;
|
|
};
|
|
|
|
if root.join(PORTABLE_MARKER).is_file() {
|
|
Self::Portable {
|
|
root: root.to_path_buf(),
|
|
}
|
|
} else {
|
|
Self::Standard
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn detect_from_root(root: &Path) -> Self {
|
|
if root.join(PORTABLE_MARKER).is_file() {
|
|
Self::Portable {
|
|
root: root.to_path_buf(),
|
|
}
|
|
} else {
|
|
Self::Standard
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct StorageLayout {
|
|
mode: StorageMode,
|
|
data_dir: PathBuf,
|
|
log_dir: PathBuf,
|
|
webview_dir: PathBuf,
|
|
}
|
|
|
|
impl StorageLayout {
|
|
pub fn resolve<R: Runtime>(
|
|
app_handle: &AppHandle<R>,
|
|
mode: StorageMode,
|
|
) -> Result<Self, String> {
|
|
let (mode, data_dir, log_dir, webview_dir) = match mode {
|
|
StorageMode::Standard => (
|
|
StorageMode::Standard,
|
|
app_handle
|
|
.path()
|
|
.app_data_dir()
|
|
.map_err(|error| format!("failed to resolve app data directory: {error}"))?,
|
|
app_handle
|
|
.path()
|
|
.app_log_dir()
|
|
.map_err(|error| format!("failed to resolve app log directory: {error}"))?,
|
|
app_handle.path().app_local_data_dir().map_err(|error| {
|
|
format!("failed to resolve app local data directory: {error}")
|
|
})?,
|
|
),
|
|
StorageMode::Portable { root } => {
|
|
let data_dir = root.join(PORTABLE_DATA_DIR);
|
|
(
|
|
StorageMode::Portable { root },
|
|
data_dir.clone(),
|
|
data_dir.join(PORTABLE_LOG_DIR),
|
|
data_dir.join(PORTABLE_WEBVIEW_DIR),
|
|
)
|
|
}
|
|
};
|
|
|
|
Ok(Self {
|
|
mode,
|
|
data_dir: canonicalize_storage_path(&data_dir)?,
|
|
log_dir: canonicalize_storage_path(&log_dir)?,
|
|
webview_dir: canonicalize_storage_path(&webview_dir)?,
|
|
})
|
|
}
|
|
|
|
pub fn is_portable(&self) -> bool {
|
|
matches!(self.mode, StorageMode::Portable { .. })
|
|
}
|
|
|
|
pub fn data_dir(&self) -> &Path {
|
|
&self.data_dir
|
|
}
|
|
|
|
pub fn log_dir(&self) -> &Path {
|
|
&self.log_dir
|
|
}
|
|
|
|
pub fn webview_dir(&self) -> &Path {
|
|
&self.webview_dir
|
|
}
|
|
}
|
|
|
|
fn canonicalize_storage_path(path: &Path) -> Result<PathBuf, String> {
|
|
if crate::path_has_symlink_component(path) {
|
|
return Err(format!(
|
|
"storage path contains a symlinked component: '{}'",
|
|
path.display()
|
|
));
|
|
}
|
|
let mut existing = path;
|
|
let mut missing = Vec::new();
|
|
loop {
|
|
match std::fs::symlink_metadata(existing) {
|
|
Ok(metadata) => {
|
|
if metadata.file_type().is_symlink() {
|
|
return Err(format!(
|
|
"storage path contains a symlinked directory: '{}'",
|
|
path.display()
|
|
));
|
|
}
|
|
break;
|
|
}
|
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
|
|
Err(error) => {
|
|
return Err(format!(
|
|
"failed to inspect storage path '{}': {error}",
|
|
path.display()
|
|
));
|
|
}
|
|
}
|
|
missing.push(
|
|
existing
|
|
.file_name()
|
|
.ok_or_else(|| format!("storage path has no existing ancestor: '{}'", path.display()))?
|
|
.to_owned(),
|
|
);
|
|
existing = existing
|
|
.parent()
|
|
.ok_or_else(|| format!("storage path has no existing ancestor: '{}'", path.display()))?;
|
|
}
|
|
let mut canonical = std::fs::canonicalize(existing)
|
|
.map_err(|error| format!("failed to canonicalize storage path '{}': {error}", path.display()))?;
|
|
for component in missing.iter().rev() {
|
|
canonical.push(component);
|
|
}
|
|
Ok(canonical)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{canonicalize_storage_path, StorageMode, PORTABLE_MARKER};
|
|
use std::fs;
|
|
use std::path::Path;
|
|
use tempfile::TempDir;
|
|
|
|
#[test]
|
|
fn marker_selects_portable_mode() {
|
|
let root = TempDir::new().unwrap();
|
|
fs::write(root.path().join(PORTABLE_MARKER), b"portable\n").unwrap();
|
|
|
|
assert_eq!(
|
|
StorageMode::detect_from_root(root.path()),
|
|
StorageMode::Portable {
|
|
root: root.path().to_path_buf()
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn missing_marker_keeps_standard_mode() {
|
|
let root = TempDir::new().unwrap();
|
|
|
|
assert_eq!(
|
|
StorageMode::detect_from_root(root.path()),
|
|
StorageMode::Standard
|
|
);
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn rejects_symlinked_storage_directories() {
|
|
use std::os::unix::fs::symlink;
|
|
|
|
let root = TempDir::new().unwrap();
|
|
let target = TempDir::new().unwrap();
|
|
let root_path = fs::canonicalize(root.path()).unwrap();
|
|
let redirected = root_path.join("logs");
|
|
symlink(target.path(), &redirected).unwrap();
|
|
|
|
assert!(canonicalize_storage_path(Path::new(&redirected)).is_err());
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn rejects_dangling_symlinked_storage_directories() {
|
|
use std::os::unix::fs::symlink;
|
|
|
|
let root = TempDir::new().unwrap();
|
|
let root_path = fs::canonicalize(root.path()).unwrap();
|
|
let redirected = root_path.join("logs");
|
|
symlink(root_path.join("missing-target"), &redirected).unwrap();
|
|
|
|
assert!(canonicalize_storage_path(Path::new(&redirected)).is_err());
|
|
}
|
|
}
|