From bb43a64d3d6613740be073bb98ce389c4ae00a3b Mon Sep 17 00:00:00 2001 From: dime-online Date: Tue, 6 Oct 2026 18:23:00 -0400 Subject: [PATCH] fix: prefer the pinned bundled adb over one found on PATH --- src/halo_frame_installer/usb.py | 16 ++++++---- tests/test_usb.py | 53 +++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/halo_frame_installer/usb.py b/src/halo_frame_installer/usb.py index bb6bb13..2254d87 100644 --- a/src/halo_frame_installer/usb.py +++ b/src/halo_frame_installer/usb.py @@ -82,17 +82,23 @@ def resolve_adb(selected: str | None = None) -> Path: if not path.is_file() or path.name.lower() not in ("adb", "adb.exe"): raise USBError("Select adb.exe from the Android SDK platform-tools folder.") return path - # Prefer an existing client to cooperate with other applications' ADB server. - existing = shutil.which("adb") - if existing: - return Path(existing).resolve() - if sys.platform == "win32": + if sys.platform == "win32" or getattr(sys, "frozen", False): + # Only the hash-verified bundle is known to be the build shipped with + # this installer. The search path (which on Windows also covers the + # working directory) can surface an unrelated or shadowing adb.exe, so + # it is never trusted while a verified tool is available. The bundle + # is consulted on every platform it ships on, not just Windows. included = bundled_adb() if included: return included sdk = Path(os.environ.get("LOCALAPPDATA", str(Path.home()))) / "Android" / "Sdk" / "platform-tools" / "adb.exe" if sdk.is_file(): return sdk + # A client found on the search path may cooperate with another + # application's ADB server, but its provenance is unknown: last resort. + existing = shutil.which("adb") + if existing: + return Path(existing).resolve() raise USBError("USB tools were not found. Install Google's Android SDK Platform Tools and select adb.exe in Advanced settings.") diff --git a/tests/test_usb.py b/tests/test_usb.py index 5843fe1..83fe3af 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -2,6 +2,7 @@ import hashlib import json import socket import subprocess +import sys from unittest.mock import Mock import paramiko @@ -291,3 +292,55 @@ def test_bundled_helper_and_notices_are_verified_then_cached(tmp_path, monkeypat def test_unknown_transport_rejected(): with pytest.raises(ValueError, match='Choose'): Settings('frame', 'secret', transport='other') + + +def _install_fake_bundle(tmp_path, monkeypatch): + root = tmp_path / 'bundle' / 'resources' / 'usb' + root.mkdir(parents=True) + files = {name: hashlib.sha256(name.encode()).hexdigest() for name in usb.ADB_FILES} + for name in (*usb.ADB_FILES, 'NOTICE.txt'): + (root / name).write_bytes(name.encode()) + manifest = {'version': '37.0.1', 'files': files, 'noticeSha256': hashlib.sha256(b'NOTICE.txt').hexdigest()} + (root / 'manifest.json').write_text(json.dumps(manifest)) + monkeypatch.setattr(usb.sys, 'frozen', True, raising=False) + monkeypatch.setattr(usb.sys, '_MEIPASS', str(root.parent.parent), raising=False) + monkeypatch.setenv('LOCALAPPDATA', str(tmp_path / 'local')) + return tmp_path / 'local' / 'HaloFrameInstaller' / 'usb-tools' / '37.0.1' / 'adb.exe' + + +def _expose_adb_on_search_path(tmp_path, monkeypatch): + folder = tmp_path / 'search-path' + folder.mkdir() + # shutil.which("adb") needs an executable file named exactly "adb" on + # POSIX and finds "adb.exe" via the Windows suffix rules, so provide both. + for name in ('adb', 'adb.exe'): + shadow = folder / name + shadow.write_bytes(b'shadowing adb') + shadow.chmod(0o755) + # shutil.which on Windows searches the working directory before PATH. + monkeypatch.chdir(tmp_path) + monkeypatch.setenv('PATH', str(folder)) + return (folder / ('adb.exe' if sys.platform == 'win32' else 'adb')).resolve() + + +def test_hash_verified_bundle_is_preferred_over_search_path_adb(tmp_path, monkeypatch): + bundled = _install_fake_bundle(tmp_path, monkeypatch) + _expose_adb_on_search_path(tmp_path, monkeypatch) + assert usb.resolve_adb() == bundled + + +def test_search_path_adb_is_used_when_no_bundle_exists(tmp_path, monkeypatch): + monkeypatch.setattr(usb.sys, 'frozen', True, raising=False) + monkeypatch.setattr(usb.sys, '_MEIPASS', str(tmp_path / 'no-bundle'), raising=False) + monkeypatch.setenv('LOCALAPPDATA', str(tmp_path / 'local')) + shadow = _expose_adb_on_search_path(tmp_path, monkeypatch) + assert usb.resolve_adb() == shadow + + +def test_explicitly_selected_adb_wins_over_bundle_and_search_path(tmp_path, monkeypatch): + _install_fake_bundle(tmp_path, monkeypatch) + _expose_adb_on_search_path(tmp_path, monkeypatch) + chosen = tmp_path / 'chosen' / 'adb.exe' + chosen.parent.mkdir() + chosen.write_bytes(b'user selected adb') + assert usb.resolve_adb(str(chosen)) == chosen.resolve()