diff --git a/CHANGELOG.md b/CHANGELOG.md index f8565f8..e057df2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,16 @@ All notable changes to Touch Manager will be documented here. The project follow ## Unreleased +### Added + +- On-demand, checksum-verified downloads for licensed official firmware. +- Content-addressed firmware cache in the application data directory. +- Curated community-directory and upstream-source links opened in the system browser. +- Official firmware redistribution notices. + ### Planned -- Signed remote catalog and content-addressed firmware cache. +- Signed remote catalog updates. - Maintained firmware builds with the TouchLink update protocol. - Signed and notarized release automation. - Windows and Linux hardware packaging. diff --git a/README.md b/README.md index 3a2b6f5..762fff5 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,10 @@ BOOT/RESET recovery path. > connected during an installation and retain a known-good firmware binary. Automated > tests never write to a connected device. -## What works in 0.1 +## What works in 0.2 - Bundled metadata for 23 official and community Touch 2 instruments. +- On-demand downloading and verified local caching for redistributable official firmware. - Searchable firmware library with trust, provenance, target, and checksum information. - Raw `.bin` import and local Cortex-M vector-table analysis. - SHA-256, size, Thumb-vector, stack-pointer, and execution-layout validation. @@ -38,9 +39,14 @@ Apple-notarized, so first launch may require Control-clicking the app, choosing or approving it in **System Settings → Privacy & Security**. Install `dfu-util` with `brew install dfu-util` before writing firmware. -Release builds contain the firmware catalog but do not redistribute upstream firmware -binaries. Users can import a `.bin` directly or provide the Synthux firmware workspace -described below. +Release builds can download the eight official firmware binaries whose upstream MIT +licenses permit redistribution. Every download is checked against its catalog SHA-256 and +target profile before it enters the local cache. TouchBass 1.0.2 remains source-only until +its upstream repository includes a redistribution license. + +Community entries link to their upstream source and the +[community-maintained Simple Touch 2 directory](https://github.com/Synthux-Academy/awesome-synthux#simple-touch-2), +rather than mirroring binaries with unspecified licenses. ## Safety model @@ -75,11 +81,11 @@ Frontend-only preview: npm run dev ``` -## Firmware workspace +## Optional firmware workspace -Firmware binaries are intentionally not copied into this repository. The bundled catalog -contains metadata, approved target profiles, paths, and SHA-256 values. At runtime the app -resolves artifacts from a Synthux workspace containing `Firmware/SHA256SUMS.txt`. +Developers can additionally resolve staged binaries from a Synthux workspace containing +`Firmware/SHA256SUMS.txt`. Normal users do not need this workspace to download the eight +licensed official releases or import their own `.bin` file. Resolution order: diff --git a/docs/OFFICIAL_FIRMWARE_NOTICES.md b/docs/OFFICIAL_FIRMWARE_NOTICES.md new file mode 100644 index 0000000..4199b3d --- /dev/null +++ b/docs/OFFICIAL_FIRMWARE_NOTICES.md @@ -0,0 +1,60 @@ +# Official firmware notices + +The official firmware binaries distributed through Touch Manager remain the work of +their upstream authors. Touch Manager verifies their published SHA-256 values before +making them available for installation. + +## Audrey Touch + +Source: + +MIT License + +Copyright (c) 2022 Infrasonic Audio, LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +## Simple Touch instruments + +Applies to TouchBass Classic, TouchString, TouchFX, TouchDrone, TouchDrumMachine, +TouchLooper, and TouchSlicer. + +Source: + +Copyright 2024 Stichting PTM Academy + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the “Software”), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +## TouchBass 1.0.2 + +TouchBass 1.0.2 is listed in the catalog as official, but its current upstream repository +does not contain a redistribution license. Touch Manager therefore links to its source +and does not mirror or download that binary. diff --git a/package-lock.json b/package-lock.json index eb7b172..6100c1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "touch-manager", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "touch-manager", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "dependencies": { "@tauri-apps/api": "^2.11.1", "@tauri-apps/plugin-dialog": "^2.7.1", + "@tauri-apps/plugin-opener": "^2.5.4", "lucide-react": "^0.468.0", "react": "^19.2.7", "react-dom": "^19.2.7" @@ -615,6 +616,15 @@ "@tauri-apps/api": "^2.11.0" } }, + "node_modules/@tauri-apps/plugin-opener": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.4.tgz", + "integrity": "sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", diff --git a/package.json b/package.json index 07b544e..56b2036 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,17 @@ { "name": "touch-manager", "private": true, - "version": "0.1.0", + "version": "0.2.0", "description": "A safe firmware library, installer, and diagnostic console for Synthux Touch 2.", "license": "MIT", "author": "Ali Bross", - "keywords": ["synthux", "touch-2", "firmware", "dfu", "tauri"], + "keywords": [ + "synthux", + "touch-2", + "firmware", + "dfu", + "tauri" + ], "engines": { "node": ">=20.19" }, @@ -21,6 +27,7 @@ "dependencies": { "@tauri-apps/api": "^2.11.1", "@tauri-apps/plugin-dialog": "^2.7.1", + "@tauri-apps/plugin-opener": "^2.5.4", "lucide-react": "^0.468.0", "react": "^19.2.7", "react-dom": "^19.2.7" diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 0419a12..b879f58 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -47,6 +47,137 @@ version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "atk" version = "0.18.2" @@ -151,6 +282,19 @@ dependencies = [ "objc2", ] +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "brotli" version = "8.0.4" @@ -342,6 +486,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "const-oid" version = "0.10.2" @@ -358,6 +511,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -381,9 +544,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ "bitflags 2.13.0", - "core-foundation", + "core-foundation 0.10.1", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -394,7 +557,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.13.0", - "core-foundation", + "core-foundation 0.10.1", "libc", ] @@ -747,6 +910,33 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -774,6 +964,27 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -839,6 +1050,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -846,7 +1066,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -860,6 +1080,12 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -907,6 +1133,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.32" @@ -1279,6 +1518,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1363,6 +1608,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1381,9 +1642,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -1587,6 +1850,25 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + [[package]] name = "itoa" version = "1.0.18" @@ -1892,6 +2174,23 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ndk" version = "0.9.0" @@ -1976,7 +2275,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "215a49c10e8cff30a0da93b258b21846c4e9242f7683748cfa8474fdc2e4d22b" dependencies = [ - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "futures-core", "io-kit-sys 0.5.0", @@ -2190,12 +2489,77 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "open" +version = "5.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd8d3b65c44123a56e0133d2cd06ce4361bd3ca99d41198b2f25e3c3db9b8b4a" +dependencies = [ + "dunce", + "is-wsl", + "libc", +] + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "pango" version = "0.18.3" @@ -2221,6 +2585,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -2309,6 +2679,17 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.33" @@ -2354,6 +2735,20 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -2556,15 +2951,19 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-tls", "hyper-util", "js-sys", "log", + "native-tls", "percent-encoding", "pin-project-lite", + "rustls-pki-types", "serde", "serde_json", "sync_wrapper", "tokio", + "tokio-native-tls", "tokio-util", "tower", "tower-http", @@ -2653,6 +3052,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls-pki-types" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +dependencies = [ + "zeroize", +] + [[package]] name = "rustversion" version = "1.0.23" @@ -2668,6 +3076,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.8.22" @@ -2725,6 +3142,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.36.1" @@ -2911,7 +3351,7 @@ checksum = "a4d91116f97173694f1642263b2ff837f80d933aa837e2314969f6728f661df3" dependencies = [ "bitflags 2.13.0", "cfg-if", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "io-kit-sys 0.4.1", "mach2 0.4.3", @@ -2958,6 +3398,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.9" @@ -3140,6 +3590,27 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -3161,7 +3632,7 @@ checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" dependencies = [ "bitflags 2.13.0", "block2", - "core-foundation", + "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", "dbus", @@ -3381,6 +3852,28 @@ dependencies = [ "url", ] +[[package]] +name = "tauri-plugin-opener" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "url", + "windows", + "zbus", +] + [[package]] name = "tauri-runtime" version = "2.11.3" @@ -3481,6 +3974,19 @@ dependencies = [ "toml 1.1.2+spec-1.1.0", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "tendril" version = "0.5.1" @@ -3599,6 +4105,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -3734,12 +4250,13 @@ checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "touch-manager" -version = "0.1.0" +version = "0.2.0" dependencies = [ "chrono", "hex", "nusb", "parking_lot", + "reqwest", "rusqlite", "serde", "serde_json", @@ -3748,6 +4265,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-dialog", + "tauri-plugin-opener", "thiserror 2.0.18", "uuid", ] @@ -3804,9 +4322,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "tracing-core" version = "0.1.36" @@ -3856,6 +4386,17 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + [[package]] name = "unescaper" version = "0.1.10" @@ -4352,6 +4893,17 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -4771,6 +5323,67 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28b97f866896a4be7aefd2b5a8e01bb6773d19a775d54ab28b4d094b9a4480e" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.3", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e05ad887425eecf5e8384dc2406a4a9313eb73468712fc1cdea362eb4fe0469" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039ca249fee9559680f3a9f05b55e0761fee51af4f6c1e7d8c1f31e549721d2" +dependencies = [ + "serde", + "winnow 1.0.3", + "zvariant", +] + [[package]] name = "zerofrom" version = "0.1.8" @@ -4792,6 +5405,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + [[package]] name = "zerotrie" version = "0.2.4" @@ -4830,3 +5449,43 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zvariant" +version = "5.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cf057bb00bf5c9ad77abb6147b0ca4818236a1858416e9d988e40d6322fefa7" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.3", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8118ca6bda77bfc0ab51d660db0c955f2505eef854c9a449435bccb616933b31" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.118", + "winnow 1.0.3", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ee2b32d..33505db 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "touch-manager" -version = "0.1.0" +version = "0.2.0" description = "Firmware library and diagnostic manager for Synthux Touch 2" authors = ["Ali Bross"] edition = "2021" @@ -20,6 +20,7 @@ chrono = { version = "0.4.42", features = ["serde"] } hex = "0.4.3" nusb = "0.2.4" parking_lot = "0.12.5" +reqwest = { version = "0.13.4", default-features = false, features = ["native-tls", "system-proxy"] } rusqlite = { version = "=0.39.0", features = ["bundled"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" @@ -27,5 +28,6 @@ serialport = { version = "4.9.0", default-features = false } sha2 = "0.11.0" tauri = { version = "2.11.5", features = [] } tauri-plugin-dialog = "2.7.1" +tauri-plugin-opener = "2.5.4" thiserror = "2.0.18" uuid = { version = "1.20.0", features = ["v4", "serde"] } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 731805f..ef69764 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -3,5 +3,13 @@ "identifier": "default", "description": "Default Touch Manager desktop capability", "windows": ["main"], - "permissions": ["core:default", "dialog:allow-open", "dialog:allow-save"] + "permissions": [ + "core:default", + "dialog:allow-open", + "dialog:allow-save", + { + "identifier": "opener:allow-open-url", + "allow": [{ "url": "https://github.com/**" }] + } + ] } diff --git a/src-tauri/catalog.json b/src-tauri/catalog.json index 02882c2..bef7be3 100644 --- a/src-tauri/catalog.json +++ b/src-tauri/catalog.json @@ -18,6 +18,7 @@ "targetProfile": "touch2-stm-internal-v1", "sha256": "c5c483cde616e72192e2e153036b150a42cc59276dde6e071ea53695d2a885e7", "artifactPath": "Firmware/official/AudreyTouch-v1.0.0/AudreyTouch.bin", + "downloadUrl": "https://github.com/alibros/touch-manager/releases/download/v0.1.0/AudreyTouch-1.0.0.bin", "sourceUrl": "https://github.com/Synthux-Academy/AudreyTouch", "license": "MIT", "runtimeUsb": false, @@ -40,7 +41,7 @@ "sha256": "706b1e7a393d20372039e7099427fd9e46b96ea6815fbd377c491815172b3572", "artifactPath": "Firmware/official/TouchBass-v1.0.2/TouchBass.bin", "sourceUrl": "https://github.com/Synthux-Academy/TouchBass", - "license": "MIT", + "license": null, "runtimeUsb": true, "managerCompatible": false }, @@ -60,6 +61,7 @@ "targetProfile": "touch2-stm-internal-v1", "sha256": "014980a6560a28aebfe906a80203e6f4267457c456a044e082ef466b0d05ff4a", "artifactPath": "Firmware/official/simple-touch-instruments-local/TouchString.bin", + "downloadUrl": "https://github.com/alibros/touch-manager/releases/download/v0.1.0/TouchString.bin", "sourceUrl": "https://github.com/Synthux-Academy/simple-touch-instruments", "license": "MIT", "runtimeUsb": true, @@ -81,6 +83,7 @@ "targetProfile": "touch2-stm-internal-v1", "sha256": "e9dd9e93784776acc8caefe1268124ea8dc4ecb23188104229db6d5a254d0490", "artifactPath": "Firmware/official/simple-touch-instruments-local/TouchFX.bin", + "downloadUrl": "https://github.com/alibros/touch-manager/releases/download/v0.1.0/TouchFX.bin", "sourceUrl": "https://github.com/Synthux-Academy/simple-touch-instruments", "license": "MIT", "runtimeUsb": true, @@ -102,6 +105,7 @@ "targetProfile": "touch2-stm-internal-v1", "sha256": "8e5f8f85629907dce2a06830327db0f41c9327cc1cde0b8ca1da750f615b3e59", "artifactPath": "Firmware/official/simple-touch-instruments-local/TouchDrone.bin", + "downloadUrl": "https://github.com/alibros/touch-manager/releases/download/v0.1.0/TouchDrone.bin", "sourceUrl": "https://github.com/Synthux-Academy/simple-touch-instruments", "license": "MIT", "runtimeUsb": true, @@ -123,6 +127,7 @@ "targetProfile": "touch2-stm-internal-v1", "sha256": "2747a5546ac59a38eb6a553879e05290f7d3e813f5e915e354bd37ab498e52ad", "artifactPath": "Firmware/official/simple-touch-instruments-local/TouchDrumMachine.bin", + "downloadUrl": "https://github.com/alibros/touch-manager/releases/download/v0.1.0/TouchDrumMachine.bin", "sourceUrl": "https://github.com/Synthux-Academy/simple-touch-instruments", "license": "MIT", "runtimeUsb": true, @@ -144,6 +149,7 @@ "targetProfile": "touch2-stm-internal-v1", "sha256": "377473dd333d75204a805bdfa4e19b3b17eee9c66cad6cc3a8e6418484c990f5", "artifactPath": "Firmware/official/simple-touch-instruments-local/TouchLooper.bin", + "downloadUrl": "https://github.com/alibros/touch-manager/releases/download/v0.1.0/TouchLooper.bin", "sourceUrl": "https://github.com/Synthux-Academy/simple-touch-instruments", "license": "MIT", "runtimeUsb": true, @@ -165,6 +171,7 @@ "targetProfile": "touch2-stm-internal-v1", "sha256": "450a580acb4a233e3f0111ebe20e7bfd656afa580b7945a0f8dd9c5450509ac2", "artifactPath": "Firmware/official/simple-touch-instruments-local/TouchSlicer.bin", + "downloadUrl": "https://github.com/alibros/touch-manager/releases/download/v0.1.0/TouchSlicer.bin", "sourceUrl": "https://github.com/Synthux-Academy/simple-touch-instruments", "license": "MIT", "runtimeUsb": true, @@ -186,6 +193,7 @@ "targetProfile": "touch2-stm-internal-v1", "sha256": "d622da618c6fe522f08135d5d1d6d9e896c309de7c0f3362a37b8b40a5b4ced9", "artifactPath": "Firmware/official/simple-touch-instruments-local/TouchBass.bin", + "downloadUrl": "https://github.com/alibros/touch-manager/releases/download/v0.1.0/TouchBass-Classic.bin", "sourceUrl": "https://github.com/Synthux-Academy/simple-touch-instruments", "license": "MIT", "runtimeUsb": true, diff --git a/src-tauri/src/catalog.rs b/src-tauri/src/catalog.rs index 574ec15..4d73b45 100644 --- a/src-tauri/src/catalog.rs +++ b/src-tauri/src/catalog.rs @@ -1,6 +1,9 @@ use crate::firmware::{analyze_firmware, FirmwareAnalysis, TargetProfile}; use serde::{Deserialize, Serialize}; -use std::{env, fs, path::PathBuf}; +use std::{ + env, fs, + path::{Path, PathBuf}, +}; use thiserror::Error; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -29,6 +32,8 @@ pub struct FirmwareRelease { pub target_profile: TargetProfile, pub sha256: String, pub artifact_path: String, + #[serde(default)] + pub download_url: Option, pub source_url: String, pub license: Option, pub runtime_usb: bool, @@ -52,26 +57,48 @@ pub enum CatalogError { Json(#[from] serde_json::Error), } -pub fn load_catalog() -> Result, CatalogError> { - let document: CatalogDocument = serde_json::from_str(include_str!("../catalog.json"))?; +pub fn catalog_document() -> Result { + Ok(serde_json::from_str(include_str!("../catalog.json"))?) +} + +pub fn find_release(id: &str) -> Result, CatalogError> { + Ok(catalog_document()? + .releases + .into_iter() + .find(|release| release.id == id)) +} + +pub fn load_catalog(cache_dir: Option<&Path>) -> Result, CatalogError> { + let document = catalog_document()?; let workspace = discover_workspace_root(); Ok(document .releases .into_iter() .map(|release| { - let local_path = workspace + let workspace_path = workspace .as_ref() .map(|root| root.join(&release.artifact_path)); - let analysis = local_path - .as_ref() + let cache_path = cache_dir.map(|root| root.join(format!("{}.bin", release.sha256))); + let candidates = [cache_path, workspace_path]; + let mut analyzed = candidates + .iter() + .flatten() .filter(|path| path.is_file()) - .and_then(|path| analyze_firmware(path).ok()); + .filter_map(|path| analyze_firmware(path).ok().map(|analysis| (path, analysis))) + .collect::>(); + let preferred = analyzed + .iter() + .position(|(_, analysis)| analysis.sha256.eq_ignore_ascii_case(&release.sha256)) + .unwrap_or(0); + let resolved = (!analyzed.is_empty()).then(|| analyzed.swap_remove(preferred)); + let local_path = resolved.as_ref().map(|(path, _)| (*path).clone()); + let analysis = resolved.map(|(_, analysis)| analysis); let checksum_matches = analysis .as_ref() .map(|item| item.sha256.eq_ignore_ascii_case(&release.sha256)); CatalogItem { - available_locally: local_path.as_ref().is_some_and(|path| path.is_file()), + available_locally: local_path.is_some(), local_path: local_path .filter(|path| path.is_file()) .map(|path| path.to_string_lossy().to_string()), @@ -111,12 +138,12 @@ mod tests { #[test] fn bundled_catalog_has_complete_unique_metadata() { - let document: CatalogDocument = - serde_json::from_str(include_str!("../catalog.json")).expect("catalog should parse"); + let document = catalog_document().expect("catalog should parse"); assert_eq!(document.schema_version, 1); assert_eq!(document.releases.len(), 23); let mut ids = HashSet::new(); + let mut downloadable = 0; for release in document.releases { assert!( ids.insert(release.id.clone()), @@ -126,7 +153,16 @@ mod tests { assert_eq!(release.sha256.len(), 64, "invalid hash: {}", release.id); assert!(!release.name.trim().is_empty()); assert!(!release.artifact_path.trim().is_empty()); + if let Some(url) = &release.download_url { + downloadable += 1; + assert_eq!(release.trust, "official"); + assert_eq!(release.license.as_deref(), Some("MIT")); + assert!( + url.starts_with("https://github.com/alibros/touch-manager/releases/download/") + ); + } } + assert_eq!(downloadable, 8, "unexpected downloadable release count"); } #[test] @@ -134,7 +170,7 @@ mod tests { if discover_workspace_root().is_none() { return; } - let catalog = load_catalog().expect("bundled catalog should parse"); + let catalog = load_catalog(None).expect("bundled catalog should parse"); assert_eq!(catalog.len(), 23, "catalog must cover every staged binary"); for item in catalog { diff --git a/src-tauri/src/download.rs b/src-tauri/src/download.rs new file mode 100644 index 0000000..891a992 --- /dev/null +++ b/src-tauri/src/download.rs @@ -0,0 +1,131 @@ +use crate::{ + catalog, + firmware::{analyze_bytes, validate_for_profile, FirmwareAnalysis}, +}; +use reqwest::{redirect::Policy, Client, Url}; +use serde::Serialize; +use std::{fs, path::PathBuf}; +use tauri::{AppHandle, Manager}; +use uuid::Uuid; + +const MAX_DOWNLOAD_BYTES: u64 = 16 * 1024 * 1024; +const RELEASE_PATH_PREFIX: &str = "/alibros/touch-manager/releases/download/"; + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DownloadResult { + pub path: String, + pub analysis: FirmwareAnalysis, +} + +pub async fn download_official( + app: &AppHandle, + firmware_id: &str, +) -> Result { + let release = catalog::find_release(firmware_id) + .map_err(|error| error.to_string())? + .ok_or_else(|| "Firmware is not in the bundled catalog".to_string())?; + + if release.trust != "official" || release.license.as_deref() != Some("MIT") { + return Err("Only explicitly licensed official firmware can be downloaded".into()); + } + let url = release.download_url.as_deref().ok_or_else(|| { + "This official firmware is not available for managed download".to_string() + })?; + let url = approved_release_url(url)?; + + let client = Client::builder() + .redirect(Policy::limited(5)) + .user_agent(concat!("Touch-Manager/", env!("CARGO_PKG_VERSION"))) + .build() + .map_err(|error| format!("Could not prepare secure download: {error}"))?; + let mut response = client + .get(url) + .send() + .await + .map_err(|error| format!("Firmware download failed: {error}"))? + .error_for_status() + .map_err(|error| format!("Firmware download failed: {error}"))?; + + if response.url().scheme() != "https" { + return Err("Firmware download redirected to an insecure address".into()); + } + if response + .content_length() + .is_some_and(|length| length > MAX_DOWNLOAD_BYTES) + { + return Err("Firmware download exceeds the 16 MB safety limit".into()); + } + + let mut bytes = Vec::new(); + while let Some(chunk) = response + .chunk() + .await + .map_err(|error| format!("Could not read firmware download: {error}"))? + { + if bytes.len() as u64 + chunk.len() as u64 > MAX_DOWNLOAD_BYTES { + return Err("Firmware download exceeds the 16 MB safety limit".into()); + } + bytes.extend_from_slice(&chunk); + } + + let cache_dir = app + .path() + .app_data_dir() + .map_err(|error| error.to_string())? + .join("firmware-cache"); + fs::create_dir_all(&cache_dir).map_err(|error| error.to_string())?; + let cache_path = cache_dir.join(format!("{}.bin", release.sha256)); + let analysis = analyze_bytes(&cache_path, &bytes).map_err(|error| error.to_string())?; + validate_for_profile(&analysis, release.target_profile, Some(&release.sha256)) + .map_err(|error| format!("Downloaded firmware was rejected: {error}"))?; + + let temporary = temporary_path(&cache_dir); + fs::write(&temporary, &bytes).map_err(|error| error.to_string())?; + if cache_path.exists() { + fs::remove_file(&cache_path).map_err(|error| error.to_string())?; + } + fs::rename(&temporary, &cache_path).map_err(|error| error.to_string())?; + + Ok(DownloadResult { + path: cache_path.to_string_lossy().to_string(), + analysis, + }) +} + +fn approved_release_url(value: &str) -> Result { + let url = Url::parse(value).map_err(|_| "Firmware download URL is invalid".to_string())?; + if url.scheme() != "https" + || url.host_str() != Some("github.com") + || !url.path().starts_with(RELEASE_PATH_PREFIX) + { + return Err("Firmware download URL is outside the approved release origin".into()); + } + Ok(url) +} + +fn temporary_path(cache_dir: &std::path::Path) -> PathBuf { + cache_dir.join(format!(".{}.part", Uuid::new_v4())) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn accepts_only_touch_manager_https_release_assets() { + assert!(approved_release_url( + "https://github.com/alibros/touch-manager/releases/download/v0.1.0/TouchString.bin" + ) + .is_ok()); + assert!(approved_release_url( + "http://github.com/alibros/touch-manager/releases/download/v0.1.0/file.bin" + ) + .is_err()); + assert!(approved_release_url("https://example.com/firmware.bin").is_err()); + assert!(approved_release_url( + "https://github.com/other/repo/releases/download/v1/file.bin" + ) + .is_err()); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 32874ee..381c4e0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,6 +1,7 @@ mod catalog; mod device; mod diagnostics; +mod download; mod firmware; mod flash; mod history; @@ -12,8 +13,21 @@ use std::{fs, path::PathBuf}; use tauri::{Manager, State}; #[tauri::command] -fn get_catalog() -> Result, String> { - catalog::load_catalog().map_err(|error| error.to_string()) +fn get_catalog(app: tauri::AppHandle) -> Result, String> { + let cache_dir = app + .path() + .app_data_dir() + .map_err(|error| error.to_string())? + .join("firmware-cache"); + catalog::load_catalog(Some(&cache_dir)).map_err(|error| error.to_string()) +} + +#[tauri::command] +async fn download_official_firmware( + app: tauri::AppHandle, + firmware_id: String, +) -> Result { + download::download_official(&app, &firmware_id).await } #[tauri::command] @@ -68,6 +82,7 @@ async fn flash_firmware( pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_opener::init()) .setup(|app| { let data_dir = app.path().app_data_dir()?; fs::create_dir_all(&data_dir)?; @@ -79,6 +94,7 @@ pub fn run() { }) .invoke_handler(tauri::generate_handler![ get_catalog, + download_official_firmware, analyze_firmware_file, scan_touch_devices, list_history, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 1bd56f2..03619ba 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Touch Manager", - "version": "0.1.0", + "version": "0.2.0", "identifier": "academy.synthux.touch-manager", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/App.tsx b/src/App.tsx index 5aea96d..4f4ddb8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,6 @@ import { listen, type UnlistenFn } from "@tauri-apps/api/event"; import { open, save } from "@tauri-apps/plugin-dialog"; +import { openUrl } from "@tauri-apps/plugin-opener"; import { Activity, ArrowLeft, @@ -28,6 +29,7 @@ import { import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { analyzeFirmware, + downloadOfficialFirmware, flashFirmware, getCatalog, getHistory, @@ -73,6 +75,7 @@ function App() { const [filter, setFilter] = useState("all"); const [installing, setInstalling] = useState(null); const [loading, setLoading] = useState(true); + const [downloadingId, setDownloadingId] = useState(null); const [error, setError] = useState(null); const refreshDevices = useCallback(async () => { @@ -173,6 +176,21 @@ function App() { } } + async function downloadFirmware(item: CatalogItem) { + setDownloadingId(item.id); + setError(null); + try { + await downloadOfficialFirmware(item.id); + const refreshed = await getCatalog(); + setCatalog(refreshed); + setSelected(refreshed.find((candidate) => candidate.id === item.id) ?? null); + } catch (reason) { + setError(String(reason)); + } finally { + setDownloadingId(null); + } + } + return (
@@ -233,6 +251,8 @@ function App() { onFilter={setFilter} onSelect={setSelected} onInstall={setInstalling} + onDownload={downloadFirmware} + downloadingId={downloadingId} onImport={importFirmware} /> ) : page === "device" ? ( @@ -278,6 +298,32 @@ function NavButton({ ); } +function ExternalLink({ + href, + className, + children, +}: { + href: string; + className?: string; + children: React.ReactNode; +}) { + return ( + { + if (!isDesktop) return; + event.preventDefault(); + void openUrl(href); + }} + > + {children} + + ); +} + function DevicePill({ runtime, dfu, @@ -310,6 +356,8 @@ function LibraryPage({ onFilter, onSelect, onInstall, + onDownload, + downloadingId, onImport, }: { items: CatalogItem[]; @@ -321,6 +369,8 @@ function LibraryPage({ onFilter: (value: string) => void; onSelect: (item: CatalogItem) => void; onInstall: (item: CatalogItem) => void; + onDownload: (item: CatalogItem) => void; + downloadingId: string | null; onImport: () => void; }) { return ( @@ -361,12 +411,25 @@ function LibraryPage({ /> ))}
+ + Community directoryExplore more Touch 2 instruments + + {items.length === 0 &&
No instruments match this view.
}