diff --git a/CLAUDE.md b/CLAUDE.md index 23cf40b4..cd0526de 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,7 +35,7 @@ python3 -I tools/i18n.py check # language files: unused strings, los pugwash flags: `--quiet --no-check --debug --no-colour --force-colour --no-log --offline --no-harbour`. -To test on a device: `./do_release.sh test`, copy `dist/PortMaster.zip` over (stream it over ssh: `scp` has truncated files on some devices), then on the device run `./PortMaster/harbourmaster --no-check install ./PortMaster.zip` (the `./` prefix makes it install a local file). `harbourmaster install` also takes a URL: `/X.zip.md5` fetches the md5 file and then `X.zip`, and `/X.zip?md5=` checks the download against that md5 (the `md5` parameter isn't sent to the server); both must agree when given together (`raw_download` in `source/__init__.py`). +To test on a device: `./do_release.sh test`, copy `dist/PortMaster.zip` over (stream it over ssh: `scp` has truncated files on some devices), then on the device run `./PortMaster/harbourmaster --no-check install ./PortMaster.zip` (the `./` prefix makes it install a local file). `harbourmaster install` also takes a URL: `/X.zip.md5` fetches the md5 file and then `X.zip`, and `/X.zip?md5=` checks the download against that md5 (the `md5` parameter isn't sent to the server). Given together, `/X.zip.md5?md5=` downloads `X.zip` straight away and checks it against the link's md5, without fetching the md5 file; older PortMasters fetch the md5 file and then `X.zip`, so DeviceHub sends this form to every version (`raw_download` in `source/__init__.py`). ## Local-dev mode (HM_TESTING) diff --git a/PortMaster/pylibs/harbourmaster/source/__init__.py b/PortMaster/pylibs/harbourmaster/source/__init__.py index 02b9c0d9..945274b3 100644 --- a/PortMaster/pylibs/harbourmaster/source/__init__.py +++ b/PortMaster/pylibs/harbourmaster/source/__init__.py @@ -52,7 +52,13 @@ def raw_download(save_path, file_url, callback=None, file_name=None, md5_source= if file_name is None: file_name = url_info.path.rsplit('/', 1)[-1] - if file_name.endswith('.md5') or file_name.endswith('.md5sum'): + if (file_name.endswith('.md5') or file_name.endswith('.md5sum')) and md5_query is not None: + ## "/X.zip.md5?md5=": the md5 is right here, so the .md5 file isn't fetched; X.zip is. + ## (Older PortMasters fetch the .md5 file and then X.zip, so the one link works for both.) + file_name = file_name.rsplit('.', 1)[0] + file_url = urlunparse(url_info._replace(path=url_info.path.rsplit('.', 1)[0])) + + elif file_name.endswith('.md5') or file_name.endswith('.md5sum'): ## If it is an md5 file, we assume the actual zip is sans the md5/md5sum md5_source = net.fetch_text(file_url) if md5_source is None: @@ -63,14 +69,6 @@ def raw_download(save_path, file_url, callback=None, file_name=None, md5_source= md5_source = md5_source.strip().split(None, 1)[0].lower() if md5_source.strip() else "" - if md5_query is not None and md5_source != md5_query: - ## Two answers to "what should it be": neither can be trusted. - if callback is not None: - callback.message_box(_("Download validation failed.")) - - logger.error(f"Unable to download file: {file_url!r} [md5 file {md5_source!r} != url md5 {md5_query!r}]") - return None - file_name = file_name.rsplit('.', 1)[0] file_url = urlunparse(url_info._replace(path=url_info.path.rsplit('.', 1)[0])) diff --git a/tests/test_source.py b/tests/test_source.py index 461cf6f9..0c5dea19 100644 --- a/tests/test_source.py +++ b/tests/test_source.py @@ -235,17 +235,21 @@ def test_raw_download_md5_file(tmp_path, callback, served): def test_raw_download_md5_file_and_query(tmp_path, callback, served): + ## The md5 is in the link, so the .md5 file isn't fetched: the zip is downloaded and checked. + ## (DeviceHub sends these: older PortMasters fetch the .md5 file, then the zip, from the same link.) url = f"https://example.com/port.zip.md5?md5={served.md5}" - assert hm_source.raw_download(tmp_path, url, callback=callback) is not None - assert served.got == ["https://example.com/port.zip"] + result = hm_source.raw_download(tmp_path, url, callback=callback) + assert result is not None and result['name'] == "port.zip" + assert served.fetched == [] and served.got == ["https://example.com/port.zip"] -def test_raw_download_md5_file_and_query_disagree(tmp_path, callback, served): - ## Which one is right? Neither is trusted: nothing is downloaded. +def test_raw_download_md5_file_and_wrong_query(tmp_path, callback, served): + ## The link's md5 is what the download is checked against. result = hm_source.raw_download(tmp_path, f"https://example.com/port.zip.md5?md5={'0' * 32}", callback=callback) - assert result is None and served.got == [] + assert result is None and served.fetched == [] assert callback.message_boxes == ["Download validation failed."] + assert not (tmp_path / "port.zip").exists() def test_raw_download_no_md5(tmp_path, callback, served):