Skip to content

Fix/update metadata#52

Closed
fufesou wants to merge 3 commits into
masterfrom
fix/update-metadata
Closed

Fix/update metadata#52
fufesou wants to merge 3 commits into
masterfrom
fix/update-metadata

Conversation

@fufesou

@fufesou fufesou commented Jun 5, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

Release Notes

  • New Features

    • Added cryptographic verification of software updates before installation to enhance security.
    • Introduced update verification UI to show progress while validating updates.
    • Added support for signed release metadata to ensure authentic updates.
  • Improvements

    • Enhanced update downloads with cancellation support.
    • Implemented SHA-256 integrity validation for all update packages across platforms.

fufesou added 3 commits June 4, 2026 08:33
Signed-off-by: fufesou <linlong1266@gmail.com>
Signed-off-by: fufesou <linlong1266@gmail.com>
Signed-off-by: fufesou <linlong1266@gmail.com>
@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR implements a comprehensive verified software update system with cryptographic signing and integrity checks. It adds Python tooling for update metadata generation, refactors the Rust updater module for SHA-256 verification, introduces HTTP timeouts and strict TLS enforcement, implements platform-specific secure update flows for Windows and macOS, and updates the Flutter UI for async download coordination.

Changes

Verified Update Infrastructure and Implementation

Layer / File(s) Summary
Update metadata generation and verification tools
res/generate_update_metadata.py, res/test_generate_update_metadata.py
Python CLI tool (fragment, sign, verify, check-version subcommands) for generating, signing with Ed25519, and verifying RustDesk update metadata. Full unit test suite exercises all code paths including fragment creation, deterministic signing, signature validation, artifact tamper detection, and version checking.
Release URL and version parsing
src/common.rs
New crate-visible helpers validate GitHub release tag URLs (HTTPS, exact path shape, no query/fragment), extract stable display versions from release IDs, derive metadata/signature download URLs, and implement finish_software_update_check for TLS-cache-aware version comparison and discovery-response handling. Includes unit tests for URL acceptance/rejection and version validation.
HTTP client and network timeout infrastructure
src/hbbs_http/http_client.rs, src/hbbs_http/downloader.rs, src/hbbs_http/account.rs, src/hbbs_http/record_upload.rs
Add force_strict_tls parameter to HTTP client constructors for strict certificate validation. Introduce bounded timeout constants (30 seconds) for download requests, HEAD/GET operations, and chunk reads. Update all callsites and downloader error handling to respect timeouts; reformatted stale-file-removal logging. Unit tests validate timeout constants and strict-TLS helper behavior.
Windows verified update implementation
src/platform/windows.rs
Add update_to_verified(file, expected_sha256) entrypoint with reparse-point safety checks, temp-file verification, SHA-256 validation, and MSI path quoting. Introduce VerifiedUpdateFile wrapper and helpers for verified temp-path naming and temp-file classification. Remove old update_to in favor of verified path. Comprehensive unit tests cover reparse-point rejection, open flags, filename rules, and SHA-256 verification.
macOS verified DMG update and mount handling
src/platform/macos.rs, src/platform/privileges_scripts/update.scpt
Refactor macOS update handling: replace fixed temp dir with process-specific OnceLock<PathBuf>, add update_from_verified_dmg_sidecar and update_to_verified_dmg public APIs for sidecar verification and secure DMG mounting with Drop-guard detachment. Rewrite update_daemon_agent as fallible function with pid argument for AppleScript. Update AppleScript to stage bundle into temp verified dir, apply root ownership, perform atomic swap, and include EXIT trap for cleanup. update_extracted now calls update_me_from_app_dir instead of spawning updater. Extensive test module covers temp-dir behavior, cleanup, mount args, and metadata rejection.
Updater module verified artifact pipeline
src/updater.rs
Major refactor of check_update to implement verified-artifact flow: platform/arch/format detection, strict GitHub release-download URL parsing, metadata/signature sidecar fetching with verification, artifact integrity validation (size + SHA256), and safe temp-file install with atomic rename. Add bounded HTTP timeouts for all downloads. Extensive unit tests cover URL validation, format mapping, metadata mismatches, cache/temp cleanup on SHA mismatch, and symlink replacement semantics.
Flutter FFI layer verified update support
src/flutter_ffi.rs
Add verified-download-url-* resolution for release pages, new helpers to push Flutter update-me error/ready events, verified artifact computation including Windows MSI detection. Extend main_set_common update handling: download-new-version now verifies metadata before downloading; update-me verifies artifact SHA256/size, dispatches to platform-specific verified update flows, triggers Flutter callbacks, and cleans up on failure.
Flutter UI async update and dialog flow
flutter/lib/desktop/pages/desktop_home_page.dart, flutter/lib/desktop/widgets/update_progress.dart
Convert update button to async handler awaiting handleUpdate. Refactor handleUpdate to async Future that fetches verified download URL, opens download-progress dialog, and implements cancellable cancel-downloader workflow with dialog dismissal. Add dialog helpers for "verifying update" modal, standardized update error display with GitHub rate-limit special-casing, and "preparing for installation" modal. Update UpdateProgress widget to accept additional cancellation state/callback. Remove macOS extraction logic; submission now directly switches to installation-preparation UI. Register update-me-ready and update-me event handlers for error display without retry.
CI/CD workflow updates for update metadata signing
.github/workflows/flutter-build.yml, .github/workflows/flutter-nightly.yml, .github/workflows/flutter-tag.yml
Add publish-update-metadata input to reusable workflow (default false). Introduce detect-update-metadata-signing job for gating. Conditionally generate and upload update-metadata fragment JSONs (EXE, MSI, Sciter EXE, DMG), upload signed binaries/DMGs, adjust Publish Release step conditions. Add new publish-signed-update-metadata job to download fragments/artifacts, run metadata tests, sign combined JSON, verify against artifacts, and publish metadata+signature. Flutter nightly sets publish-update-metadata: false; Flutter tag workflow adds upload-tag input and sets publish-update-metadata: true. Pin sciter-rs dependency.
Update verification localization keys
src/lang/ar.rs through src/lang/vi.rs (45 files)
Add two new translation keys ("Verifying update" and "Please wait while {} verifies the update.") to all language localization maps with placeholder (empty) values pending translation.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

This is a substantial, intricate, high-density logic PR with multi-pattern changes across cryptographic tooling, platform-specific update flows, HTTP networking, Flutter UI, and CI/CD orchestration. It introduces new verification pipelines, safe temp-file handling, and security-critical features (EdDSA signing, SHA-256 validation, reparse-point checks) requiring careful review across multiple subsystems with interdependencies.

