From d41da4fc6470ea7a17dc7a5834ac9b1a00bbc826 Mon Sep 17 00:00:00 2001 From: Kayshen-X Date: Wed, 15 Jul 2026 21:10:08 +0800 Subject: [PATCH] fix: harden workspace version reader inputs --- scripts/workspace-version.sh | 136 ++++++++++++++++++++ scripts/workspace-version.test.sh | 202 ++++++++++++++++++++++++++++++ 2 files changed, 338 insertions(+) create mode 100755 scripts/workspace-version.sh create mode 100755 scripts/workspace-version.test.sh diff --git a/scripts/workspace-version.sh b/scripts/workspace-version.sh new file mode 100755 index 000000000..428e43f54 --- /dev/null +++ b/scripts/workspace-version.sh @@ -0,0 +1,136 @@ +#!/bin/sh + +set -eu + +if [ "$#" -gt 1 ]; then + printf 'workspace-version: expected zero or one manifest path argument\n' >&2 + exit 1 +fi + +script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd) +manifest=${1-"$script_dir/../Cargo.toml"} + +if [ ! -f "$manifest" ] || [ ! -r "$manifest" ]; then + printf 'workspace-version: cannot read manifest: %s\n' "$manifest" >&2 + exit 1 +fi + +if version=$(awk ' +function valid_core_number(value) { + return value == "0" || value ~ /^[1-9][0-9]*$/ +} + +function valid_identifiers(value, reject_leading_zero, count, i, identifiers) { + if (value == "") { + return 0 + } + + count = split(value, identifiers, "[.]") + for (i = 1; i <= count; i++) { + if (identifiers[i] !~ /^[0-9A-Za-z-]+$/) { + return 0 + } + if (reject_leading_zero && identifiers[i] ~ /^[0-9]+$/ && + length(identifiers[i]) > 1 && substr(identifiers[i], 1, 1) == "0") { + return 0 + } + } + return 1 +} + +function valid_semver(value, base, core, prerelease, build, plus, dash, count, parts) { + base = value + plus = index(base, "+") + if (plus > 0) { + build = substr(base, plus + 1) + if (!valid_identifiers(build, 0)) { + return 0 + } + base = substr(base, 1, plus - 1) + } + + dash = index(base, "-") + if (dash > 0) { + prerelease = substr(base, dash + 1) + if (!valid_identifiers(prerelease, 1)) { + return 0 + } + core = substr(base, 1, dash - 1) + } else { + core = base + } + + count = split(core, parts, "[.]") + return count == 3 && valid_core_number(parts[1]) && + valid_core_number(parts[2]) && valid_core_number(parts[3]) +} + +/^[[:space:]]*\[[[:space:]]*workspace[[:space:]]*[.][[:space:]]*package[[:space:]]*\][[:space:]]*(#.*)?$/ { + section_count++ + in_workspace_package = 1 + next +} + +/^[[:space:]]*\[/ { + in_workspace_package = 0 + next +} + +in_workspace_package && /^[[:space:]]*version[[:space:]]*=/ { + version_count++ + if ($0 !~ /^[[:space:]]*version[[:space:]]*=[[:space:]]*"[^"]*"[[:space:]]*(#.*)?$/) { + invalid_assignment = 1 + next + } + + value = $0 + sub(/^[^"]*"/, "", value) + sub(/".*$/, "", value) + workspace_version = value +} + +END { + if (section_count == 0) { + exit 10 + } + if (section_count > 1) { + exit 11 + } + if (version_count == 0) { + exit 12 + } + if (version_count > 1) { + exit 13 + } + if (invalid_assignment || !valid_semver(workspace_version)) { + exit 14 + } + + print workspace_version +} +' < "$manifest"); then + printf '%s\n' "$version" +else + status=$? + case "$status" in + 10) + printf 'workspace-version: missing [workspace.package] section in %s\n' "$manifest" >&2 + ;; + 11) + printf 'workspace-version: expected exactly one [workspace.package] section in %s\n' "$manifest" >&2 + ;; + 12) + printf 'workspace-version: missing version in [workspace.package] in %s\n' "$manifest" >&2 + ;; + 13) + printf 'workspace-version: expected exactly one version in [workspace.package] in %s\n' "$manifest" >&2 + ;; + 14) + printf 'workspace-version: invalid version in [workspace.package] in %s\n' "$manifest" >&2 + ;; + *) + printf 'workspace-version: failed to parse manifest: %s\n' "$manifest" >&2 + ;; + esac + exit 1 +fi diff --git a/scripts/workspace-version.test.sh b/scripts/workspace-version.test.sh new file mode 100755 index 000000000..dafdabb0a --- /dev/null +++ b/scripts/workspace-version.test.sh @@ -0,0 +1,202 @@ +#!/bin/sh + +set -eu + +script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd) +reader="$script_dir/workspace-version.sh" +tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/workspace-version.XXXXXX") +trap 'rm -rf "$tmp_dir"' 0 HUP INT TERM + +test_count=0 + +fail() { + printf 'not ok %s - %s\n' "$test_count" "$1" >&2 + exit 1 +} + +write_manifest() { + name=$1 + content=$2 + printf '%s\n' "$content" > "$tmp_dir/$name" +} + +assert_success() { + name=$1 + label=$2 + expected=$3 + test_count=$((test_count + 1)) + + if ! "$reader" "$tmp_dir/$name" > "$tmp_dir/stdout" 2> "$tmp_dir/stderr"; then + sed 's/^/ /' "$tmp_dir/stderr" >&2 + fail "$label unexpectedly failed" + fi + + printf '%s\n' "$expected" > "$tmp_dir/expected" + if ! cmp -s "$tmp_dir/expected" "$tmp_dir/stdout"; then + fail "$label did not print exactly '$expected'" + fi + if [ -s "$tmp_dir/stderr" ]; then + fail "$label wrote unexpected stderr" + fi + + printf 'ok %s - %s\n' "$test_count" "$label" +} + +assert_relative_success() { + name=$1 + label=$2 + expected=$3 + test_count=$((test_count + 1)) + + if ! (cd "$tmp_dir" && "$reader" "$name" "$tmp_dir/stdout" 2> "$tmp_dir/stderr"; then + sed 's/^/ /' "$tmp_dir/stderr" >&2 + fail "$label unexpectedly failed" + fi + + printf '%s\n' "$expected" > "$tmp_dir/expected" + if ! cmp -s "$tmp_dir/expected" "$tmp_dir/stdout"; then + fail "$label did not print exactly '$expected'" + fi + if [ -s "$tmp_dir/stderr" ]; then + fail "$label wrote unexpected stderr" + fi + + printf 'ok %s - %s\n' "$test_count" "$label" +} + +assert_failure() { + name=$1 + label=$2 + expected_error=$3 + test_count=$((test_count + 1)) + + if "$reader" "$tmp_dir/$name" > "$tmp_dir/stdout" 2> "$tmp_dir/stderr"; then + fail "$label unexpectedly succeeded" + fi + if [ -s "$tmp_dir/stdout" ]; then + fail "$label wrote unexpected stdout" + fi + printf 'workspace-version: %s in %s\n' "$expected_error" "$tmp_dir/$name" > "$tmp_dir/expected" + if ! cmp -s "$tmp_dir/expected" "$tmp_dir/stderr"; then + sed 's/^/ /' "$tmp_dir/stderr" >&2 + fail "$label stderr did not match the expected diagnostic" + fi + + printf 'ok %s - %s\n' "$test_count" "$label" +} + +write_manifest case01.toml '[workspace] +members = [] + +[workspace.package] +version = "0.8.1"' + +write_manifest case02.toml '[workspace.package] +version = "1.2.3-rc.1+build.4"' + +write_manifest case03.toml '[workspace.package] +version = "1.2.3+build.004"' + +write_manifest 'version=fixture' '[workspace.package] +version = "0.8.1"' + +write_manifest case04.toml '[workspace] +members = [] + +[package] +name = "decoy" +version = "9.9.9"' + +write_manifest case05.toml '[workspace.package] +edition = "2021" + +[workspace.dependencies] +serde = { version = "1.0", features = ["derive"] }' + +write_manifest case06.toml '[workspace.package] +version = "0.8.1" + +[workspace.dependencies] +serde = "1" + +[workspace.package] +version = "0.8.2"' + +write_manifest case07.toml '[workspace.package] +version = "0.8.1" +version = "0.8.2"' + +write_manifest case08.toml '[workspace.package] +version = "v0.8.1"' + +write_manifest case09.toml '[workspace.package] +version = "0.8"' + +write_manifest case10.toml '[workspace.package] +version = "latest"' + +write_manifest case11.toml '[workspace.package] +version = "01.2.3"' + +write_manifest case12.toml '[workspace.package] +version = "1.02.3"' + +write_manifest case13.toml '[workspace.package] +version = "1.2.03"' + +write_manifest case14.toml '[workspace.package] +version = "1.2.3-01"' + +write_manifest case15.toml '[workspace.package] +version = "1.2.3-"' + +write_manifest case16.toml '[workspace.package] +version = "1.2.3+"' + +write_manifest case17.toml '[workspace.package] +version = "1.2.3-rc_1"' + +write_manifest case18.toml '[workspace.package] +version = "1.2.3-rc..1"' + +write_manifest case19.toml '[workspace.package] +version = "1.2.3+build..4"' + +write_manifest case20.toml '[workspace.package] +version = 0.8.1' + +write_manifest case21.toml "[workspace.package] +version = '0.8.1'" + +write_manifest case22.toml '[workspace.package] +version = "0.8.1' + +write_manifest case23.toml '[workspace.package] +version = "0.8.1" trailing' + +assert_success case01.toml stable '0.8.1' +assert_success case02.toml prerelease_build '1.2.3-rc.1+build.4' +assert_success case03.toml build_with_leading_zero '1.2.3+build.004' +assert_relative_success 'version=fixture' assignment_shaped_relative_path '0.8.1' +assert_failure case04.toml missing_workspace_package 'missing [workspace.package] section' +assert_failure case05.toml missing_version 'missing version in [workspace.package]' +assert_failure case06.toml duplicate_sections 'expected exactly one [workspace.package] section' +assert_failure case07.toml duplicate_versions 'expected exactly one version in [workspace.package]' +assert_failure case08.toml malformed_v_prefix 'invalid version in [workspace.package]' +assert_failure case09.toml malformed_short 'invalid version in [workspace.package]' +assert_failure case10.toml malformed_latest 'invalid version in [workspace.package]' +assert_failure case11.toml leading_zero_major 'invalid version in [workspace.package]' +assert_failure case12.toml leading_zero_minor 'invalid version in [workspace.package]' +assert_failure case13.toml leading_zero_patch 'invalid version in [workspace.package]' +assert_failure case14.toml leading_zero_numeric_prerelease 'invalid version in [workspace.package]' +assert_failure case15.toml empty_prerelease 'invalid version in [workspace.package]' +assert_failure case16.toml empty_build 'invalid version in [workspace.package]' +assert_failure case17.toml illegal_identifier_character 'invalid version in [workspace.package]' +assert_failure case18.toml repeated_prerelease_separator 'invalid version in [workspace.package]' +assert_failure case19.toml repeated_build_separator 'invalid version in [workspace.package]' +assert_failure case20.toml unquoted_assignment 'invalid version in [workspace.package]' +assert_failure case21.toml single_quoted_assignment 'invalid version in [workspace.package]' +assert_failure case22.toml unterminated_quoted_assignment 'invalid version in [workspace.package]' +assert_failure case23.toml trailing_assignment_content 'invalid version in [workspace.package]' + +printf '1..%s\n' "$test_count"