From 3789ca6f01b8d840fa1de370c992417c82f3a5eb Mon Sep 17 00:00:00 2001 From: Samueru-sama Date: Fri, 13 Nov 2026 03:43:47 -0400 Subject: [PATCH 1/2] feat(sidecar): add a fixed 128 KiB zstd metadata SquashFS Place a small zstd SquashFS between the runtime and the DwarFS payload containing .DirIcon, the final desktop entry, and a stub AppRun. This lets SquashFS-only thumbnailers and desktop integration tools read AppImage metadata while uruntime still mounts the DwarFS payload. The sidecar is built in Rust with backhand, padded to a fixed 128 KiB slot for stable zsync offsets, and compressed with zstd level 22. The stub AppRun explains that the metadata image is not the real payload. The pinned runtime is the reissued Anylinux-uruntime v0.8.1 release, which scans the fixed 128 KiB slot without adding a SquashFS reader to -lite runtimes. --- Cargo.lock | 330 +++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + README.md | 32 ++++- src/appimage.rs | 17 ++- src/dwarfs.rs | 7 +- src/error.rs | 16 +++ src/lib.rs | 2 + src/metadata.rs | 211 +++++++++++++++++++++++++++++++ src/pinned.rs | 27 ++-- 9 files changed, 620 insertions(+), 23 deletions(-) create mode 100644 src/metadata.rs diff --git a/Cargo.lock b/Cargo.lock index ca69b42..e95dba2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,6 +62,7 @@ dependencies = [ name = "appimagetool" version = "0.5.6" dependencies = [ + "backhand", "clap", "libc", "sha2", @@ -70,12 +71,39 @@ dependencies = [ "zsync-rs", ] +[[package]] +name = "backhand" +version = "0.25.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb8799e91f66c730e3ae50e8a260d63401e9479f236fb74f793d7656f143f0c" +dependencies = [ + "deku", + "no_std_io2", + "solana-nohash-hasher", + "thiserror", + "xxhash-rust", + "zstd", + "zstd-safe", +] + [[package]] name = "base64" version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -107,6 +135,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -147,7 +177,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 3.0.9", ] [[package]] @@ -214,6 +244,66 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "deku" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebf55291257a2a5c90cf50ae17b6bbaabc3fd13642cf3895a71c412513c19630" +dependencies = [ + "bitvec", + "deku_derive", + "no_std_io2", + "rustversion", +] + +[[package]] +name = "deku_derive" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec2a42b511fc5efd9183f4f71c17885d627b17e7fd9a61a92089406caa4397e" +dependencies = [ + "darling", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "digest" version = "0.10.7" @@ -235,6 +325,12 @@ dependencies = [ "crypto-common 0.2.1", ] +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -251,6 +347,18 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "generic-array" version = "0.14.7" @@ -272,6 +380,23 @@ dependencies = [ "wasi", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "heck" version = "0.5.0" @@ -303,6 +428,22 @@ dependencies = [ "typenum", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -315,6 +456,16 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + [[package]] name = "libc" version = "0.2.186" @@ -336,6 +487,12 @@ dependencies = [ "digest 0.11.2", ] +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -346,6 +503,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -364,6 +530,21 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -382,6 +563,18 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "ring" version = "0.17.14" @@ -390,7 +583,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -431,6 +624,32 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "sha1" version = "0.11.0" @@ -465,6 +684,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "solana-nohash-hasher" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b8a731ed60e89177c8a7ab05fe0f1511cedd3e70e773f288f9de33a9cfdc21e" + [[package]] name = "strsim" version = "0.11.1" @@ -477,6 +702,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "3.0.9" @@ -488,6 +724,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "thiserror" version = "2.0.21" @@ -505,7 +747,37 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.9", +] + +[[package]] +name = "toml_datetime" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173f25c883cd2176a2d8212155638c0a60d484a9d84546d270866f40f56b6e49" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.19+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd43688c4858d7264ae1cf9a9f874a67c3feb74a883b1cd7bebd6a4ea8307218" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow", ] [[package]] @@ -676,12 +948,64 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3b93199df0ab874cdf0c6a94f0a0dc3423fe41f1cafb62b8bd7b1deb277aef" +dependencies = [ + "memchr", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "xxhash-rust" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "550a2b930b62486a393c52d5c3b84bff264b28aa437ed64694d31e93b1757af7" + [[package]] name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +[[package]] +name = "zstd" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50adf04cf3ec914b0b9905c9dde90c80fe74bfd701e331fc80a39c512c525278" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498d94ad68720c89b4fbe41b9090850651e68cb2dd9210ce7ffbff3a94653819" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.1.3+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f729a06d0257428fd9d997ac8db7e1407bd0262ab016b085b36bc52957c0052d" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zsync-rs" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index d376f28..82b6e9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ embed-uruntime = [] embed-mkdwarfs = [] [dependencies] +backhand = { version = "0.25.5", default-features = false, features = ["zstd"] } clap = { version = "4.6.1", features = ["derive", "env"] } libc = "0.2.186" sha2 = "0.10.9" diff --git a/README.md b/README.md index ae7243e..054cb27 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A Rust implementation of `appimagetool` for the [Anylinux-AppImages](https://github.com/pkgforge-dev/Anylinux-AppImages) project. -It takes a prepared AppDir and produces a finished `.AppImage` using DWARFS compression and the [uruntime](https://github.com/VHSgunzo/uruntime) AppImage runtime. Single binary, no Python, no C++ deps. +It takes a prepared AppDir and produces a finished `.AppImage` using DWARFS compression and the [Anylinux-uruntime](https://github.com/pkgforge-dev/Anylinux-uruntime) AppImage runtime. Single binary, no Python, no C++ deps. ## Quick start @@ -25,7 +25,8 @@ That's it. The tool will: ## Features - DWARFS compression for small, delta-friendly AppImages. -- Drop-in uruntime integration with automatic download and ELF section patching. +- Drop-in Anylinux-uruntime integration with automatic download and ELF section patching. +- A fixed-size SquashFS metadata sidecar (`.DirIcon`, desktop entry, and a stub `AppRun`) so SquashFS-only thumbnailers and desktop integration tools work with DwarFS AppImages. - Optional profile-guided optimization (DWARFS hotness profiling) for faster launches. - Built-in zsync generation, no `zsyncmake` shell-out. - No central repository, no daemon, no extra runtime deps. @@ -131,6 +132,33 @@ appimagetool --dwarfs-comp 'zstd:level=22 -S26 -B2' ./AppDir An explicit `--dwarfs-comp` is passed to `mkdwarfs` unchanged. +### SquashFS metadata sidecar + +Each AppImage gets a fixed **128 KiB** SquashFS sidecar between the runtime and +the DwarFS payload. It contains the AppDir's `.DirIcon`, main `.desktop` file, +and a stub `AppRun` that tells tools such as `appimage-run` that the image must +be mounted as DwarFS. The sidecar is compressed with zstd level 22. + +The sidecar makes SquashFS-only thumbnailers and desktop-integration tools work +without understanding DwarFS. The stub `AppRun` does not launch the application; +the real payload is the following DwarFS image. + +The sidecar is zero-padded to a constant size. This keeps the DwarFS payload at +a stable offset across releases, which is what preserves zsync delta-update +efficiency. Changing the slot size shifts the payload and costs a large update; +the first release that adopts the sidecar will also have a large update from +pre-sidecar versions. + +The build fails if the metadata image does not fit in the slot. The 128 KiB +slot covers typical 256x256 and 512x512 icons and most SVGs, including +translation-heavy desktop files. + +The pinned runtime is the +[Anylinux-uruntime](https://github.com/pkgforge-dev/Anylinux-uruntime) fork, +which skips the sidecar and mounts the DwarFS payload. A custom `--runtime` +must include the same metadata-image support, otherwise the runtime will try to +mount the metadata SquashFS as the application. + ### AppDir requirements The AppDir must contain: diff --git a/src/appimage.rs b/src/appimage.rs index c432264..c20c918 100644 --- a/src/appimage.rs +++ b/src/appimage.rs @@ -11,6 +11,7 @@ use crate::config::Config; use crate::desktop::{self, DesktopEntry}; use crate::dwarfs; use crate::error::Result; +use crate::metadata; use crate::uruntime; use crate::util; @@ -129,15 +130,23 @@ pub fn build(config: &Config) -> Result<()> { .clone() .unwrap_or_else(|| dwarfs::default_compression(&config.appdir)); - // Build the final AppImage - dwarfs::build_appimage( + // Build the final AppImage. The header is the configured runtime followed + // by a small metadata SquashFS so SquashFS-only thumbnailers and desktop + // integration tools can read .DirIcon and the desktop entry. + crate::log_info!("Building metadata SquashFS sidecar..."); + let sidecar = metadata::build(&config.appdir, &desktop.path)?; + let header_path = metadata::header_path(&config.tmpdir); + metadata::write_header(&header_path, &runtime_path, &sidecar)?; + let build_result = dwarfs::build_appimage( &mkdwarfs, &config.appdir, - &runtime_path, + &header_path, &output_path, &compression, profile.as_deref(), - )?; + ); + let _ = std::fs::remove_file(&header_path); + build_result?; // Make output executable use std::os::unix::fs::PermissionsExt; diff --git a/src/dwarfs.rs b/src/dwarfs.rs index 452ceb9..5d1881e 100644 --- a/src/dwarfs.rs +++ b/src/dwarfs.rs @@ -150,11 +150,12 @@ fn mkdwarfs_source(config: &Config) -> (String, Option<&'static str>) { (url, expected_checksum) } -/// Build a DWARFS AppImage. The runtime is embedded via `--header`. +/// Build a DWARFS AppImage. `header` is prepended via `--header`; it is the +/// configured runtime followed by the metadata SquashFS sidecar. pub fn build_appimage( mkdwarfs: &Path, appdir: &Path, - runtime: &Path, + header: &Path, output: &Path, compression: &str, profile: Option<&Path>, @@ -171,7 +172,7 @@ pub fn build_appimage( .arg("--no-history") .arg("--no-create-timestamp") .arg("--header") - .arg(runtime) + .arg(header) .arg("--input") .arg(appdir); diff --git a/src/error.rs b/src/error.rs index ab941a3..c86bf16 100644 --- a/src/error.rs +++ b/src/error.rs @@ -84,6 +84,22 @@ pub enum Error { #[error("mksquashfs failed: {0}")] SquashfsFailed(String), + /// Failed to build the metadata SquashFS sidecar. + #[error("failed to build metadata SquashFS sidecar: {0}")] + MetadataSidecar(String), + + /// The metadata SquashFS is larger than its fixed slot. + #[error( + "metadata SquashFS is {size} bytes, exceeding the fixed {limit}-byte sidecar slot\n \ + hint: use a smaller .DirIcon (for example a 256x256 PNG) or reduce the desktop entry size" + )] + MetadataSidecarTooLarge { + /// Size of the built metadata image. + size: usize, + /// Fixed slot size available for the sidecar. + limit: usize, + }, + /// Failed to generate the `.zsync` control file. #[error( "zsync generation failed: {0}\n \ diff --git a/src/lib.rs b/src/lib.rs index 4704ace..36027fd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,8 @@ pub mod elf; pub mod embed; pub mod error; pub mod log; +/// Builds the metadata SquashFS sidecar placed before the DwarFS payload. +pub mod metadata; /// Pinned upstream artifact URLs and SHA-256 digests, shared with `build.rs`. pub mod pinned; pub mod uruntime; diff --git a/src/metadata.rs b/src/metadata.rs new file mode 100644 index 0000000..aa0aeb0 --- /dev/null +++ b/src/metadata.rs @@ -0,0 +1,211 @@ +//! Build the small SquashFS metadata sidecar placed between the runtime and +//! the DwarFS payload. +//! +//! Many thumbnailers and desktop-integration tools only know how to read a +//! SquashFS at the end of the runtime ELF. The sidecar carries `.DirIcon`, the +//! main `.desktop` entry, and a stub `AppRun` so those tools work against a +//! DwarFS AppImage. The runtime skips this image and mounts the following +//! DwarFS payload. +//! +//! The image is always padded to [`SIDECAR_SLOT_BYTES`]. Keeping the slot size +//! constant keeps the DwarFS payload at a stable offset across releases, which +//! preserves zsync delta-update efficiency. + +use std::io::Cursor; +use std::path::{Path, PathBuf}; + +use backhand::compression::{CompressionOptions, Compressor, Zstd}; +use backhand::{BackhandError, FilesystemCompressor, FilesystemWriter, NodeHeader}; + +use crate::error::{Error, Result}; + +/// zstd level used for the sidecar. +const SIDECAR_ZSTD_LEVEL: u32 = 22; + +/// Fixed size of the metadata slot between the runtime and the DwarFS payload. +/// +/// The image is zero-padded to this size. The patched runtime scans the fixed +/// 128 KiB slot for the following DwarFS payload. +pub const SIDECAR_SLOT_BYTES: usize = 128 * 1024; + +/// Stub `AppRun` for tools that mount or extract the metadata image instead of +/// using uruntime. It must never be mistaken for the real DwarFS payload. +const DUMMY_APPRUN: &[u8] = b"#!/bin/sh\n\ +>&2 echo 'This is not the real AppImage payload, this is a DwarFS AppImage, please use dwarfs to mount it instead'\n\ +exit 1\n"; + +/// Build a padded metadata SquashFS image containing `.DirIcon`, the main +/// desktop entry, and a stub `AppRun` from `appdir`. +/// +/// `desktop_path` must be the final desktop file (after X-AppImage metadata +/// has been added), so the sidecar advertises the same metadata as the real +/// payload. +pub fn build(appdir: &Path, desktop_path: &Path) -> Result> { + let dir_icon = std::fs::read(appdir.join(".DirIcon"))?; + let desktop = std::fs::read(desktop_path)?; + let desktop_name = desktop_path + .file_name() + .ok_or_else(|| Error::MetadataSidecar("desktop entry has no file name".to_string()))? + .to_owned(); + + let mut fs = FilesystemWriter::default(); + // A fixed timestamp and root ownership keep the sidecar reproducible. + fs.set_time(0); + fs.set_only_root_id(); + fs.set_root_mode(0o755); + fs.set_root_uid(0); + fs.set_root_gid(0); + fs.set_no_padding(); + let compressor = FilesystemCompressor::new( + Compressor::Zstd, + Some(CompressionOptions::Zstd(Zstd { + compression_level: SIDECAR_ZSTD_LEVEL, + })), + ) + .map_err(metadata_error)?; + fs.set_compressor(compressor); + // The zstd level is a build-time choice; readers only need the compressor + // id, so don't spend bytes storing the options in the image. + fs.set_emit_compression_options(false); + + let file_header = NodeHeader::new(0o644, 0, 0, 0); + fs.push_file(Cursor::new(dir_icon), Path::new(".DirIcon"), file_header) + .map_err(metadata_error)?; + fs.push_file(Cursor::new(desktop), Path::new(&desktop_name), file_header) + .map_err(metadata_error)?; + fs.push_file( + Cursor::new(DUMMY_APPRUN), + Path::new("AppRun"), + NodeHeader::new(0o755, 0, 0, 0), + ) + .map_err(metadata_error)?; + + let mut output = Cursor::new(Vec::new()); + let (superblock, _) = fs.write(&mut output).map_err(metadata_error)?; + let mut image = output.into_inner(); + let actual_size = usize::try_from(superblock.bytes_used).map_err(|_| { + Error::MetadataSidecar("metadata image size does not fit usize".to_string()) + })?; + + if actual_size > SIDECAR_SLOT_BYTES { + return Err(Error::MetadataSidecarTooLarge { + size: actual_size, + limit: SIDECAR_SLOT_BYTES, + }); + } + + image.resize(SIDECAR_SLOT_BYTES, 0); + crate::log_debug!( + "metadata sidecar: {} bytes, padded to {} bytes", + actual_size, + SIDECAR_SLOT_BYTES + ); + Ok(image) +} + +/// Write `runtime || sidecar` to `dest` for use as `mkdwarfs --header`. +pub fn write_header(dest: &Path, runtime: &Path, sidecar: &[u8]) -> Result<()> { + use std::io::Write; + + let runtime_bytes = std::fs::read(runtime)?; + let mut file = std::fs::File::create(dest)?; + file.write_all(&runtime_bytes)?; + file.write_all(sidecar)?; + file.flush()?; + Ok(()) +} + +/// Temporary header path for a single build. +pub fn header_path(tmpdir: &Path) -> PathBuf { + crate::util::process_unique_path(tmpdir, "appimage-header") +} + +fn metadata_error(error: BackhandError) -> Error { + Error::MetadataSidecar(error.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn temp_dir(name: &str) -> PathBuf { + let dir = std::env::temp_dir().join(format!( + "appimagetool-metadata-{name}-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::create_dir_all(&dir).unwrap(); + dir + } + + #[test] + fn builds_fixed_size_squashfs_with_expected_entries() { + let dir = temp_dir("build"); + std::fs::write(dir.join(".DirIcon"), b"fake png bytes").unwrap(); + std::fs::write(dir.join("TestApp.desktop"), b"[Desktop Entry]\nName=Test\n").unwrap(); + + let image = build(&dir, &dir.join("TestApp.desktop")).unwrap(); + assert_eq!(image.len(), SIDECAR_SLOT_BYTES); + assert_eq!(&image[..4], b"hsqs"); + + let bytes_used = u64::from_le_bytes(image[40..48].try_into().unwrap()); + assert!(bytes_used <= SIDECAR_SLOT_BYTES as u64); + + let reader = + backhand::FilesystemReader::from_reader(Cursor::new(image.as_slice())).unwrap(); + let paths: Vec<_> = reader.files().map(|node| node.fullpath.clone()).collect(); + assert!(paths.iter().any(|path| path == Path::new("/.DirIcon"))); + assert!( + paths + .iter() + .any(|path| path == Path::new("/TestApp.desktop")) + ); + assert!(paths.iter().any(|path| path == Path::new("/AppRun"))); + + let apprun = reader + .files() + .find(|node| node.fullpath == Path::new("/AppRun")) + .unwrap(); + assert_eq!(apprun.header.permissions, 0o755); + + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn is_reproducible() { + let dir = temp_dir("repro"); + std::fs::write(dir.join(".DirIcon"), b"fake png bytes").unwrap(); + std::fs::write(dir.join("TestApp.desktop"), b"[Desktop Entry]\nName=Test\n").unwrap(); + + let first = build(&dir, &dir.join("TestApp.desktop")).unwrap(); + let second = build(&dir, &dir.join("TestApp.desktop")).unwrap(); + assert_eq!(first, second); + + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn rejects_metadata_larger_than_slot() { + let dir = temp_dir("large"); + // Pseudo-random bytes resist zstd better than a repeating pattern. + let mut state = 0x9e37_79b9_u32; + let large: Vec = (0..SIDECAR_SLOT_BYTES * 2) + .map(|_| { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + (state & 0xff) as u8 + }) + .collect(); + std::fs::write(dir.join(".DirIcon"), &large).unwrap(); + std::fs::write(dir.join("TestApp.desktop"), b"[Desktop Entry]\nName=Test\n").unwrap(); + + let error = build(&dir, &dir.join("TestApp.desktop")).unwrap_err(); + assert!(matches!(error, Error::MetadataSidecarTooLarge { .. })); + + let _ = std::fs::remove_dir_all(&dir); + } +} diff --git a/src/pinned.rs b/src/pinned.rs index 24742bc..67dc36e 100644 --- a/src/pinned.rs +++ b/src/pinned.rs @@ -1,18 +1,23 @@ -// Pinned upstream artifact URLs and SHA-256 digests. +// Pinned artifact URLs and SHA-256 digests. // // Uses line comments rather than `//!` module docs because `build.rs` pulls this // file in with `include!`, which cannot carry inner doc comments. The module // documentation lives on the `pub mod pinned;` declaration in `lib.rs`. -/// Default uruntime download URL. Pinned for reproducible builds; override with -/// `--runtime-url` / `URUNTIME_LINK`. `{arch}` is replaced with the target -/// architecture. +/// Default Anylinux-uruntime download URL. Pinned for reproducible builds; +/// override with `--runtime-url` / `URUNTIME_LINK`. `{arch}` is replaced with +/// the target architecture. +/// +/// This is the pkgforge-dev fork of uruntime. It tracks upstream version 0.8.1 +/// and adds support for skipping a leading metadata SquashFS image, which the +/// AppImage builder uses so SquashFS-only thumbnailers and desktop integration +/// tools can read `.DirIcon` and the desktop entry. /// /// The `-lite` in this asset name is a licensing boundary, not just a size /// choice. The lite variant embeds DwarFS `dwarfs-fuse-extract`, which holds /// only the MIT-licensed reader and extractor. The non-lite variant embeds /// `dwarfs-universal`, which also contains the GPL-3.0 `mkdwarfs` writer. -pub const URUNTIME_URL_TEMPLATE: &str = "https://github.com/VHSgunzo/uruntime/releases/download/v0.8.1/uruntime-appimage-dwarfs-lite-{arch}"; +pub const URUNTIME_URL_TEMPLATE: &str = "https://github.com/pkgforge-dev/Anylinux-uruntime/releases/download/v0.8.1/uruntime-appimage-dwarfs-lite-{arch}"; /// Default mkdwarfs download URL. `{arch}` is the host arch at run time, and /// the build target in `build.rs` when embedding the blob. @@ -30,27 +35,27 @@ pub const MKDWARFS_URL_TEMPLATE: &str = pub const URUNTIME_CHECKSUMS: &[(&str, &str)] = &[ ( "aarch64", - "c1641dfe465f4cb70ae545fbfb9de7a221aa6e9a0b1d7ea600b213a1f10738a0", + "3abaff7ab3f060ee0decfae106b08c1a08577ce623b9ff6c0aeba3107c4399fc", ), ( "loongarch64", - "7f149441fbb772477c8748e58c1233e4e0e141d6ca2507c4ddad97b69b4008bc", + "6e10bf259359fd5fb22ac11f4354e8b9aff36976d35077a020ad0681ccd7380a", ), ( "ppc64", - "db7a7834c1cb657c2708eab206d9a140231962d22f776cd0ea4fd9a482be3037", + "6798d59314bdc42b756afb05038778706b26a22eb96a27e47fc563d386cbbe10", ), ( "ppc64le", - "0fa3983c5c18794e841d2ad20938b87c467cbf24034a0fad608c5fe00a1668a9", + "e8cda02eab73180c241dfd2352426edff28fd7f46524275d44b27aa474f49eee", ), ( "riscv64", - "25d1137942a24fed3d1126686960de5dc5ec44070d5e3a6c3544cca229d230cb", + "13dcf97dc0b2a7fd35e719d2b3d26ad33be8df1ddf65b9f3000a013bbbd25b3a", ), ( "x86_64", - "b3c2916153e089d703cee5a7ebc540941d2f1d71baa90eb3092b15eef48345a8", + "b5e361879fdd8d6056f814cfdfcecc02bb74b679cf9d9d81ea6f0d65b521fb18", ), ]; From 319a0edbff8fdc1b8ba2257baa403ade6a34aa37 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Nov 2026 07:44:57 +0000 Subject: [PATCH 2/2] chore(deps): update rust crate sha2 to 0.11.0 --- Cargo.lock | 72 +++++++----------------------------------------------- Cargo.toml | 4 +-- 2 files changed, 11 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e95dba2..8170e15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,15 +104,6 @@ dependencies = [ "wyz", ] -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - [[package]] name = "block-buffer" version = "0.12.0" @@ -198,15 +189,6 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - [[package]] name = "cpufeatures" version = "0.3.0" @@ -225,16 +207,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "crypto-common" version = "0.2.1" @@ -304,25 +276,15 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer 0.10.4", - "crypto-common 0.1.7", -] - [[package]] name = "digest" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" dependencies = [ - "block-buffer 0.12.0", + "block-buffer", "const-oid", - "crypto-common 0.2.1", + "crypto-common", ] [[package]] @@ -359,16 +321,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "getrandom" version = "0.2.17" @@ -484,7 +436,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd76fb0fd6b2e4be62a73f8e0858ca97f81babcb1af322dcaca196f735f17f80" dependencies = [ - "digest 0.11.2", + "digest", ] [[package]] @@ -657,19 +609,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.2", + "cpufeatures", + "digest", ] [[package]] name = "sha2" -version = "0.10.9" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", + "cpufeatures", + "digest", ] [[package]] @@ -839,12 +791,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 82b6e9c..0a48812 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,10 +21,10 @@ embed-mkdwarfs = [] backhand = { version = "0.25.5", default-features = false, features = ["zstd"] } clap = { version = "4.6.1", features = ["derive", "env"] } libc = "0.2.186" -sha2 = "0.10.9" +sha2 = "0.11.0" thiserror = "2.0.18" ureq = "3.3.0" zsync-rs = "0.3.0" [build-dependencies] -sha2 = "0.10.9" +sha2 = "0.11.0"