mirror of
https://github.com/nimbold/Firelink.git
synced 2026-09-01 13:38:01 +00:00
fix(startup): support redirected Windows storage paths (#37)
- Issue #37: allow Windows AppData junctions to resolve before the main window starts. - Keep reparse-point rejection for user-selected download and recovery paths. - Add Windows junction coverage and v1.3.1 schema migration data-preservation coverage.
This commit is contained in:
@@ -2523,6 +2523,57 @@ mod tests {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn migrates_v1_database_and_creates_backup() {
|
||||||
|
let temp = TempDir::new().unwrap();
|
||||||
|
let path = temp.path().join(DATABASE_NAME);
|
||||||
|
let connection = Connection::open(&path).unwrap();
|
||||||
|
connection
|
||||||
|
.execute_batch(
|
||||||
|
"
|
||||||
|
CREATE TABLE downloads (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
queue_id TEXT,
|
||||||
|
data TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE settings (id INTEGER PRIMARY KEY, data TEXT NOT NULL);
|
||||||
|
CREATE TABLE queues (id TEXT PRIMARY KEY, data TEXT NOT NULL);
|
||||||
|
CREATE TABLE download_ownership (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
primary_path TEXT NOT NULL
|
||||||
|
);
|
||||||
|
INSERT INTO download_ownership VALUES ('download-1', '/downloads/file.bin');
|
||||||
|
PRAGMA user_version = 1;
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
drop(connection);
|
||||||
|
|
||||||
|
let state = init_at_path(temp.path()).unwrap();
|
||||||
|
let connection = state.lock().unwrap();
|
||||||
|
let version: i64 = connection
|
||||||
|
.pragma_query_value(None, "user_version", |row| row.get(0))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(version, CURRENT_SCHEMA_VERSION);
|
||||||
|
assert!(table_exists(&connection, "download_owned_paths").unwrap());
|
||||||
|
assert!(table_exists(&connection, "download_removal_paths").unwrap());
|
||||||
|
assert_eq!(
|
||||||
|
load_ownership(&connection).unwrap(),
|
||||||
|
vec![(
|
||||||
|
"download-1".to_string(),
|
||||||
|
"/downloads/file.bin".to_string(),
|
||||||
|
vec!["/downloads/file.bin".to_string()]
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
assert!(fs::read_dir(temp.path()).unwrap().flatten().any(|entry| {
|
||||||
|
entry
|
||||||
|
.file_name()
|
||||||
|
.to_string_lossy()
|
||||||
|
.starts_with("firelink.sqlite.backup-schema-v1-")
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
#[test]
|
#[test]
|
||||||
fn refuses_to_open_a_database_symlink() {
|
fn refuses_to_open_a_database_symlink() {
|
||||||
|
|||||||
+16
-1
@@ -3328,6 +3328,21 @@ fn metadata_is_link_or_reparse(metadata: &std::fs::Metadata) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn path_has_symlink_component(path: &std::path::Path) -> bool {
|
pub(crate) fn path_has_symlink_component(path: &std::path::Path) -> bool {
|
||||||
|
path_has_component_matching(path, metadata_is_link_or_reparse)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect only symbolic-link components, without treating every Windows
|
||||||
|
/// reparse point as a link. Trusted application-data directories may use
|
||||||
|
/// junctions for Windows folder redirection; user-selected download and
|
||||||
|
/// recovery paths continue to use the stricter helper above.
|
||||||
|
pub(crate) fn path_has_symbolic_link_component(path: &std::path::Path) -> bool {
|
||||||
|
path_has_component_matching(path, |metadata| metadata.file_type().is_symlink())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_has_component_matching(
|
||||||
|
path: &std::path::Path,
|
||||||
|
matches: impl Fn(&std::fs::Metadata) -> bool,
|
||||||
|
) -> bool {
|
||||||
use std::path::Component;
|
use std::path::Component;
|
||||||
|
|
||||||
let mut current = std::path::PathBuf::new();
|
let mut current = std::path::PathBuf::new();
|
||||||
@@ -3339,7 +3354,7 @@ pub(crate) fn path_has_symlink_component(path: &std::path::Path) -> bool {
|
|||||||
Component::Normal(name) => {
|
Component::Normal(name) => {
|
||||||
current.push(name);
|
current.push(name);
|
||||||
if std::fs::symlink_metadata(¤t)
|
if std::fs::symlink_metadata(¤t)
|
||||||
.is_ok_and(|metadata| metadata_is_link_or_reparse(&metadata))
|
.is_ok_and(|metadata| matches(&metadata))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ fn aria2_server_stat_is_valid(contents: &str) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn canonicalize_storage_path(path: &Path) -> Result<PathBuf, String> {
|
fn canonicalize_storage_path(path: &Path) -> Result<PathBuf, String> {
|
||||||
if crate::path_has_symlink_component(path) {
|
if crate::path_has_symbolic_link_component(path) {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"storage path contains a symlinked component: '{}'",
|
"storage path contains a symlinked component: '{}'",
|
||||||
path.display()
|
path.display()
|
||||||
@@ -455,4 +455,32 @@ mod tests {
|
|||||||
|
|
||||||
assert!(canonicalize_storage_path(Path::new(&redirected)).is_err());
|
assert!(canonicalize_storage_path(Path::new(&redirected)).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
#[test]
|
||||||
|
fn accepts_windows_junctions_for_redirected_storage_paths() {
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
let parent = TempDir::new().unwrap();
|
||||||
|
let spaced_parent = parent.path().join("firelink test data");
|
||||||
|
fs::create_dir(&spaced_parent).unwrap();
|
||||||
|
let root = TempDir::new_in(&spaced_parent).unwrap();
|
||||||
|
let target = TempDir::new_in(&spaced_parent).unwrap();
|
||||||
|
let redirected = root.path().join("redirected");
|
||||||
|
let target_storage = target.path().join("firelink");
|
||||||
|
fs::create_dir(&target_storage).unwrap();
|
||||||
|
|
||||||
|
let status = Command::new("cmd")
|
||||||
|
.args(["/D", "/C", "mklink", "/J"])
|
||||||
|
.arg(&redirected)
|
||||||
|
.arg(target.path())
|
||||||
|
.status()
|
||||||
|
.expect("Windows junction creation command should start");
|
||||||
|
assert!(status.success(), "mklink /J failed with status {status}");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
canonicalize_storage_path(&redirected.join("firelink")).unwrap(),
|
||||||
|
fs::canonicalize(target_storage).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user