Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package dysk for openSUSE:Factory checked in at 2026-11-05 18:22:17 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/dysk (Old) and /work/SRC/openSUSE:Factory/.dysk.new.3759290 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "dysk" Thu Nov 5 18:22:17 2026 rev:19 rq:1395123 version:3.8.2 Changes: -------- --- /work/SRC/openSUSE:Factory/dysk/dysk.changes 2026-10-20 12:39:13.616761091 +0200 +++ /work/SRC/openSUSE:Factory/.dysk.new.3759290/dysk.changes 2026-11-05 18:23:32.415762900 +0100 @@ -1,0 +2,11 @@ +Wed Nov 04 09:56:07 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 3.8.2: + * explain file locations + * update CONTRIBUTING rules + * update changelog + * Show control characters as \xNN escapes + * fix terminal escape injection from mount metadata + * on release, upload per target binaries + +------------------------------------------------------------------- Old: ---- dysk-3.8.1.obscpio New: ---- dysk-3.8.2.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ dysk.spec ++++++ --- /var/tmp/diff_new_pack.6dP1Iz/_old 2026-11-05 18:23:34.933867827 +0100 +++ /var/tmp/diff_new_pack.6dP1Iz/_new 2026-11-05 18:23:34.935867910 +0100 @@ -17,7 +17,7 @@ Name: dysk -Version: 3.8.1 +Version: 3.8.2 Release: 0 Summary: Get information on filesystems, like df but better License: MIT ++++++ _service ++++++ --- /var/tmp/diff_new_pack.6dP1Iz/_old 2026-11-05 18:23:35.063873244 +0100 +++ /var/tmp/diff_new_pack.6dP1Iz/_new 2026-11-05 18:23:35.077873827 +0100 @@ -4,7 +4,7 @@ <param name="scm">git</param> <param name="exclude">.git</param> <param name="versionformat">@PARENT_TAG@</param> - <param name="revision">refs/tags/v3.8.1</param> + <param name="revision">refs/tags/v3.8.2</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> </service> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.6dP1Iz/_old 2026-11-05 18:23:35.167877577 +0100 +++ /var/tmp/diff_new_pack.6dP1Iz/_new 2026-11-05 18:23:35.178878036 +0100 @@ -3,6 +3,6 @@ <param name="url">https://github.com/Canop/dysk</param> <param name="changesrevision">b8c61ea5f565759b12785658a1c6a80f3b13722c</param></service><service name="tar_scm"> <param name="url">https://github.com/Canop/dysk.git</param> - <param name="changesrevision">c002b844cfc33d56aaa7a4c46b80b033b07e0dee</param></service></servicedata> + <param name="changesrevision">990df3edc2a1312d2f943064f605025a45691c55</param></service></servicedata> (No newline at EOF) ++++++ dysk-3.8.1.obscpio -> dysk-3.8.2.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dysk-3.8.1/CHANGELOG.md new/dysk-3.8.2/CHANGELOG.md --- old/dysk-3.8.1/CHANGELOG.md 2026-10-17 18:04:08.000000000 +0200 +++ new/dysk-3.8.2/CHANGELOG.md 2026-11-02 17:55:48.000000000 +0100 @@ -1,3 +1,8 @@ +<a name="v3.8.2"></a> +### v3.8.2 - 2026/11/02 +- GitHub releases come with one archive per target, and checksums - Fix #67 +- sanitize terminal output to prevent escape sequence injection - Fix #125 - Thanks @itulo + <a name="v3.8.1"></a> ### v3.8.1 - 2026/10/17 - fix `free_percent` filter, eg `-f 'free_percent>20%'` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dysk-3.8.1/CONTRIBUTING.md new/dysk-3.8.2/CONTRIBUTING.md --- old/dysk-3.8.1/CONTRIBUTING.md 2026-10-17 18:04:08.000000000 +0200 +++ new/dysk-3.8.2/CONTRIBUTING.md 2026-11-02 17:55:48.000000000 +0100 @@ -1,5 +1,11 @@ +This open-source non commercial project favors human interaction over LLM-assisted productivity. -Before you start, unless you're fixing a typo or proposing a trivial change, please +AI agents are not allowed to open PRs on this project. + +A PR description MUST be written by an human. +The text of an issue MUST be written by an human. + +Before you start a PR, unless you're fixing a typo or proposing a trivial change, please - discuss the need and technical design first, either in an issue or on [miaou](https://miaou.dystroy.org/4847?dysk) - keep it simple and focused diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dysk-3.8.1/Cargo.lock new/dysk-3.8.2/Cargo.lock --- old/dysk-3.8.1/Cargo.lock 2026-10-17 18:04:08.000000000 +0200 +++ new/dysk-3.8.2/Cargo.lock 2026-11-02 17:55:48.000000000 +0100 @@ -337,7 +337,7 @@ [[package]] name = "dysk" -version = "3.8.1" +version = "3.8.2" dependencies = [ "clap", "clap_complete", @@ -349,7 +349,7 @@ [[package]] name = "dysk-cli" -version = "3.8.1" +version = "3.8.2" dependencies = [ "bet", "clap", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dysk-3.8.1/Cargo.toml new/dysk-3.8.2/Cargo.toml --- old/dysk-3.8.1/Cargo.toml 2026-10-17 18:04:08.000000000 +0200 +++ new/dysk-3.8.2/Cargo.toml 2026-11-02 17:55:48.000000000 +0100 @@ -1,6 +1,6 @@ [package] name = "dysk" -version = "3.8.1" +version = "3.8.2" authors = ["dystroy <[email protected]>"] edition = "2024" keywords = ["linux", "mac", "filesystem", "disk"] @@ -22,13 +22,13 @@ ] [dependencies] -dysk-cli = { version = "3.8.1", path = "cli" } # beware: version is also in build dependencies +dysk-cli = { version = "3.8.2", path = "cli" } # beware: version is also in build dependencies [build-dependencies] clap = { version = "4.6", features = ["derive", "cargo"] } clap_complete = "4.6" clap_mangen = "0.3" -dysk-cli = { version = "3.8.1", path = "cli" } +dysk-cli = { version = "3.8.2", path = "cli" } serde = { version = "1.0", features = ["derive"] } toml = "1" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dysk-3.8.1/build-scripts/README.md new/dysk-3.8.2/build-scripts/README.md --- old/dysk-3.8.1/build-scripts/README.md 2026-10-17 18:04:08.000000000 +0200 +++ new/dysk-3.8.2/build-scripts/README.md 2026-11-02 17:55:48.000000000 +0100 @@ -31,9 +31,15 @@ Refuses to go on unless HEAD is exactly `origin/main` (not unpushed, not behind) and no `v<version>` tag exists on origin at another commit. Then - fetches the staged artifacts, checks every target is present, verifies each - binary, and produces `dysk_<version>.zip` (also the artifact to attach to the - GitHub release), with a copy in `releases/`. + fetches the staged artifacts (or, without staging, builds locally, which only + a Mac can do completely), checks every target is present, verifies each + binary, and produces `dysk_<version>.zip`, with a copy in `releases/`, and + `dist/`, the files of the GitHub release (see [The archives](#the-archives)). + + `--dry-run` packages without committing to a release, to inspect `dist/`: + it neither stops nor asks, but lists at the end everything a real release + would stop on or ask about (exiting with 1 if it would stop), and nothing + goes to `releases/`. 3. Publish: @@ -47,19 +53,24 @@ a link to `x86_64-unknown-linux-gnu`, for third-party install scripts using that old path. - It then creates and pushes the `v<version>` tag on HEAD, and runs - `website/deploy.sh`, last, so the site never describes a version whose - binaries aren't online yet. + It then creates and pushes the `v<version>` tag on HEAD, runs + `github-release.sh`, and runs `website/deploy.sh`, last, so the site never + describes a version whose binaries aren't online yet. + + `github-release.sh` creates the GitHub release from that tag as a **draft**, + with the files of `dist/` and the version's `CHANGELOG.md` section as notes; + when the release already exists, its files are replaced instead. If it fails, + `deploy.sh` warns and goes on with the website: fix the cause, then run + `./build-scripts/github-release.sh` alone. It refuses a dry run's `dist/`. -4. Create the GitHub release from that tag, not from a branch: + Both need `gh`, logged in to github.com (`gh auth login`); `deploy.sh` checks + it before building. - gh release create v<version> dysk_<version>.zip --verify-tag - - With "target: main" in the GitHub form, the tag lands on whatever `main` is - on GitHub at that moment. +4. Review the draft on GitHub, edit its notes if needed, and publish it. Staging and deploy settings come from `build-scripts/_local.sh` (see below). -Without it, `release.sh` builds locally on a single host and `deploy.sh` won't run. +Without it, `release.sh` builds locally on a single host, which must be a Mac, +and `deploy.sh` won't run. A dirty tree doesn't block either step, but both ask first: `build-all-targets.sh` offers to stage it anyway (recorded in a `<version>-<commit>.dirty` marker beside @@ -96,7 +107,7 @@ duplicate linked dylib, and a binary built for the host triple is run to confirm it reports the current version. -## The archive +## The archives `dysk_<version>.zip` holds the contents of `build/`, with no wrapping directory: @@ -114,6 +125,18 @@ script `OUT_DIR`; `build-all-targets.sh` finds that directory under `.build-cache/` and copies them out. +`dist/` holds exactly the files of the GitHub release: that zip, one archive per +target, and `sha256sums`. Each per-target archive is a `.tar.gz` (a `.zip` for +Windows) unpacking to a single directory: + + dysk-v<version>-<triple>/ + dysk dysk.exe on Windows + completion/ + man/dysk.1 + CHANGELOG.md + LICENSE + README.md + ## Machine-local config (`_local.sh`) `build-scripts/_local.sh` holds per-machine settings and is **gitignored**, so it @@ -144,5 +167,7 @@ - `build-target.sh <filter>` — build one target (`--list` to see them), e.g. `./build-scripts/build-target.sh aarch64-apple-darwin` +- `github-release.sh` — create the draft GitHub release from `dist/` (run by + `deploy.sh`) - `version.sh`, `target.sh` — print the dysk version and the host triple - `_common.sh`, `_targets.sh` — sourced libraries, not run directly diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dysk-3.8.1/build-scripts/_common.sh new/dysk-3.8.2/build-scripts/_common.sh --- old/dysk-3.8.1/build-scripts/_common.sh 2026-10-17 18:04:08.000000000 +0200 +++ new/dysk-3.8.2/build-scripts/_common.sh 2026-11-02 17:55:48.000000000 +0100 @@ -77,6 +77,15 @@ release_tag() { printf 'v%s\n' "$(dysk_version)"; } +DYSK_GITHUB_REPO=Canop/dysk + +# Die unless gh is installed and logged in to github.com. +need_gh() { + need gh "see https://cli.github.com" + gh auth status --hostname github.com >/dev/null 2>&1 \ + || die "gh isn't logged in to github.com — run 'gh auth login'" +} + # The commit the tag points at on origin, peeled; empty when there's no such tag. remote_tag_commit() { # remote_tag_commit <tag> local refs peeled @@ -141,6 +150,15 @@ ok "tag $tag created and pushed" } +# The body of the CHANGELOG.md section of a version, without its heading. +changelog_notes() { # changelog_notes <version> + awk -v h="### v$1" ' + $0 == h || index($0, h " ") == 1 { on = 1; next } + on && (/^### / || /^<a name=/) { exit } + on { print } + ' CHANGELOG.md +} + # Ask a yes/no question on the terminal, defaulting to no. With no terminal to # ask on (cron, CI) the answer is no, unless DYSK_YES is set. confirm() { # confirm <question> @@ -171,6 +189,12 @@ rsync -az "$dir"/ "$DYSK_STAGE_HOST:$dest/" } +# Prints yes or no; fails when the server can't be reached. +stage_has_release() { + ssh "$DYSK_STAGE_HOST" \ + "if test -d '$DYSK_STAGE_DIR/$(release_id)'; then echo yes; else echo no; fi" +} + stage_fetch() { # stage_fetch <local-dir> <- pulls <dir>/<id>/ into it local dir=$1 id id=$(release_id) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dysk-3.8.1/build-scripts/deploy.sh new/dysk-3.8.2/build-scripts/deploy.sh --- old/dysk-3.8.1/build-scripts/deploy.sh 2026-10-17 18:04:08.000000000 +0200 +++ new/dysk-3.8.2/build-scripts/deploy.sh 2026-11-02 17:55:48.000000000 +0100 @@ -1,7 +1,8 @@ #!/usr/bin/env bash # -# Build the release and rsync it into the download directory on the server, then -# deploy the website. +# Build the release, rsync it into the download directory on the server, tag it, +# create the GitHub release as a draft (github-release.sh), then deploy the +# website. # # Nothing goes through ~/dev/www/dystroy: that tree is a per-machine mirror of # the whole site, so pushing it from one machine republishes stale copies of @@ -17,8 +18,9 @@ # A mistyped destination would spray the binaries over the site itself. [[ $DYSK_DEPLOY_TARGET == *:*/dysk/download ]] \ || die "DYSK_DEPLOY_TARGET should end in /dysk/download, got '$DYSK_DEPLOY_TARGET'" +need_gh -# build the release zip (and the build/ directory) +# build the release zip, the build/ directory, and dist/ "$here/release.sh" version=$(dysk_version) @@ -39,6 +41,12 @@ h2 "Tagging the release" push_release_tag +# A draft, to be reviewed and published in GitHub's interface. A failure there +# doesn't hold back the website. +if ! "$here/github-release.sh"; then + warn "the GitHub release failed — once fixed, run ./build-scripts/github-release.sh" +fi + # Last: the site must never describe a version whose binaries aren't up yet. h2 "Deploying the website" "$here/../website/deploy.sh" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dysk-3.8.1/build-scripts/github-release.sh new/dysk-3.8.2/build-scripts/github-release.sh --- old/dysk-3.8.1/build-scripts/github-release.sh 1970-01-01 01:00:00.000000000 +0100 +++ new/dysk-3.8.2/build-scripts/github-release.sh 2026-11-02 17:55:48.000000000 +0100 @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# +# Create the GitHub release of the current version as a draft, with the files +# of dist/ and the version's CHANGELOG.md section as notes. When the release +# already exists, its files are replaced instead. +# +# Run by deploy.sh after the tag is pushed; run it again by hand if it failed +# there. It needs the dist/ of a real (not dry) release.sh run. + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh" + +need_gh + +version=$(dysk_version) +tag=$(release_tag) + +[[ -f "dist/dysk_$version.zip" ]] || die "no dist/ for $version — run release.sh first" +[[ ! -e dist/.dry-run ]] || die "dist/ comes from a dry run — run release.sh without --dry-run" +assets=(dist/*.tar.gz dist/*.zip dist/sha256sums) + +h2 "Creating the GitHub release $tag" +tags=$(gh release list --repo "$DYSK_GITHUB_REPO" --limit 1000 --json tagName --jq '.[].tagName') +if grep -qxF "$tag" <<< "$tags"; then + gh release upload "$tag" "${assets[@]}" --repo "$DYSK_GITHUB_REPO" --clobber + ok "files of the existing release $tag replaced" +else + notes=$(mktemp) + changelog_notes "$version" > "$notes" + [[ -s $notes ]] || warn "no section for v$version in CHANGELOG.md, the release notes are empty" + gh release create "$tag" "${assets[@]}" --repo "$DYSK_GITHUB_REPO" \ + --verify-tag --draft --title "dysk $tag" --notes-file "$notes" + rm -f "$notes" + ok "draft release $tag created with ${#assets[@]} files — review and publish it on GitHub" +fi diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dysk-3.8.1/build-scripts/release.sh new/dysk-3.8.2/build-scripts/release.sh --- old/dysk-3.8.1/build-scripts/release.sh 2026-10-17 18:04:08.000000000 +0200 +++ new/dysk-3.8.2/build-scripts/release.sh 2026-11-02 17:55:48.000000000 +0100 @@ -1,36 +1,86 @@ #!/usr/bin/env bash # -# Assemble and package a full dysk release: build/ plus dysk_<version>.zip in the -# repo root (the zip is both a download and the artifact to attach to the GitHub -# release), with a copy kept in releases/. +# Assemble and package a full dysk release: build/, dysk_<version>.zip in the +# repo root (the download served on dystroy.org), with a copy kept in releases/, +# and dist/, which holds exactly the files to attach to the GitHub release: one +# archive per target, the global zip, and sha256sums. # # With staging configured (DYSK_STAGE_HOST, see _local.sh) this does NOT build — # it fetches the artifacts every host pushed for the current commit and checks # the set is complete (so the macOS binaries from the Mac and the linux/windows # ones from wherever they were built come together). Without staging it falls # back to a local build-all (single host). Either way it then verifies every -# binary and zips. +# binary and packages. +# +# --dry-run packages for inspection: it neither stops nor asks, but lists at the +# end everything a real release would stop on or ask about, and archives nothing +# in releases/. It exits with 1 when a real release would stop. source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_targets.sh" here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +dry_run=no +case ${1:-} in + "") ;; + --dry-run|-n) dry_run=yes ;; + *) die "usage: $(basename "$0") [--dry-run]" ;; +esac +[[ $# -le 1 ]] || die "usage: $(basename "$0") [--dry-run]" + +stops=() +asks=() +# Die, or in a dry run note it and go on. +stop() { # stop <message> + [[ $dry_run == yes ]] || die "$1" + warn "$1" + stops+=("$1") +} +# Ask, or in a dry run note the question and go on. +ask() { # ask <question> <abort message> + if [[ $dry_run == yes ]]; then + asks+=("$1") + return 0 + fi + confirm "$1" || die "$2" +} + version=$(dysk_version) +[[ $dry_run == yes ]] && h1 "DRY RUN — packaging $version for inspection" # The GitHub release and the source archives come from origin, so the binaries # must be built from the commit that's there. h2 "Checking the release source" -check_release_source +if [[ $dry_run == yes ]]; then + # A separate process, so that errexit applies inside the check, and no ERR + # trap here, as bash 3.2 runs it even in a condition. + if out=$(trap - ERR; "$BASH" -c 'source "$1"; check_release_source' _ "$here/_common.sh" 2>&1); then + printf '%s\n' "$out" + else + reason= + while IFS= read -r line; do + if [[ $line == "✗ "* ]]; then + reason=${line#"✗ "} + break + fi + done <<< "$out" + stop "${reason:-the release source check failed}" + fi +else + check_release_source +fi # Publishing a binary built from uncommitted work is worth a question, whether # or not the artifacts travelled through a staging server. if ! tree_is_clean; then warn "working tree has uncommitted changes, so this release won't match commit $(git rev-parse --short HEAD)" - confirm "assemble it anyway?" || die "aborted — commit your changes, then re-run" + ask "uncommitted changes — assemble it anyway?" "aborted — commit your changes, then re-run" fi if staging_configured; then id=$(release_id) h1 "Assembling release $version from $DYSK_STAGE_HOST ($id)" + staged=$(stage_has_release) || die "couldn't reach $DYSK_STAGE_HOST" + [[ $staged == yes ]] || die "nothing staged for $id — run build-all-targets.sh first" rm -rf build && mkdir build h2 "Fetching staged artifacts" stage_fetch build @@ -43,19 +93,19 @@ while IFS= read -r line; do [[ -n $line ]] && info "$line" done <<< "$dirty_report" - confirm "package them anyway?" || die "aborted — rebuild those targets from a clean tree" + ask "staged artifacts built from uncommitted changes — package them anyway?" \ + "aborted — rebuild those targets from a clean tree" fi - manifest_fn=all_release_targets # a full release must contain every target else - h1 "Building release $version locally — THIS host's targets only" + h1 "Building release $version locally" # A Mac covers the whole matrix (zig for linux/windows, native for Apple); - # elsewhere the Apple binaries are missing, and that's worth saying. + # elsewhere the Apple binaries are missing, which only a dry run accepts. if [[ $(all_targets | wc -l) -lt $(all_release_targets | wc -l) ]]; then - warn "this host can't build every release target (no Apple binary outside a Mac)." - warn "to assemble a full release, set DYSK_STAGE_HOST in build-scripts/_local.sh on each machine." + hint="this host can't build every release target (no Apple binary outside a Mac) — set DYSK_STAGE_HOST in build-scripts/_local.sh on each machine" + [[ $dry_run == yes ]] || die "$hint" + warn "$hint" fi "$here/build-all-targets.sh" - manifest_fn=all_targets # can only expect what this host builds fi # Completeness: every target of the full (cross-host) release manifest must be @@ -69,15 +119,23 @@ else missing+=("$label ($triple)") fi -done < <("$manifest_fn") -[[ ${#missing[@]} -eq 0 ]] || die "release incomplete — missing binaries: ${missing[*]}" -ok "all release targets present and verified" +done < <(all_release_targets) +if [[ ${#missing[@]} -gt 0 ]]; then + stop "release incomplete — missing binaries: ${missing[*]}" +else + ok "all release targets present and verified" +fi # The non-binary artifacts must be there too. +missing=() for f in README.md CHANGELOG.md version install.md completion man/dysk.1; do - [[ -e "build/$f" ]] || die "release artifact missing from build/: $f" + [[ -e "build/$f" ]] || missing+=("$f") done -ok "metadata, completions and man page present" +if [[ ${#missing[@]} -gt 0 ]]; then + stop "release artifacts missing from build/: ${missing[*]}" +else + ok "metadata, completions and man page present" +fi # build the release archive, whose entries are the contents of build/, not a # build/ directory: dysk_3.6.1.zip unzips to aarch64-apple-darwin/, man/, etc. @@ -85,10 +143,60 @@ ( cd build && zip -rq "../dysk_$version.zip" -- * ) ok "created dysk_$version.zip" +# One archive per target, each unpacking to a single dysk-v<version>-<triple>/ +# directory: a .zip for Windows, a .tar.gz elsewhere. +h2 "Packaging one archive per target in dist/" +rm -rf dist && mkdir -p dist/.tmp +while IFS='|' read -r label triple _tool _features; do + bin=$(target_binary "$triple") + [[ -f $bin ]] || continue + name="$NAME-v$version-${triple%%.*}" + mkdir "dist/.tmp/$name" + cp "$bin" "dist/.tmp/$name/" + for f in build/README.md build/CHANGELOG.md LICENSE build/completion build/man; do + if [[ -e $f ]]; then + cp -R "$f" "dist/.tmp/$name/" + fi + done + if [[ $triple == *windows* ]]; then + ( cd dist/.tmp && zip -rqX "../$name.zip" "$name" ) + ok "dist/$name.zip" + else + # keep macOS metadata (._ files, xattrs) out of the archive + COPYFILE_DISABLE=1 tar --no-xattrs -czf "dist/$name.tar.gz" -C dist/.tmp "$name" + ok "dist/$name.tar.gz" + fi +done < <(all_release_targets) +rm -rf dist/.tmp +cp "dysk_$version.zip" dist/ +sha256() { if have sha256sum; then sha256sum "$@"; else shasum -a 256 "$@"; fi; } +( cd dist && sha256 *.tar.gz *.zip > sha256sums ) +ok "dist/sha256sums" + +if [[ $dry_run == yes ]]; then + # github-release.sh refuses to publish a dry run's dist/ + touch dist/.dry-run + h1 "DRY RUN done — inspect dist/ (nothing archived in releases/)" + if [[ ${#stops[@]} -gt 0 ]]; then + warn "a real release would stop:" + for s in "${stops[@]}"; do info "- $s"; done + fi + if [[ ${#asks[@]} -gt 0 ]]; then + warn "a real release would ask:" + for a in "${asks[@]}"; do info "- $a"; done + fi + [[ ${#stops[@]} -eq 0 ]] || exit 1 + if [[ ${#asks[@]} -gt 0 ]]; then + ok "a real release would go through, once those questions are answered" + else + ok "a real release would go through" + fi + exit 0 +fi + mkdir -p releases cp "dysk_$version.zip" releases/ ok "archived in releases/" -h1 "Release $version ready: dysk_$version.zip" -info "deploy.sh publishes it and tags $(release_tag); then create the GitHub release with" -info " gh release create $(release_tag) dysk_$version.zip --verify-tag" +h1 "Release $version ready: dysk_$version.zip and dist/" +info "deploy.sh publishes it, tags $(release_tag), and creates the GitHub release as a draft" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dysk-3.8.1/cli/Cargo.toml new/dysk-3.8.2/cli/Cargo.toml --- old/dysk-3.8.1/cli/Cargo.toml 2026-10-17 18:04:08.000000000 +0200 +++ new/dysk-3.8.2/cli/Cargo.toml 2026-11-02 17:55:48.000000000 +0100 @@ -1,6 +1,6 @@ [package] name = "dysk-cli" -version = "3.8.1" +version = "3.8.2" authors = ["dystroy <[email protected]>"] edition = "2024" license = "MIT" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dysk-3.8.1/cli/src/csv.rs new/dysk-3.8.2/cli/src/csv.rs --- old/dysk-3.8.1/cli/src/csv.rs 2026-10-17 18:04:08.000000000 +0200 +++ new/dysk-3.8.2/cli/src/csv.rs 2026-11-02 17:55:48.000000000 +0100 @@ -2,6 +2,7 @@ crate::{ Args, col::Col, + sanitize::sanitize, }, lfs_core::*, std::{ @@ -27,7 +28,8 @@ &mut self, content: D, ) -> Result<(), std::io::Error> { - let s = content.to_string(); + let raw = content.to_string(); + let s = sanitize(&raw); let needs_quotes = s.contains(self.separator) || s.contains('"') || s.contains('\n'); if needs_quotes { write!(self.w, "\"")?; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dysk-3.8.1/cli/src/lib.rs new/dysk-3.8.2/cli/src/lib.rs --- old/dysk-3.8.1/cli/src/lib.rs 2026-10-17 18:04:08.000000000 +0200 +++ new/dysk-3.8.2/cli/src/lib.rs 2026-11-02 17:55:48.000000000 +0100 @@ -9,6 +9,7 @@ pub mod list_cols; pub mod normal; pub mod order; +pub mod sanitize; pub mod sorting; pub mod table; pub mod timeout; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dysk-3.8.1/cli/src/sanitize.rs new/dysk-3.8.2/cli/src/sanitize.rs --- old/dysk-3.8.1/cli/src/sanitize.rs 1970-01-01 01:00:00.000000000 +0100 +++ new/dysk-3.8.2/cli/src/sanitize.rs 2026-11-02 17:55:48.000000000 +0100 @@ -0,0 +1,196 @@ +use std::borrow::Cow; + +/// Replace C0 (including ESC), DEL, and C1 controls with a visible `\xNN` +/// escape (e.g. `\x1b`), for strings which may be printed to a terminal +/// and may contain untrusted bytes. +pub fn sanitize(s: &str) -> Cow<'_, str> { + if !s.contains(char::is_control) { + return Cow::Borrowed(s); + } + let mut out = String::with_capacity(s.len() + 8); + for c in s.chars() { + if c.is_control() { + // C0, DEL and C1 are all below U+0100. + out.push_str(&format!("\\x{:02x}", c as u32)); + } else { + out.push(c); + } + } + Cow::Owned(out) +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::{ + args::Args, + csv, + json, + table, + units::Units, + }, + clap::Parser, + lfs_core::*, + std::io::Cursor, + }; + + #[test] + fn keeps_text_without_controls() { + assert_eq!(sanitize("/dev/sda1"), "/dev/sda1"); + assert_eq!(sanitize("café /mnt"), "café /mnt"); + } + + #[test] + fn replaces_c0_del_c1_and_esc() { + // ESC, BEL, DEL, and a C1 CSI. + assert_eq!(sanitize("a\u{1b}\u{7}\u{7f}\u{9b}b"), "a\\x1b\\x07\\x7f\\x9bb"); + assert!(!sanitize("a\u{1b}b").contains(char::is_control)); + } + + #[test] + fn table_and_csv_are_neutralized_json_is_not() { + let evil = mount("fuse\u{1b}[31msrc", "/mnt/\u{1b}]0;pwned\u{7}", true); + let clean = mount("/dev/sda1", "/safe/path", false); + let mounts = [&evil, &clean]; + + // One column at a time so termimad's terminal-width wrapping cannot + // split the sanitized text across lines. + assert_col("fs", &mounts, &["fuse\\x1b[31msrc", "/dev/sda1"]); + assert_col("mp", &mounts, &["/mnt/\\x1b]0;pwned\\x07", "/safe/path"]); + assert_col("type", &mounts, &["fu\\x9bse", "ext4"]); + assert_col("label", &mounts, &["lab\\x9del"]); + assert_col("options", &mounts, &["rw,compress=zstd\\x1b:3"]); + assert_col("uuid", &mounts, &["uu\\x7fid"]); + assert_col("partuuid", &mounts, &["part\\x1b"]); + assert_col("compress", &mounts, &["zstd\\x1b:3"]); + + let colored = render_table("mp", &mounts, true); + assert!( + !colored.contains("\u{1b}]0;pwned"), + "OSC payload survived color output: {colored:?}" + ); + assert!(!colored.contains('\u{7}'), "{colored:?}"); + assert!(colored.contains("/mnt/\\x1b]0;pwned\\x07"), "{colored:?}"); + + let args = args_with("fs+type+label+mp+options+uuid+partuuid+compress"); + let mut csv_buf = Cursor::new(Vec::new()); + csv::write(&mut csv_buf, &mounts, &args).unwrap(); + let csv_out = String::from_utf8(csv_buf.into_inner()).unwrap(); + assert_terminal_safe(&csv_out); + for needle in [ + "/mnt/\\x1b]0;pwned\\x07", + "fuse\\x1b[31msrc", + "fu\\x9bse", + "lab\\x9del", + "rw,compress=zstd\\x1b:3", + "uu\\x7fid", + "part\\x1b", + "zstd\\x1b:3", + "/safe/path", + "/dev/sda1", + ] { + assert!(csv_out.contains(needle), "csv missing {needle:?}: {csv_out:?}"); + } + + let json_out = serde_json::to_string(&json::output_value(&mounts, Units::Si)).unwrap(); + // serde escapes C0 (so the ESC byte is gone) and keeps the original text. + assert!(json_out.contains(r"\u001b"), "{json_out}"); + assert!(json_out.contains("]0;pwned"), "{json_out}"); + assert!(json_out.contains("/safe/path"), "{json_out}"); + assert!(json_out.contains("lab\u{9d}el"), "{json_out}"); + assert!(!json_out.contains("/mnt/\\x1b]0;pwned\\x07"), "{json_out}"); + assert!(!json_out.contains('\u{1b}'), "{json_out}"); + } + + fn assert_col( + col: &str, + mounts: &[&Mount], + needles: &[&str], + ) { + let out = render_table(col, mounts, false); + assert_terminal_safe(&out); + for needle in needles { + assert!( + out.contains(needle), + "{col} missing {needle:?} in {out:?}" + ); + } + } + + fn render_table( + cols: &str, + mounts: &[&Mount], + color: bool, + ) -> String { + let mut buf = Cursor::new(Vec::new()); + table::write(&mut buf, mounts, color, &args_with(cols)).unwrap(); + String::from_utf8(buf.into_inner()).unwrap() + } + + fn args_with(cols: &str) -> Args { + Args::try_parse_from(["dysk", "--color", "no", "--ascii", "-c", cols]).unwrap() + } + + fn assert_terminal_safe(out: &str) { + for c in ['\u{1b}', '\u{7}', '\u{7f}', '\u{9d}', '\u{9b}'] { + assert!(!out.contains(c), "output contains {c:?}: {out:?}"); + } + } + + fn mount( + fs: &str, + mount_point: &str, + hostile: bool, + ) -> Mount { + let (label, options, uuid, part_uuid, fs_type) = if hostile { + ( + Some("lab\u{9d}el".to_string()), + vec![ + MountOption::new("rw", None), + MountOption::new("compress", Some("zstd\u{1b}:3")), + ], + Some("uu\u{7f}id".to_string()), + Some("part\u{1b}".to_string()), + "fu\u{9b}se".to_string(), + ) + } else { + ( + None, + vec![MountOption::new("rw", None)], + None, + None, + "ext4".to_string(), + ) + }; + Mount { + info: MountInfo { + id: Some(1), + parent: Some(0), + dev: device_id(), + root: "/".into(), + mount_point: mount_point.into(), + options, + fs: fs.to_string(), + fs_type, + bound: false, + }, + fs_label: label, + disk: None, + stats: Err(StatsError::Excluded), + uuid, + part_uuid, + } + } + + fn device_id() -> DeviceId { + #[cfg(windows)] + { + DeviceId::new(0x1234_5678) + } + #[cfg(not(windows))] + { + DeviceId::new(8, 16) + } + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dysk-3.8.1/cli/src/table.rs new/dysk-3.8.2/cli/src/table.rs --- old/dysk-3.8.1/cli/src/table.rs 2026-10-17 18:04:08.000000000 +0200 +++ new/dysk-3.8.2/cli/src/table.rs 2026-11-02 17:55:48.000000000 +0100 @@ -2,6 +2,7 @@ crate::{ Args, col::Col, + sanitize::sanitize, }, lfs_core::*, std::{ @@ -60,16 +61,22 @@ .map_or(String::new(), |i| i.to_string()), ) .set("dev", mount.info.dev) - .set("filesystem", &mount.info.fs) + .set("filesystem", sanitize(&mount.info.fs)) .set("disk", mount.disk.as_ref().map_or("", |d| d.disk_type())) - .set("type", &mount.info.fs_type) - .set("mount-point", mount.info.mount_point.to_string_lossy()) - .set("mount-options", mount.info.options_string()) - .set_option("uuid", mount.uuid.as_ref()) - .set_option("part_uuid", mount.part_uuid.as_ref()) - .set_option("compress-level", mount.info.option_value("compress")); + .set("type", sanitize(&mount.info.fs_type)) + .set( + "mount-point", + sanitize(&mount.info.mount_point.to_string_lossy()), + ) + .set("mount-options", sanitize(&mount.info.options_string())) + .set_option("uuid", mount.uuid.as_deref().map(sanitize)) + .set_option("part_uuid", mount.part_uuid.as_deref().map(sanitize)) + .set_option( + "compress-level", + mount.info.option_value("compress").map(sanitize), + ); if let Some(label) = &mount.fs_label { - sub.set("label", label); + sub.set("label", sanitize(label)); } if mount.is_remote() { sub.set("remote", "x"); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dysk-3.8.1/website/src/install.md new/dysk-3.8.2/website/src/install.md --- old/dysk-3.8.1/website/src/install.md 2026-10-17 18:04:08.000000000 +0200 +++ new/dysk-3.8.2/website/src/install.md 2026-11-02 17:55:48.000000000 +0100 @@ -25,10 +25,17 @@ The archives also contain dysk's man page and shell completion scripts. -You may download previous releases on [GitHub releases](https://github.com/Canop/dysk/releases). +[GitHub releases](https://github.com/Canop/dysk/releases) hold previous releases too, and, for each release, one archive per target (eg `dysk-v3.8.1-x86_64-unknown-linux-musl.tar.gz`) with a `sha256sums` file. When you download executable files, you'll have to ensure the shell can find them. An easy solution on linux is for example to put them in `/usr/local/bin`. You may also have to set them executable using `chmod +x dysk`. +On Linux or macOS, typical destinations for the other files of the archive are: + +* `man/dysk.1` -> `/usr/local/share/man/man1/` +* `completion/dysk.bash` -> `/usr/local/share/bash-completion/completions/dysk` +* `completion/_dysk` -> `/usr/local/share/zsh/site-functions/` +* `completion/dysk.fish` -> `~/.config/fish/completions/` + # From crates.io You'll need to have the [Rust development environment](https://www.rustup.rs) installed and up to date. ++++++ dysk.obsinfo ++++++ --- /var/tmp/diff_new_pack.6dP1Iz/_old 2026-11-05 18:23:35.690899371 +0100 +++ /var/tmp/diff_new_pack.6dP1Iz/_new 2026-11-05 18:23:35.706900038 +0100 @@ -1,5 +1,5 @@ name: dysk -version: 3.8.1 -mtime: 1792253048 -commit: c002b844cfc33d56aaa7a4c46b80b033b07e0dee +version: 3.8.2 +mtime: 1793638548 +commit: 990df3edc2a1312d2f943064f605025a45691c55 ++++++ vendor.tar.zst ++++++ /work/SRC/openSUSE:Factory/dysk/vendor.tar.zst /work/SRC/openSUSE:Factory/.dysk.new.3759290/vendor.tar.zst differ: char 7, line 1
