fix: close post-release lifecycle state gaps

This commit is contained in:
NimBold
2026-07-15 10:13:53 +03:30
parent 9224117d77
commit 82f2914077
4 changed files with 139 additions and 4 deletions
+49 -1
View File
@@ -756,7 +756,14 @@ fn remove_persisted_transfer_secrets(value: &mut Value) {
// These values are accepted from users, browser extensions, or URLs and
// may contain credentials or bearer tokens. Portable queues keep their
// useful metadata, but never persist these values beside the executable.
let mut removed_transfer_context = false;
for key in ["password", "cookies", "headers", "mirrors", "proxy"] {
if object
.get(key)
.is_some_and(|value| !value.is_null() && !value_is_empty(value))
{
removed_transfer_context = true;
}
object.remove(key);
}
@@ -790,6 +797,20 @@ fn remove_persisted_transfer_secrets(value: &mut Value) {
mark_portable_download_unresumable(object);
}
}
// Do not silently resume a queued transfer after removing request
// credentials or other transfer-specific context. The URL may still be
// valid, but its semantics have changed and the user must re-add it with
// the required request settings.
if removed_transfer_context {
mark_portable_download_unresumable(object);
}
}
fn value_is_empty(value: &Value) -> bool {
value.as_str().is_some_and(str::is_empty)
|| value.as_array().is_some_and(Vec::is_empty)
|| value.as_object().is_some_and(serde_json::Map::is_empty)
}
fn mark_portable_download_unresumable(object: &mut serde_json::Map<String, Value>) {
@@ -803,7 +824,7 @@ fn mark_portable_download_unresumable(object: &mut serde_json::Map<String, Value
object.insert(
"lastError".to_string(),
Value::String(
"Portable mode removed credentials from this persisted download; add it again to resume."
"Portable mode removed credentials or transfer settings from this persisted download; add it again to resume."
.to_string(),
),
);
@@ -1644,6 +1665,33 @@ mod tests {
}
}
#[test]
fn portable_download_persistence_marks_context_dependent_queue_items_unresumable() {
let temp = TempDir::new().unwrap();
let state = init_at_path(temp.path()).unwrap();
let mut connection = state.lock().unwrap();
let data = json!([{
"id": "download-context",
"status": "queued",
"queueId": "main",
"url": "https://example.com/file",
"headers": "Authorization: Bearer secret"
}])
.to_string();
replace_downloads(&mut connection, &data, true).unwrap();
let saved: Value = serde_json::from_str(&load_downloads(&connection).unwrap()[0]).unwrap();
assert_eq!(saved["url"], "https://example.com/file");
assert_eq!(saved["status"], "failed");
assert_eq!(saved["resumable"], false);
assert_eq!(
saved["lastError"],
"Portable mode removed credentials or transfer settings from this persisted download; add it again to resume."
);
assert!(saved.get("headers").is_none());
}
#[test]
fn portable_download_persistence_redacts_error_secrets_but_preserves_safe_errors_and_standard_details() {
let temp = TempDir::new().unwrap();
+6
View File
@@ -3515,6 +3515,12 @@ async fn pause_download(
state.queue_manager.next_aria2_control_epoch(&id).await;
state.queue_manager.cancel_aria2_retries(&id).await;
if retrying && matches!(terminal, "error" | "removed") {
// The terminal GID was forgotten above, so this
// lifecycle can no longer be resumed in place. Release
// backend ownership as well; otherwise a paused row
// keeps a dead registration until another command happens
// to repair it.
state.queue_manager.release_registered_id(&id).await;
use tauri::Emitter;
let _ = app_handle.emit(
"download-state",