From a7e647cf401cf035d13c67cc24bc0fc8248343e6 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 5 Aug 2026 23:28:40 +0800 Subject: [PATCH] fix(portable): delete files a later package no longer carries The extraction directory is wiped only when the packer's compiled-in timestamp changes. That used to be per customer, because generate.py ran for each build; now the packer is compiled once per release, so every customer and every rebuild within a release share one timestamp and nothing is ever wiped. A customer who removes their logo and rebuilds would therefore keep showing it: the new package simply omits logo.png, and md5 skipping only covers files that are still present. Record the package's paths in the extraction's meta file and delete the ones a later package drops. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Q7fBdTwziR5BHTkSz7Tzcm --- libs/portable/src/bin_reader.rs | 32 +++++++++++++++++++-- libs/portable/src/main.rs | 49 +++++++++++++++++++++++++++++---- 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/libs/portable/src/bin_reader.rs b/libs/portable/src/bin_reader.rs index 0adc9575e..4b7e12361 100644 --- a/libs/portable/src/bin_reader.rs +++ b/libs/portable/src/bin_reader.rs @@ -33,12 +33,23 @@ pub(crate) struct BinaryData { pub(crate) struct BinaryReader { pub files: Vec, pub exe: String, + // Paths supplied by the per-customer package. Recorded so that a file dropped + // from a later package -- a logo the customer removed, say -- can be deleted + // from an existing extraction, which the timestamp wipe no longer covers now + // that the packer is built once per release rather than once per customer. + pub package_paths: Vec, } impl Default for BinaryReader { fn default() -> Self { - let (files, exe) = merge(read_embedded(), read_package()); - Self { files, exe } + let package = read_package(); + let package_paths = package.0.iter().map(|f| f.path.clone()).collect(); + let (files, exe) = merge(read_embedded(), package); + Self { + files, + exe, + package_paths, + } } } @@ -347,6 +358,23 @@ mod tests { assert_eq!(entry(&files, "./librustdesk.dll").unwrap().raw, b"core"); } + #[test] + fn package_paths_are_recorded_for_the_dropped_file_sweep() { + let package = parse(blob( + &[("./custom.txt", b"cfg"), ("./data/logo.png", b"img")], + "./acme.exe", + )) + .unwrap(); + let mut paths: Vec = package.0.iter().map(|f| f.path.clone()).collect(); + paths.sort(); + assert_eq!(paths, vec!["./custom.txt", "./data/logo.png"]); + + // Merging must not disturb them: the generic payload contributes none. + let embedded = parse(blob(&[("./librustdesk.dll", b"core")], "./rustdesk.exe")).unwrap(); + let (files, _) = merge(embedded, package); + assert!(entry(&files, "./data/logo.png").is_some()); + } + #[test] fn matches_paths_across_separator_styles() { // generate.py emits backslashes when it runs on Windows. diff --git a/libs/portable/src/main.rs b/libs/portable/src/main.rs index f3a1ff3fa..f8aef63d0 100644 --- a/libs/portable/src/main.rs +++ b/libs/portable/src/main.rs @@ -17,6 +17,7 @@ const APP_METADATA: &[u8] = include_bytes!("../app_metadata.toml"); const APP_METADATA: &[u8] = &[]; const APP_METADATA_CONFIG: &str = "meta.toml"; const META_LINE_PREFIX_TIMESTAMP: &str = "timestamp = "; +const META_LINE_PREFIX_FILE: &str = "file = "; const APP_PREFIX: &str = "rustdesk"; const APPNAME_RUNTIME_ENV_KEY: &str = "RUSTDESK_APPNAME"; #[cfg(windows)] @@ -62,12 +63,47 @@ fn is_timestamp_matches(dir: &Path, ts: &mut u64) -> bool { false } -fn write_meta(dir: &Path, ts: u64) { +fn write_meta(dir: &Path, ts: u64, package_paths: &[String]) { let meta_file = dir.join(APP_METADATA_CONFIG); - if ts != 0 { - let content = format!("{}{}", META_LINE_PREFIX_TIMESTAMP, ts); - // Ignore is ok here - let _ = std::fs::write(meta_file, content); + let mut content = format!("{}{}\n", META_LINE_PREFIX_TIMESTAMP, ts); + for path in package_paths { + content.push_str(&format!("{}{}\n", META_LINE_PREFIX_FILE, path)); + } + // Ignore is ok here + let _ = std::fs::write(meta_file, content); +} + +fn previous_package_files(dir: &Path) -> Vec { + let Ok(content) = std::fs::read_to_string(dir.join(APP_METADATA_CONFIG)) else { + return Vec::new(); + }; + content + .lines() + .filter_map(|line| line.strip_prefix(META_LINE_PREFIX_FILE)) + .map(|path| path.trim().to_owned()) + .collect() +} + +fn normalized(path: &str) -> String { + path.replace('\\', "/").trim_start_matches("./").to_lowercase() +} + +// A customer who drops a branding asset gets a package without it, and the file +// would otherwise linger in an existing extraction and keep being used. The wipe +// cannot cover this: it is keyed on the packer's build timestamp, which is now the +// same for every customer of a release. +fn remove_dropped_package_files(dir: &Path, current: &[String]) { + let keep: std::collections::HashSet = current.iter().map(|p| normalized(p)).collect(); + for previous in previous_package_files(dir) { + let normalized_previous = normalized(&previous); + if keep.contains(&normalized_previous) || normalized_previous.contains("..") { + continue; + } + let path = dir.join(&previous); + if path.is_file() { + println!("removing dropped {}", previous); + let _ = std::fs::remove_file(path); + } } } @@ -99,10 +135,11 @@ fn setup( } std::fs::remove_dir_all(&dir).ok(); } + remove_dropped_package_files(&dir, &reader.package_paths); for file in reader.files.iter() { file.write_to_file(&dir); } - write_meta(&dir, ts); + write_meta(&dir, ts, &reader.package_paths); #[cfg(windows)] win::copy_runtime_broker(&dir); #[cfg(linux)]