From 4bf8e66d665a8d251505de9630845265c47b5cc5 Mon Sep 17 00:00:00 2001 From: Dimitris Panokostas Date: Wed, 22 Jul 2026 14:43:02 +0300 Subject: [PATCH 1/3] fix: harden the 2.6 release Prevent stale release metadata and partial overwrites from reaching artifacts. Tighten host command handling and driver lifecycle behavior, with regression coverage and release documentation. --- .github/workflows/build.yml | 49 ++++++++---- CHANGELOG.md | 46 +++++++++++ Makefile | 43 ++++++---- README.md | 34 +++++--- docs/RELEASING.md | 43 ++++++++++ drivers/ahi/Makefile | 3 +- drivers/ahi/src/v2/uaesnd.audio.asm | 11 ++- drivers/mhi/Makefile | 13 ++- drivers/mhi/src/mhiuae.c | 10 +-- drivers/mhi/src/mhiuae.h | 2 - package/Help/Host-Tools.guide | 14 ++-- src/host-download.c | 119 +++++++++++++++++++++++----- src/host-env.c | 4 + src/host-shell.c | 2 +- src/host_capture.h | 23 ++++-- src/host_clip_command.h | 17 ++-- src/host_download_command.h | 49 ++++++++++-- src/host_env_command.h | 41 +++++++--- tests/test_ahi_driver_source.sh | 3 + tests/test_host_command_builders.c | 105 +++++++++++++++++++++++- tests/test_host_download_command.c | 47 +++++++++-- tests/test_package_layout.sh | 43 ++++++---- tests/test_runtime_source.sh | 32 ++++++++ version.mk | 6 ++ 24 files changed, 623 insertions(+), 136 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 docs/RELEASING.md create mode 100644 tests/test_runtime_source.sh create mode 100644 version.mk diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6ccfe10..95a232c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,46 +17,67 @@ on: permissions: contents: read +concurrency: + group: build-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: runs-on: ubuntu-latest + timeout-minutes: 20 container: sacredbanana/amiga-compiler:m68k-amigaos steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + + - name: Resolve release metadata + shell: sh + run: | + version="$(make -s print-version)" + date="$(make -s print-date)" + if [ "${{ github.ref_type }}" = "tag" ]; then + tag_version="${{ github.ref_name }}" + tag_version="${tag_version#v}" + if [ "$tag_version" != "$version" ]; then + echo "Tag version $tag_version does not match version.mk ($version)" >&2 + exit 1 + fi + fi + echo "VERSION=$version" >> "$GITHUB_ENV" + echo "DATE=$date" >> "$GITHUB_ENV" - name: Build tools - run: make all + run: make VERSION="$VERSION" DATE="$DATE" all - name: Run tests - run: make test + run: make VERSION="$VERSION" DATE="$DATE" test - name: Package tools - run: | - if [ "${{ github.ref_type }}" = "tag" ]; then - VERSION="${{ github.ref_name }}" - else - VERSION="latest" - fi - make package PACKAGE="Host-Tools-${VERSION}.lha" + run: make VERSION="$VERSION" DATE="$DATE" package + + - name: Generate checksum + run: sha256sum "Host-Tools-${VERSION}.lha" > "Host-Tools-${VERSION}.lha.sha256" - name: Upload Artifacts - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: host-tools-package - path: "*.lha" + path: | + *.lha + *.lha.sha256 release: needs: build if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest + timeout-minutes: 10 permissions: contents: write steps: - name: Download Artifacts - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: host-tools-package @@ -64,4 +85,4 @@ jobs: env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} - run: gh release create ${{ github.ref_name }} *.lha --title "Host-Tools ${{ github.ref_name }}" --generate-notes + run: gh release create ${{ github.ref_name }} *.lha *.lha.sha256 --title "Host-Tools ${{ github.ref_name }}" --generate-notes diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..43805fb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,46 @@ + + +# Changelog + +Notable user-visible changes are recorded here. This project follows semantic +versioning for release tags. + +## [2.6] - Unreleased + +### Added + +- Publish a SHA-256 checksum beside each release archive. +- Validate every packaged command, the AmigaGuide, and `mhiuae.library` against + the release version and date before creating an archive. +- Support native tests on both x86-64 and ARM64 hosts in the AmigaOS 3 Docker + image. + +### Changed + +- Preserve trailing line breaks when pasting from POSIX host clipboards. +- Make `host-download` replacements transactional and limit downloads to HTTP, + HTTPS, FTP, and FTPS, including curl redirects. +- Store the POSIX `host-env` file atomically with owner-only permissions and + reject multiline values. +- Allow Ctrl-C cancellation and bound idle waits in status-aware host commands. + +### Fixed + +- Keep the `host-shell` login wrapper within the HostShell command trap limit. +- Signal MHI buffer completion only after Amiberry has consumed a buffer. +- Close `utility.library` when UAESND AHI driver initialization fails. +- Prevent the package-layout test directory from leaking into the recursive MHI + build. +- Propagate release metadata into binaries instead of applying the tag only to + the archive filename. + +## [2.5] - 2026-06-14 + +- Added `host-env` and the UAE MHI MP3 decoder library. +- Added UAESND recording support and driver refinements. + +[2.6]: https://github.com/BlitterStudio/host-tools/compare/v2.5...HEAD +[2.5]: https://github.com/BlitterStudio/host-tools/releases/tag/v2.5 diff --git a/Makefile b/Makefile index 1d18035..e4010c4 100644 --- a/Makefile +++ b/Makefile @@ -3,8 +3,8 @@ TOOLS = host-run host-multiview host-shell host-path host-reveal host-notify host-edit host-clip host-info host-download host-env TEST_BINS = tests/test_host_common.out tests/test_host_command_builders.out tests/test_host_edit_command.out tests/test_host_download_command.out tests/test_host_terminal_filter.out -TEST_SCRIPTS = tests/test_package_layout.sh tests/test_ahi_driver_source.sh -TESTS = $(TEST_BINS) $(TEST_SCRIPTS) +TEST_SCRIPTS = tests/test_package_layout.sh tests/test_ahi_driver_source.sh tests/test_runtime_source.sh +include version.mk COMMON_HEADERS = src/host_common.h src/host_path.h src/host_capture.h src/host_base64.h src/host_clip_command.h src/host_download_command.h src/host_edit_command.h src/host_env_command.h src/host_info_command.h src/host_notify_command.h src/host_powershell.h src/host_reveal_command.h src/host_shell_command.h src/uae_pragmas.h PACKAGE = Host-Tools-$(VERSION).lha PACKAGE_ROOT = Host-Tools @@ -44,19 +44,27 @@ README_ICON = package/icons/readme.info GUIDE_ICON = package/icons/guide.info .SUFFIXES: -.PHONY: all test debug package package-dir ahi ahi-v2 mhi clean +.PHONY: all test test-unit test-package debug package package-dir verify-package print-version print-date ahi ahi-v2 mhi clean all: $(TOOLS) $(AHI_FILES) $(AHI_V2_FILES) $(MHI_FILES) -test: $(TESTS) - @for test in $(TESTS); do \ - case "$$test" in \ - *.sh) sh "$$test" ;; \ - *) ./$$test ;; \ - esac || exit $$?; \ + +test: test-unit test-package + +test-unit: $(TEST_BINS) + @for test in $(TEST_BINS); do \ + ./$$test || exit $$?; \ + done + +test-package: $(TEST_SCRIPTS) + @for test in $(TEST_SCRIPTS); do \ + VERSION="$(VERSION)" DATE="$(DATE)" sh "$$test" || exit $$?; \ done -VERSION = 2.4 -DATE = 2026-06-10 +print-version: + @printf '%s\n' '$(VERSION)' + +print-date: + @printf '%s\n' '$(DATE)' ifeq ($(origin CC),default) CC = m68k-amigaos-gcc @@ -64,8 +72,12 @@ endif INCLUDES = -Isrc CFLAGS = -mcpu=68020 -noixemul -Os -fomit-frame-pointer -std=c99 -Wall -Wextra -Wstrict-prototypes VERFLAGS = -DVERSION_STR="\"$(VERSION)\"" -DDATE_STR="\"$(DATE)\"" -HOST_CC ?= $(shell command -v x86_64-linux-gnu-gcc 2>/dev/null || command -v cc 2>/dev/null || printf cc) -HOST_NATIVE_FLAGS = $(if $(findstring x86_64-linux-gnu-gcc,$(notdir $(HOST_CC))),-B/usr/bin/x86_64-linux-gnu- -fuse-ld=bfd,) +HOST_ARCH = $(shell uname -m) +HOST_GNU_ARCH = $(if $(filter arm64,$(HOST_ARCH)),aarch64,$(HOST_ARCH)) +HOST_CC ?= $(shell command -v $(HOST_GNU_ARCH)-linux-gnu-gcc 2>/dev/null || command -v cc 2>/dev/null || printf cc) +HOST_CC_NAME = $(notdir $(HOST_CC)) +HOST_GNU_PREFIX = $(patsubst %-gcc,%,$(HOST_CC_NAME)) +HOST_NATIVE_FLAGS = $(if $(findstring -linux-gnu-gcc,$(HOST_CC_NAME)),-B/usr/bin/$(HOST_GNU_PREFIX)- -fuse-ld=bfd,) HOST_CFLAGS = -std=c99 -Wall -Wextra -Wstrict-prototypes -Isrc host-run: src/host-run.c $(COMMON_HEADERS) @@ -162,7 +174,10 @@ package-dir: all package/Install $(HELP_GUIDE) $(DRAWER_ICON) $(HELP_ICON) $(INS cp $(MHI_LIBRARY) $(PACKAGE_STAGE)/Libs/MHI/mhiuae.library; \ fi -package: package-dir +verify-package: package-dir + PACKAGE_READY=1 PACKAGE_DIR="$(PACKAGE_DIR)" VERSION="$(VERSION)" DATE="$(DATE)" sh tests/test_package_layout.sh + +package: verify-package rm -f $(PACKAGE) cd $(PACKAGE_DIR) && lha a $(CURDIR)/$(PACKAGE) $(PACKAGE_ROOT) $(PACKAGE_ROOT).info diff --git a/README.md b/README.md index d5293f5..455f6f2 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ host-edit [path2 ...] ### 8. host-clip **Use the host clipboard.** -`host-clip` copies text to the host clipboard or prints the current host clipboard contents. Without text arguments, `host-clip copy` reads standard input verbatim, so multi-line text and command output can be piped or redirected to the host clipboard. Text is converted between the Amiga's ISO-8859-1 character set and the host's encoding: through `iconv` on Linux and macOS, and inherently through PowerShell's Unicode pipeline on Windows. +`host-clip` copies text to the host clipboard or prints the current host clipboard contents. Without text arguments, `host-clip copy` reads standard input verbatim, so multi-line text and command output can be piped or redirected to the host clipboard. Paste also preserves trailing line breaks. Text is converted between the Amiga's ISO-8859-1 character set and the host's encoding: through `iconv` on Linux and macOS, and inherently through PowerShell's Unicode pipeline on Windows. **Usage:** ```shell @@ -105,9 +105,9 @@ host-info ### 10. host-download **Download files through the host.** -`host-download` fetches a URL with the host's `curl` (or `wget`) and saves it to any Amiga path — `RAM:`, hardfiles, and directory mounts all work, because the file is written by the tool through AmigaDOS. The host handles HTTPS/TLS, giving classic AmigaOS access to modern servers. +`host-download` fetches an HTTP, HTTPS, FTP, or FTPS URL with the host's `curl` (or `wget`) and saves it to any Amiga path — `RAM:`, hardfiles, and directory mounts all work, because the file is written by the tool through AmigaDOS. The host handles HTTPS/TLS, giving classic AmigaOS access to modern servers. - **Live Progress**: With a current Amiberry, the file streams to the Amiga as it downloads, with a percentage display when the server reports a size. -- **Safe**: Failed or aborted downloads (Ctrl-C) never leave a partial file behind, and an existing destination is only overwritten with `FORCE`. +- **Transactional**: Data is written to a temporary file beside the destination. Failed, stalled, or aborted downloads (Ctrl-C) never leave a partial result, and `FORCE` preserves the previous file until the replacement is complete. - **Flexible Destination**: With no destination the file is saved in the current directory under its URL name; a directory destination keeps the URL name. **Usage:** @@ -119,6 +119,7 @@ host-download [] [FORCE] **Get and set host user environment variables.** `host-env` reads, writes, removes, and lists environment variables on the host. On Windows it updates the user's persistent environment. On Linux and macOS it writes persistent values to `$HOME/.host-tools-env`; source that file from your host shell startup files if you want future host login shells to import those values automatically. - **Persistent**: Changes are intended for future host processes. +- **Private by Default**: On Linux and macOS, the managed file is written atomically with mode `0600`. Values must fit on one line. - **Scoped Safely**: Existing host shells, desktop apps, and Amiberry's parent process cannot have their live environment changed by a child process. **Usage:** @@ -135,10 +136,18 @@ host-env list - **Amiberry v6.0+** (or a version with updated `uaelib` support). - "Native Code" execution must be enabled in Amiberry settings. -- All tools work on **Linux and macOS hosts**. On **Windows hosts**, `host-path`, `host-download`, `host-clip`, `host-reveal`, `host-info`, and `host-env` are supported with a current Amiberry (PowerShell and Explorer handle the Windows side; `curl.exe` ships with Windows 10 and later). The remaining tools (`host-run`, `host-multiview`, `host-shell`, `host-edit`, `host-notify`) currently require a Linux or macOS host, since their host commands run through the POSIX shell. -- For status-aware tools (`host-reveal`, `host-notify`, `host-clip`, and `host-info`), a newer Amiberry build with the `HostShell_Status` trap reports host command failures immediately. Older builds still work, but use timeout-based completion detection. +- All tools work on **Linux and macOS hosts**. Current Windows support is summarized below. + +| Tool | Linux | macOS | Windows | +| --- | :---: | :---: | :---: | +| `host-run`, `host-multiview`, `host-shell`, `host-edit`, `host-notify` | Yes | Yes | — | +| `host-path`, `host-reveal`, `host-clip`, `host-info`, `host-download`, `host-env` | Yes | Yes | Yes | + +Windows integrations require a current Amiberry. PowerShell and Explorer handle desktop operations, and `curl.exe` ships with Windows 10 and later. + +- For status-aware tools (`host-reveal`, `host-notify`, `host-clip`, `host-info`, and `host-env`), a newer Amiberry build with the `HostShell_Status` trap reports host command failures immediately. Press Ctrl-C to cancel a stuck host command. An idle command times out after approximately 30 seconds on status-aware builds or 5 seconds on older builds. - Linux desktop integration uses `xdg-utils` (`xdg-open`, `xdg-mime`) and GTK's `gtk-launch` when available. Notifications use `notify-send`; clipboard support uses `wl-clipboard`, `xclip`, or `xsel`; file selection in `host-reveal` uses `gdbus` when present. Character set conversion uses `iconv` when present. -- `host-download` uses the host's `curl` or `wget` (`curl.exe` on Windows). Live streaming progress requires an Amiberry build with pipe-based HostShell sessions; on older Linux and macOS builds the tool falls back to a two-phase transfer that downloads on the host first. +- `host-download` uses the host's `curl` or `wget` (`curl.exe` on Windows) and accepts HTTP, HTTPS, FTP, and FTPS URLs. Live streaming progress requires an Amiberry build with pipe-based HostShell sessions; on older Linux and macOS builds the tool falls back to a two-phase transfer that downloads on the host first. ## Exit Codes @@ -203,9 +212,14 @@ To build locally with the same Docker image used by CI: docker run --rm -v "$PWD":/work -w /work sacredbanana/amiga-compiler:m68k-amigaos make all ``` -To run the host-side command-builder tests: +To run only the native command-builder tests: ```shell -make test +make test-unit +``` + +To run the complete test suite, including a fresh cross-build and package verification, use the CI image: +```shell +docker run --rm -v "$PWD":/work -w /work sacredbanana/amiga-compiler:m68k-amigaos make clean test ``` To build a release archive: @@ -213,10 +227,12 @@ To build a release archive: make package ``` -The package target creates `Host-Tools-.lha`, containing a structured `Host-Tools` drawer with the Installer script, command tools, README, AmigaGuide documentation, the UAE and UAESND AHI driver files, and the UAE MHI MP3 decoder library. +The package target creates and verifies `Host-Tools-.lha`, containing a structured `Host-Tools` drawer with the Installer script, command tools, README, AmigaGuide documentation, the UAE and UAESND AHI driver files, and the UAE MHI MP3 decoder library. Native package builds also require `lha`. The Docker build image contains this tool. +Release metadata is defined once in [`version.mk`](version.mk). See [`docs/RELEASING.md`](docs/RELEASING.md) for the release checklist and [`CHANGELOG.md`](CHANGELOG.md) for user-visible changes. + To build with debug output enabled: ```shell make debug diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 0000000..06d4f26 --- /dev/null +++ b/docs/RELEASING.md @@ -0,0 +1,43 @@ + + +# Releasing Host-Tools + +Release metadata lives in `version.mk`. The tag must be `v` followed by that +exact version; CI rejects a mismatch before building. + +## Checklist + +1. Update `VERSION` and `DATE` in `version.mk`. +2. Update the `$VER` line in `package/Help/Host-Tools.guide`. +3. Move the pending changelog entry from `Unreleased` to the release date. +4. Run the complete release build from a clean tree: + + ```shell + docker run --rm -v "$PWD":/work -w /work \ + sacredbanana/amiga-compiler:m68k-amigaos make clean test package + ``` + +5. Confirm that `git status --short` contains only intentional source changes. + `make package` already verifies the package layout and all release version + strings before writing the archive. +6. Review the archive and checksum locally if desired: + + ```shell + lha l "Host-Tools-$(make -s print-version).lha" + shasum -a 256 "Host-Tools-$(make -s print-version).lha" + ``` + +7. Merge the release pull request, then create and push the matching annotated + tag: + + ```shell + version="$(make -s print-version)" + git tag -a "v$version" -m "Host-Tools v$version" + git push origin "v$version" + ``` + +The tag workflow rebuilds and verifies the package, publishes the `.lha` and +`.lha.sha256` artifacts, and creates the GitHub release with generated notes. diff --git a/drivers/ahi/Makefile b/drivers/ahi/Makefile index 762da87..6a41efa 100644 --- a/drivers/ahi/Makefile +++ b/drivers/ahi/Makefile @@ -1,8 +1,7 @@ # SPDX-FileCopyrightText: 2020-2026 Dimitris Panokostas # SPDX-License-Identifier: GPL-3.0-or-later -VERSION ?= 2.4 -DATE ?= 2026-06-10 +include ../../version.mk VASMM68K_FALLBACK := $(shell command -v vasmm68k_mot 2>/dev/null || { test -x /opt/m68k-amigaos/bin/vasmm68k_mot && printf /opt/m68k-amigaos/bin/vasmm68k_mot; } || printf vasmm68k_mot) ifeq ($(origin VASMM68K),undefined) diff --git a/drivers/ahi/src/v2/uaesnd.audio.asm b/drivers/ahi/src/v2/uaesnd.audio.asm index 6bf701b..93b6fb4 100644 --- a/drivers/ahi/src/v2/uaesnd.audio.asm +++ b/drivers/ahi/src/v2/uaesnd.audio.asm @@ -1710,7 +1710,7 @@ initRoutine: moveq #0,d0 call OpenLibrary tst.l d0 - beq.s .end + beq.s .init_failed move.l d0,a6 sub.l a0,a0 move.w #6502,d0 @@ -1722,11 +1722,18 @@ initRoutine: call CloseLibrary move.l ub_ConfigDev(a5),d0 - beq.s .end + beq.s .init_failed move.l d0,a0 move.l cd_BoardAddr(a0),ub_Base(a5) move.l a5,a4 + bra.s .end +.init_failed + move.l ub_UtilBase(a5),a1 + beq.s .end + move.l ub_SysLib(a5),a6 + call CloseLibrary + clr.l ub_UtilBase(a5) .end move.l a4,d0 movem.l (sp)+,d1/a0/a1/a4/a5/a6 diff --git a/drivers/mhi/Makefile b/drivers/mhi/Makefile index e80aba3..ba8afe1 100644 --- a/drivers/mhi/Makefile +++ b/drivers/mhi/Makefile @@ -1,8 +1,7 @@ # SPDX-FileCopyrightText: 2020-2026 Dimitris Panokostas # SPDX-License-Identifier: GPL-3.0-or-later -VERSION ?= 2.4 -DATE ?= 2026-06-10 +include ../../version.mk VERSION_WORDS = $(subst ., ,$(VERSION)) LIB_VERSION = $(word 1,$(VERSION_WORDS)) LIB_REVISION = $(word 2,$(VERSION_WORDS)) @@ -13,8 +12,8 @@ CC = m68k-amigaos-gcc endif BUILD_DIR = build -PACKAGE_DIR = package -LIB_OUT = $(PACKAGE_DIR)/Libs/MHI/mhiuae.library +MHI_PACKAGE_DIR = package +LIB_OUT = $(MHI_PACKAGE_DIR)/Libs/MHI/mhiuae.library SOURCES = src/mhiuae_startup.c src/mhiuae.c HEADERS = src/mhi_abi.h src/mhiuae.h ../../src/uae_pragmas.h CFLAGS = -mcpu=68020 -noixemul -Os -fomit-frame-pointer -std=c99 -Wall -Wextra -Wstrict-prototypes -I../../src @@ -29,14 +28,14 @@ all: $(LIB_OUT) $(BUILD_DIR)/mhiuae.library: $(SOURCES) $(HEADERS) | $(BUILD_DIR) $(CC) $(CFLAGS) $(VERFLAGS) $(LDFLAGS) $(SOURCES) -o $@ -$(LIB_OUT): $(BUILD_DIR)/mhiuae.library | $(PACKAGE_DIR)/Libs/MHI +$(LIB_OUT): $(BUILD_DIR)/mhiuae.library | $(MHI_PACKAGE_DIR)/Libs/MHI cp $< $@ $(BUILD_DIR): mkdir -p $@ -$(PACKAGE_DIR)/Libs/MHI: +$(MHI_PACKAGE_DIR)/Libs/MHI: mkdir -p $@ clean: - rm -rf $(BUILD_DIR) $(PACKAGE_DIR) + rm -rf $(BUILD_DIR) $(MHI_PACKAGE_DIR) diff --git a/drivers/mhi/src/mhiuae.c b/drivers/mhi/src/mhiuae.c index 7128bc6..0921df8 100644 --- a/drivers/mhi/src/mhiuae.c +++ b/drivers/mhi/src/mhiuae.c @@ -55,8 +55,6 @@ APTR i_MHIAllocDecoder(struct Task *task __asm("a0"), ULONG sigmask __asm("d0"), return NULL; } - player->task = task; - player->sigmask = sigmask; player->host_handle = host_handle; player->status = MHIF_STOPPED; base->allocated_decoders++; @@ -84,18 +82,12 @@ void i_MHIFreeDecoder(APTR handle __asm("a3"), struct MHIUAEBase *base __asm("a6 BOOL i_MHIQueueBuffer(APTR handle __asm("a3"), APTR buffer __asm("a0"), ULONG size __asm("d0"), struct MHIUAEBase *base __asm("a6")) { struct MHIUAEPlayer *player = valid_player(handle); - ULONG queued; - (void)base; if (player == NULL || buffer == NULL || size == 0) { return FALSE; } - queued = UaeMHIQueue(player->host_handle, buffer, size, (ULONG)buffer); - if (queued && player->task != NULL && player->sigmask != 0) { - Signal(player->task, player->sigmask); - } - return queued ? TRUE : FALSE; + return UaeMHIQueue(player->host_handle, buffer, size, (ULONG)buffer) ? TRUE : FALSE; } APTR i_MHIGetEmpty(APTR handle __asm("a3"), struct MHIUAEBase *base __asm("a6")) diff --git a/drivers/mhi/src/mhiuae.h b/drivers/mhi/src/mhiuae.h index 0ae2190..1390ac7 100644 --- a/drivers/mhi/src/mhiuae.h +++ b/drivers/mhi/src/mhiuae.h @@ -27,8 +27,6 @@ struct MHIUAEBase { }; struct MHIUAEPlayer { - struct Task *task; - ULONG sigmask; ULONG host_handle; UBYTE status; }; diff --git a/package/Help/Host-Tools.guide b/package/Help/Host-Tools.guide index e9ac1b5..7ef18f2 100644 --- a/package/Help/Host-Tools.guide +++ b/package/Help/Host-Tools.guide @@ -1,5 +1,5 @@ @database -@$VER: Host-Tools.guide 2.4 (2026-06-10) +@$VER: Host-Tools.guide 2.6 (2026-07-22) @index Main @wordwrap @@ -167,7 +167,7 @@ host-clip [copy] host-clip copy < file host-clip paste -Without text arguments, host-clip copy reads standard input verbatim, preserving line breaks. Text is converted between the Amiga ISO-8859-1 character set and the host character set automatically: through iconv on Linux and macOS, and through PowerShell on Windows hosts. +Without text arguments, host-clip copy reads standard input verbatim, preserving line breaks. Pasted text also preserves trailing line breaks. Text is converted between the Amiga ISO-8859-1 character set and the host character set automatically: through iconv on Linux and macOS, and through PowerShell on Windows hosts. @endnode @@ -181,7 +181,7 @@ host-info @endnode @node HostDownload "host-download" -host-download downloads a URL on the host and saves it to an Amiga path. The host's curl or wget performs the transfer, so HTTPS and modern servers work. +host-download downloads an HTTP, HTTPS, FTP, or FTPS URL on the host and saves it to an Amiga path. The host's curl or wget performs the transfer, so HTTPS and modern servers work. Usage: @@ -189,7 +189,7 @@ host-download [] [FORCE] Without a destination, the file is saved in the current directory under its URL filename. A destination naming an existing directory keeps the URL filename. An existing destination file is only overwritten when FORCE is given. -The file is written through AmigaDOS, so any writable volume works, including RAM: and hardfiles. Failed or aborted downloads do not leave a partial file behind. Press Ctrl-C to abort. +The file is written through AmigaDOS, so any writable volume works, including RAM: and hardfiles. It is first written to a temporary file beside the destination and moved into place only after a successful transfer. Failed, stalled, or aborted downloads do not leave a partial file behind. With FORCE, the old destination is preserved until the replacement is ready. Press Ctrl-C to abort. host-download works on Linux, macOS, and Windows hosts. On Windows a current Amiberry build is required. @@ -207,11 +207,11 @@ host-env list On Windows, host-env updates the user's persistent environment through PowerShell and the Windows user environment store. New host processes see the changed values. -On Linux and macOS, host-env writes persistent values to $HOME/.host-tools-env. Source that file from your host shell startup files if you want future host login shells to import those values automatically. +On Linux and macOS, host-env writes persistent values to $HOME/.host-tools-env. The file is replaced atomically and restricted to the host user with mode 0600. Source that file from your host shell startup files if you want future host login shells to import those values automatically. host-env cannot change the live environment of already-running host shells, desktop apps, or Amiberry's parent process. -Variable names must use ASCII letters, digits, and underscores, and must not start with a digit. +Variable names must use ASCII letters, digits, and underscores, and must not start with a digit. Values must not contain line breaks. @endnode @@ -224,4 +224,6 @@ Native Code execution must be enabled in Amiberry for these tools to communicate All tools work on Linux and macOS hosts. On Windows hosts, host-path, host-download, host-clip, host-reveal, host-info, and host-env are supported with a current Amiberry; the remaining tools require a Linux or macOS host. +Host commands used by host-reveal, host-notify, host-clip, host-info, and host-env can be cancelled with Ctrl-C. Idle commands time out after approximately 30 seconds on status-aware Amiberry builds or 5 seconds on older builds. + @endnode diff --git a/src/host-download.c b/src/host-download.c index ae3eee3..22e8d46 100644 --- a/src/host-download.c +++ b/src/host-download.c @@ -11,7 +11,7 @@ #include "host_capture.h" #include "host_download_command.h" -#define DOWNLOAD_STALL_LIMIT 1500 /* ticks without data on builds without the status trap */ +#define DOWNLOAD_STALL_LIMIT 1500 /* ticks without data */ static const char version[] = "$VER: Host-Download " VERSION_STR " (" DATE_STR ")"; @@ -51,6 +51,74 @@ static int lock_is_directory(BPTR lock) return fib.fib_DirEntryType > 0; } +static int make_unused_sidecar_path(const char *destination, const char *kind, + char *path, size_t path_size) +{ + unsigned long token = (unsigned long)FindTask(NULL); + + for (unsigned int attempt = 0; attempt < 100; attempt++) { + BPTR lock; + + if (!host_download_sidecar_path(destination, kind, token, attempt, + path, path_size)) { + return 0; + } + lock = quiet_lock(path); + if (lock == 0) { + return 1; + } + UnLock(lock); + } + return 0; +} + +static int install_download(const char *temp_path, const char *destpath, int force, + char *backup_path, size_t backup_size, + const char **failure) +{ + BPTR lock = quiet_lock(destpath); + + if (lock == 0) { + if (Rename((STRPTR)temp_path, (STRPTR)destpath)) { + return 1; + } + *failure = "Cannot move download to destination"; + return 0; + } + + if (lock_is_directory(lock)) { + UnLock(lock); + *failure = "Destination path is a directory"; + return 0; + } + UnLock(lock); + + if (!force) { + *failure = "Destination appeared during download"; + return 0; + } + if (!make_unused_sidecar_path(destpath, "backup", backup_path, backup_size)) { + *failure = "Cannot reserve a destination backup"; + return 0; + } + if (!Rename((STRPTR)destpath, (STRPTR)backup_path)) { + *failure = "Cannot preserve the existing destination"; + return 0; + } + if (Rename((STRPTR)temp_path, (STRPTR)destpath)) { + if (!DeleteFile((STRPTR)backup_path)) { + printf("\nWarning: old destination remains at %s\n", backup_path); + } + return 1; + } + + if (!Rename((STRPTR)backup_path, (STRPTR)destpath)) { + printf("\nOriginal destination remains at %s\n", backup_path); + } + *failure = "Cannot replace destination"; + return 0; +} + static void show_progress(unsigned long received, long expected) { if (expected > 0) { @@ -69,6 +137,8 @@ int main(int argc, char *argv[]) { static char command[HOST_MAX_COMMAND_LEN]; static char destpath[HOST_MAX_PATH_LEN]; + static char temp_path[HOST_MAX_PATH_LEN]; + static char backup_path[HOST_MAX_PATH_LEN]; static char urlname[108]; static char buffer[4096]; static unsigned char decoded[(sizeof(buffer) * 3) / 4 + 3]; @@ -89,6 +159,7 @@ int main(int argc, char *argv[]) unsigned long shown = (unsigned long)-1; BPTR out_file = 0; BPTR lock; + int temp_exists = 0; int rc = HOST_RETURN_ERROR; const char *failure = NULL; @@ -123,8 +194,8 @@ int main(int argc, char *argv[]) return HOST_RETURN_ERROR; } - if (!host_is_uri(url)) { - printf("Not a URL: %s\n", url); + if (!host_download_url_supported(url)) { + printf("Unsupported URL scheme (use HTTP, HTTPS, FTP, or FTPS): %s\n", url); return HOST_RETURN_ERROR; } @@ -196,6 +267,19 @@ int main(int argc, char *argv[]) return HOST_RETURN_ERROR; } + if (!make_unused_sidecar_path(destpath, "part", temp_path, sizeof(temp_path))) { + HostShell_Close(handle); + printf("Cannot reserve a temporary destination file\n"); + return HOST_RETURN_ERROR; + } + out_file = Open((STRPTR)temp_path, MODE_NEWFILE); + if (out_file == 0) { + HostShell_Close(handle); + printf("Cannot open temporary destination file\n"); + return HOST_RETURN_ERROR; + } + temp_exists = 1; + host_base64_init(&b64); for (;;) @@ -247,13 +331,6 @@ int main(int argc, char *argv[]) } if (data_len > 0) { - if (out_file == 0) { - out_file = Open((STRPTR)destpath, MODE_NEWFILE); - if (out_file == 0) { - failure = "Cannot open destination file"; - break; - } - } if (Write(out_file, (APTR)data, data_len) != data_len) { failure = "Write to destination failed"; break; @@ -284,7 +361,7 @@ int main(int argc, char *argv[]) break; } Delay(1); - if (!status_supported && ++idle_count > DOWNLOAD_STALL_LIMIT) { + if (++idle_count > DOWNLOAD_STALL_LIMIT) { failure = "Timed out waiting for download data"; break; } @@ -313,21 +390,23 @@ int main(int argc, char *argv[]) } } - if (failure == NULL && out_file == 0) { - /* zero length download: still create the destination file */ - out_file = Open((STRPTR)destpath, MODE_NEWFILE); - if (out_file == 0) { - failure = "Cannot open destination file"; + if (out_file != 0) { + if (!Close(out_file) && failure == NULL) { + failure = "Cannot finish temporary destination file"; } + out_file = 0; } - if (out_file != 0) { - Close(out_file); + if (failure == NULL) { + if (install_download(temp_path, destpath, force, + backup_path, sizeof(backup_path), &failure)) { + temp_exists = 0; + } } if (failure != NULL) { - if (out_file != 0) { - DeleteFile((STRPTR)destpath); + if (temp_exists && !DeleteFile((STRPTR)temp_path)) { + printf("\nWarning: partial download remains at %s\n", temp_path); } printf("\n%s: %s\n", failure, url); } else { diff --git a/src/host-env.c b/src/host-env.c index 261e0aa..7e4e4fb 100644 --- a/src/host-env.c +++ b/src/host-env.c @@ -66,6 +66,10 @@ int main(int argc, char *argv[]) if (!require_name(argv[2])) { return HOST_RETURN_ERROR; } + if (!host_env_valid_value(argv[3])) { + printf("Environment variable values cannot contain line breaks\n"); + return HOST_RETURN_ERROR; + } if (windows) { if (!host_append_env_get_command_windows(command, sizeof(command), argv[2])) { printf("Command is too long\n"); diff --git a/src/host-shell.c b/src/host-shell.c index 8d93473..71ee582 100644 --- a/src/host-shell.c +++ b/src/host-shell.c @@ -123,7 +123,7 @@ static int append_size_prefix(char *dest, size_t dest_size, int rows, int cols) int main(int argc, char *argv[]) { static char command[HOST_MAX_COMMAND_LEN]; - static char session_command[HOST_MAX_COMMAND_LEN + 64]; + static char session_command[HOST_MAX_COMMAND_LEN]; static char pending[128]; static char buffer[4096]; BPTR in = 0; diff --git a/src/host_capture.h b/src/host_capture.h index 2853d87..c269c7d 100644 --- a/src/host_capture.h +++ b/src/host_capture.h @@ -10,7 +10,8 @@ #include "host_common.h" #include "uae_pragmas.h" -#define HOST_CAPTURE_IDLE_LIMIT 250 +#define HOST_CAPTURE_LEGACY_IDLE_LIMIT 250 +#define HOST_CAPTURE_STATUS_IDLE_LIMIT 1500 #ifndef HOST_SHELL_STATUS_INVALID #define HOST_SHELL_STATUS_INVALID 0UL #endif @@ -53,7 +54,16 @@ static inline int host_print_command_output(const char *command) return HOST_RETURN_ERROR; } - while (idle_count < HOST_CAPTURE_IDLE_LIMIT) { + for (;;) { + int idle_limit; + + if (SetSignal(0, 0) & SIGBREAKF_CTRL_C) { + SetSignal(0, SIGBREAKF_CTRL_C); + HostShell_Close(handle); + printf("\nAborted host command.\n"); + return HOST_RETURN_ERROR; + } + actual = HostShell_Read(handle, (UBYTE *)buffer, sizeof(buffer)); if (actual > 0) { fwrite(buffer, 1, actual, stdout); @@ -81,12 +91,13 @@ static inline int host_print_command_output(const char *command) fflush(stdout); return HOST_RETURN_ERROR; } - if (status_supported) { - Delay(1); - continue; - } Delay(1); idle_count++; + idle_limit = status_supported ? HOST_CAPTURE_STATUS_IDLE_LIMIT + : HOST_CAPTURE_LEGACY_IDLE_LIMIT; + if (idle_count >= idle_limit) { + break; + } } } diff --git a/src/host_clip_command.h b/src/host_clip_command.h index 30d5746..482c7f3 100644 --- a/src/host_clip_command.h +++ b/src/host_clip_command.h @@ -11,13 +11,16 @@ #include "host_powershell.h" #define HOST_CLIP_PASTE_COMMAND \ - "out=$(if command -v pbpaste >/dev/null 2>&1; then pbpaste; " \ - "elif command -v wl-paste >/dev/null 2>&1; then wl-paste -n; " \ - "elif command -v xclip >/dev/null 2>&1; then xclip -selection clipboard -o; " \ - "elif command -v xsel >/dev/null 2>&1; then xsel --clipboard --output; " \ - "else printf 'No host clipboard backend found\\n' >&2; exit 127; fi) || exit $?; " \ - "printf %s \"$out\" | if command -v iconv >/dev/null 2>&1; then " \ - "iconv -c -f UTF-8 -t ISO-8859-1//TRANSLIT 2>/dev/null || cat; else cat; fi" + "t=$(mktemp) || exit 1; c=\"${t}.converted\"; " \ + "trap 'rm -f \"$t\" \"$c\"' 0 1 2 15; " \ + "if command -v pbpaste >/dev/null 2>&1; then pbpaste >\"$t\"; " \ + "elif command -v wl-paste >/dev/null 2>&1; then wl-paste -n >\"$t\"; " \ + "elif command -v xclip >/dev/null 2>&1; then xclip -selection clipboard -o >\"$t\"; " \ + "elif command -v xsel >/dev/null 2>&1; then xsel --clipboard --output >\"$t\"; " \ + "else printf 'No host clipboard backend found\\n' >&2; exit 127; fi || exit $?; " \ + "if command -v iconv >/dev/null 2>&1 && " \ + "iconv -c -f UTF-8 -t ISO-8859-1//TRANSLIT <\"$t\" >\"$c\" 2>/dev/null; " \ + "then cat \"$c\"; else cat \"$t\"; fi" /* * Windows clipboard access goes through PowerShell. ISO-8859-1 text diff --git a/src/host_download_command.h b/src/host_download_command.h index 93ec9e5..3481d2e 100644 --- a/src/host_download_command.h +++ b/src/host_download_command.h @@ -7,9 +7,19 @@ #define HOST_DOWNLOAD_COMMAND_H #include +#include #include #include "host_common.h" +static inline int host_download_url_supported(const char *url) +{ + return url != NULL && + (host_has_prefix_ci(url, "http://") || + host_has_prefix_ci(url, "https://") || + host_has_prefix_ci(url, "ftp://") || + host_has_prefix_ci(url, "ftps://")); +} + /* * Live download for binary-safe pipe sessions: the first line reports * the expected size (<= 0 when unknown), then the body streams raw as @@ -20,10 +30,12 @@ static inline int host_append_download_stream_command(char *command, size_t comm { return host_append_literal(command, command_size, "if command -v curl >/dev/null 2>&1; then" - " curl -sfIL -o /dev/null -w '%{content_length}\\n' ") && + " curl --proto '=http,https,ftp,ftps' --proto-redir '=http,https,ftp,ftps'" + " -sfIL -o /dev/null -w '%{content_length}\\n' ") && host_append_shell_arg(command, command_size, url, 0) && host_append_literal(command, command_size, - " 2>/dev/null || echo 0; curl -sfL ") && + " 2>/dev/null || echo 0; curl --proto '=http,https,ftp,ftps'" + " --proto-redir '=http,https,ftp,ftps' -sfL ") && host_append_shell_arg(command, command_size, url, 0) && host_append_literal(command, command_size, "; elif command -v wget >/dev/null 2>&1; then echo 0; wget -q -O - ") && @@ -40,9 +52,13 @@ static inline int host_append_download_stream_command_windows(char *command, siz const char *url) { return host_append_literal(command, command_size, - "(curl -sIL -o NUL -w \"%{content_length}\\n\" ") && + "(curl --proto \"=http,https,ftp,ftps\"" + " --proto-redir \"=http,https,ftp,ftps\"" + " -sIL -o NUL -w \"%{content_length}\\n\" ") && host_append_cmd_arg(command, command_size, url) && - host_append_literal(command, command_size, " || echo 0) & curl -sfL ") && + host_append_literal(command, command_size, + " || echo 0) & curl --proto \"=http,https,ftp,ftps\"" + " --proto-redir \"=http,https,ftp,ftps\" -sfL ") && host_append_cmd_arg(command, command_size, url); } @@ -57,7 +73,9 @@ static inline int host_append_download_b64_command(char *command, size_t command { return host_append_literal(command, command_size, "t=$(mktemp) || exit 1;" - " if command -v curl >/dev/null 2>&1; then curl -sfL -o \"$t\" ") && + " if command -v curl >/dev/null 2>&1; then" + " curl --proto '=http,https,ftp,ftps'" + " --proto-redir '=http,https,ftp,ftps' -sfL -o \"$t\" ") && host_append_shell_arg(command, command_size, url, 0) && host_append_literal(command, command_size, "; elif command -v wget >/dev/null 2>&1; then wget -q -O \"$t\" ") && @@ -68,6 +86,27 @@ static inline int host_append_download_b64_command(char *command, size_t command " wc -c < \"$t\"; base64 < \"$t\"; rm -f \"$t\""); } +static inline int host_download_sidecar_path(const char *destination, const char *kind, + unsigned long token, unsigned int attempt, + char *path, size_t path_size) +{ + char suffix[48]; + int suffix_len; + + if (destination == NULL || kind == NULL || path == NULL || path_size == 0) { + return 0; + } + + suffix_len = snprintf(suffix, sizeof(suffix), ".%s.%08lx.%u", + kind, token, attempt); + if (suffix_len < 0 || (size_t)suffix_len >= sizeof(suffix)) { + return 0; + } + path[0] = '\0'; + return host_append_literal(path, path_size, destination) && + host_append_literal(path, path_size, suffix); +} + /* * Derive a destination filename from a URL: the last path segment * with any query or fragment stripped, or "download" when the URL diff --git a/src/host_env_command.h b/src/host_env_command.h index 7659e7a..e32b4aa 100644 --- a/src/host_env_command.h +++ b/src/host_env_command.h @@ -34,10 +34,25 @@ static inline int host_env_valid_name(const char *name) return 1; } +static inline int host_env_valid_value(const char *value) +{ + if (value == NULL) { + return 0; + } + + for (const char *p = value; *p; p++) { + if (*p == '\n' || *p == '\r') { + return 0; + } + } + return 1; +} + static inline int host_append_env_export_line(char *line, size_t line_size, const char *name, const char *value) { - if (!host_append_literal(line, line_size, "export ") || + if (!host_env_valid_name(name) || !host_env_valid_value(value) || + !host_append_literal(line, line_size, "export ") || !host_append_literal(line, line_size, name) || !host_append_literal(line, line_size, "='")) { return 0; @@ -65,7 +80,7 @@ static inline int host_append_env_export_line(char *line, size_t line_size, static inline int host_append_env_file_prefix(char *command, size_t command_size) { return host_append_literal(command, command_size, - "f=\"${HOME:?}/.host-tools-env\"; "); + "umask 077; f=\"${HOME:?}/.host-tools-env\"; "); } static inline int host_append_env_get_command(char *command, size_t command_size, @@ -92,7 +107,7 @@ static inline int host_append_env_set_command(char *command, size_t command_size { static char line[HOST_MAX_COMMAND_LEN]; - if (!host_env_valid_name(name)) { + if (!host_env_valid_name(name) || !host_env_valid_value(value)) { return 0; } @@ -103,11 +118,16 @@ static inline int host_append_env_set_command(char *command, size_t command_size return host_append_env_file_prefix(command, command_size) && host_append_literal(command, command_size, - "t=\"${f}.$$\"; touch \"$f\" || exit $?; grep -v '^export ") && + "t=$(mktemp \"${f}.XXXXXX\") || exit 1; " + "trap 'rm -f \"$t\"' 0 1 2 15; " + "if [ -f \"$f\" ]; then grep -v '^export ") && host_append_literal(command, command_size, name) && - host_append_literal(command, command_size, "=' \"$f\" > \"$t\" 2>/dev/null || true; printf '%s\\n' ") && + host_append_literal(command, command_size, + "=' \"$f\" > \"$t\"; s=$?; [ \"$s\" -le 1 ] || exit \"$s\"; fi; " + "printf '%s\\n' ") && host_append_shell_arg(command, command_size, line, 0) && - host_append_literal(command, command_size, " >> \"$t\" && mv \"$t\" \"$f\""); + host_append_literal(command, command_size, + " >> \"$t\" || exit $?; chmod 600 \"$t\" || exit $?; mv \"$t\" \"$f\""); } static inline int host_append_env_unset_command(char *command, size_t command_size, @@ -119,9 +139,12 @@ static inline int host_append_env_unset_command(char *command, size_t command_si return host_append_env_file_prefix(command, command_size) && host_append_literal(command, command_size, - "t=\"${f}.$$\"; if [ -f \"$f\" ]; then grep -v '^export ") && + "if [ -f \"$f\" ]; then t=$(mktemp \"${f}.XXXXXX\") || exit 1; " + "trap 'rm -f \"$t\"' 0 1 2 15; grep -v '^export ") && host_append_literal(command, command_size, name) && - host_append_literal(command, command_size, "=' \"$f\" > \"$t\" || true; mv \"$t\" \"$f\"; fi"); + host_append_literal(command, command_size, + "=' \"$f\" > \"$t\"; s=$?; [ \"$s\" -le 1 ] || exit \"$s\"; " + "chmod 600 \"$t\" || exit $?; mv \"$t\" \"$f\"; fi"); } static inline int host_append_env_list_command(char *command, size_t command_size) @@ -156,7 +179,7 @@ static inline int host_append_env_set_command_windows(char *command, size_t comm { static char script[HOST_MAX_COMMAND_LEN]; - if (!host_env_valid_name(name)) { + if (!host_env_valid_name(name) || !host_env_valid_value(value)) { return 0; } diff --git a/tests/test_ahi_driver_source.sh b/tests/test_ahi_driver_source.sh index 5fd67ea..1209eb4 100644 --- a/tests/test_ahi_driver_source.sh +++ b/tests/test_ahi_driver_source.sh @@ -205,6 +205,9 @@ if grep -F -q 'cmp.l #$003b0002,d0' "$V2_DRIVER_SOURCE" || grep -F -q 'bsr.w get fi grep -F -q 'DEBUG EQU 0' "$V2_DRIVER_SOURCE" grep -F -q 'IFNE DEBUG' "$V2_DRIVER_SOURCE" +grep -F -q 'beq.s .init_failed' "$V2_DRIVER_SOURCE" +grep -A7 -F '.init_failed' "$V2_DRIVER_SOURCE" | grep -F -q 'call CloseLibrary' +grep -A8 -F '.init_failed' "$V2_DRIVER_SOURCE" | grep -F -q 'clr.l ub_UtilBase(a5)' if grep -F -q 'IFD DEBUG=1' "$V2_DRIVER_SOURCE"; then echo "UAESND v2 debug blocks must be value-gated so release builds do not write to DEBUG_ADDR" >&2 exit 1 diff --git a/tests/test_host_command_builders.c b/tests/test_host_command_builders.c index 2c0da5b..edc1396 100644 --- a/tests/test_host_command_builders.c +++ b/tests/test_host_command_builders.c @@ -3,9 +3,14 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +#define _POSIX_C_SOURCE 200809L +#define _DARWIN_C_SOURCE + #include #include #include +#include +#include #include "host_base64.h" #include "host_clip_command.h" #include "host_env_command.h" @@ -134,12 +139,33 @@ static void test_clip_copy_command_multiline(void) static void test_clip_paste_command(void) { + char command[HOST_MAX_COMMAND_LEN * 2]; + char output[32]; + FILE *pipe; + size_t output_len; + require_contains(HOST_CLIP_PASTE_COMMAND, "pbpaste"); require_contains(HOST_CLIP_PASTE_COMMAND, "wl-paste -n"); require_contains(HOST_CLIP_PASTE_COMMAND, "xclip -selection clipboard -o"); require_contains(HOST_CLIP_PASTE_COMMAND, "xsel --clipboard --output"); require_contains(HOST_CLIP_PASTE_COMMAND, "iconv -c -f UTF-8 -t ISO-8859-1//TRANSLIT"); + require_contains(HOST_CLIP_PASTE_COMMAND, "t=$(mktemp)"); + require(strstr(HOST_CLIP_PASTE_COMMAND, "out=$(") == NULL, + "clipboard paste must not use command substitution for clipboard data"); require_shell_syntax(HOST_CLIP_PASTE_COMMAND); + + command[0] = '\0'; + require(host_append_literal(command, sizeof(command), + "pbpaste() { printf 'line\\n\\n'; }; "), + "clipboard behavior prefix should fit"); + require(host_append_literal(command, sizeof(command), HOST_CLIP_PASTE_COMMAND), + "clipboard behavior command should fit"); + pipe = popen(command, "r"); + require(pipe != NULL, "clipboard behavior command should start"); + output_len = fread(output, 1, sizeof(output), pipe); + require(pclose(pipe) == 0, "clipboard behavior command should succeed"); + require(output_len == 6 && memcmp(output, "line\n\n", 6) == 0, + "clipboard paste must preserve trailing newlines"); } static void test_info_command(void) @@ -160,6 +186,12 @@ static void test_env_name_validation(void) require(!host_env_valid_name("1FOO"), "env name must not start with a digit"); require(!host_env_valid_name("BAD-NAME"), "env name must not contain hyphens"); require(!host_env_valid_name("BAD=NAME"), "env name must not contain equals"); + require(host_env_valid_value("one line"), "single-line env value should be valid"); + require(host_env_valid_value(""), "empty env value should be valid"); + require(!host_env_valid_value("line one\nline two"), + "env value must not contain line feeds"); + require(!host_env_valid_value("line one\rline two"), + "env value must not contain carriage returns"); } static void test_env_get_command(void) @@ -170,7 +202,7 @@ static void test_env_get_command(void) require(host_append_env_get_command(command, sizeof(command), "FOO"), "env get command should build"); - require_contains(command, "f=\"${HOME:?}/.host-tools-env\""); + require_contains(command, "umask 077; f=\"${HOME:?}/.host-tools-env\""); require_contains(command, "if [ \"${FOO+x}\" = x ]; then printf %s \"$FOO\""); require_contains(command, ". \"$f\""); require_contains(command, "else exit 1; fi"); @@ -186,11 +218,18 @@ static void test_env_set_command(void) "FOO", "a b's $HOME"), "env set command should build"); - require_contains(command, "f=\"${HOME:?}/.host-tools-env\""); + require_contains(command, "umask 077; f=\"${HOME:?}/.host-tools-env\""); + require_contains(command, "t=$(mktemp \"${f}.XXXXXX\")"); + require_contains(command, "trap 'rm -f \"$t\"' 0 1 2 15"); require_contains(command, "grep -v '^export FOO=' \"$f\""); require_contains(command, "printf '%s\\n' 'export FOO='\\''a b'\\''\\'\\'''\\''s $HOME'\\'''"); require_contains(command, "mv \"$t\" \"$f\""); + require_contains(command, "chmod 600 \"$t\""); require_shell_syntax(command); + + command[0] = '\0'; + require(!host_append_env_set_command(command, sizeof(command), "FOO", "one\ntwo"), + "multiline env values should be rejected"); } static void test_env_unset_command(void) @@ -201,12 +240,58 @@ static void test_env_unset_command(void) require(host_append_env_unset_command(command, sizeof(command), "FOO"), "env unset command should build"); - require_contains(command, "f=\"${HOME:?}/.host-tools-env\""); + require_contains(command, "umask 077; f=\"${HOME:?}/.host-tools-env\""); + require_contains(command, "t=$(mktemp \"${f}.XXXXXX\")"); require_contains(command, "grep -v '^export FOO=' \"$f\" > \"$t\""); require_contains(command, "mv \"$t\" \"$f\""); + require_contains(command, "chmod 600 \"$t\""); require_shell_syntax(command); } +static void test_env_set_behavior(void) +{ + char temp_dir[] = "/tmp/host-tools-env.XXXXXX"; + char env_path[256]; + char command[HOST_MAX_COMMAND_LEN]; + char invoke[HOST_MAX_COMMAND_LEN * 4]; + char contents[128]; + struct stat st; + FILE *env_file; + size_t contents_len; + + require(mkdtemp(temp_dir) != NULL, "env behavior temp directory should be created"); + command[0] = '\0'; + require(host_append_env_set_command(command, sizeof(command), "FOO", "private value"), + "env behavior command should build"); + + invoke[0] = '\0'; + require(host_append_literal(invoke, sizeof(invoke), "HOME="), + "env behavior HOME prefix should fit"); + require(host_append_shell_arg(invoke, sizeof(invoke), temp_dir, 0), + "env behavior HOME should fit"); + require(host_append_literal(invoke, sizeof(invoke), " sh -c "), + "env behavior shell prefix should fit"); + require(host_append_shell_arg(invoke, sizeof(invoke), command, 0), + "env behavior shell command should fit"); + require(system(invoke) == 0, "env behavior command should succeed"); + + require(snprintf(env_path, sizeof(env_path), "%s/.host-tools-env", temp_dir) > 0, + "env behavior path should build"); + require(stat(env_path, &st) == 0, "env file should exist"); + require((st.st_mode & 0777) == 0600, "env file should be owner-readable only"); + + env_file = fopen(env_path, "r"); + require(env_file != NULL, "env file should be readable"); + contents_len = fread(contents, 1, sizeof(contents) - 1, env_file); + require(fclose(env_file) == 0, "env file should close"); + contents[contents_len] = '\0'; + require(strcmp(contents, "export FOO='private value'\n") == 0, + "env file should contain the requested export"); + + require(unlink(env_path) == 0, "env behavior file should be removed"); + require(rmdir(temp_dir) == 0, "env behavior temp directory should be removed"); +} + static void test_env_list_command(void) { char command[HOST_MAX_COMMAND_LEN]; @@ -249,6 +334,18 @@ static void test_shell_login_explicit_command(void) require_shell_syntax(command); } +static void test_shell_login_command_limit(void) +{ + char command[HOST_MAX_COMMAND_LEN]; + char explicit_command[HOST_MAX_COMMAND_LEN]; + + memset(explicit_command, 'x', sizeof(explicit_command) - 1); + explicit_command[sizeof(explicit_command) - 1] = '\0'; + command[0] = '\0'; + require(!host_append_shell_login_command(command, sizeof(command), explicit_command), + "login wrapper must reject a command that exceeds the trap buffer"); +} + static void require_ps_script(const char *command, const char *script) { static const char prefix[] = "powershell -NoProfile -EncodedCommand "; @@ -390,9 +487,11 @@ int main(void) test_env_get_command(); test_env_set_command(); test_env_unset_command(); + test_env_set_behavior(); test_env_list_command(); test_shell_login_interactive_command(); test_shell_login_explicit_command(); + test_shell_login_command_limit(); test_ps_quoting(); test_windows_clip_commands(); test_windows_reveal_command(); diff --git a/tests/test_host_download_command.c b/tests/test_host_download_command.c index 0120bcd..d9094cb 100644 --- a/tests/test_host_download_command.c +++ b/tests/test_host_download_command.c @@ -47,8 +47,9 @@ static void test_stream_command(void) "https://example.com/file's.lha"), "stream download command should build"); - require_contains(command, "curl -sfIL -o /dev/null -w '%{content_length}\\n'"); - require_contains(command, "curl -sfL 'https://example.com/file'\\''s.lha'"); + require_contains(command, "curl --proto '=http,https,ftp,ftps' --proto-redir '=http,https,ftp,ftps'"); + require_contains(command, "-sfIL -o /dev/null -w '%{content_length}\\n'"); + require_contains(command, "-sfL 'https://example.com/file'\\''s.lha'"); require_contains(command, "echo 0; wget -q -O - 'https://example.com/file'\\''s.lha'"); require_contains(command, "else exit 127; fi"); require_shell_syntax(command); @@ -64,7 +65,7 @@ static void test_b64_command(void) "base64 download command should build"); require_contains(command, "t=$(mktemp) || exit 1"); - require_contains(command, "curl -sfL -o \"$t\" 'https://example.com/a b.adf'"); + require_contains(command, "--proto-redir '=http,https,ftp,ftps' -sfL -o \"$t\""); require_contains(command, "wget -q -O \"$t\" 'https://example.com/a b.adf'"); require_contains(command, "|| { rm -f \"$t\"; exit 22; }"); require_contains(command, "wc -c < \"$t\"; base64 < \"$t\"; rm -f \"$t\""); @@ -80,9 +81,9 @@ static void test_windows_stream_command(void) "https://example.com/file.lha"), "windows stream download command should build"); - require_contains(command, - "(curl -sIL -o NUL -w \"%{content_length}\\n\" \"https://example.com/file.lha\" || echo 0)"); - require_contains(command, "& curl -sfL \"https://example.com/file.lha\""); + require_contains(command, "--proto \"=http,https,ftp,ftps\""); + require_contains(command, "-sIL -o NUL -w \"%{content_length}\\n\" \"https://example.com/file.lha\" || echo 0)"); + require_contains(command, "-sfL \"https://example.com/file.lha\""); command[0] = '\0'; require(!host_append_download_stream_command_windows(command, sizeof(command), @@ -130,6 +131,38 @@ static void test_url_filename(void) } } +static void test_supported_urls(void) +{ + require(host_download_url_supported("https://example.com/file.lha"), + "HTTPS should be supported"); + require(host_download_url_supported("HTTP://example.com/file.lha"), + "URL scheme matching should be case-insensitive"); + require(host_download_url_supported("ftp://example.com/file.lha"), + "FTP should be supported"); + require(host_download_url_supported("ftps://example.com/file.lha"), + "FTPS should be supported"); + require(!host_download_url_supported("file:///etc/passwd"), + "local file URLs should be rejected"); + require(!host_download_url_supported("data:text/plain,hello"), + "data URLs should be rejected"); + require(!host_download_url_supported("ssh://example.com/file"), + "arbitrary URL schemes should be rejected"); +} + +static void test_sidecar_path(void) +{ + char path[128]; + + require(host_download_sidecar_path("RAM:archive.lha", "part", 0x12, 3, + path, sizeof(path)), + "sidecar path should build"); + require(strcmp(path, "RAM:archive.lha.part.00000012.3") == 0, + "sidecar path should be adjacent and unique"); + require(!host_download_sidecar_path("RAM:archive.lha", "part", 0x12, 3, + path, 12), + "small sidecar path buffer should fail"); +} + static void require_decoded(const char *encoded, const char *expected, long expected_len) { struct host_base64_state st; @@ -197,6 +230,8 @@ int main(void) test_windows_stream_command(); test_small_buffer_failures(); test_url_filename(); + test_supported_urls(); + test_sidecar_path(); test_base64_decode(); test_base64_encode_utf16le(); return 0; diff --git a/tests/test_package_layout.sh b/tests/test_package_layout.sh index 490bbf1..709101c 100644 --- a/tests/test_package_layout.sh +++ b/tests/test_package_layout.sh @@ -3,6 +3,8 @@ set -eu PACKAGE_DIR="${PACKAGE_DIR:-build/package-test}" PACKAGE_ROOT="${PACKAGE_DIR}/Host-Tools" +: "${VERSION:?VERSION must be set}" +: "${DATE:?DATE must be set}" clean_dry_run="$(make -n clean)" case "$clean_dry_run" in @@ -12,8 +14,10 @@ case "$clean_dry_run" in ;; esac -rm -rf "$PACKAGE_DIR" -make package-dir PACKAGE_DIR="$PACKAGE_DIR" +if [ "${PACKAGE_READY:-0}" != "1" ]; then + rm -rf "$PACKAGE_DIR" + make package-dir PACKAGE_DIR="$PACKAGE_DIR" VERSION="$VERSION" DATE="$DATE" +fi test -d "$PACKAGE_ROOT" test -f "${PACKAGE_ROOT}.info" @@ -105,17 +109,27 @@ fi for tool in host-run host-multiview host-shell host-path host-reveal host-notify host-edit host-clip host-info host-download host-env; do test -f "$PACKAGE_ROOT/C/$tool" done -grep -a -q '\$VER: Host-Run 2.4 (2026-06-10)' "$PACKAGE_ROOT/C/host-run" -grep -a -q '\$VER: Host-MultiView 2.4 (2026-06-10)' "$PACKAGE_ROOT/C/host-multiview" -grep -a -q '\$VER: Host-Shell 2.4 (2026-06-10)' "$PACKAGE_ROOT/C/host-shell" -grep -a -q '\$VER: Host-Path 2.4 (2026-06-10)' "$PACKAGE_ROOT/C/host-path" -grep -a -q '\$VER: Host-Reveal 2.4 (2026-06-10)' "$PACKAGE_ROOT/C/host-reveal" -grep -a -q '\$VER: Host-Notify 2.4 (2026-06-10)' "$PACKAGE_ROOT/C/host-notify" -grep -a -q '\$VER: Host-Edit 2.4 (2026-06-10)' "$PACKAGE_ROOT/C/host-edit" -grep -a -q '\$VER: Host-Clip 2.4 (2026-06-10)' "$PACKAGE_ROOT/C/host-clip" -grep -a -q '\$VER: Host-Info 2.4 (2026-06-10)' "$PACKAGE_ROOT/C/host-info" -grep -a -q '\$VER: Host-Download 2.4 (2026-06-10)' "$PACKAGE_ROOT/C/host-download" -grep -a -q '\$VER: Host-Env 2.4 (2026-06-10)' "$PACKAGE_ROOT/C/host-env" +assert_ver() { + label="$1" + file="$2" + expected="\$VER: $label $VERSION ($DATE)" + if ! grep -a -F -q "$expected" "$file"; then + echo "$file does not contain expected version string: $expected" >&2 + exit 1 + fi +} + +assert_ver "Host-Run" "$PACKAGE_ROOT/C/host-run" +assert_ver "Host-MultiView" "$PACKAGE_ROOT/C/host-multiview" +assert_ver "Host-Shell" "$PACKAGE_ROOT/C/host-shell" +assert_ver "Host-Path" "$PACKAGE_ROOT/C/host-path" +assert_ver "Host-Reveal" "$PACKAGE_ROOT/C/host-reveal" +assert_ver "Host-Notify" "$PACKAGE_ROOT/C/host-notify" +assert_ver "Host-Edit" "$PACKAGE_ROOT/C/host-edit" +assert_ver "Host-Clip" "$PACKAGE_ROOT/C/host-clip" +assert_ver "Host-Info" "$PACKAGE_ROOT/C/host-info" +assert_ver "Host-Download" "$PACKAGE_ROOT/C/host-download" +assert_ver "Host-Env" "$PACKAGE_ROOT/C/host-env" if grep -a '\$VER: Host-[A-Za-z]* v[0-9]' "$PACKAGE_ROOT/C/"*; then echo "Host command \$VER strings must be parseable by Installer getversion, without a v prefix before the version number" >&2 exit 1 @@ -128,6 +142,7 @@ fi test -f "$PACKAGE_ROOT/Help/Host-Tools.guide" test -f "$PACKAGE_ROOT/Help/Host-Tools.guide.info" +assert_ver "Host-Tools.guide" "$PACKAGE_ROOT/Help/Host-Tools.guide" if [ -e "$PACKAGE_ROOT/Help/English" ]; then echo "Package Help drawer must contain the guide directly, not an English subdrawer" >&2 exit 1 @@ -313,7 +328,7 @@ grep -q "(default 31)" "$PACKAGE_ROOT/Install" grep -q "UAESND AHI: uaesnd.audio and UAESND AudioMode." "$PACKAGE_ROOT/Install" test -f "$PACKAGE_ROOT/Libs/MHI/mhiuae.library" -grep -a -q '\$VER: mhiuae.library 2.4 (2026-06-10)' "$PACKAGE_ROOT/Libs/MHI/mhiuae.library" +assert_ver "mhiuae.library" "$PACKAGE_ROOT/Libs/MHI/mhiuae.library" grep -q "UAE MHI MP3 decoder library" "$PACKAGE_ROOT/Install" grep -q "(procedure P_InstallMHILibrary" "$PACKAGE_ROOT/Install" grep -q '"Libs/MHI/mhiuae.library"' "$PACKAGE_ROOT/Install" diff --git a/tests/test_runtime_source.sh b/tests/test_runtime_source.sh new file mode 100644 index 0000000..d24572f --- /dev/null +++ b/tests/test_runtime_source.sh @@ -0,0 +1,32 @@ +#!/bin/sh +set -eu + +CAPTURE_HEADER="src/host_capture.h" +DOWNLOAD_SOURCE="src/host-download.c" +SHELL_SOURCE="src/host-shell.c" +MHI_HEADER="drivers/mhi/src/mhiuae.h" +MHI_SOURCE="drivers/mhi/src/mhiuae.c" + +grep -F -q '#define HOST_CAPTURE_STATUS_IDLE_LIMIT 1500' "$CAPTURE_HEADER" +grep -F -q 'SetSignal(0, 0) & SIGBREAKF_CTRL_C' "$CAPTURE_HEADER" +grep -F -q 'Aborted host command.' "$CAPTURE_HEADER" + +grep -F -q 'static char session_command[HOST_MAX_COMMAND_LEN];' "$SHELL_SOURCE" + +grep -F -q 'make_unused_sidecar_path(destpath, "part"' "$DOWNLOAD_SOURCE" +grep -F -q 'install_download(temp_path, destpath, force' "$DOWNLOAD_SOURCE" +if grep -F -q 'Open((STRPTR)destpath, MODE_NEWFILE)' "$DOWNLOAD_SOURCE"; then + echo "host-download must write to a sidecar before replacing the destination" >&2 + exit 1 +fi + +grep -F -q 'UaeMHIAlloc(task, sigmask)' "$MHI_SOURCE" +grep -F -q 'return UaeMHIQueue(player->host_handle, buffer, size, (ULONG)buffer) ? TRUE : FALSE;' "$MHI_SOURCE" +if grep -F -q 'Signal(player->task' "$MHI_SOURCE"; then + echo "MHI completion signals must come from the host after buffer consumption" >&2 + exit 1 +fi +if grep -q 'struct Task \*task;\|ULONG sigmask;' "$MHI_HEADER"; then + echo "MHI player must not retain completion state used for eager signalling" >&2 + exit 1 +fi diff --git a/version.mk b/version.mk new file mode 100644 index 0000000..e27acaf --- /dev/null +++ b/version.mk @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: 2020-2026 Dimitris Panokostas +# SPDX-License-Identifier: GPL-3.0-or-later + +# Release metadata shared by the command tools and bundled drivers. +VERSION ?= 2.6 +DATE ?= 2026-07-22 From 1d51c8fc2a1f75b9d55f6aaeba588614f38beed6 Mon Sep 17 00:00:00 2001 From: Dimitris Panokostas Date: Wed, 22 Jul 2026 14:44:05 +0300 Subject: [PATCH 2/3] build(mhi): silence initializer warnings --- drivers/mhi/src/mhiuae_startup.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/drivers/mhi/src/mhiuae_startup.c b/drivers/mhi/src/mhiuae_startup.c index 59a2730..ee03667 100644 --- a/drivers/mhi/src/mhiuae_startup.c +++ b/drivers/mhi/src/mhiuae_startup.c @@ -5,6 +5,7 @@ #define __USE_SYSBASE +#include #include #include @@ -95,11 +96,11 @@ struct MyDataInit { ULONG end_mark; } DataTab = { INITBYTE(OFFSET(Node, ln_Type), NT_LIBRARY), - 0x80, (UBYTE)OFFSET(Node, ln_Name), MHIUAE_INIT_PTR(&lib_name[0]), + 0x80, (UBYTE)offsetof(struct Node, ln_Name), MHIUAE_INIT_PTR(&lib_name[0]), INITBYTE(OFFSET(Library, lib_Flags), LIBF_SUMUSED | LIBF_CHANGED), INITWORD(OFFSET(Library, lib_Version), LIB_VERSION), INITWORD(OFFSET(Library, lib_Revision), LIB_REVISION), - 0x80, (UBYTE)OFFSET(Library, lib_IdString), MHIUAE_INIT_PTR(&lib_id[0]), + 0x80, (UBYTE)offsetof(struct Library, lib_IdString), MHIUAE_INIT_PTR(&lib_id[0]), 0 }; From 856b4cf598c6c9987cd532dc0b96ced47de1ea4d Mon Sep 17 00:00:00 2001 From: Dimitris Panokostas Date: Wed, 22 Jul 2026 16:35:34 +0300 Subject: [PATCH 3/3] fix(download): keep sidecar names FFS-safe --- src/host-download.c | 6 +++--- src/host_download_command.h | 34 +++++++++++++++++++++--------- tests/test_host_download_command.c | 33 ++++++++++++++++++++++++----- tests/test_runtime_source.sh | 2 +- 4 files changed, 56 insertions(+), 19 deletions(-) diff --git a/src/host-download.c b/src/host-download.c index 22e8d46..72ce2d0 100644 --- a/src/host-download.c +++ b/src/host-download.c @@ -51,7 +51,7 @@ static int lock_is_directory(BPTR lock) return fib.fib_DirEntryType > 0; } -static int make_unused_sidecar_path(const char *destination, const char *kind, +static int make_unused_sidecar_path(const char *destination, char kind, char *path, size_t path_size) { unsigned long token = (unsigned long)FindTask(NULL); @@ -97,7 +97,7 @@ static int install_download(const char *temp_path, const char *destpath, int for *failure = "Destination appeared during download"; return 0; } - if (!make_unused_sidecar_path(destpath, "backup", backup_path, backup_size)) { + if (!make_unused_sidecar_path(destpath, 'b', backup_path, backup_size)) { *failure = "Cannot reserve a destination backup"; return 0; } @@ -267,7 +267,7 @@ int main(int argc, char *argv[]) return HOST_RETURN_ERROR; } - if (!make_unused_sidecar_path(destpath, "part", temp_path, sizeof(temp_path))) { + if (!make_unused_sidecar_path(destpath, 'p', temp_path, sizeof(temp_path))) { HostShell_Close(handle); printf("Cannot reserve a temporary destination file\n"); return HOST_RETURN_ERROR; diff --git a/src/host_download_command.h b/src/host_download_command.h index 3481d2e..e144493 100644 --- a/src/host_download_command.h +++ b/src/host_download_command.h @@ -86,25 +86,39 @@ static inline int host_append_download_b64_command(char *command, size_t command " wc -c < \"$t\"; base64 < \"$t\"; rm -f \"$t\""); } -static inline int host_download_sidecar_path(const char *destination, const char *kind, +/* Keep the sibling component short enough for classic 30-character FFS names. */ +static inline int host_download_sidecar_path(const char *destination, char kind, unsigned long token, unsigned int attempt, char *path, size_t path_size) { - char suffix[48]; - int suffix_len; + const char *filename; + char sidecar[16]; + size_t prefix_len; + int sidecar_len; - if (destination == NULL || kind == NULL || path == NULL || path_size == 0) { + if (destination == NULL || path == NULL || path_size == 0 || + (kind != 'p' && kind != 'b') || attempt >= 100) { return 0; } - suffix_len = snprintf(suffix, sizeof(suffix), ".%s.%08lx.%u", - kind, token, attempt); - if (suffix_len < 0 || (size_t)suffix_len >= sizeof(suffix)) { + filename = destination; + for (const char *p = destination; *p; p++) { + if (*p == '/' || *p == ':') { + filename = p + 1; + } + } + prefix_len = (size_t)(filename - destination); + + sidecar_len = snprintf(sidecar, sizeof(sidecar), ".ht%c%06lx%02u", + kind, token & 0xffffffUL, attempt); + if (sidecar_len < 0 || (size_t)sidecar_len >= sizeof(sidecar) || + prefix_len + (size_t)sidecar_len >= path_size) { return 0; } - path[0] = '\0'; - return host_append_literal(path, path_size, destination) && - host_append_literal(path, path_size, suffix); + + memcpy(path, destination, prefix_len); + memcpy(path + prefix_len, sidecar, (size_t)sidecar_len + 1); + return 1; } /* diff --git a/tests/test_host_download_command.c b/tests/test_host_download_command.c index d9094cb..b033e6d 100644 --- a/tests/test_host_download_command.c +++ b/tests/test_host_download_command.c @@ -152,15 +152,38 @@ static void test_supported_urls(void) static void test_sidecar_path(void) { char path[128]; + const char *component; - require(host_download_sidecar_path("RAM:archive.lha", "part", 0x12, 3, + require(host_download_sidecar_path("RAM:Host-Tools-2.6.lha", 'p', 0x12, 3, path, sizeof(path)), "sidecar path should build"); - require(strcmp(path, "RAM:archive.lha.part.00000012.3") == 0, - "sidecar path should be adjacent and unique"); - require(!host_download_sidecar_path("RAM:archive.lha", "part", 0x12, 3, - path, 12), + require(strcmp(path, "RAM:.htp00001203") == 0, + "sidecar path should use a short sibling filename"); + component = strrchr(path, ':'); + require(component != NULL && strlen(component + 1) == 12, + "sidecar component should fit classic FFS limits"); + + require(host_download_sidecar_path("Work:Downloads/very-long-destination-name.lha", + 'b', 0xabcdef, 99, path, sizeof(path)), + "nested backup sidecar path should build"); + require(strcmp(path, "Work:Downloads/.htbabcdef99") == 0, + "backup sidecar should preserve only the destination directory"); + + require(host_download_sidecar_path("relative-name-with-thirty-chars.bin", + 'p', 1, 0, path, sizeof(path)), + "relative sidecar path should build"); + require(strcmp(path, ".htp00000100") == 0, + "relative sidecar should remain in the current directory"); + + require(!host_download_sidecar_path("RAM:archive.lha", 'p', 0x12, 3, + path, 16), "small sidecar path buffer should fail"); + require(!host_download_sidecar_path("RAM:archive.lha", 'x', 0x12, 3, + path, sizeof(path)), + "unknown sidecar kind should fail"); + require(!host_download_sidecar_path("RAM:archive.lha", 'p', 0x12, 100, + path, sizeof(path)), + "sidecar attempt outside the two-digit range should fail"); } static void require_decoded(const char *encoded, const char *expected, long expected_len) diff --git a/tests/test_runtime_source.sh b/tests/test_runtime_source.sh index d24572f..fa596b5 100644 --- a/tests/test_runtime_source.sh +++ b/tests/test_runtime_source.sh @@ -13,7 +13,7 @@ grep -F -q 'Aborted host command.' "$CAPTURE_HEADER" grep -F -q 'static char session_command[HOST_MAX_COMMAND_LEN];' "$SHELL_SOURCE" -grep -F -q 'make_unused_sidecar_path(destpath, "part"' "$DOWNLOAD_SOURCE" +grep -F -q "make_unused_sidecar_path(destpath, 'p'" "$DOWNLOAD_SOURCE" grep -F -q 'install_download(temp_path, destpath, force' "$DOWNLOAD_SOURCE" if grep -F -q 'Open((STRPTR)destpath, MODE_NEWFILE)' "$DOWNLOAD_SOURCE"; then echo "host-download must write to a sidecar before replacing the destination" >&2