🐰 A veritable feast of updates so grand,
With signatures signed by a cryptographic hand,
SHA-256 checks and DMGs secured tight,
Flutter now waits as the downloads take flight!
Platform by platform, the artifacts dance,
In orchestrated CI/CD's digital trance. 🔐✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Fix/update metadata' is vague and uses generic terminology. While it refers to update metadata changes in the PR, it doesn't clearly summarize the main objective—implementing verified update metadata signing, validation, and a complete metadata pipeline. Revise the title to be more specific about the primary changes, such as: 'Add verified update metadata signing and validation pipeline' or 'Implement signed update metadata verification for release artifacts'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/update-metadata
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch fix/update-metadata

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 31

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/flutter-build.yml:
- Around line 854-887: The macOS metadata fragment and upload steps ("Generate
update metadata fragment", "Upload macOS DMG fragment", "Upload signed macOS
DMG") are currently gated only by UPDATE_METADATA_SIGNING_ENABLED and can run
when the DMG wasn't actually codesigned; update each step's if-expression to
also require the same macOS signing prerequisite used by the codesign/notarize
step (the same env var or condition that the codesign step checks, e.g. the
MACOS_SIGNING_* or MACOS_CODESIGNING_ENABLED variable) so unsigned DMGs cannot
enter the signed-metadata path, and add an early validation step that fails fast
when PUBLISH_UPDATE_METADATA==true && UPDATE_METADATA_SIGNING_ENABLED==true but
the macOS signing material/flag is missing.
- Around line 1024-1029: The GitHub Action usage for publishing signed updates
currently pins softprops/action-gh-release@v1 which uses an older Node runtime;
update the step that declares "Publish signed update release" by changing the
action reference in the step using softprops/action-gh-release (the line
beginning with uses: softprops/action-gh-release@v1) to
softprops/action-gh-release@v3, keep the existing inputs (prerelease, tag_name,
files) as-is unless v3 introduces breaking input changes, and run the workflow
to verify the publish job still succeeds with the new runtime.

In `@flutter/lib/desktop/widgets/update_progress.dart`:
- Around line 24-31: The initial verification path handles backend errors from
bind.mainGetCommon('verified-download-url-$releasePageUrl') by passing raw error
text to _showUpdateError, but it doesn't normalize GitHub rate-limit messages
like _onError does; update the code after the downloadUrl.startsWith('error:')
check to detect the same throttling/rate-limit pattern used in _onError and
replace or map that backend error to the friendly throttling message before
calling _showUpdateError (i.e., normalize the error string for the
'verified-download-url-*' branch to the same user-facing message _onError uses).

In `@res/generate_update_metadata.py`:
- Around line 42-46: The current display_version_from_release_id function only
accepts plain semver and must be extended to accept workflow-supported tags like
"v1.4.6-1" or "1.4.6-1"; update the regex used in
display_version_from_release_id (where re.fullmatch is called) to allow an
optional hyphenated build number suffix (e.g. /v?(\d+\.\d+\.\d+)(?:-\d+)?$/) and
continue returning only the captured X.Y.Z group so check-version correctly
recognizes and strips the -N suffix.

In `@res/test_generate_update_metadata.py`:
- Around line 219-225: The test
test_check_version_accepts_parseable_tags_rejects_mismatch_and_skips_nightly
misses coverage for build-suffixed tags like vX.Y.Z-N; update it to also assert
that run_script("check-version", "--release-id", "v1.4.6-1", "--version",
"1.4.6-1") and run_script("check-version", "--release-id", "1.4.6-1",
"--version", "1.4.6-1") succeed (returncode 0 / no error), and add a negative
case asserting that run_script("check-version", "--release-id", "v1.4.7-1",
"--version", "1.4.6-1", check=False) has a nonzero returncode; keep the existing
nightly assertion. Use the same test function name and the existing
run_script/check-version calls to locate where to add these assertions.

In `@src/common.rs`:
- Around line 953-959: Tighten the GitHub URL validation around the existing url
and update_url checks by rejecting URLs with explicit ports or embedded
credentials: after parsing into url (Url::parse(update_url)? and the existing
scheme/host checks), add checks that url.port().is_none(),
url.username().is_empty(), and url.password().is_none(); if any fail, bail! with
a clear message including update_url. Also add regression tests that exercise a
URL with an explicit port (e.g., https://github.com:8443/...) and a URL with
credentials (e.g., https://user@github.com/...) to ensure resp.url cannot be
used to inject non‑canonical release URLs.

In `@src/core_main.rs`:
- Around line 26-35: parse_macos_update_args currently treats any
present-but-invalid second argument as MacosUpdateArgs::App, which hides bad
invocations and bypasses the DMG update path called by platform::update_me();
change the logic in parse_macos_update_args so that: if args.get(1) is None ->
return Ok(MacosUpdateArgs::App); if args.get(1) is Some and ends_with(".dmg") ->
return Ok(MacosUpdateArgs::Dmg(dmg_path.clone())); otherwise return Err(...)
indicating a malformed `--update <path>` (e.g., invalid file extension) so bad
`--update foo.pkg` or wrong-cased names fail fast instead of falling through to
App.

In `@src/flutter_ffi.rs`:
- Around line 2981-2989: The code re-fetches and re-verifies artifact metadata
by calling crate::updater::verified_update_artifact_for_download_url(&_value)
during install; instead, propagate the already-verified artifact metadata
(sha256 and size) from the earlier download step into this install code path and
use that data to construct the artifact instead of calling
verified_update_artifact_for_download_url again. Change the caller to pass the
verified fields (e.g., sha256 and size or an Artifact struct) into the
function/block that currently binds let artifact = ..., remove the network call
and the Err path that logs and calls push_update_me_error, and use the passed-in
verified values to verify the on-disk file before installing. Ensure references
to the symbols artifact, _value, verified_update_artifact_for_download_url, and
push_update_me_error are updated accordingly so no second network fetch occurs.

In `@src/hbbs_http/downloader.rs`:
- Around line 181-189: The client warm-up currently awaits
tokio::time::timeout(create_http_client_async_with_url(...)) without observing
rx_cancel.recv(), so cancel(id) is ignored during warm-up; change this to use
tokio::select! to race create_http_client_async_with_url (with
DOWNLOAD_REQUEST_TIMEOUT) against rx_cancel.recv() and the timeout error,
returning early (bail or propagate a cancel error) if rx_cancel.recv() signals
cancellation, otherwise proceed to assign the client on success; refer to the
create_http_client_async_with_url call, DOWNLOAD_REQUEST_TIMEOUT, and
rx_cancel.recv() when updating the warm-up block so it matches the cancel
handling used for HEAD/GET/chunk stages.

In `@src/lang/ca.rs`:
- Around line 748-749: The two Catalan localization entries for the keys
"Verifying update" and "Please wait while {} verifies the update." are empty;
update the tuple values in src/lang/ca.rs to proper Catalan strings (replace the
empty "" values for those keys). For example, set "Verifying update" ->
"Verificant l'actualització" and "Please wait while {} verifies the update." ->
"Espereu mentre {} verifica l'actualització." ensuring the {} placeholder is
preserved exactly.

In `@src/lang/cs.rs`:
- Around line 748-749: In src/lang/cs.rs fill in Czech translations for the two
new localization keys "Verifying update" and "Please wait while {} verifies the
update." (the tuple entries currently have empty strings); update the second
string to include the "{}" placeholder preserved in the Czech sentence so
runtime interpolation still works (e.g., translate the messages into natural
Czech while keeping the placeholder token intact).

In `@src/lang/da.rs`:
- Around line 748-749: Add Danish translations for the two new localization keys
currently left empty: replace the empty string paired with "Verifying update"
and the empty string paired with "Please wait while {} verifies the update." in
src/lang/da.rs with appropriate Danish text (e.g., "Bekræfter opdatering" and
"Vent venligst mens {} bekræfter opdateringen."). Keep the placeholders intact
(the {} token) and ensure punctuation and capitalization follow the surrounding
translations.

In `@src/lang/de.rs`:
- Around line 748-749: The two new localization entries in src/lang/de.rs — the
tuple keys ("Verifying update", "") and ("Please wait while {} verifies the
update.", "") — are missing German translations; replace the empty strings with
appropriate German text (e.g., "Überprüfung des Updates" for "Verifying update"
and "Bitte warten, während {} das Update prüft." for the second) so the update
verification dialog shows localized strings instead of blanks.

In `@src/lang/el.rs`:
- Around line 748-749: Fill the two empty Greek translation entries in
src/lang/el.rs for the update-verification copy: replace the tuple for
"Verifying update" and the tuple for "Please wait while {} verifies the update."
with appropriate Greek strings (e.g., "Επαλήθευση ενημέρωσης" for "Verifying
update" and "Παρακαλώ περιμένετε ενώ το {} επαληθεύει την ενημέρωση." for the
templated sentence), keeping the original keys unchanged and preserving the
placeholder {} in the second string.

In `@src/lang/ptbr.rs`:
- Around line 748-749: The two new translation entries ("Verifying update" and
"Please wait while {} verifies the update.") in src/lang/ptbr.rs currently have
empty values; replace the empty strings with proper Portuguese translations
(e.g., "Verificando atualização" for "Verifying update" and "Aguarde enquanto {}
verifica a atualização." for the formatted string) so the update dialog shows
text; ensure the second entry preserves the {} placeholder exactly as in the
key.

In `@src/lang/ro.rs`:
- Around line 748-749: The two localization entries ("Verifying update", "") and
("Please wait while {} verifies the update.", "") must not have empty values —
replace the empty RHS with appropriate Romanian translations (e.g. "Se verifică
actualizarea" for "Verifying update" and "Vă rugăm să așteptați în timp ce {}
verifică actualizarea." for the templated string) so users see meaningful
messages; update these entries in src/lang/ro.rs (the tuple keys "Verifying
update" and "Please wait while {} verifies the update.") ensuring the
placeholder {} is preserved exactly in the translated string.

In `@src/lang/ru.rs`:
- Around line 748-749: The two Russian translation entries for the keys
"Verifying update" and "Please wait while {} verifies the update." are empty
causing blank status text; open src/lang/ru.rs, find the tuple entries matching
those exact English strings and replace the empty strings with appropriate
Russian translations (or temporary fallback text) preserving the {} placeholder
in the second string so formatting still works (e.g., supply translations like
"Проверка обновления" and "Пожалуйста, подождите, пока {} проверяет
обновление.").

In `@src/lang/sc.rs`:
- Around line 748-749: Two localization entries ("Verifying update", "") and
("Please wait while {} verifies the update.", "") were added with empty
translations; update those tuples to provide non-empty localized strings. Locate
the tuple list where these keys are defined (the localization entries for the
update verification messages) and replace the empty second elements with
appropriate translated text (preserving the {} placeholder in "Please wait while
{} verifies the update."). Ensure the translated strings match the original
meaning and keep the placeholder syntax intact.

In `@src/lang/sk.rs`:
- Around line 748-749: The two new Slovak translation entries for the
verification UI ("Verifying update" and "Please wait while {} verifies the
update.") are currently empty; populate them in src/lang/sk.rs with proper
Slovak strings (e.g., "Overovanie aktualizácie" and "Počkajte, kým {} overí
aktualizáciu.") replacing the empty values so the UI shows text during update
verification and preserve the placeholder {} in the second string.

In `@src/lang/sl.rs`:
- Around line 748-749: The two localization entries for the keys "Verifying
update" and "Please wait while {} verifies the update." are empty; replace the
empty strings with appropriate Slovene translations (non-empty text) so the
update UI shows labels/messages — update the values for those keys in
src/lang/sl.rs (the tuple entries with "Verifying update" and "Please wait while
{} verifies the update.") to proper Slovene strings.

In `@src/lang/sq.rs`:
- Around line 748-749: Two new Albanian translation entries are left as empty
strings for the keys "Verifying update" and "Please wait while {} verifies the
update." Update the translation tuples in src/lang/sq.rs where these entries
appear (the 2-tuple pairs containing "Verifying update" and "Please wait while
{} verifies the update.") with appropriate Albanian strings so the UI shows
non-empty text during update verification (e.g., provide translations for the
short label and the formatted waiting message, preserving the {} placeholder).

In `@src/lang/sr.rs`:
- Around line 748-749: Missing Serbian translations for the new
update-verification keys will show blank text; add Serbian strings for the keys
"Verifying update" and "Please wait while {} verifies the update." in the
translations list in sr.rs (the tuple entries currently set to ""), replacing
the empty second elements with appropriate Serbian translations that include the
placeholder {} in the second string for the verifier name/agent.

In `@src/lang/sv.rs`:
- Around line 748-749: These two translation entries currently have empty
Swedish values; update the tuple entries for "Verifying update" and "Please wait
while {} verifies the update." in src/lang/sv.rs to include proper Swedish text
(for example, "Verifierar uppdatering" for "Verifying update" and "Vänligen
vänta medan {} verifierar uppdateringen." for the templated message), replacing
the empty strings so the updater UI shows a Swedish label and message.

In `@src/lang/ta.rs`:
- Around line 748-749: Two translation strings are empty for the keys "Verifying
update" and "Please wait while {} verifies the update."; add appropriate Tamil
translations in place of the empty strings so the progress/status text appears
during update verification. Locate the tuple entries where the source strings
are "Verifying update" and "Please wait while {} verifies the update." in the
translations array and replace the second element of each tuple with the correct
Tamil text (e.g., "புதுப்பிப்பை உறுதி செய்கிறோம்" and "புதுப்பிப்பை {} உறுதி
செய்துகொள்ளும் வரை தயவுசெய்து காத்திருக்கவும்." or equivalent), preserving the
placeholder "{}" exactly. Ensure punctuation and string quoting follow the
surrounding Rust translation entries.

In `@src/lang/th.rs`:
- Around line 748-749: Populate the two untranslated entries in the Thai
translations map: replace the empty string for the key "Verifying update" with a
Thai translation such as "กำลังตรวจสอบการอัปเดต" and replace the empty string
for "Please wait while {} verifies the update." with a Thai sentence like
"กรุณารอสักครู่ ขณะที่ {} กำลังตรวจสอบการอัปเดต." Update these values in the
th.rs translation tuples so the keys "Verifying update" and "Please wait while
{} verifies the update." return proper Thai text.

In `@src/lang/tr.rs`:
- Around line 748-749: Add Turkish translations for the two empty localization
entries whose keys are "Verifying update" and "Please wait while {} verifies the
update." in the tr language map; replace the empty strings with appropriate
Turkish phrases (e.g., "Güncelleme doğrulanıyor" and "Lütfen {} güncellemeyi
doğrularken bekleyin." or similar) so the verification dialog shows proper
Turkish text.

In `@src/lang/tw.rs`:
- Around line 748-749: Add Traditional Chinese translations for the two new keys
in src/lang/tw.rs: replace the empty strings for the keys "Verifying update" and
"Please wait while {} verifies the update." with appropriate Traditional Chinese
text (e.g., "正在驗證更新" and "請稍候,{} 正在驗證更新。") so status/progress messages are not
blank during update verification; ensure the translations preserve the
placeholder {} in the second string.

In `@src/lang/uk.rs`:
- Around line 748-749: Replace the two empty translation strings for the
verification UI: change the tuple ("Verifying update", "") to include the
Ukrainian "Перевірка оновлення" and change ("Please wait while {} verifies the
update.", "") to "Зачекайте, будь ласка, поки {} перевіряє оновлення." so the
keys "Verifying update" and "Please wait while {} verifies the update." have
proper Ukrainian translations.

In `@src/lang/vi.rs`:
- Around line 748-749: The two localization entries ("Verifying update", "") and
("Please wait while {} verifies the update.", "") in src/lang/vi.rs are missing
Vietnamese translations and therefore show up blank; replace the empty strings
with appropriate Vietnamese translations (e.g., "Đang kiểm tra bản cập nhật" for
"Verifying update" and "Vui lòng chờ trong khi {} đang kiểm tra bản cập nhật."
for "Please wait while {} verifies the update.") ensuring the placeholder {} is
preserved exactly in the second string and that the translations are added as
the second element of the tuple for the keys "Verifying update" and "Please wait
while {} verifies the update.".

In `@src/platform/macos.rs`:
- Around line 1070-1073: The code currently calls
copy_dmg_to_update_temp_file(dmg_path) before verifying artifact.size, which can
duplicate a large/malicious DMG into temp; instead, perform the size/hash check
against the original file first using the signed metadata. Reorder to call
verify_dmg_file_size_and_sha256 (or add/overload a helper that accepts a file
path) on dmg_path using artifact.size and artifact.sha256 immediately after
verify_dmg_sidecar_metadata, and only if that passes call
copy_dmg_to_update_temp_file(dmg_path) to create the temp copy; update function
signatures as needed (e.g., make verify_dmg_file_size_and_sha256 accept &Path or
&str rather than &mut File) and adjust callers verify_dmg_sidecar_metadata,
copy_dmg_to_update_temp_file, and the surrounding logic to reflect the new
order.

In `@src/updater.rs`:
- Around line 489-496: The download request uses
client.get(download_url).send()? with no timeout, so a stalled transfer can
hang; update the GET call in ensure_verified_update_artifact (the code that
creates response from client.get(download_url)) to apply the same
UPDATE_HTTP_REQUEST_TIMEOUT used elsewhere by calling
.timeout(Duration::from_secs(UPDATE_HTTP_REQUEST_TIMEOUT)) (or the existing
constant) before .send(), so the request will error on timeout and the rest of
the flow (write_verified_update_artifact, error handling) remains unchanged;
reference the variables/client: client, download_url, and the response handling
that calls write_verified_update_artifact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fe003b9b-c1ea-4b30-981c-6771a8107a60

📥 Commits

Reviewing files that changed from the base of the PR and between 7c41f99 and 1964ad6.

📒 Files selected for processing (69)
  • .github/workflows/flutter-build.yml
  • .github/workflows/flutter-nightly.yml
  • .github/workflows/flutter-tag.yml
  • flutter/lib/desktop/pages/desktop_home_page.dart
  • flutter/lib/desktop/widgets/update_progress.dart
  • libs/hbb_common
  • res/generate_update_metadata.py
  • res/test_generate_update_metadata.py
  • src/common.rs
  • src/core_main.rs
  • src/flutter_ffi.rs
  • src/hbbs_http/account.rs
  • src/hbbs_http/downloader.rs
  • src/hbbs_http/http_client.rs
  • src/hbbs_http/record_upload.rs
  • src/lang/ar.rs
  • src/lang/be.rs
  • src/lang/bg.rs
  • src/lang/ca.rs
  • src/lang/cn.rs
  • src/lang/cs.rs
  • src/lang/da.rs
  • src/lang/de.rs
  • src/lang/el.rs
  • src/lang/eo.rs
  • src/lang/es.rs
  • src/lang/et.rs
  • src/lang/eu.rs
  • src/lang/fa.rs
  • src/lang/fi.rs
  • src/lang/fr.rs
  • src/lang/ge.rs
  • src/lang/gu.rs
  • src/lang/he.rs
  • src/lang/hi.rs
  • src/lang/hr.rs
  • src/lang/hu.rs
  • src/lang/id.rs
  • src/lang/it.rs
  • src/lang/ja.rs
  • src/lang/ko.rs
  • src/lang/kz.rs
  • src/lang/lt.rs
  • src/lang/lv.rs
  • src/lang/ml.rs
  • src/lang/nb.rs
  • src/lang/nl.rs
  • src/lang/pl.rs
  • src/lang/pt_PT.rs
  • src/lang/ptbr.rs
  • src/lang/ro.rs
  • src/lang/ru.rs
  • src/lang/sc.rs
  • src/lang/sk.rs
  • src/lang/sl.rs
  • src/lang/sq.rs
  • src/lang/sr.rs
  • src/lang/sv.rs
  • src/lang/ta.rs
  • src/lang/template.rs
  • src/lang/th.rs
  • src/lang/tr.rs
  • src/lang/tw.rs
  • src/lang/uk.rs
  • src/lang/vi.rs
  • src/platform/macos.rs
  • src/platform/privileges_scripts/update.scpt
  • src/platform/windows.rs
  • src/updater.rs

Comment on lines +854 to 887
- name: Install Python
if: env.UPLOAD_ARTIFACT == 'true' && env.PUBLISH_UPDATE_METADATA == 'true' && env.UPDATE_METADATA_SIGNING_ENABLED == 'true'
uses: actions/setup-python@v5
with:
python-version: "3.x"

- name: Generate update metadata fragment
if: env.UPLOAD_ARTIFACT == 'true' && env.PUBLISH_UPDATE_METADATA == 'true' && env.UPDATE_METADATA_SIGNING_ENABLED == 'true'
shell: bash
env:
TAG_NAME: ${{ env.TAG_NAME }}
VERSION: ${{ env.VERSION }}
ARCH: ${{ matrix.job.arch }}
run: |
python3 -m pip install cryptography
python3 res/generate_update_metadata.py fragment --artifact "./rustdesk-${VERSION}-${ARCH}.dmg" --artifact-url "https://github.com/rustdesk/rustdesk/releases/download/${TAG_NAME}/rustdesk-${VERSION}-${ARCH}.dmg" --platform macos --arch "$ARCH" --format dmg --fragment-out "./fragments/macos-${ARCH}-dmg.json"

- name: Upload macOS DMG fragment
if: env.UPLOAD_ARTIFACT == 'true' && env.PUBLISH_UPDATE_METADATA == 'true' && env.UPDATE_METADATA_SIGNING_ENABLED == 'true'
uses: actions/upload-artifact@master
with:
name: macos-${{ matrix.job.arch }}-dmg.json
path: ./fragments/macos-${{ matrix.job.arch }}-dmg.json

- name: Upload signed macOS DMG
if: env.UPLOAD_ARTIFACT == 'true' && env.PUBLISH_UPDATE_METADATA == 'true' && env.UPDATE_METADATA_SIGNING_ENABLED == 'true'
uses: actions/upload-artifact@master
with:
name: macos-${{ matrix.job.arch }}-dmg
path: ./rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.dmg

- name: Publish DMG package
if: env.UPLOAD_ARTIFACT == 'true'
if: env.UPLOAD_ARTIFACT == 'true' && (env.PUBLISH_UPDATE_METADATA != 'true' || env.UPDATE_METADATA_SIGNING_ENABLED != 'true')
uses: softprops/action-gh-release@v1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t feed unsigned macOS DMGs into the signed-metadata path.

These steps run even when the macOS codesign/notarize step was skipped, so the workflow can publish signed metadata that points at an unsigned macOS update artifact. The client-side macOS updater is expected to reject that artifact, which turns this into a release-time breakage rather than a fallback path.

Gate the macOS fragment/upload path on the same signing prerequisite as the codesign step, or fail early when metadata publishing is requested without macOS signing material. Based on learnings, the macOS updater intentionally requires SHA256 verification plus a valid codesign --verify --deep --strict bundle signature, so unsigned DMGs should not enter the signed-metadata path.

🧰 Tools
🪛 actionlint (1.7.12)

[error] 887-887: the runner of "softprops/action-gh-release@v1" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🪛 zizmor (1.25.2)

[error] 856-856: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 873-873: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 880-880: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 887-887: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[info] 887-887: action functionality is already included by the runner (superfluous-actions): use gh release in a script step

(superfluous-actions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/flutter-build.yml around lines 854 - 887, The macOS
metadata fragment and upload steps ("Generate update metadata fragment", "Upload
macOS DMG fragment", "Upload signed macOS DMG") are currently gated only by
UPDATE_METADATA_SIGNING_ENABLED and can run when the DMG wasn't actually
codesigned; update each step's if-expression to also require the same macOS
signing prerequisite used by the codesign/notarize step (the same env var or
condition that the codesign step checks, e.g. the MACOS_SIGNING_* or
MACOS_CODESIGNING_ENABLED variable) so unsigned DMGs cannot enter the
signed-metadata path, and add an early validation step that fails fast when
PUBLISH_UPDATE_METADATA==true && UPDATE_METADATA_SIGNING_ENABLED==true but the
macOS signing material/flag is missing.

Comment on lines +1024 to +1029
- name: Publish signed update release
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/flutter-build.yml"

echo "== Snippet around requested lines =="
sed -n '1000,1060p' "$FILE" | nl -ba | sed -n '1,80p'

echo
echo "== All occurrences of softprops/action-gh-release in repo =="
rg -n "softprops/action-gh-release@v" -S .github/workflows || true

echo
echo "== Snippet around publish signed update release step (wider context) =="
# show from 980 to 1120 to include dependencies/steps
sed -n '980,1120p' "$FILE" | nl -ba | sed -n '1,160p'

Repository: fufesou/rustdesk

Length of output: 158


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/flutter-build.yml"

echo "== Snippet around requested lines (best-effort) =="
sed -n '1000,1060p' "$FILE" | cat -n

echo
echo "== All occurrences of softprops/action-gh-release in repo =="
rg -n "softprops/action-gh-release@v" -S .github/workflows || true

echo
echo "== Wider context around publish signed update release step =="
sed -n '960,1120p' "$FILE" | cat -n

Repository: fufesou/rustdesk

Length of output: 13136


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE=".github/workflows/flutter-build.yml"

rg -n "publish_signed:|Sign update metadata|Verify update metadata|Publish signed update release|softprops/action-gh-release@v1" "$FILE"

echo
echo "== Context: job block around publish_signed =="
# Print a wider slice around the signed job
# (use grep to locate the job header first)
START="$(rg -n "publish_signed:" "$FILE" | head -n1 | cut -d: -f1)"
# if not found, fallback
if [[ -z "${START:-}" ]]; then START=900; fi
sed -n "$((START-40)), $((START+180))p" "$FILE" | cat -n

Repository: fufesou/rustdesk

Length of output: 1037


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/flutter-build.yml"

# Find the exact line number for the publish step
LINE="$(rg -n "Publish signed update release" "$FILE" | head -n1 | cut -d: -f1)"
echo "Publish signed update release step line: $LINE"

echo
echo "== Containing job header + needs (scan upward) =="
# Scan upward until we find a top-level job key (no leading spaces, ends with colon)
# Print a window of context around that job header.
awk -v L="$LINE" '
  NR<=L {
    if ($0 ~ /^[A-Za-z0-9_.-]+:[[:space:]]*$/) last_job=NR
  }
  END{
    if (last_job=="") last_job=1;
    start=last_job-10; if (start<1) start=1;
    end=L+40;
    for (i=start; i<=end; i++) {
      # We cannot directly print by line number in awk without storing;
      # so re-run with sed for the selected range.
    }
  }
' "$FILE" >/dev/null

# Use sed directly for readability (awk was used only to find job header position)
JOB_HEADER="$(awk -v L="$LINE" '
  NR<=L {
    if ($0 ~ /^[A-Za-z0-9_.-]+:[[:space:]]*$/) last_job=NR
  }
  END{ print last_job }
' "$FILE")"

echo "Job header line: $JOB_HEADER"
echo
sed -n "$((JOB_HEADER-5)),$((JOB_HEADER+60))p" "$FILE" | cat -n

Repository: fufesou/rustdesk

Length of output: 3207


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE=".github/workflows/flutter-build.yml"

LINE="$(rg -n "Publish signed update release" "$FILE" | head -n1 | cut -d: -f1)"
echo "Publish step line: $LINE"

# Find nearest job key above with exactly 2 leading spaces and ending colon
JOB_LINE="$(awk -v L="$LINE" '
  NR<=L {
    if ($0 ~ /^  [A-Za-z0-9_.-]+:[[:space:]]*$/) cand=NR
  }
  END { print cand }
' "$FILE")"

echo "Containing job header line: $JOB_LINE"

echo
sed -n "$((JOB_LINE-10)),$((JOB_LINE+80))p" "$FILE" | cat -n

Repository: fufesou/rustdesk

Length of output: 3886


🌐 Web query:

softprops/action-gh-release v1 node runtime version actionlint outdated Actions runtime

💡 Result:

To address the GitHub Actions runtime deprecations and the corresponding softprops/action-gh-release updates, you should upgrade to version 3.0.0. Major Update for softprops/action-gh-release - softprops/action-gh-release version 3.0.0 was released on April 12, 2026, to migrate the action's runtime from Node 20 to Node 24 [1][2]. - This change is required because GitHub Actions has deprecated Node 20 runtimes [3]. If you are still using v2 (specifically versions prior to 2.6.2), your workflows may encounter deprecation warnings or compatibility issues as GitHub completes its transition to Node 24 [1][3]. - If your environment specifically requires a Node 20-compatible version, the last supported version is v2.6.2 [4][1]. actionlint and Runtime Version Checks - actionlint is a static analysis tool that detects outdated or deprecated GitHub Actions runtimes [5]. - Following the GitHub Actions migration to Node 24, actionlint was updated to support and recognize the node24 runtime [6]. - If you encounter actionlint errors regarding "outdated" runtimes or "invalid runner" warnings after upgrading your action, ensure that you are using a version of actionlint that supports Node 24 (generally v1.7.11 or newer) [6]. In some cases, if you are developing your own actions, you may need to update your action.yml to specify runs: using: 'node24' [7][8]. Recommendation - Update your workflow to use softprops/action-gh-release@v3 [4][2]. - Ensure your actionlint version is up to date to avoid false positives or failure to detect necessary runtime upgrades [6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE=".github/workflows/flutter-build.yml"

LINE="$(rg -n "Publish signed update release" "$FILE" | head -n1 | cut -d: -f1)"
echo "Publish step line: $LINE"

# Find nearest job key above with exactly 2 leading spaces and ending colon
JOB_LINE="$(awk -v L="$LINE" '
  NR<=L {
    if ($0 ~ /^  [A-Za-z0-9_.-]+:[[:space:]]*$/) cand=NR
  }
  END { print cand }
' "$FILE")"

echo "Containing job header line: $JOB_LINE"

echo
sed -n "$((JOB_LINE-10)),$((JOB_LINE+110))p" "$FILE" | cat -n

Repository: fufesou/rustdesk

Length of output: 5396


Upgrade softprops/action-gh-release@v1 for the signed publish job to avoid Node runtime deprecation failures.

The publish-signed-update-metadata job signs + verifies update metadata and then publishes it using uses: softprops/action-gh-release@v1; if GitHub blocks the action’s older runtime, the job can fail at publish after the prior work completed. softprops/action-gh-release moved its runtime to Node 24 in v3, so use softprops/action-gh-release@v3 instead.

Original snippet
      - name: Publish signed update release
        uses: softprops/action-gh-release@v1
        with:
          prerelease: true
          tag_name: ${{ env.TAG_NAME }}
          files: |
🧰 Tools
🪛 actionlint (1.7.12)

[error] 1025-1025: the runner of "softprops/action-gh-release@v1" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🪛 zizmor (1.25.2)

[error] 1025-1025: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[info] 1025-1025: action functionality is already included by the runner (superfluous-actions): use gh release in a script step

(superfluous-actions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/flutter-build.yml around lines 1024 - 1029, The GitHub
Action usage for publishing signed updates currently pins
softprops/action-gh-release@v1 which uses an older Node runtime; update the step
that declares "Publish signed update release" by changing the action reference
in the step using softprops/action-gh-release (the line beginning with uses:
softprops/action-gh-release@v1) to softprops/action-gh-release@v3, keep the
existing inputs (prerelease, tag_name, files) as-is unless v3 introduces
breaking input changes, and run the workflow to verify the publish job still
succeeds with the new runtime.

Comment on lines +24 to +31
final downloadUrl =
await bind.mainGetCommon(key: 'verified-download-url-$releasePageUrl');
if (canceled.value) {
return;
}
if (downloadUrl.startsWith('error:')) {
final error = downloadUrl.replaceFirst('error:', '');
_showUpdateError(releasePageUrl, error);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize rate-limit errors on the initial verification path too.

The GitHub throttling message is only mapped in _onError(). If the first verified-download-url-* lookup fails for the same reason, the dialog falls back to the raw backend error instead of the friendly user message.

Suggested fix
   if (downloadUrl.startsWith('error:')) {
     final error = downloadUrl.replaceFirst('error:', '');
-    _showUpdateError(releasePageUrl, error);
+    if (error.contains(_githubRateLimitErrorMarker)) {
+      _showUpdateError(releasePageUrl, error,
+          userMessage: _githubRateLimitUserMessage);
+    } else {
+      _showUpdateError(releasePageUrl, error);
+    }
     return;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@flutter/lib/desktop/widgets/update_progress.dart` around lines 24 - 31, The
initial verification path handles backend errors from
bind.mainGetCommon('verified-download-url-$releasePageUrl') by passing raw error
text to _showUpdateError, but it doesn't normalize GitHub rate-limit messages
like _onError does; update the code after the downloadUrl.startsWith('error:')
check to detect the same throttling/rate-limit pattern used in _onError and
replace or map that backend error to the friendly throttling message before
calling _showUpdateError (i.e., normalize the error string for the
'verified-download-url-*' branch to the same user-facing message _onError uses).

Comment on lines +42 to +46
def display_version_from_release_id(release_id):
match = re.fullmatch(r"v?(\d+\.\d+\.\d+)", release_id)
if not match:
return None
return match.group(1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Parse the workflow-supported vX.Y.Z-N release tags here.

flutter-tag.yml explicitly publishes metadata for tags like v1.4.6-1 and 1.4.6-1, but this parser only accepts plain semver. In that case check-version silently skips the guard, so a mismatched VERSION can still flow into signed metadata.

Suggested fix
 def display_version_from_release_id(release_id):
-    match = re.fullmatch(r"v?(\d+\.\d+\.\d+)", release_id)
+    match = re.fullmatch(r"v?(\d+\.\d+\.\d+)(?:-\d+)?", release_id)
     if not match:
         return None
     return match.group(1)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@res/generate_update_metadata.py` around lines 42 - 46, The current
display_version_from_release_id function only accepts plain semver and must be
extended to accept workflow-supported tags like "v1.4.6-1" or "1.4.6-1"; update
the regex used in display_version_from_release_id (where re.fullmatch is called)
to allow an optional hyphenated build number suffix (e.g.
/v?(\d+\.\d+\.\d+)(?:-\d+)?$/) and continue returning only the captured X.Y.Z
group so check-version correctly recognizes and strips the -N suffix.

Comment on lines +219 to +225
def test_check_version_accepts_parseable_tags_rejects_mismatch_and_skips_nightly(self):
self.run_script("check-version", "--release-id", "v1.4.6", "--version", "1.4.6")
self.run_script("check-version", "--release-id", "1.4.6", "--version", "1.4.6")
mismatch = self.run_script("check-version", "--release-id", "v1.4.7", "--version", "1.4.6", check=False)
self.assertNotEqual(mismatch.returncode, 0)
nightly = self.run_script("check-version", "--release-id", "nightly", "--version", "1.4.6")
self.assertIn("not an official client update source", nightly.stdout)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Cover the vX.Y.Z-N tags that the release workflow accepts.

This test only exercises plain semver and nightly, so it would not catch the current gap for v1.4.6-1 / 1.4.6-1.

Suggested test additions
     def test_check_version_accepts_parseable_tags_rejects_mismatch_and_skips_nightly(self):
         self.run_script("check-version", "--release-id", "v1.4.6", "--version", "1.4.6")
         self.run_script("check-version", "--release-id", "1.4.6", "--version", "1.4.6")
+        self.run_script("check-version", "--release-id", "v1.4.6-1", "--version", "1.4.6")
+        self.run_script("check-version", "--release-id", "1.4.6-1", "--version", "1.4.6")
         mismatch = self.run_script("check-version", "--release-id", "v1.4.7", "--version", "1.4.6", check=False)
         self.assertNotEqual(mismatch.returncode, 0)
         nightly = self.run_script("check-version", "--release-id", "nightly", "--version", "1.4.6")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@res/test_generate_update_metadata.py` around lines 219 - 225, The test
test_check_version_accepts_parseable_tags_rejects_mismatch_and_skips_nightly
misses coverage for build-suffixed tags like vX.Y.Z-N; update it to also assert
that run_script("check-version", "--release-id", "v1.4.6-1", "--version",
"1.4.6-1") and run_script("check-version", "--release-id", "1.4.6-1",
"--version", "1.4.6-1") succeed (returncode 0 / no error), and add a negative
case asserting that run_script("check-version", "--release-id", "v1.4.7-1",
"--version", "1.4.6-1", check=False) has a nonzero returncode; keep the existing
nightly assertion. Use the same test function name and the existing
run_script/check-version calls to locate where to add these assertions.

Comment thread src/lang/tw.rs
Comment on lines +748 to +749
("Verifying update", ""),
("Please wait while {} verifies the update.", ""),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Please add Traditional Chinese translations for the new verification keys.

Empty values here can cause blank status/progress text during update verification.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lang/tw.rs` around lines 748 - 749, Add Traditional Chinese translations
for the two new keys in src/lang/tw.rs: replace the empty strings for the keys
"Verifying update" and "Please wait while {} verifies the update." with
appropriate Traditional Chinese text (e.g., "正在驗證更新" and "請稍候,{} 正在驗證更新。") so
status/progress messages are not blank during update verification; ensure the
translations preserve the placeholder {} in the second string.

Comment thread src/lang/uk.rs
Comment on lines +748 to +749
("Verifying update", ""),
("Please wait while {} verifies the update.", ""),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Provide non-empty Ukrainian translations for new verification strings.

Both new keys are mapped to "", so update verification UI text will be blank in Ukrainian locale.

💬 Suggested fix
-        ("Verifying update", ""),
-        ("Please wait while {} verifies the update.", ""),
+        ("Verifying update", "Перевірка оновлення"),
+        ("Please wait while {} verifies the update.", "Зачекайте, поки {} перевіряє оновлення."),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
("Verifying update", ""),
("Please wait while {} verifies the update.", ""),
("Verifying update", "Перевірка оновлення"),
("Please wait while {} verifies the update.", "Зачекайте, поки {} перевіряє оновлення."),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lang/uk.rs` around lines 748 - 749, Replace the two empty translation
strings for the verification UI: change the tuple ("Verifying update", "") to
include the Ukrainian "Перевірка оновлення" and change ("Please wait while {}
verifies the update.", "") to "Зачекайте, будь ласка, поки {} перевіряє
оновлення." so the keys "Verifying update" and "Please wait while {} verifies
the update." have proper Ukrainian translations.

Comment thread src/lang/vi.rs
Comment on lines +748 to +749
("Verifying update", ""),
("Please wait while {} verifies the update.", ""),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fill Vietnamese translations for the newly added update-verification keys.

These entries currently resolve to empty strings, which can hide verification status text in the UI.

💬 Suggested fix
-        ("Verifying update", ""),
-        ("Please wait while {} verifies the update.", ""),
+        ("Verifying update", "Đang xác minh bản cập nhật"),
+        ("Please wait while {} verifies the update.", "Vui lòng chờ trong khi {} xác minh bản cập nhật."),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lang/vi.rs` around lines 748 - 749, The two localization entries
("Verifying update", "") and ("Please wait while {} verifies the update.", "")
in src/lang/vi.rs are missing Vietnamese translations and therefore show up
blank; replace the empty strings with appropriate Vietnamese translations (e.g.,
"Đang kiểm tra bản cập nhật" for "Verifying update" and "Vui lòng chờ trong khi
{} đang kiểm tra bản cập nhật." for "Please wait while {} verifies the update.")
ensuring the placeholder {} is preserved exactly in the second string and that
the translations are added as the second element of the tuple for the keys
"Verifying update" and "Please wait while {} verifies the update.".

Comment thread src/platform/macos.rs
Comment on lines +1070 to +1073
let artifact = verify_dmg_sidecar_metadata(dmg_path, metadata_bytes, signature_bytes)?;
let (mut dmg_file, temp_dmg_path) = copy_dmg_to_update_temp_file(dmg_path)?;
verify_dmg_file_size_and_sha256(&mut dmg_file, artifact.size, &artifact.sha256, dmg_path)?;
let temp_dmg_path = temp_dmg_path.to_string_lossy();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the signed artifact size to fail fast before duplicating the DMG.

This path already has artifact.size, but it copies the whole image into /tmp before checking it. A wrong or maliciously oversized file can therefore consume temp disk space twice before the validation fails.

Suggested fix
 fn update_from_verified_dmg_sidecar_(
     dmg_path: &str,
     metadata_bytes: &[u8],
     signature_bytes: &[u8],
 ) -> ResultType<()> {
     let artifact = verify_dmg_sidecar_metadata(dmg_path, metadata_bytes, signature_bytes)?;
+    let source_size = std::fs::metadata(dmg_path)?.len();
+    if source_size != artifact.size {
+        bail!(
+            "DMG size mismatch for {}: expected {}, got {}",
+            dmg_path,
+            artifact.size,
+            source_size
+        );
+    }
     let (mut dmg_file, temp_dmg_path) = copy_dmg_to_update_temp_file(dmg_path)?;
     verify_dmg_file_size_and_sha256(&mut dmg_file, artifact.size, &artifact.sha256, dmg_path)?;
     let temp_dmg_path = temp_dmg_path.to_string_lossy();
     update_from_mounted_dmg_with_options(&temp_dmg_path, true, None)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/platform/macos.rs` around lines 1070 - 1073, The code currently calls
copy_dmg_to_update_temp_file(dmg_path) before verifying artifact.size, which can
duplicate a large/malicious DMG into temp; instead, perform the size/hash check
against the original file first using the signed metadata. Reorder to call
verify_dmg_file_size_and_sha256 (or add/overload a helper that accepts a file
path) on dmg_path using artifact.size and artifact.sha256 immediately after
verify_dmg_sidecar_metadata, and only if that passes call
copy_dmg_to_update_temp_file(dmg_path) to create the temp copy; update function
signatures as needed (e.g., make verify_dmg_file_size_and_sha256 accept &Path or
&str rather than &mut File) and adjust callers verify_dmg_sidecar_metadata,
copy_dmg_to_update_temp_file, and the surrounding logic to reflect the new
order.

Comment thread src/updater.rs
Comment on lines +489 to +496
let mut response = client.get(download_url).send()?;
if !response.status().is_success() {
bail!(
"Failed to download the new version file: {}",
response.status()
);
}
write_verified_update_artifact(file_path, &mut response, expected_size, expected_sha256)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate file and surrounding code
ls -la src/updater.rs || true
rg -n "UPDATE_HTTP_REQUEST_TIMEOUT" src/updater.rs
rg -n "timeout\\(" src/updater.rs
rg -n "download_url" src/updater.rs
rg -n "write_verified_update_artifact" src/updater.rs

# Show relevant sections around the cited lines
sed -n '440,540p' src/updater.rs | nl -ba | sed -n '1,120p'
sed -n '540,700p' src/updater.rs | nl -ba | sed -n '1,200p'

# Extract implementation of write_verified_update_artifact if present
# Use rg to find its definition line numbers.
line=$(rg -n "fn write_verified_update_artifact" src/updater.rs | head -n1 | cut -d: -f1 || true)
if [ -n "${line:-}" ]; then
  start=$((line-30))
  end=$((line+220))
  sed -n "${start},${end}p" src/updater.rs | nl -ba | sed -n "1,260p"
else
  echo "write_verified_update_artifact not defined in src/updater.rs (or different signature)."
  # Search for it in repository
  rg -n "write_verified_update_artifact\\b" -S .
fi

Repository: fufesou/rustdesk

Length of output: 2539


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate file and surrounding code
ls -la src/updater.rs || true
rg -n "UPDATE_HTTP_REQUEST_TIMEOUT" src/updater.rs
rg -n "timeout\\(" src/updater.rs
rg -n "download_url" src/updater.rs
rg -n "write_verified_update_artifact" src/updater.rs

# Show relevant sections around the cited lines
sed -n '440,540p' src/updater.rs | nl -ba | sed -n '1,120p'
sed -n '540,700p' src/updater.rs | nl -ba | sed -n '1,200p'

# Extract implementation of write_verified_update_artifact if present
line=$(rg -n "fn write_verified_update_artifact" src/updater.rs | head -n1 | cut -d: -f1 || true)
if [ -n "${line:-}" ]; then
  start=$((line-30))
  end=$((line+260))
  sed -n "${start},${end}p" src/updater.rs | nl -ba | sed -n "1,300p"
else
  echo "write_verified_update_artifact not defined in src/updater.rs (or different signature)."
  rg -n "write_verified_update_artifact\\b" -S .
fi

Repository: fufesou/rustdesk

Length of output: 2539


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ls -la src/updater.rs || true
rg -n "UPDATE_HTTP_REQUEST_TIMEOUT" src/updater.rs
rg -n "timeout\\(" src/updater.rs
rg -n "download_url" src/updater.rs
rg -n "write_verified_update_artifact" src/updater.rs

echo "----- snippet 440-540 -----"
sed -n '440,540p' src/updater.rs | nl -ba | sed -n '1,120p'
echo "----- snippet 540-700 -----"
sed -n '540,700p' src/updater.rs | nl -ba | sed -n '1,200p'

line=$(rg -n "fn write_verified_update_artifact" src/updater.rs | head -n1 | cut -d: -f1 || true)
if [ -n "${line:-}" ]; then
  start=$((line-30))
  end=$((line+260))
  echo "----- write_verified_update_artifact around line $line -----"
  sed -n "${start},${end}p" src/updater.rs | nl -ba | sed -n "1,300p"
else
  echo "write_verified_update_artifact not defined in src/updater.rs; searching entire repo..."
  rg -n "write_verified_update_artifact\\b" -S .
fi

Repository: fufesou/rustdesk

Length of output: 2567


🏁 Script executed:

#!/bin/bash
set -euo pipefail
ls -la src/updater.rs || true
rg -n "UPDATE_HTTP_REQUEST_TIMEOUT" src/updater.rs
rg -n "timeout\\(" src/updater.rs
rg -n "download_url" src/updater.rs
rg -n "write_verified_update_artifact" src/updater.rs

echo "----- snippet 440-540 -----"
sed -n '440,540p' src/updater.rs | nl -ba | sed -n '1,120p'
echo "----- snippet 540-700 -----"
sed -n '540,700p' src/updater.rs | nl -ba | sed -n '1,200p'

line=$(rg -n "fn write_verified_update_artifact" src/updater.rs | head -n1 | cut -d: -f1 || true)
if [ -n "${line:-}" ]; then
  start=$((line-30))
  end=$((line+260))
  echo "----- write_verified_update_artifact around line $line -----"
  sed -n "${start},${end}p" src/updater.rs | nl -ba | sed -n "1,300p"
else
  echo "write_verified_update_artifact not defined in src/updater.rs; searching entire repo..."
  rg -n "write_verified_update_artifact\\b" -S .
fi

Repository: fufesou/rustdesk

Length of output: 2567


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "UPDATE_HTTP_REQUEST_TIMEOUT" src/updater.rs
rg -n "timeout\\(" src/updater.rs
rg -n "client\\.get\\(download_url\\)" -n src/updater.rs || true
rg -n "write_verified_update_artifact" src/updater.rs
sed -n '470,520p' src/updater.rs | nl -ba
# show definition of write_verified_update_artifact (first ~250 lines around it)
line=$(rg -n "fn write_verified_update_artifact" src/updater.rs | head -n1 | cut -d: -f1 || true)
if [ -n "${line:-}" ]; then
  start=$((line-20)); end=$((line+250))
  sed -n "${start},${end}p" src/updater.rs | nl -ba | sed -n '1,280p'
fi

Repository: fufesou/rustdesk

Length of output: 767


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find create_http_client_with_url definition and inspect its timeout config
line=$(rg -n "fn create_http_client_with_url\\b" src/updater.rs | head -n1 | cut -d: -f1 || true)
echo "create_http_client_with_url at line: ${line:-<not found>}"
if [ -n "${line:-}" ]; then
  start=$((line-30)); end=$((line+160))
  sed -n "${start},${end}p" src/updater.rs | cat -n
fi

# Inspect the exact callsite block around 461-500
echo "----- callsite around line 461-510 -----"
sed -n '440,520p' src/updater.rs | cat -n

# Inspect write_verified_update_artifact to see if it adds any timeout
line2=$(rg -n "fn write_verified_update_artifact\\b" src/updater.rs | head -n1 | cut -d: -f1 || true)
echo "write_verified_update_artifact at line: ${line2:-<not found>}"
if [ -n "${line2:-}" ]; then
  start=$((line2-20)); end=$((line2+140))
  sed -n "${start},${end}p" src/updater.rs | cat -n
fi

Repository: fufesou/rustdesk

Length of output: 9784


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find where create_http_client_with_url is defined
rg -n "fn create_http_client_with_url\\b|create_http_client_with_url\\b" -S . > /tmp/create_http_client_hits.txt || true
head -n 50 /tmp/create_http_client_hits.txt
echo "---- total hits ----"
wc -l /tmp/create_http_client_hits.txt

# Print the definition(s)
rg -n "fn create_http_client_with_url\\b" -S . || true

# If found, print surrounding code for each definition
defs=$(rg -n "fn create_http_client_with_url\\b" -S . | cut -d: -f1 | sort -u)
for f in $defs; do
  echo "===== $f ====="
  l=$(rg -n "fn create_http_client_with_url\\b" "$f" | head -n1 | cut -d: -f1)
  start=$((l-30)); end=$((l+120))
  sed -n "${start},${end}p" "$f" | cat -n
done

# Also check if UPDATE_HTTP_REQUEST_TIMEOUT is used in any client builder elsewhere
rg -n "UPDATE_HTTP_REQUEST_TIMEOUT" -S . || true

Repository: fufesou/rustdesk

Length of output: 7721


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find create_http_client (sync) implementation and the configure_http_client macro usage
rg -n "fn create_http_client\\b" -S src/hbbs_http/http_client.rs src/hbbs_http -S || true
rg -n "configure_http_client!" -S src/hbbs_http/http_client.rs src/hbbs_http -S || true

# Print around create_http_client definition
file="src/hbbs_http/http_client.rs"
line=$(rg -n "fn create_http_client\\b" "$file" | head -n1 | cut -d: -f1 || true)
echo "create_http_client line: ${line:-<not found>}"
if [ -n "${line:-}" ]; then
  start=$((line-60)); end=$((line+180))
  sed -n "${start},${end}p" "$file" | cat -n
fi

# Locate macro definition of configure_http_client
macro_file=$(rg -n "macro_rules!\\s*configure_http_client" -S src | head -n1 | cut -d: -f1 || true)
echo "macro_rules! configure_http_client in: ${macro_file:-<not found>}"
if [ -n "${macro_file:-}" ]; then
  line2=$(rg -n "macro_rules!\\s*configure_http_client" "$macro_file" | head -n1 | cut -d: -f1)
  start=$((line2-20)); end=$((line2+260))
  sed -n "${start},${end}p" "$macro_file" | cat -n
fi

# If macro is in another crate, search workspace root
macro_file2=$(rg -n "configure_http_client" -S src | head -n20 || true)
echo "Top configure_http_client occurrences:"
echo "$macro_file2"

Repository: fufesou/rustdesk

Length of output: 12907


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="src/hbbs_http/http_client.rs"

echo "--- rg timeout* in $file ---"
rg -n "timeout\\b|connect_timeout\\b|read_timeout\\b|write_timeout\\b" "$file" || true

macro_line=$(rg -n "macro_rules!\\s*configure_http_client\\b" "$file" | head -n1 | cut -d: -f1 || true)
echo "macro_line=$macro_line"

if [ -n "${macro_line:-}" ]; then
  start=$((macro_line-30)); if [ "$start" -lt 1 ]; then start=1; fi
  end=$((macro_line+220))
  echo "--- configure_http_client macro around lines $start..$end ---"
  sed -n "${start},${end}p" "$file" | cat -n
fi

Repository: fufesou/rustdesk

Length of output: 10521


Add request timeout to the artifact download request

ensure_verified_update_artifact() downloads the main update file with client.get(download_url).send()? but only the sidecar path applies UPDATE_HTTP_REQUEST_TIMEOUT (via .timeout(...)). The shared HTTP client builder configures TLS/proxy only and does not set a default request timeout, so a stalled artifact transfer can hang the updater thread.

Suggested fix
-        let mut response = client.get(download_url).send()?;
+        let mut response = client
+            .get(download_url)
+            .timeout(UPDATE_HTTP_REQUEST_TIMEOUT)
+            .send()?;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let mut response = client.get(download_url).send()?;
if !response.status().is_success() {
bail!(
"Failed to download the new version file: {}",
response.status()
);
}
write_verified_update_artifact(file_path, &mut response, expected_size, expected_sha256)?;
let mut response = client
.get(download_url)
.timeout(UPDATE_HTTP_REQUEST_TIMEOUT)
.send()?;
if !response.status().is_success() {
bail!(
"Failed to download the new version file: {}",
response.status()
);
}
write_verified_update_artifact(file_path, &mut response, expected_size, expected_sha256)?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/updater.rs` around lines 489 - 496, The download request uses
client.get(download_url).send()? with no timeout, so a stalled transfer can
hang; update the GET call in ensure_verified_update_artifact (the code that
creates response from client.get(download_url)) to apply the same
UPDATE_HTTP_REQUEST_TIMEOUT used elsewhere by calling
.timeout(Duration::from_secs(UPDATE_HTTP_REQUEST_TIMEOUT)) (or the existing
constant) before .send(), so the request will error on timeout and the rest of
the flow (write_verified_update_artifact, error handling) remains unchanged;
reference the variables/client: client, download_url, and the response handling
that calls write_verified_update_artifact.

@fufesou fufesou closed this Jun 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant