Commit Graph

138 Commits

Author SHA1 Message Date
Eric Seidel f633b0a34b test: add coverage for state recovery, download validation, rollback, and resume edge cases (#323)
* test: add coverage for state recovery, download validation, rollback, and resume edge cases

Add 18 new unit tests across 4 test modules to increase confidence in
the updater's most sensitive code paths — areas that are hard to test
manually and where bugs could brick apps.

- state_recovery_tests: corrupt/missing/truncated state files, crash
  during boot with missing artifacts, crash recovery event queuing,
  client_id preservation across state corruption
- download_validation_tests: download failures, empty downloads,
  non-zstd data, malformed server responses, network failures
  preserving existing patch state
- rollback_unit_tests: rollback of non-existent patches, multi-patch
  rollback
- resume_edge_case_tests: sidecar without partial file, server ignoring
  Range header, failed download preserving sidecar for retry, resume
  after failure

* fix: replace 'Reinit' with 'Reinitialize' for cspell

* fix: assert state files exist before corrupting them in tests

Ensures corruption tests fail loudly if the hardcoded file paths
(patches_state.json, state.json, patches/) don't match the actual
constants used by the updater, rather than silently passing.

* style: fix formatting and clippy warnings in new tests

- Apply cargo fmt to new test code
- Remove needless borrows on PATCH_BYTES (clippy)
2026-04-02 13:49:30 -07:00
Eric Seidel 0cb43c6349 style: apply cargo fmt and add formatting check to CI (#319)
Runs `cargo fmt --check` in the rust_crate CI action to catch
formatting issues before they land.
2026-04-01 19:30:44 -07:00
Eric Seidel 50954da68a style: fix clippy warnings and add clippy check to CI (#321)
* style: fix clippy warnings and add clippy check to CI

Runs `cargo clippy --all-targets -- -D warnings` in the rust_crate CI
action to catch lint issues before they land.

Fixes: redundant field names, needless returns, unnecessary closures,
io_other_error, needless borrows, single_match, unnecessary_unwrap,
and adds missing safety docs.

* chore: add 'clippy' to cspell dictionary
2026-04-02 02:02:05 +00:00
Eric Seidel 463ace0c56 feat: add resumable downloads with streaming to disk (#313)
* feat: add resumable downloads with streaming to disk

Previously, failed patch downloads were lost entirely — the updater
would re-download from scratch on every retry, creating a doom loop for
users on poor networks. Downloads were also fully buffered in memory.

This changes the download abstraction from returning bytes in memory
(`DownloadFileFn`) to streaming directly to disk with resume support
(`DownloadToPathFn`). The new signature accepts a `resume_from` byte
offset and the default implementation uses HTTP Range headers.

Key changes:
- DownloadToPathFn streams to file, sends Range header when resuming
- DownloadResult returns total_bytes and content_length from server
- DownloadState sidecar JSON tracks URL/patch/size for resume decisions
- compute_resume_offset detects valid partial downloads to resume
- Post-download size validation catches truncated downloads
- cleanup_download_artifacts removes compressed files + sidecars after
  successful install (fixes pre-existing leak of download artifacts)

The fn-pointer signature maps directly to a future C callback for
platform-native download backends (iOS NSURLSession, Android
DownloadManager).

* fix: address self-review issues in download implementation

- Parse Content-Range header for 206 responses to get total file size
  (reqwest's content_length() only returns the partial body size)
- Only append to existing file when server actually returns 206, not
  just when resume_from > 0 (handles servers that ignore Range header)
- Remove incorrect resume_from offset addition to expected_size
- Clean up download artifacts on size mismatch before bailing
- Restore parent directory creation in download_to_path wrapper

* refactor: remove expected_hash from DownloadState

The hash stored in the sidecar was the *inflated* file hash from the
server response — it couldn't validate the compressed partial download
and was never used for resume decisions. The URL match already
determines whether to resume, and the real hash check happens after
inflate using the fresh server response. Simplifies the sidecar to
just url, patch_number, and expected_size.

* test: add coverage for resumable downloads + review fixes

- Add 12 new tests covering:
  - compute_resume_offset: no sidecar, matching sidecar, mismatched URL,
    empty file
  - cleanup_download_artifacts: removes file+sidecar, noop when missing
  - Integration: successful update cleans up artifacts
  - Integration: partial download resumes via 206 with mockito
  - Integration: URL change triggers fresh download
  - parse_content_range_total: valid, missing header, unknown size (*)

- Extract parse_content_range_total as testable helper (was inline chain)
- Add expected_hash back to DownloadState — catches the case where a
  patch is deleted and re-added with same number but different content
- Add "rsplit" to cspell config

* feat: add orphan cleanup for download directory + WriteFile comment

Scan the download directory before each download and remove any files
that don't belong to the current patch number. We own this directory
entirely, so anything from a prior patch, a crashed inflate (.full),
or an unrecognized file is safe to delete. This prevents gradual
accumulation of orphaned partial downloads over time.

Also adds a TODO comment explaining the reuse of WriteFile context for
seek operations (FileOperation lacks a SeekFile variant).

* test: add coverage for hash mismatch, patch number mismatch, corrupt sidecar

- compute_resume_offset_mismatched_hash: same URL but hash changed
  (patch deleted and re-added), verifies fresh download
- compute_resume_offset_mismatched_patch_number: sidecar for different
  patch number, verifies fresh download
- compute_resume_offset_corrupt_sidecar: garbage JSON in sidecar,
  verifies graceful fallback to fresh download

* test: cover download size mismatch and unknown content-length paths

- update_fails_on_download_size_mismatch: mock returns content_length
  that doesn't match total_bytes, verifies error + artifact cleanup
- update_succeeds_when_content_length_unknown: verifies the size
  validation is skipped when content_length is None

* fix: replace fake hash strings to pass cspell

* chore: remove unnecessary TODO comment about FileOperation::SeekFile

* refactor: panic in test download mocks that should never be called

Tests where the download is never reached (patch check fails or no
patch available) now panic instead of returning dummy data, making it
explicit that the mock shouldn't be invoked.

* refactor: add UNEXPECTED_DOWNLOAD/UNEXPECTED_REPORT test constants

Shared panicking constants for test mocks that should never be called.
Tests use these by name instead of writing inline panic closures,
making intent clearer and avoiding uncoverable dead code in closures.

* test: add coverage for handle_download_result

Tests for the download-specific HTTP response handler:
- 200 OK: accepted
- 206 Partial Content: accepted (for resumed downloads)
- 500: rejected with error message

* fix: update missed download fn signatures in tests

Two test sites in updater.rs were not updated to the new
DownloadToPathFn 3-argument signature:
- set_noop_network_hooks in multi_engine_tests used old 1-arg closure
- update_starts_fresh_when_url_changes had unused patch_bytes variable

* test: add coverage for handle_download_result error branches

Mirror the existing handle_network_result_no_internet and
handle_network_result_unknown_error tests for the download variant.
These exercise the connection error and builder error paths in
handle_download_result that were previously uncovered.

* fix: adapt resumable downloads to ureq (post-rebase cleanup)

- Replace reqwest with ureq for download_to_path_default
- Remove handle_download_result (ureq's handle_network_result handles 206)
- Fix TempDir::new("prefix") → TempDir::new() for tempfile crate
- Remove unused Read/Write imports
2026-04-01 15:51:16 +00:00
Eric Seidel 2057fd4f46 fix: resolve all Dependabot security vulnerabilities (#316)
Run `cargo update` to bump transitive dependencies, fixing 10 of 11
alerts (h2, ring, idna, mio, tokio, bytes, time, quinn-proto,
rustls-webpki, unsafe-libyaml).

Replace deprecated `tempdir` dev-dependency with `tempfile` to
eliminate the `remove_dir_all` vulnerability (the last alert).
2026-04-01 15:36:12 +00:00
Eric Seidel c6647a2dfe refactor: replace reqwest with ureq to reduce binary size (#317)
* refactor: replace reqwest with ureq to reduce binary size

reqwest's blocking API is built on top of its async implementation,
pulling in tokio, hyper, futures, and ~84 other transitive dependencies
even though we only make simple synchronous HTTP calls.

ureq is a synchronous-only HTTP client that eliminates the async
runtime entirely. This reduces transitive dependencies from 227 to 143
and the linked dylib from 4.5 MB to 3.7 MB (-18%). The .a archive
drops from 28 MB to 26 MB, but real savings will be larger once
linked into libflutter with dead code stripping.

The network API surface is unchanged — three functions (patch check,
file download, event reporting) using POST/GET with JSON.

* chore: add ureq to spell check dictionary

* refactor: use into_body() instead of body_mut() where response is consumed

* fix: simplify network error matching to avoid fragile string checks

Consolidate HostNotFound, ConnectionFailed, and all Io errors into
a single network-error arm instead of pattern-matching on error
message strings that could change across OS versions or locales.

* chore: add TODO for misleading network error message
2026-04-01 08:23:48 -07:00
Eric Seidel 4ff2839cdb feat: add tests and docs for boot state machine (#309)
* test: test api calls

* chore: add docs

* chore: fix cspell

* fix: use no-op network hooks in multi_engine tests

Set no-op network hooks after init_for_testing so the fire-and-forget
thread spawned by report_launch_success completes instantly without
network I/O, preventing leaked threads from interfering with subsequent
serial tests that use mock servers.
2026-03-30 23:01:50 +00:00
Eric Seidel 6d0e4a1193 chore(deps): bump Rust and Dart dependencies (#315)
* chore(deps): bump Rust and Dart dependencies

Bump Rust dependencies in library/ and patch/:
- comde: 0.2.3 → 0.3.1 (library), 0.2.3 → 0.3.0 (patch)
- zip: 0.6.4 → 3.0.0 (breaking: FileOptions → SimpleFileOptions)
- android_logger: 0.13.0 → 0.15.0
- mockall: 0.12.1 → 0.13.1
- serial_test: 2.0.0 → 3.2.0
- cbindgen: 0.24.0 → 0.28.0

Bump Dart dev dependency in shorebird_code_push/:
- ffigen: upper bound <17.0.0 → <19.0.0

Updated zip API usage (FileOptions → SimpleFileOptions) and
adjusted test assertion for changed error message.

Binary size impact (macOS release, arm64):
- libupdater.a: +83 KB (+0.28%)
- libupdater.dylib: +34 KB (+0.75%)

Closes #206, #271, #273.

* chore: add EOCD to cspell dictionary

The zip 3.0 crate changed its error message to reference "EOCD"
(End of Central Directory), which cspell doesn't recognize.
2026-03-30 15:50:44 -07:00
Eric Seidel ed8cea1463 fix: improve inflate error handling and validate compressed patches (#314)
* fix: improve inflate error handling and validate compressed patches

Addresses #2989: "pipe reader has been dropped" / "failed to fill whole
buffer" errors during patch inflation were masking the real root cause.

Three changes:

1. Join the decompression thread and propagate its error as the primary
   failure, rather than fire-and-forget logging. The patching thread's
   broken-pipe error is a side-effect, not the cause. Also drop the pipe
   reader before joining to avoid deadlock when patching fails.

2. Validate the downloaded compressed patch (non-empty, valid zstd magic
   bytes) before attempting decompression, so corrupt/truncated downloads
   produce a clear error instead of cryptic pipe errors.

3. Log the download size in download_to_path to help diagnose truncated
   downloads in the field.

* docs: expand comment on drop(fresh_r) to clarify deadlock risk

* feat: log app_id, patch number, and version before download

* test: add inflate tests for corrupt data and invalid magic

* test: cover decompression-error-as-primary-cause path in inflate

Uses a valid zstd frame followed by a corrupt second frame so that
bipatch::Reader::new succeeds but decompression fails midway, verifying
that the decompression error is reported as the primary cause.
2026-03-27 12:54:00 -07:00
Brandon DeRosier eeec42efb7 feat: add enhanced error messages for file operations (#310)
* feat: add enhanced error messages for file operations

Add a file_errors module that provides context-aware error messages
for file operations. When file operations fail, users now see:
- The specific operation that failed (create, read, write, rename, etc.)
- The full path involved
- Helpful hints about possible causes based on error type
- Android-specific hints for permission errors (SELinux, Work Profile,
  MDM/Knox policies, app cloning features)

This helps diagnose issues like "Permission denied (os error 13)" by
indicating which operation failed and suggesting possible causes.
2026-02-04 12:36:39 -08:00
Eric Seidel 08fb9df932 fix: Rare bug if rollback happens during second update call
The scenario is:

1. User is running patch 2 (booted successfully, so last_booted_patch = 2)
2. While the app is still running, they call the check-for-update API
3. Patch 3 is downloaded and installed (next_boot_patch = 3)
4. Before the app restarts, they check again and patch 4 is available
5. The buggy code is supposed to delete patch 3 (never booted), but instead deletes patch 2 (the last known-good patch)
6. Patch 4 is set as next_boot_patch

If patch 4 boots fine, nobody notices. But if patch 4 fails to boot and the system tries to roll back to patch 2, those artifacts are gone.
2026-01-29 10:18:14 -08:00
Eric Seidel 8691c8f60e feat: add verification_mode config option (#308)
* feat: move patch verification from boot time to install time

* feat: make it switchable

* chore: update comments

* fix: test invalid yaml

* chore: update readme

* feat: add more comments to readme

* doc: more readme updates

* chore: rename to patch_verification
2026-01-11 15:04:59 -08:00
Bryan Oltman 9db198a634 fix: checking for update should not overwrite good next patch (#307)
* chore: remove unnecessary mutability of self in next_boot_patch

* fix: checking for update should not overwrite good next patch
2025-12-19 13:18:08 -05:00
Bryan Oltman 58a5bcc0b0 chore: remove unnecessary mutability of self in next_boot_patch (#305) 2025-12-19 10:02:33 -05:00
Bryan Oltman 76f005940d feat: add uuid to updater state, patch check request (#300)
* feat: add uuid to updater state

* add client_id to patch check request

* cleanup

* comments

* cleanup

* more comments

* delete commented-out code

* Update library/src/cache/updater_state.rs

Co-authored-by: Eric Seidel <eric@shorebird.dev>

* formatting

---------

Co-authored-by: Eric Seidel <eric@shorebird.dev>
2025-10-29 15:57:06 -04:00
Bryan Oltman 8bfe1bac47 fix: separate validation checks from next_boot_patch getter (#297)
* fix: separate validation checks from next_boot_patch getter

* coverage

* coverage

* coverage
2025-10-08 17:55:29 -04:00
Bryan Oltman ab23721e35 fix: roll back patches in check_for_downloadable_update (#270)
* fix: roll back patches in check_for_downloadable_update

* Update docs
2025-02-12 20:29:06 +00:00
Bryan Oltman 78c84e5bf7 chore: more logging 2025-02-07 14:18:03 -05:00
Bryan Oltman 67f8643242 chore: add more info-level logging around patch downloads (#267) 2025-02-07 14:07:19 -05:00
Bryan Oltman a4a7255796 feat: configure logging on linux (#263) 2025-01-28 10:28:48 -05:00
Bryan Oltman 69468a0c9f fix multiple definitions of patch_base 2025-01-24 15:00:09 -05:00
Bryan Oltman 707346df33 fix multiple definitions of patch_base 2025-01-24 14:55:41 -05:00
Bryan Oltman ba52a62b5d feat: change updater to support intel macs (#262)
* feat: change updater to support intel macs

* cleanup

* Fix targeting in patch_base function
2025-01-24 13:58:30 -05:00
Bryan Oltman 71b5ed65fa feat: add windows support (#259)
* feat: add windows support

* Cleanup
2024-12-20 14:35:28 -08:00
Felix Angelov 54e1e2ce2f fix(updater): re-export shorebird_check_for_update for backward compat (#258) 2024-12-19 15:51:07 -06:00
Bryan Oltman 38aadee1c5 feat: enable updater logging on windows (#256) 2024-12-18 14:10:37 -08:00
Bryan Oltman a6c761f50f fix: build updater with static runtime to prevent flutter engine build linking errors (#254)
* fix: build updater with static runtime to prevent flutter engine build linking errors

* Update library/.cargo/config.toml

Co-authored-by: Eric Seidel <eric@shorebird.dev>

* cspell

---------

Co-authored-by: Eric Seidel <eric@shorebird.dev>
2024-12-17 21:34:20 +00:00
Bryan Oltman 1e4efce65f fix: make tests pass on Windows (#252)
* fix: make tests pass on Windows

* Run ci on all supported building OSes

* Add os name to CI step

* Build rust crates on all oses

* tweak

* Only run rust on multiple oses for now
2024-12-11 12:09:04 -05:00
Bryan Oltman b4775d30dd feat: support macOS (#247)
* feat: support macOS

* fix tests
2024-12-05 17:53:45 -05:00
Eric Seidel 6298d37d86 chore: add cspell checking and make pass (#244)
* chore: add cspell checking and make pass

* chore(shorebird_code_push): minor improvements to example (#242)

* chore(shorebird_code_push): v2.0.2 (#243)

* chore: fix cspell

---------

Co-authored-by: Felix Angelov <felix@shorebird.dev>
2024-11-20 13:50:16 -05:00
Felix Angelov ee3f5ec669 feat(shorebird_code_push): track support (#232)
* feat(shorebird_code_push): track support

* cleanup and add todos

* docs

* run ffigen

* use c_char instead of char

* Add channel support

* tests

* tests

* update podfile.lock

* Update example to include tracks selector

---------

Co-authored-by: Bryan Oltman <bryan@shorebird.dev>
Co-authored-by: Bryan Oltman <bryanoltman@gmail.com>
2024-11-12 14:58:50 -05:00
Bryan Oltman c06b5df4e9 fix: reset patches when updater state fails to deserialize (#233)
* fix: reset patches when updater state fails to deserialize

* cleanup
2024-11-08 22:10:30 +00:00
Bryan Oltman 0988c854af Revert "feat: generate a rollout group number on UpdaterState creation (#227)" (#231)
This reverts commit 8e7ec5a9b6.
2024-11-07 09:34:32 -05:00
Felix Angelov 6f1be35bd3 feat(shorebird_code_push): rewrite Dart API (#225) 2024-11-04 12:22:37 -06:00
Bryan Oltman 8e7ec5a9b6 feat: generate a rollout group number on UpdaterState creation (#227)
* feat: generate a rollout group number on UpdaterState creation

* update test

* use inclusive range
2024-10-30 17:40:46 -04:00
Bryan Oltman 5ace5b84f5 chore: improve logging (#226) 2024-10-30 12:33:02 -04:00
Bryan Oltman 9294c545e8 feat: prepend all log messages with "[shorebird]" (#221)
* feat: prepend all log messages with "[shorebird]"

* update doc

* PR feedback
2024-10-30 11:53:20 -04:00
Bryan Oltman b2d5eced03 feat: add message to patch event (#210) 2024-08-30 17:03:43 +00:00
Bryan Oltman e4e4c57b59 chore: log when reporting successful launch (#209) 2024-08-30 16:30:38 +00:00
Bryan Oltman bec79a4f8d fix: update current_boot_patch to always return currently running patch (#202)
* fix: don't return true from isNewPatchAvailableForDownload if the new patch will not be installed

* fix: update current_boot_patch to always return currently running patch

* add C api tests

* add extra test check
2024-07-25 21:13:58 +00:00
Bryan Oltman 7c559c0915 fix: don't return true from isNewPatchAvailableForDownload if the new patch will not be installed (#201) 2024-07-25 16:50:55 -04:00
Bryan Oltman 75de37ccb3 feat: report patch downloads (#195)
* feat: add rollback support

* Cleanup

* add test

* docs and cleanup

* feat: report patch downloads
2024-07-23 13:53:53 -04:00
Bryan Oltman 9ab417882c refactor: make patch_check_request a constructor (#196) 2024-07-23 13:48:43 -04:00
Bryan Oltman a9fa67c95a feat: add rollback support (#194)
* feat: add rollback support

* Cleanup

* add test

* docs and cleanup

* log full PatchCheckResponse

* remove unnecessary file permission spec from test helper
2024-07-23 10:41:01 -04:00
Bryan Oltman 6ba524716c fix: report launch failure when a patch was in the process of booting on app start (#189)
* report launch failure when a patch was in the process of booting on app start

* refactor: extract shared fake patch test logic into function

* cleanup

* remove commented-out code

* cleanup

* Cleanup

* cleanup

* rename

* update comments

* add todo

* remove added logs

* update comments

* make patch_event a proper constructor

* Split with_mut_state out from with_state

* update comment

* feat: track known_bad_patches instead of highest_seen_patch to support rollbacks (#191)

* feat: track known_bad_patches instead of highest_seen_patch to support rollbacks

* docs

* do not send patch number, do not install already installed patches

* remove patch number from PatchCheckRequest

* tests

* more tests

* fix merge issues
2024-07-19 17:41:04 -04:00
Bryan Oltman 6c36ab68b9 refactor: extract shared fake patch test logic into function (#190)
* refactor: extract shared fake patch test logic into function

* cleanup
2024-07-17 12:50:09 -04:00
Bryan Oltman 3073a76dbc refactor: only check whether a patch failed to boot on initialization (#186)
* fix: track patches while they are booting

* update comment

* update comment

* add test

* refactor: only check whether a patch failed to boot on initialization

* fix test
2024-07-16 16:39:53 -04:00
Bryan Oltman a18b90f01c fix: track patches while they are booting (#185)
* fix: track patches while they are booting

* update comment

* update comment

* add test

* only init UpdaterState if set_config succeeds

* pr feedback

* cleanup

* pr feedback

* add todo referencing github bug re: patch install failure events

* document lifetime of currently_booting_patch

* more details about on_init lifetime

* add todo re: validation

* update test comments

* update test to regenerate patch manager when simulating a fresh start

* Introduce InitError for better testing

* fix test

* add extra assert in PatchManager on_init_tests
2024-07-16 15:23:02 -04:00
Bryan Oltman 3cae153d11 refactor: remove duplicated sent_patch_check_request code (#182)
* refactor: remove duplicated sent_patch_check_request code

* maintain debug log
2024-07-12 17:16:40 -04:00
Bryan Oltman 3da6c38ef4 feat: add timestamp to patch events (#179)
* feat: add timestamp to patch events

* fix test
2024-06-20 15:47:59 -04:00