mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-14 00:11:01 +03:00
feat(portable): load per-customer payload from a PE resource
Customizing a Windows client recompiled the packer for every customer, because data.bin was baked in with include_bytes!. The generic payload is identical across customers, so only the small per-customer delta needs to vary: the branded runner exe, custom.txt and the icons. The packer now also reads an RDPKG RCDATA resource holding a second blob in the same format, and folds it over the compiled-in payload. A build can then inject that resource into a prebuilt template instead of running cargo. The executable to launch comes from the package trailer, and the extraction directory follows its stem, which replaces the sed of APP_PREFIX. Where the executable itself is not customized (sciter x86) it stays in the generic payload and is only renamed, so the merge covers both shapes. custom.txt keeps being written to disk next to the app: that is what the client reads at startup and what the updater stages so a customization survives an upgrade to a stock build. Also fixes generate.py restoring os.curdir (the literal ".") instead of the previous working directory, which left it inside the source folder. CI: ship windows-aarch64 in the unsigned tarball, so ARM custom clients have a template to build from. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7fBdTwziR5BHTkSz7Tzcm
This commit is contained in:
8
.github/workflows/flutter-build.yml
vendored
8
.github/workflows/flutter-build.yml
vendored
@@ -919,6 +919,12 @@ jobs:
|
|||||||
name: rustdesk-unsigned-windows-x86_64
|
name: rustdesk-unsigned-windows-x86_64
|
||||||
path: ./windows-x86_64/
|
path: ./windows-x86_64/
|
||||||
|
|
||||||
|
- name: Download Artifacts
|
||||||
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
|
with:
|
||||||
|
name: rustdesk-unsigned-windows-aarch64
|
||||||
|
path: ./windows-aarch64/
|
||||||
|
|
||||||
- name: Download Artifacts
|
- name: Download Artifacts
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
@@ -927,7 +933,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Combine unsigned app
|
- name: Combine unsigned app
|
||||||
run: |
|
run: |
|
||||||
tar czf rustdesk-${{ env.VERSION }}-unsigned.tar.gz *.dmg windows-x86_64 windows-x86
|
tar czf rustdesk-${{ env.VERSION }}-unsigned.tar.gz *.dmg windows-x86_64 windows-aarch64 windows-x86
|
||||||
|
|
||||||
- name: Publish unsigned app
|
- name: Publish unsigned app
|
||||||
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ build = "build.rs"
|
|||||||
brotli = "3.4"
|
brotli = "3.4"
|
||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
md5 = "0.7"
|
md5 = "0.7"
|
||||||
winapi = { version = "0.3", features = ["winbase"] }
|
winapi = { version = "0.3", features = ["winbase", "libloaderapi"] }
|
||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
windows = { version = "0.61", features = [
|
windows = { version = "0.61", features = [
|
||||||
|
|||||||
@@ -15,15 +15,27 @@ encoding = 'utf-8'
|
|||||||
# output: {path: (compressed_data, file_md5)}
|
# output: {path: (compressed_data, file_md5)}
|
||||||
|
|
||||||
|
|
||||||
def generate_md5_table(folder: str, level) -> dict:
|
def normalize(path: str) -> str:
|
||||||
|
path = path.replace('\\', '/')
|
||||||
|
while path.startswith('./'):
|
||||||
|
path = path[2:]
|
||||||
|
return path.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def generate_md5_table(folder: str, level, exclude: str = None) -> dict:
|
||||||
res: dict = dict()
|
res: dict = dict()
|
||||||
curdir = os.curdir
|
skip = normalize(exclude) if exclude else None
|
||||||
|
# os.curdir is the literal ".", so restoring it left us inside `folder`.
|
||||||
|
curdir = os.getcwd()
|
||||||
os.chdir(folder)
|
os.chdir(folder)
|
||||||
for root, _, files in os.walk('.'):
|
for root, _, files in os.walk('.'):
|
||||||
# remove ./
|
# remove ./
|
||||||
for f in files:
|
for f in files:
|
||||||
md5_generator = md5()
|
md5_generator = md5()
|
||||||
full_path = os.path.join(root, f)
|
full_path = os.path.join(root, f)
|
||||||
|
if skip and normalize(full_path) == skip:
|
||||||
|
print(f"Excluding {full_path}...")
|
||||||
|
continue
|
||||||
print(f"Processing {full_path}...")
|
print(f"Processing {full_path}...")
|
||||||
f = open(full_path, "rb")
|
f = open(full_path, "rb")
|
||||||
content = f.read()
|
content = f.read()
|
||||||
@@ -37,7 +49,10 @@ def generate_md5_table(folder: str, level) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def write_package_metadata(md5_table: dict, output_folder: str, exe: str):
|
def write_package_metadata(md5_table: dict, output_folder: str, exe: str):
|
||||||
output_path = os.path.join(output_folder, "data.bin")
|
write_blob(md5_table, os.path.join(output_folder, "data.bin"), exe)
|
||||||
|
|
||||||
|
|
||||||
|
def write_blob(md5_table: dict, output_path: str, exe: str):
|
||||||
with open(output_path, "wb") as f:
|
with open(output_path, "wb") as f:
|
||||||
f.write("rustdesk".encode(encoding=encoding))
|
f.write("rustdesk".encode(encoding=encoding))
|
||||||
for path in md5_table.keys():
|
for path in md5_table.keys():
|
||||||
@@ -92,6 +107,14 @@ if __name__ == '__main__':
|
|||||||
help="the target used by cargo")
|
help="the target used by cargo")
|
||||||
parser.add_option("-l", "--level", dest="level", type="int",
|
parser.add_option("-l", "--level", dest="level", type="int",
|
||||||
help="compression level, default is 11, highest", default=11)
|
help="compression level, default is 11, highest", default=11)
|
||||||
|
parser.add_option("--package", dest="package",
|
||||||
|
help="write the per-customer blob to this path instead of "
|
||||||
|
"data.bin, and skip the cargo build. Injected into the "
|
||||||
|
"template's RDPKG resource so customizing needs no rebuild")
|
||||||
|
parser.add_option("--exclude-exe", dest="exclude_exe", action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="omit the executable from the blob, for a template whose "
|
||||||
|
"executable ships in the package instead")
|
||||||
(options, args) = parser.parse_args()
|
(options, args) = parser.parse_args()
|
||||||
folder = options.folder or './rustdesk'
|
folder = options.folder or './rustdesk'
|
||||||
output_folder = os.path.abspath(options.output_folder or './')
|
output_folder = os.path.abspath(options.output_folder or './')
|
||||||
@@ -107,7 +130,11 @@ if __name__ == '__main__':
|
|||||||
exe = '.' + exe[len(os.path.abspath(folder)):]
|
exe = '.' + exe[len(os.path.abspath(folder)):]
|
||||||
print("Executable path: " + exe)
|
print("Executable path: " + exe)
|
||||||
print("Compression level: " + str(options.level))
|
print("Compression level: " + str(options.level))
|
||||||
md5_table = generate_md5_table(folder, options.level)
|
md5_table = generate_md5_table(
|
||||||
write_package_metadata(md5_table, output_folder, exe)
|
folder, options.level, exe if options.exclude_exe else None)
|
||||||
write_app_metadata(output_folder)
|
if options.package:
|
||||||
build_portable(output_folder, options.target)
|
write_blob(md5_table, os.path.abspath(options.package), exe)
|
||||||
|
else:
|
||||||
|
write_package_metadata(md5_table, output_folder, exe)
|
||||||
|
write_app_metadata(output_folder)
|
||||||
|
build_portable(output_folder, options.target)
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
use std::{
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
fs::{self},
|
fs::{self},
|
||||||
io::{Cursor, Read},
|
io::{Cursor, Read},
|
||||||
path::Path,
|
path::Path,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The generic payload, shared by every customer and compiled in once per release.
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
const BIN_DATA: &[u8] = include_bytes!("../data.bin");
|
const BIN_DATA: &[u8] = include_bytes!("../data.bin");
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
const BIN_DATA: &[u8] = &[];
|
const BIN_DATA: &[u8] = &[];
|
||||||
|
|
||||||
|
// The per-customer payload, injected into the RCDATA resource after the template
|
||||||
|
// has been built, so that customizing a client needs no recompilation.
|
||||||
|
#[cfg(windows)]
|
||||||
|
const PACKAGE_RESOURCE_NAME: &str = "RDPKG";
|
||||||
|
|
||||||
// 4bytes
|
// 4bytes
|
||||||
const LENGTH: usize = 4;
|
const LENGTH: usize = 4;
|
||||||
|
const IDENTIFIER: &[u8] = b"rustdesk";
|
||||||
const IDENTIFIER_LENGTH: usize = 8;
|
const IDENTIFIER_LENGTH: usize = 8;
|
||||||
const MD5_LENGTH: usize = 32;
|
const MD5_LENGTH: usize = 32;
|
||||||
const BUF_SIZE: usize = 4096;
|
const BUF_SIZE: usize = 4096;
|
||||||
@@ -28,11 +37,149 @@ pub(crate) struct BinaryReader {
|
|||||||
|
|
||||||
impl Default for BinaryReader {
|
impl Default for BinaryReader {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let (files, exe) = BinaryReader::read();
|
let (files, exe) = merge(read_embedded(), read_package());
|
||||||
Self { files, exe }
|
Self { files, exe }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Folds the per-customer package into the generic payload.
|
||||||
|
fn merge(
|
||||||
|
embedded: (Vec<BinaryData>, String),
|
||||||
|
package: (Vec<BinaryData>, String),
|
||||||
|
) -> (Vec<BinaryData>, String) {
|
||||||
|
let (mut files, generic_exe) = embedded;
|
||||||
|
let (package_files, package_exe) = package;
|
||||||
|
|
||||||
|
let exe = if package_exe.is_empty() {
|
||||||
|
generic_exe.clone()
|
||||||
|
} else {
|
||||||
|
package_exe
|
||||||
|
};
|
||||||
|
|
||||||
|
// The generic payload ships the executable under its stock name, the package
|
||||||
|
// decides the final one. Rename on extraction so the process is always
|
||||||
|
// `<appname>.exe`, which the app itself relies on to find its own sessions.
|
||||||
|
if !generic_exe.is_empty() && normalize_path(&exe) != normalize_path(&generic_exe) {
|
||||||
|
let generic_key = normalize_path(&generic_exe);
|
||||||
|
for file in files.iter_mut() {
|
||||||
|
if normalize_path(&file.path) == generic_key {
|
||||||
|
file.path = exe.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-customer entries replace the generic ones they shadow.
|
||||||
|
if !package_files.is_empty() {
|
||||||
|
let overridden: HashSet<String> = package_files
|
||||||
|
.iter()
|
||||||
|
.map(|file| normalize_path(&file.path))
|
||||||
|
.collect();
|
||||||
|
files.retain(|file| !overridden.contains(&normalize_path(&file.path)));
|
||||||
|
files.extend(package_files);
|
||||||
|
}
|
||||||
|
|
||||||
|
(files, exe)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_path(path: &str) -> String {
|
||||||
|
path.replace('\\', "/")
|
||||||
|
.trim_start_matches("./")
|
||||||
|
.to_lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u32(blob: &[u8], at: usize) -> Option<u32> {
|
||||||
|
let bytes = blob.get(at..at + LENGTH)?;
|
||||||
|
Some(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the files and the executable to launch, or None if the blob is absent or malformed.
|
||||||
|
fn parse(blob: &'static [u8]) -> Option<(Vec<BinaryData>, String)> {
|
||||||
|
let mut base = 0usize;
|
||||||
|
let mut parsed = Vec::new();
|
||||||
|
if blob.get(base..base + IDENTIFIER_LENGTH)? != IDENTIFIER {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
base += IDENTIFIER_LENGTH;
|
||||||
|
loop {
|
||||||
|
if blob.get(base..base + IDENTIFIER_LENGTH)? == IDENTIFIER {
|
||||||
|
base += IDENTIFIER_LENGTH;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let path_length = read_u32(blob, base)? as usize;
|
||||||
|
base += LENGTH;
|
||||||
|
let path = String::from_utf8_lossy(blob.get(base..base + path_length)?).to_string();
|
||||||
|
base += path_length;
|
||||||
|
let file_length = read_u32(blob, base)? as usize;
|
||||||
|
base += LENGTH;
|
||||||
|
let raw = blob.get(base..base + file_length)?;
|
||||||
|
base += file_length;
|
||||||
|
let md5_code = blob.get(base..base + MD5_LENGTH)?;
|
||||||
|
base += MD5_LENGTH;
|
||||||
|
parsed.push(BinaryData {
|
||||||
|
md5_code,
|
||||||
|
raw,
|
||||||
|
path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let executable = String::from_utf8_lossy(blob.get(base..)?).to_string();
|
||||||
|
Some((parsed, executable))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn read_embedded() -> (Vec<BinaryData>, String) {
|
||||||
|
parse(BIN_DATA).expect("bin file is not valid!")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
fn read_embedded() -> (Vec<BinaryData>, String) {
|
||||||
|
parse(BIN_DATA).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn read_package() -> (Vec<BinaryData>, String) {
|
||||||
|
read_resource(PACKAGE_RESOURCE_NAME)
|
||||||
|
.and_then(parse)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
fn read_package() -> (Vec<BinaryData>, String) {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads an RCDATA resource out of the running image. Resources live in the mapped
|
||||||
|
// image for the lifetime of the process, so the slice is genuinely 'static and no
|
||||||
|
// copy is needed.
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn read_resource(name: &str) -> Option<&'static [u8]> {
|
||||||
|
use std::ptr::null_mut;
|
||||||
|
use winapi::um::libloaderapi::{FindResourceW, LoadResource, LockResource, SizeofResource};
|
||||||
|
|
||||||
|
// MAKEINTRESOURCEW(10), avoids depending on the winuser feature for RT_RCDATA.
|
||||||
|
const RT_RCDATA: *const u16 = 10 as _;
|
||||||
|
|
||||||
|
let name: Vec<u16> = name.encode_utf16().chain(std::iter::once(0)).collect();
|
||||||
|
unsafe {
|
||||||
|
let info = FindResourceW(null_mut(), name.as_ptr(), RT_RCDATA);
|
||||||
|
if info.is_null() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let size = SizeofResource(null_mut(), info) as usize;
|
||||||
|
if size == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let handle = LoadResource(null_mut(), info);
|
||||||
|
if handle.is_null() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let data = LockResource(handle) as *const u8;
|
||||||
|
if data.is_null() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(std::slice::from_raw_parts(data, size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl BinaryData {
|
impl BinaryData {
|
||||||
fn decompress(&self) -> Vec<u8> {
|
fn decompress(&self) -> Vec<u8> {
|
||||||
let cursor = Cursor::new(self.raw);
|
let cursor = Cursor::new(self.raw);
|
||||||
@@ -68,59 +215,6 @@ impl BinaryData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl BinaryReader {
|
impl BinaryReader {
|
||||||
fn read() -> (Vec<BinaryData>, String) {
|
|
||||||
let mut base: usize = 0;
|
|
||||||
let mut parsed = vec![];
|
|
||||||
assert!(BIN_DATA.len() > IDENTIFIER_LENGTH, "bin data invalid!");
|
|
||||||
let mut iden = String::from_utf8_lossy(&BIN_DATA[base..base + IDENTIFIER_LENGTH]);
|
|
||||||
if iden != "rustdesk" {
|
|
||||||
panic!("bin file is not valid!");
|
|
||||||
}
|
|
||||||
base += IDENTIFIER_LENGTH;
|
|
||||||
loop {
|
|
||||||
iden = String::from_utf8_lossy(&BIN_DATA[base..base + IDENTIFIER_LENGTH]);
|
|
||||||
if iden == "rustdesk" {
|
|
||||||
base += IDENTIFIER_LENGTH;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// start reading
|
|
||||||
let mut offset = 0;
|
|
||||||
let path_length = u32::from_be_bytes([
|
|
||||||
BIN_DATA[base + offset],
|
|
||||||
BIN_DATA[base + offset + 1],
|
|
||||||
BIN_DATA[base + offset + 2],
|
|
||||||
BIN_DATA[base + offset + 3],
|
|
||||||
]) as usize;
|
|
||||||
offset += LENGTH;
|
|
||||||
let path =
|
|
||||||
String::from_utf8_lossy(&BIN_DATA[base + offset..base + offset + path_length])
|
|
||||||
.to_string();
|
|
||||||
offset += path_length;
|
|
||||||
// file sz
|
|
||||||
let file_length = u32::from_be_bytes([
|
|
||||||
BIN_DATA[base + offset],
|
|
||||||
BIN_DATA[base + offset + 1],
|
|
||||||
BIN_DATA[base + offset + 2],
|
|
||||||
BIN_DATA[base + offset + 3],
|
|
||||||
]) as usize;
|
|
||||||
offset += LENGTH;
|
|
||||||
let raw = &BIN_DATA[base + offset..base + offset + file_length];
|
|
||||||
offset += file_length;
|
|
||||||
// md5
|
|
||||||
let md5 = &BIN_DATA[base + offset..base + offset + MD5_LENGTH];
|
|
||||||
offset += MD5_LENGTH;
|
|
||||||
parsed.push(BinaryData {
|
|
||||||
md5_code: md5,
|
|
||||||
raw: raw,
|
|
||||||
path: path,
|
|
||||||
});
|
|
||||||
base += offset;
|
|
||||||
}
|
|
||||||
// executable
|
|
||||||
let executable = String::from_utf8_lossy(&BIN_DATA[base..]).to_string();
|
|
||||||
(parsed, executable)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(linux)]
|
#[cfg(linux)]
|
||||||
pub fn configure_permission(&self, prefix: &Path) {
|
pub fn configure_permission(&self, prefix: &Path) {
|
||||||
use std::os::unix::prelude::PermissionsExt;
|
use std::os::unix::prelude::PermissionsExt;
|
||||||
@@ -137,3 +231,132 @@ impl BinaryReader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// Builds a blob in the same layout generate.py writes, so these tests pin the
|
||||||
|
// cross-language format contract as well as the merge rules.
|
||||||
|
fn blob(files: &[(&str, &[u8])], exe: &str) -> &'static [u8] {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
out.extend_from_slice(IDENTIFIER);
|
||||||
|
for (path, data) in files {
|
||||||
|
out.extend_from_slice(&(path.len() as u32).to_be_bytes());
|
||||||
|
out.extend_from_slice(path.as_bytes());
|
||||||
|
out.extend_from_slice(&(data.len() as u32).to_be_bytes());
|
||||||
|
out.extend_from_slice(data);
|
||||||
|
out.extend_from_slice(&[b'a'; MD5_LENGTH]);
|
||||||
|
}
|
||||||
|
out.extend_from_slice(IDENTIFIER);
|
||||||
|
out.extend_from_slice(exe.as_bytes());
|
||||||
|
Box::leak(out.into_boxed_slice())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entry<'a>(files: &'a [BinaryData], path: &str) -> Option<&'a BinaryData> {
|
||||||
|
files
|
||||||
|
.iter()
|
||||||
|
.find(|file| normalize_path(&file.path) == normalize_path(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_the_generate_py_layout() {
|
||||||
|
let (files, exe) = parse(blob(
|
||||||
|
&[("./rustdesk.exe", b"app"), ("./custom.txt", b"cfg")],
|
||||||
|
"./rustdesk.exe",
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(exe, "./rustdesk.exe");
|
||||||
|
assert_eq!(files.len(), 2);
|
||||||
|
assert_eq!(entry(&files, "./custom.txt").unwrap().raw, b"cfg");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_malformed_blobs() {
|
||||||
|
assert!(parse(b"".as_slice()).is_none());
|
||||||
|
assert!(parse(b"notrustd".as_slice()).is_none());
|
||||||
|
// Truncated mid-record rather than panicking on a slice out of range.
|
||||||
|
assert!(parse(b"rustdesk\x00\x00\x00\x40partial".as_slice()).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn without_a_package_the_stock_payload_is_untouched() {
|
||||||
|
let embedded = parse(blob(&[("./rustdesk.exe", b"app")], "./rustdesk.exe")).unwrap();
|
||||||
|
let (files, exe) = merge(embedded, Default::default());
|
||||||
|
assert_eq!(exe, "./rustdesk.exe");
|
||||||
|
assert!(entry(&files, "./rustdesk.exe").is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn renames_the_stock_executable_to_the_package_name() {
|
||||||
|
// x86: the big executable stays in the generic payload and only gets renamed.
|
||||||
|
let embedded = parse(blob(
|
||||||
|
&[("./rustdesk.exe", b"app"), ("./sciter.dll", b"dll")],
|
||||||
|
"./rustdesk.exe",
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
let package = parse(blob(&[("./custom.txt", b"cfg")], "./acme.exe")).unwrap();
|
||||||
|
|
||||||
|
let (files, exe) = merge(embedded, package);
|
||||||
|
|
||||||
|
assert_eq!(exe, "./acme.exe");
|
||||||
|
assert!(entry(&files, "./acme.exe").is_some());
|
||||||
|
assert!(entry(&files, "./rustdesk.exe").is_none());
|
||||||
|
// Untouched neighbours survive.
|
||||||
|
assert_eq!(entry(&files, "./sciter.dll").unwrap().raw, b"dll");
|
||||||
|
assert_eq!(entry(&files, "./custom.txt").unwrap().raw, b"cfg");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn package_entries_win_over_the_generic_payload() {
|
||||||
|
// x64: the customized executable and icons ship in the package instead.
|
||||||
|
let embedded = parse(blob(
|
||||||
|
&[
|
||||||
|
("./data/flutter_assets/assets/icon.ico", b"stock-icon"),
|
||||||
|
("./librustdesk.dll", b"core"),
|
||||||
|
],
|
||||||
|
"./rustdesk.exe",
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
let package = parse(blob(
|
||||||
|
&[
|
||||||
|
("./acme.exe", b"branded"),
|
||||||
|
("./data/flutter_assets/assets/icon.ico", b"acme-icon"),
|
||||||
|
],
|
||||||
|
"./acme.exe",
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (files, exe) = merge(embedded, package);
|
||||||
|
|
||||||
|
assert_eq!(exe, "./acme.exe");
|
||||||
|
assert_eq!(
|
||||||
|
entry(&files, "./data/flutter_assets/assets/icon.ico")
|
||||||
|
.unwrap()
|
||||||
|
.raw,
|
||||||
|
b"acme-icon"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
files
|
||||||
|
.iter()
|
||||||
|
.filter(|f| normalize_path(&f.path)
|
||||||
|
== "data/flutter_assets/assets/icon.ico")
|
||||||
|
.count(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
assert_eq!(entry(&files, "./librustdesk.dll").unwrap().raw, b"core");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn matches_paths_across_separator_styles() {
|
||||||
|
// generate.py emits backslashes when it runs on Windows.
|
||||||
|
let embedded = parse(blob(&[(".\\rustdesk.exe", b"app")], ".\\rustdesk.exe")).unwrap();
|
||||||
|
let package = parse(blob(&[("./custom.txt", b"cfg")], "./acme.exe")).unwrap();
|
||||||
|
|
||||||
|
let (files, exe) = merge(embedded, package);
|
||||||
|
|
||||||
|
assert_eq!(exe, "./acme.exe");
|
||||||
|
assert!(entry(&files, "./acme.exe").is_some());
|
||||||
|
assert!(entry(&files, ".\\rustdesk.exe").is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,18 @@ const APPNAME_RUNTIME_ENV_KEY: &str = "RUSTDESK_APPNAME";
|
|||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
const SET_FOREGROUND_WINDOW_ENV_KEY: &str = "SET_FOREGROUND_WINDOW";
|
const SET_FOREGROUND_WINDOW_ENV_KEY: &str = "SET_FOREGROUND_WINDOW";
|
||||||
|
|
||||||
|
// The extraction directory follows whatever executable the payload asks for, so a
|
||||||
|
// custom client gets its own directory instead of sharing RustDesk's. Falls back to
|
||||||
|
// APP_PREFIX when no package is injected, which keeps stock builds unchanged.
|
||||||
|
fn app_dir_name(exe: &str) -> String {
|
||||||
|
Path::new(&exe.replace('\\', "/"))
|
||||||
|
.file_stem()
|
||||||
|
.and_then(|stem| stem.to_str())
|
||||||
|
.map(|stem| stem.trim().to_lowercase())
|
||||||
|
.filter(|stem| !stem.is_empty())
|
||||||
|
.unwrap_or_else(|| APP_PREFIX.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
fn is_timestamp_matches(dir: &Path, ts: &mut u64) -> bool {
|
fn is_timestamp_matches(dir: &Path, ts: &mut u64) -> bool {
|
||||||
let Ok(app_metadata) = std::str::from_utf8(APP_METADATA) else {
|
let Ok(app_metadata) = std::str::from_utf8(APP_METADATA) else {
|
||||||
return true;
|
return true;
|
||||||
@@ -71,7 +83,7 @@ fn setup(
|
|||||||
} else {
|
} else {
|
||||||
// home dir
|
// home dir
|
||||||
if let Some(dir) = dirs::data_local_dir() {
|
if let Some(dir) = dirs::data_local_dir() {
|
||||||
dir.join(APP_PREFIX)
|
dir.join(app_dir_name(&reader.exe))
|
||||||
} else {
|
} else {
|
||||||
eprintln!("not found data local dir");
|
eprintln!("not found data local dir");
|
||||||
return None;
|
return None;
|
||||||
|
|||||||
Reference in New Issue
Block a user