From 2bf8c8ede3e42839979982c5b4a65552b45f8f74 Mon Sep 17 00:00:00 2001 From: Dmitrii Krosh Date: Mon, 20 Jul 2026 00:56:41 +0300 Subject: [PATCH] WIP --- Docs/compare-sisyphus-set-versions.md | 125 ++++ README.md | 4 + scripts/compare_sisyphus_set_versions.sh | 611 ++++++++++++++++++++ scripts/completed_set_versions.awk | 10 + scripts/newset_compat.h | 48 ++ scripts/newset_mkset.c | 63 ++ scripts/parse_sisyphus_packages.awk | 39 ++ scripts/rpm_payload_safety.sh | 126 ++++ tests/test_compare_sisyphus_set_versions.py | 319 ++++++++++ tt | 17 + 10 files changed, 1362 insertions(+) create mode 100644 Docs/compare-sisyphus-set-versions.md create mode 100755 scripts/compare_sisyphus_set_versions.sh create mode 100644 scripts/completed_set_versions.awk create mode 100644 scripts/newset_compat.h create mode 100644 scripts/newset_mkset.c create mode 100644 scripts/parse_sisyphus_packages.awk create mode 100644 scripts/rpm_payload_safety.sh create mode 100644 tests/test_compare_sisyphus_set_versions.py create mode 100644 tt diff --git a/Docs/compare-sisyphus-set-versions.md b/Docs/compare-sisyphus-set-versions.md new file mode 100644 index 0000000..6d774d4 --- /dev/null +++ b/Docs/compare-sisyphus-set-versions.md @@ -0,0 +1,125 @@ +# Сравнение set-зависимостей Sisyphus с `newset.c` + +Скрипт [`scripts/compare_sisyphus_set_versions.sh`](../scripts/compare_sisyphus_set_versions.sh) +сравнивает set-строки из RPM-заголовка с результатом `reimplement/newset.c`. + +## Что именно сравнивается + +1. Из изолированных APT-индексов `Sisyphus/x86_64` и `Sisyphus/noarch` + берутся `Package`, `Architecture`, `Version`, `Filename`, `MD5Sum`, + `Provides` и `Depends`/`Pre-Depends`. +2. RPM скачивается непосредственно из `Sisyphus/files//RPMS` и + проверяется по `MD5Sum` из индекса. +3. Эталонные зависимости читаются из RPM-заголовка через массивы тегов: + `PROVIDENAME/PROVIDEFLAGS/PROVIDEVERSION` и + `REQUIRENAME/REQUIREFLAGS/REQUIREVERSION`. +4. Для воспроизведения зависимостей вызываются установленные вместе с + `rpm-build` программы `%_rpmlibdir/find-provides` и + `%_rpmlibdir/find-requires` с методом `none,lib`. +5. В каталоге инструментов заменяется **только** `mkset`: вместо штатного + `mkset` подставляется совместимая обвязка над `newset.c`. `provided_symbols`, + `ldd --bindings`, `eu-readelf`, `objdump`, `file` и остальные средства + отбора/извлечения символов остаются штатными инструментами `rpm-build`. + +Для `Requires` APT получает точный уже скачанный RPM-файл как вход и скачивает +его полный runtime dependency closure в пустой пользовательский RPM root. Так +версия и архитектура целевого пакета не выбираются повторно по имени. APT +запускается с пустым `APT_CONFIG`; source list, package cache, archives, lists, +status и preferences перенаправлены во временный каталог, поэтому настройки, +hooks и RPM-база основной системы не участвуют в разрешении зависимостей. + +Перед распаковкой проверяются имена элементов cpio и цели символических ссылок. +Абсолютная RPM-ссылка считается путём внутри виртуального package root: например, +`/usr/sbin/update-alternatives -> /bin/true` безопасно переписывается в +`../../bin/true`, а не направляется в `/bin/true` основной системы. Все symlink +сначала собираются из RPM-заголовков и создаются в порядке от родительских путей +к дочерним; затем `cpio` извлекает только нессылочные элементы. Благодаря этому +usrmerge-ссылки вроде `/bin -> usr/bin` создаются до файлов `/bin/*`. + +Имена элементов с выходом через `..` и относительные symlink, фактически +выходящие из временного root, по-прежнему отклоняются. После проверки RPM-файлы +распаковываются без установки. Это позволяет штатному `rpm-build/ldd` +использовать интерпретатор и библиотеки из того же снимка Sisyphus, не изменяя +систему. + +## Ограниченный тест + +На ALT p11 с установленным `rpm-build`: + +```sh +bash scripts/compare_sisyphus_set_versions.sh \ + --package strace \ + --report strace-set-report.tsv +``` + +Несколько пакетов: + +```sh +bash scripts/compare_sisyphus_set_versions.sh \ + --package strace \ + --package libdw \ + --report sample-set-report.tsv +``` + +Либо первые десять записей индекса: + +```sh +bash scripts/compare_sisyphus_set_versions.sh \ + --limit 10 \ + --report sample-set-report.tsv +``` + +Без `--all`, `--limit` или `--package` скрипт завершится до обращения к +репозиторию. Это защищает от случайного полного запуска. + +## Полный запуск + +Команда предусмотрена, но в ходе разработки не запускалась: + +```sh +bash scripts/compare_sisyphus_set_versions.sh \ + --all \ + --report sisyphus-set-report.tsv +``` + +Продолжение после прерывания: + +```sh +bash scripts/compare_sisyphus_set_versions.sh \ + --all \ + --resume \ + --report sisyphus-set-report.tsv +``` + +`--resume` пропускает только уже завершённую комбинацию +`package/architecture/version`; обновившийся пакет будет обработан заново. +Завершённость подтверждается маркером `complete=1` в полной 11-польной строке +`SUMMARY`, поэтому оборванная при записи строка не считается результатом. + +Во время работы отчёт можно наблюдать отдельно: + +```sh +tail -f sisyphus-set-report.tsv +``` + +`START` записывается до обработки пакета, а `DEPENDENCY` и `SUMMARY` — сразу +после сравнения пакета. Поэтому файл обновляется в процессе, а не только после +завершения всего обхода. + +## Статусы отчёта + +- `match` — set-строка из RPM совпала с результатом `newset.c`; +- `mismatch` — capability и оператор совпали, set-строки различаются; +- `missing_generated` — RPM содержит set-зависимость, но повторный запуск + `rpm-build` её не сгенерировал; +- `extra_generated` — повторный запуск сгенерировал зависимость, которой нет в + RPM-заголовке; +- `no_set_metadata` — в APT metadata пакета нет set-строк; +- `*_error` — ошибка скачивания, контрольной суммы, распаковки либо генератора. + +`extra_generated` показывается в строках `DEPENDENCY` и делает итоговый статус +пакета равным `mismatch`. При анализе нужно учитывать, что бинарный RPM не +содержит исходных spec-фильтров (`filter_from_requires` и аналогичных), поэтому +часть таких расхождений может относиться не к `newset.c`, а к невозможности +полностью воспроизвести фильтрацию исходного spec. Их число отдельно указано в +поле `extras=N` строки `SUMMARY`. diff --git a/README.md b/README.md index 5590e2c..3991589 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ # alt-rpm-set-version Research on ALT Linux RPM set.c and dependency comparison using encoded symbol sets. + +## Tools + +- [Compare Sisyphus set dependencies with `newset.c`](Docs/compare-sisyphus-set-versions.md) diff --git a/scripts/compare_sisyphus_set_versions.sh b/scripts/compare_sisyphus_set_versions.sh new file mode 100755 index 0000000..d89bf2e --- /dev/null +++ b/scripts/compare_sisyphus_set_versions.sh @@ -0,0 +1,611 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +REPO_ROOT=$(cd -- "$SCRIPT_DIR/.." && pwd) +NEWSET_C="$REPO_ROOT/reimplement/newset.c" +NEWSET_COMPAT="$SCRIPT_DIR/newset_compat.h" +NEWSET_WRAPPER="$SCRIPT_DIR/newset_mkset.c" +PACKAGE_PARSER="$SCRIPT_DIR/parse_sisyphus_packages.awk" +RESUME_PARSER="$SCRIPT_DIR/completed_set_versions.awk" +PAYLOAD_HELPERS="$SCRIPT_DIR/rpm_payload_safety.sh" + +# shellcheck source=scripts/rpm_payload_safety.sh +source "$PAYLOAD_HELPERS" + +MIRROR=https://ftp.altlinux.org/pub/distributions/ALTLinux +REPORT=sisyphus-set-compare.tsv +LIMIT= +ALL=0 +RESUME=0 +KEEP_WORK=0 +BUILD_MKSET= +PACKAGES=() + +usage() +{ + cat <<'EOF' +Usage: + compare_sisyphus_set_versions.sh --all [OPTIONS] + compare_sisyphus_set_versions.sh --limit N [OPTIONS] + compare_sisyphus_set_versions.sh --package NAME [--package NAME ...] [OPTIONS] + compare_sisyphus_set_versions.sh --build-mkset PATH + +Scopes: + --all Process every x86_64/noarch package record. + --limit N Process only the first N records (testing). + --package NAME Process one named package; may be repeated (testing). + +Options: + --report FILE Streaming TSV report (default: sisyphus-set-compare.tsv). + --resume Skip completed package/architecture/version records. + --keep-work Preserve the temporary working directory. + --mirror URL ALT repository root. + --build-mkset PATH Build only the mkset-compatible newset.c wrapper and exit. + -h, --help Show this help. + +The report gets a START row before processing and DEPENDENCY/SUMMARY rows are +appended after every package, so it can be monitored while the script runs. +No repository-wide run is possible without the explicit --all option. +EOF +} + +die() +{ + printf 'error: %s\n' "$*" >&2 + exit 2 +} + +while (($#)); do + case "$1" in + --all) ALL=1; shift ;; + --limit) (($# >= 2)) || die '--limit requires N'; LIMIT=$2; shift 2 ;; + --package) (($# >= 2)) || die '--package requires NAME'; PACKAGES+=("$2"); shift 2 ;; + --report) (($# >= 2)) || die '--report requires FILE'; REPORT=$2; shift 2 ;; + --resume) RESUME=1; shift ;; + --keep-work) KEEP_WORK=1; shift ;; + --mirror) (($# >= 2)) || die '--mirror requires URL'; MIRROR=${2%/}; shift 2 ;; + --build-mkset) (($# >= 2)) || die '--build-mkset requires PATH'; BUILD_MKSET=$2; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) die "unknown option: $1" ;; + esac +done + +build_mkset() +( +{ + local output=$1 build + build=$(mktemp -d "${TMPDIR:-/tmp}/arsv-newset-build.XXXXXX") + trap 'rm -rf "$build"' EXIT + : >"$build/rpmlib.h" + : >"$build/system.h" + mkdir -p "$(dirname -- "$output")" + + "${CC:-cc}" -O2 -std=gnu11 -D_GNU_SOURCE \ + -I"$build" -include "$NEWSET_COMPAT" \ + -c "$NEWSET_C" -o "$build/newset.o" + "${CC:-cc}" -O2 -std=gnu11 -D_GNU_SOURCE \ + "$NEWSET_WRAPPER" "$build/newset.o" -o "$output" + chmod 755 "$output" +} +) + +if [[ -n $BUILD_MKSET ]]; then + build_mkset "$BUILD_MKSET" + exit 0 +fi + +[[ $MIRROR == http://* || $MIRROR == https://* ]] || die '--mirror must use http:// or https://' +[[ $MIRROR != *[$'\t\r\n ']* ]] || die '--mirror must not contain whitespace' +for package in "${PACKAGES[@]}"; do + [[ $package =~ ^[A-Za-z0-9][A-Za-z0-9+_.-]*$ ]] || die "invalid package name: $package" +done + +scope_count=$ALL +[[ -n $LIMIT ]] && scope_count=$((scope_count + 1)) +((${#PACKAGES[@]} > 0)) && scope_count=$((scope_count + 1)) +((scope_count == 1)) || die 'use --all, --limit, or --package (exactly one scope)' +if [[ -n $LIMIT && ! $LIMIT =~ ^[1-9][0-9]*$ ]]; then + die '--limit must be a positive integer' +fi + +for command in apt-get apt-cache rpm rpmquery rpm2cpio cpio curl md5sum awk sed sort find cp cc realpath; do + command -v "$command" >/dev/null || die "required command not found: $command" +done + +rpmlibdir=$(rpm --eval '%_rpmlibdir') +[[ -x $rpmlibdir/find-provides ]] || die "$rpmlibdir/find-provides is missing (install rpm-build)" +[[ -x $rpmlibdir/find-requires ]] || die "$rpmlibdir/find-requires is missing (install rpm-build)" + +case $REPORT in + /*) ;; + *) REPORT="$PWD/$REPORT" ;; +esac +mkdir -p "$(dirname -- "$REPORT")" +if [[ ! -s $REPORT ]]; then + printf 'timestamp\trecord\tpackage\tarchitecture\tstatus\tside\tcapability\toperator\texpected\tgenerated\tdetail\n' >"$REPORT" +fi + +clean_field() +{ + local value=${1-} + value=${value//$'\t'/ } + value=${value//$'\r'/ } + value=${value//$'\n'/ } + printf '%s' "$value" +} + +report_row() +{ + local fields=() field row + (($# == 11)) || { + printf 'internal error: report row has %s fields, expected 11\n' "$#" >&2 + return 1 + } + for field in "$@"; do + fields+=("$(clean_field "$field")") + done + if [[ ${fields[1]} == SUMMARY ]]; then + case ${fields[10]} in + '') fields[10]='complete=1' ;; + *'; ') fields[10]="${fields[10]}complete=1" ;; + *) fields[10]="${fields[10]}; complete=1" ;; + esac + fi + row=${fields[0]} + for field in "${fields[@]:1}"; do + printf -v row '%s\t%s' "$row" "$field" + done + printf '%s\n' "$row" >>"$REPORT" +} + +timestamp() +{ + date -u '+%Y-%m-%dT%H:%M:%SZ' +} + +WORK=$(mktemp -d "${TMPDIR:-/tmp}/arsv-sisyphus-sets.XXXXXX") +cleanup() +{ + if ((KEEP_WORK)); then + printf 'work directory preserved: %s\n' "$WORK" >&2 + else + rm -rf "$WORK" + fi +} +trap cleanup EXIT +trap 'exit 130' INT +trap 'exit 143' TERM + +mkdir -p "$WORK/apt/lists/partial" "$WORK/apt/archives/partial" "$WORK/tools" +: >"$WORK/apt/apt.conf" +: >"$WORK/apt/status" +printf '%s\n' \ + "rpm [alt] $MIRROR Sisyphus/x86_64 classic" \ + "rpm [alt] $MIRROR Sisyphus/noarch classic" >"$WORK/apt/sources.list" + +APT_OPTIONS=( + -o 'Dir::Etc::main=-' + -o 'Dir::Etc::parts=-' + -o "Dir::Etc::sourcelist=$WORK/apt/sources.list" + -o 'Dir::Etc::sourceparts=-' + -o 'Dir::Etc::preferences=-' + -o 'Dir::Etc::preferencesparts=-' + -o "Dir::State::lists=$WORK/apt/lists" + -o "Dir::State::status=$WORK/apt/status" + -o "Dir::Cache::archives=$WORK/apt/archives" + -o "Dir::Cache::pkgcache=$WORK/apt/pkgcache.bin" + -o "Dir::Cache::srcpkgcache=$WORK/apt/srcpkgcache.bin" +) + +apt_get() +{ + APT_CONFIG="$WORK/apt/apt.conf" apt-get "${APT_OPTIONS[@]}" "$@" +} + +apt_cache() +{ + APT_CONFIG="$WORK/apt/apt.conf" apt-cache "${APT_OPTIONS[@]}" "$@" +} + +NEW_MKSET="$WORK/new-mkset" +build_mkset "$NEW_MKSET" +cp -as "$rpmlibdir"/. "$WORK/tools/" +rm -f "$WORK/tools/mkset" +ln -s "$NEW_MKSET" "$WORK/tools/mkset" + +report_row "$(timestamp)" RUN - - start - - - - - "updating isolated Sisyphus indexes" +if ! apt_get update >"$WORK/apt-update.log" 2>&1; then + detail=$(sed -n '/./{p;q;}' "$WORK/apt-update.log") + report_row "$(timestamp)" RUN - - apt_update_error - - - - - "$detail" + exit 1 +fi + +QUEUE="$WORK/packages.tsv" +if ! apt_cache dumpavail | + awk -f "$PACKAGE_PARSER" | + LC_ALL=C sort -t$'\034' -k1,1 -k2,2 >"$QUEUE"; then + report_row "$(timestamp)" RUN - - metadata_error - - - - - 'unable to parse apt-cache dumpavail' + exit 1 +fi + +SELECTED="$WORK/selected.tsv" +if ((${#PACKAGES[@]})); then + : >"$SELECTED" + for package in "${PACKAGES[@]}"; do + if ! awk -F '\034' -v package="$package" '$1 == package { print; found=1 } END { exit !found }' \ + "$QUEUE" >>"$SELECTED"; then + die "package not found in Sisyphus x86_64/noarch: $package" + fi + done +elif [[ -n $LIMIT ]]; then + sed -n "1,${LIMIT}p" "$QUEUE" >"$SELECTED" +else + cp "$QUEUE" "$SELECTED" +fi + +selected_count=$(wc -l <"$SELECTED") +report_row "$(timestamp)" RUN - - ready - - - - - "selected packages: $selected_count" +printf 'selected packages: %s; report: %s\n' "$selected_count" "$REPORT" + +declare -A COMPLETED=() +if ((RESUME)); then + while IFS=$'\t' read -r package architecture version; do + COMPLETED["$package"$'\t'"$architecture"$'\t'"$version"]=1 + done < <(awk -f "$RESUME_PARSER" "$REPORT") +fi + +first_log_line() +{ + local file=$1 + [[ -s $file ]] || return 0 + sed -n '/./{p;q;}' "$file" | cut -c1-500 +} + +last_log_line() +{ + local file=$1 + [[ -s $file ]] || return 0 + awk 'NF { line=$0 } END { print line }' "$file" | cut -c1-500 +} + +extract_rpm() +{ + local package_file=$1 root=$2 log=$3 mode=$4 + local metadata="$root/../.arsv-file-metadata" + local patterns="$root/../.arsv-nonlink-patterns" + local symlinks="$root/../.arsv-symlinks" + local field_separator=$'\034' record_separator=$'\035' record + local filename link_target relative archive_name + local query_format="[%{FILENAMES}${field_separator}%{FILELINKTOS}${record_separator}]" + + if ! rpmquery -p --qf "$query_format" \ + "$package_file" >"$metadata" 2>>"$log"; then + return 1 + fi + case $mode in + symlinks) : >"$symlinks" ;; + files) : >"$patterns" ;; + *) printf 'internal error: invalid RPM extraction mode: %s\n' "$mode" >>"$log"; return 1 ;; + esac + + while IFS= read -r -d "$record_separator" record; do + if ! rpm_split_file_record "$record"; then + printf 'unsupported RPM filename or symlink record\n' >>"$log" + return 1 + fi + filename=$RPM_FILENAME + link_target=$RPM_LINK_TARGET + [[ -n $filename && $filename == /* ]] || { + printf 'unsafe RPM filename: %q\n' "$filename" >>"$log" + return 1 + } + relative=${filename#/} + if [[ $relative == '..' || $relative == ../* || $relative == */../* || $relative == */.. ]]; then + printf 'unsafe RPM filename: %q\n' "$filename" >>"$log" + return 1 + fi + + if [[ -n $link_target && $link_target != '(none)' ]]; then + if [[ $mode == symlinks ]]; then + printf '%s\t%s\n' "$relative" "$link_target" >>"$symlinks" + fi + elif [[ $mode == files ]]; then + archive_name=./$relative + if ! rpm_cpio_literal_pattern "$archive_name" >>"$patterns"; then + printf 'unsupported RPM filename: %q\n' "$filename" >>"$log" + return 1 + fi + fi + done <"$metadata" + + if [[ $mode == files ]]; then + if ! rpm2cpio "$package_file" | + cpio -it --quiet --no-absolute-filenames 2>>"$log" | + while IFS= read -r member; do + case $member in + /*|../*|*/../*|*/..) + printf 'unsafe cpio member: %q\n' "$member" >>"$log" + exit 1 + ;; + esac + done; then + return 1 + fi + + if [[ -s $patterns ]]; then + if ! rpm2cpio "$package_file" | + (cd "$root" && cpio -idm --quiet --no-absolute-filenames -E "$patterns") \ + 2>>"$log"; then + return 1 + fi + fi + return 0 + fi + + cat "$symlinks" +} + +normalize_expected() +{ + local side=$1 package_file=$2 output=$3 + if [[ $side == provides ]]; then + rpmquery -p --qf \ + '[%{PROVIDENAME}\t%{PROVIDEFLAGS:depflags}\t%{PROVIDEVERSION}\n]' \ + "$package_file" + else + rpmquery -p --qf \ + '[%{REQUIRENAME}\t%{REQUIREFLAGS:depflags}\t%{REQUIREVERSION}\n]' \ + "$package_file" + fi | awk -F '\t' '$3 ~ /^set:/ { print $1 "\t" $2 "\t" $3 }' | + LC_ALL=C sort -u >"$output" +} + +normalize_generated() +{ + local side=$1 input=$2 output=$3 + if [[ $side == provides ]]; then + awk '$2 == "=" && $3 ~ /^set:/ { print $1 "\t" $2 "\t" $3 }' "$input" + else + awk '$2 == ">=" && $3 ~ /^set:/ { print $1 "\t" $2 "\t" $3 }' "$input" + fi | LC_ALL=C sort -u >"$output" +} + +compare_side() +{ + local side=$1 package=$2 architecture=$3 expected=$4 generated=$5 output=$6 + if ! awk -F '\t' -v OFS='\034' ' + FILENAME == ARGV[1] { key=$1 "\034" $2; expected[key]=$3; keys[key]=1; next } + { key=$1 "\034" $2; generated[key]=$3; keys[key]=1 } + END { + for (key in keys) { + split(key, parts, "\034") + e=expected[key]; g=generated[key] + if (e == "") status="extra_generated" + else if (g == "") status="missing_generated" + else if (e == g) status="match" + else status="mismatch" + print status, parts[1], parts[2], e, g + } + } + ' "$expected" "$generated" | + LC_ALL=C sort -t$'\034' -k2,2 -k3,3 >"$output"; then + return 1 + fi + + # A non-whitespace separator preserves empty expected/generated fields. + while IFS=$'\034' read -r status capability operator expected_set generated_set; do + [[ -n $status ]] || continue + report_row "$(timestamp)" DEPENDENCY "$package" "$architecture" "$status" \ + "$side" "$capability" "$operator" "$expected_set" "$generated_set" - + done <"$output" +} + +process_package() +{ + local package=$1 architecture=$2 version=$3 filename=$4 md5=$5 + local has_provides=$6 has_requires=$7 + local key=$package$'\t'$architecture$'\t'$version + if [[ -n ${COMPLETED[$key]-} ]]; then + printf 'skip completed: %s.%s\n' "$package" "$architecture" + return 0 + fi + + report_row "$(timestamp)" START "$package" "$architecture" processing - - - - - "$version" + printf 'process: %s.%s\n' "$package" "$architecture" + + if ((has_provides == 0 && has_requires == 0)); then + report_row "$(timestamp)" SUMMARY "$package" "$architecture" no_set_metadata \ + - - - - - 'APT metadata contains no set: Provides/Requires' + return 0 + fi + + local package_work="$WORK/package" + rm -rf "$package_work" + mkdir -p "$package_work/root" "$package_work/archives/partial" + local target="$package_work/target.rpm" + local url="$MIRROR/Sisyphus/files/$architecture/RPMS/$filename" + local log="$package_work/package.log" + + if ! curl --silent --show-error -fL --retry 3 --retry-delay 2 \ + -o "$target" "$url" >"$log" 2>&1; then + report_row "$(timestamp)" SUMMARY "$package" "$architecture" download_error \ + - - - - - "$(first_log_line "$log")" + return 0 + fi + if [[ -n $md5 ]]; then + local actual_md5 + if ! actual_md5=$(md5sum "$target" | awk '{print $1}'); then + report_row "$(timestamp)" SUMMARY "$package" "$architecture" checksum_error \ + - - - "$md5" - "$filename" + return 0 + fi + if [[ $actual_md5 != "$md5" ]]; then + report_row "$(timestamp)" SUMMARY "$package" "$architecture" checksum_error \ + - - - "$md5" "$actual_md5" "$filename" + return 0 + fi + fi + + local -a payload_rpms=("$target") + if ((has_requires)); then + mkdir -p "$package_work/root/var/lib/rpm" + if ! rpm --root "$package_work/root" --initdb >>"$log" 2>&1; then + report_row "$(timestamp)" SUMMARY "$package" "$architecture" rpmdb_error \ + - - - - - "$(last_log_line "$log")" + return 0 + fi + if ! apt_get -y -d \ + -o "Dir::Cache::archives=$package_work/archives" \ + -o "RPM::RootDir=$package_work/root" \ + install "$target" >>"$log" 2>&1; then + report_row "$(timestamp)" SUMMARY "$package" "$architecture" dependency_download_error \ + - - - - - "$(last_log_line "$log")" + return 0 + fi + local dependency_rpm + for dependency_rpm in "$package_work"/archives/*.rpm; do + [[ -f $dependency_rpm ]] || continue + payload_rpms+=("$dependency_rpm") + done + fi + + local payload_rpm extraction_status + local symlink_manifest="$package_work/symlinks.tsv" + : >"$symlink_manifest" + for payload_rpm in "${payload_rpms[@]}"; do + if ! extract_rpm "$payload_rpm" "$package_work/root" "$log" symlinks \ + >>"$symlink_manifest"; then + if [[ $payload_rpm == "$target" ]]; then + extraction_status=extract_error + else + extraction_status=dependency_extract_error + fi + report_row "$(timestamp)" SUMMARY "$package" "$architecture" "$extraction_status" \ + - - - - - "$(last_log_line "$log")" + return 0 + fi + done + if ! rpm_install_symlink_manifest "$package_work/root" "$symlink_manifest" "$log"; then + if ((has_requires)); then + extraction_status=dependency_extract_error + else + extraction_status=extract_error + fi + report_row "$(timestamp)" SUMMARY "$package" "$architecture" "$extraction_status" \ + - - - - - "$(last_log_line "$log")" + return 0 + fi + + for payload_rpm in "${payload_rpms[@]}"; do + if ! extract_rpm "$payload_rpm" "$package_work/root" "$log" files; then + if [[ $payload_rpm == "$target" ]]; then + extraction_status=extract_error + else + extraction_status=dependency_extract_error + fi + report_row "$(timestamp)" SUMMARY "$package" "$architecture" "$extraction_status" \ + - - - - - "$(last_log_line "$log")" + return 0 + fi + done + + if ! rpmquery -p --qf '[%{FILENAMES}\n]' "$target" | + awk -v root="$package_work/root" \ + '{ if (substr($0,1,1)=="/") print root $0; else print root "/" $0 }' | + while IFS= read -r path; do + if [[ -e $path || -L $path ]]; then + printf '%s\n' "$path" + fi + done >"$package_work/files"; then + report_row "$(timestamp)" SUMMARY "$package" "$architecture" file_list_error \ + - - - - - 'rpmquery failed while reading package filenames' + return 0 + fi + + local scanner_env=( + "RPM_BUILD_ROOT=$package_work/root" + "RPM_SUBPACKAGE_NAME=$package" + "RPM_PACKAGE_NAME=$package" + "RPMB_TOOLS_DIR=$WORK/tools" + "RPMB_LIB_DIR=$rpmlibdir" + "RPMB_AUTODEPS_DIR=$rpmlibdir" + 'RPM_FINDPROV_METHOD=none,lib' + 'RPM_FINDREQ_METHOD=none,lib' + 'RPM_SCRIPTS_DEBUG=0' + ) + + local side generator expected generated comparison + local total_mismatches=0 detail='' + for side in provides requires; do + [[ $side == provides && $has_provides == 1 ]] || + [[ $side == requires && $has_requires == 1 ]] || continue + + generator="$package_work/generated.$side.raw" + expected="$package_work/expected.$side.tsv" + generated="$package_work/generated.$side.tsv" + comparison="$package_work/comparison.$side.tsv" + if ! normalize_expected "$side" "$target" "$expected"; then + report_row "$(timestamp)" SUMMARY "$package" "$architecture" metadata_error \ + "$side" - - - - 'rpmquery failed while reading set dependencies' + return 0 + fi + + if [[ $side == provides ]]; then + if ! env "${scanner_env[@]}" "$rpmlibdir/find-provides" \ + <"$package_work/files" >"$generator" 2>"$package_work/$side.log"; then + report_row "$(timestamp)" SUMMARY "$package" "$architecture" generator_error \ + "$side" - - - - "$(first_log_line "$package_work/$side.log")" + return 0 + fi + else + if ! env "${scanner_env[@]}" "$rpmlibdir/find-requires" \ + <"$package_work/files" >"$generator" 2>"$package_work/$side.log"; then + report_row "$(timestamp)" SUMMARY "$package" "$architecture" generator_error \ + "$side" - - - - "$(first_log_line "$package_work/$side.log")" + return 0 + fi + fi + + if ! normalize_generated "$side" "$generator" "$generated"; then + report_row "$(timestamp)" SUMMARY "$package" "$architecture" generator_output_error \ + "$side" - - - - 'unable to normalize generator output' + return 0 + fi + if ! compare_side "$side" "$package" "$architecture" "$expected" "$generated" "$comparison"; then + report_row "$(timestamp)" SUMMARY "$package" "$architecture" comparison_error \ + "$side" - - - - 'unable to compare generated dependencies' + return 0 + fi + + local expected_count generated_count mismatch_count extra_count + expected_count=$(wc -l <"$expected") + generated_count=$(wc -l <"$generated") + mismatch_count=$(awk -F '\034' \ + '$1 == "mismatch" || $1 == "missing_generated" { count++ } END { print count+0 }' \ + "$comparison") + extra_count=$(awk -F '\034' '$1 == "extra_generated" { count++ } END { print count+0 }' \ + "$comparison") + total_mismatches=$((total_mismatches + mismatch_count + extra_count)) + detail+="$side expected=$expected_count generated=$generated_count differences=$mismatch_count extras=$extra_count; " + done + + local status=match + ((total_mismatches == 0)) || status=mismatch + report_row "$(timestamp)" SUMMARY "$package" "$architecture" "$status" - - - - - "$detail" + sync -f "$REPORT" 2>/dev/null || true +} + +while IFS=$'\034' read -r package architecture version filename md5 has_provides has_requires extra; do + if [[ -n ${extra-} || -z $package || -z $architecture || -z $version || -z $filename || + ! $has_provides =~ ^[01]$ || ! $has_requires =~ ^[01]$ ]]; then + report_row "$(timestamp)" RUN "$package" "$architecture" queue_error - - - - - \ + 'invalid package queue record' + continue + fi + process_package "$package" "$architecture" "$version" "$filename" "$md5" \ + "$has_provides" "$has_requires" +done <"$SELECTED" + +report_row "$(timestamp)" RUN - - complete - - - - - "processed selection: $selected_count" +printf 'complete; report: %s\n' "$REPORT" diff --git a/scripts/completed_set_versions.awk b/scripts/completed_set_versions.awk new file mode 100644 index 0000000..95f2369 --- /dev/null +++ b/scripts/completed_set_versions.awk @@ -0,0 +1,10 @@ +BEGIN { FS = "\t" } + +NF == 11 && $2 == "START" { + version[$3 "\034" $4] = $11 +} + +NF == 11 && $2 == "SUMMARY" && $11 ~ /(^|; )complete=1$/ && \ + version[$3 "\034" $4] != "" { + print $3 "\t" $4 "\t" version[$3 "\034" $4] +} diff --git a/scripts/newset_compat.h b/scripts/newset_compat.h new file mode 100644 index 0000000..8bf29ec --- /dev/null +++ b/scripts/newset_compat.h @@ -0,0 +1,48 @@ +#ifndef ARSV_NEWSET_COMPAT_H +#define ARSV_NEWSET_COMPAT_H + +#include +#include +#include +#include + +static inline void *arsv_xmalloc(size_t size) +{ + void *value = malloc(size); + if (!value) { + fputs("newset: out of memory\n", stderr); + abort(); + } + return value; +} + +static inline void *arsv_xrealloc(void *pointer, size_t size) +{ + void *value = realloc(pointer, size); + if (!value) { + fputs("newset: out of memory\n", stderr); + abort(); + } + return value; +} + +static inline char *arsv_xstrdup(const char *source) +{ + size_t size = strlen(source) + 1; + char *copy = (char *)arsv_xmalloc(size); + memcpy(copy, source, size); + return copy; +} + +static inline void *arsv_free(void *pointer) +{ + free(pointer); + return NULL; +} + +#define xmalloc arsv_xmalloc +#define xrealloc arsv_xrealloc +#define xstrdup arsv_xstrdup +#define _free arsv_free + +#endif diff --git a/scripts/newset_mkset.c b/scripts/newset_mkset.c new file mode 100644 index 0000000..3215b73 --- /dev/null +++ b/scripts/newset_mkset.c @@ -0,0 +1,63 @@ +/* mkset-compatible wrapper around reimplement/newset.c. + * The stdin/argv contract matches rpm-build/tools/mkset.c. */ +#include +#include +#include + +struct set; +struct set *set_new(void); +void set_add(struct set *set, const char *symbol); +const char *set_fini(struct set *set, int bpp); +struct set *set_free(struct set *set); + +int main(int argc, char **argv) +{ + if (argc != 2) { + fprintf(stderr, "usage: %s BPP\n", argv[0]); + return 2; + } + + char *end = NULL; + errno = 0; + long parsed_bpp = strtol(argv[1], &end, 10); + if (errno || !end || *end || parsed_bpp < 10 || parsed_bpp > 32) { + fprintf(stderr, "invalid BPP: %s (expected 10..32)\n", argv[1]); + return 2; + } + int bpp = (int)parsed_bpp; + + struct set *set = set_new(); + char *line = NULL; + size_t allocated = 0; + ssize_t length; + int added = 0; + + while ((length = getline(&line, &allocated, stdin)) >= 0) { + if (length > 0 && line[length - 1] == '\n') + line[--length] = '\0'; + if (length == 0) + continue; + set_add(set, line); + ++added; + } + + if (!added) { + fputs("mkset: no symbols on standard input\n", stderr); + free(line); + set_free(set); + return 2; + } + const char *encoded = set_fini(set, bpp); + if (!encoded) { + fputs("mkset: unable to encode set\n", stderr); + free(line); + set_free(set); + return 1; + } + printf("set:%s\n", encoded); + + free((void *)encoded); + free(line); + set_free(set); + return 0; +} diff --git a/scripts/parse_sisyphus_packages.awk b/scripts/parse_sisyphus_packages.awk new file mode 100644 index 0000000..feb628e --- /dev/null +++ b/scripts/parse_sisyphus_packages.awk @@ -0,0 +1,39 @@ +BEGIN { + RS = "" + FS = "\n" + OFS = "\034" +} + +{ + package = architecture = version = filename = md5 = "" + has_provides = has_requires = 0 + dependency_field = "" + + for (i = 1; i <= NF; ++i) { + if ($i ~ /^Package: /) + package = substr($i, 10) + else if ($i ~ /^Architecture: /) + architecture = substr($i, 15) + else if ($i ~ /^Version: /) + version = substr($i, 10) + else if ($i ~ /^Filename: /) + filename = substr($i, 11) + else if ($i ~ /^MD5Sum: /) + md5 = substr($i, 9) + + if ($i ~ /^Provides: /) + dependency_field = "provides" + else if ($i ~ /^(Pre-Depends|Depends): /) + dependency_field = "requires" + else if ($i !~ /^[[:space:]]/) + dependency_field = "" + + if (dependency_field == "provides" && index($i, "set:")) + has_provides = 1 + if (dependency_field == "requires" && index($i, "set:")) + has_requires = 1 + } + + if (package != "" && architecture != "" && version != "" && filename != "") + print package, architecture, version, filename, md5, has_provides, has_requires +} diff --git a/scripts/rpm_payload_safety.sh b/scripts/rpm_payload_safety.sh new file mode 100644 index 0000000..370f5c2 --- /dev/null +++ b/scripts/rpm_payload_safety.sh @@ -0,0 +1,126 @@ +#!/bin/bash + +rpm_safe_symlink_target() +{ + local root=$1 relative_path=$2 link_target=$3 + local canonical_root link_directory resolved_target + + canonical_root=$(realpath -m -- "$root") || return 1 + link_directory=$(realpath -m -- "$canonical_root/$(dirname -- "$relative_path")") || return 1 + + if [[ $link_target == /* ]]; then + resolved_target=$(realpath -m -- "$canonical_root/${link_target#/}") || return 1 + else + resolved_target=$(realpath -m -- "$link_directory/$link_target") || return 1 + fi + + [[ $resolved_target == "$canonical_root" || $resolved_target == "$canonical_root"/* ]] || return 1 + + if [[ $link_target == /* ]]; then + realpath -m --relative-to="$link_directory" -- "$resolved_target" + else + printf '%s\n' "$link_target" + fi +} + +rpm_cpio_literal_pattern() +{ + local input=$1 output= character index + + [[ $input != *$'\n'* && $input != *$'\r'* ]] || return 1 + for ((index = 0; index < ${#input}; ++index)); do + character=${input:index:1} + case $character in + '[') output+='[[]' ;; + ']') output+='[]]' ;; + '*') output+='[*]' ;; + '?') output+='[?]' ;; + '\\') output+='[\\]' ;; + *) output+=$character ;; + esac + done + printf '%s\n' "$output" +} + +rpm_split_file_record() +{ + local record=$1 separator=$'\034' + + RPM_FILENAME= + RPM_LINK_TARGET= + [[ $record == *"$separator"* ]] || return 1 + RPM_FILENAME=${record%%"$separator"*} + RPM_LINK_TARGET=${record#*"$separator"} + [[ $RPM_LINK_TARGET != *"$separator"* ]] || return 1 + [[ $RPM_FILENAME != *$'\t'* && $RPM_FILENAME != *$'\n'* && $RPM_FILENAME != *$'\r'* ]] || return 1 + [[ $RPM_LINK_TARGET != *$'\t'* && $RPM_LINK_TARGET != *$'\n'* && $RPM_LINK_TARGET != *$'\r'* ]] || return 1 +} + +rpm_install_symlink_manifest() +{ + local root=$1 manifest=$2 log=$3 separator=$'\034' + local canonical_root sorted row relative link_target safe_target destination parent existing + + canonical_root=$(realpath -m -- "$root") || return 1 + + sorted=$(mktemp "${manifest}.sorted.XXXXXX") || return 1 + if ! awk -F '\t' -v separator="$separator" ' + NF >= 2 { + path=$1 + depth=gsub(/\//, "/", path) + printf "%08d%s%s\n", depth, separator, $0 + } + ' "$manifest" | LC_ALL=C sort -t "$separator" -k1,1n -k2,2 >"$sorted"; then + rm -f -- "$sorted" + return 1 + fi + + while IFS=$separator read -r _depth row; do + IFS=$'\t' read -r relative link_target <<<"$row" + [[ -n $relative && -n $link_target ]] || continue + destination=$canonical_root/$relative + parent=$(realpath -m -- "$(dirname -- "$destination")") || { + rm -f -- "$sorted" + return 1 + } + [[ $parent == "$canonical_root" || $parent == "$canonical_root"/* ]] || { + printf 'escaping RPM symlink parent rejected: %q\n' "/$relative" >>"$log" + rm -f -- "$sorted" + return 1 + } + if ! safe_target=$(rpm_safe_symlink_target "$canonical_root" "$relative" "$link_target"); then + printf 'escaping RPM symlink rejected: %q -> %q\n' "/$relative" "$link_target" >>"$log" + rm -f -- "$sorted" + return 1 + fi + mkdir -p -- "$parent" || { + rm -f -- "$sorted" + return 1 + } + + if [[ -L $destination ]]; then + existing=$(readlink -- "$destination") || { + rm -f -- "$sorted" + return 1 + } + if [[ $existing == "$safe_target" ]]; then + continue + fi + printf 'conflicting RPM symlinks: %q -> %q and %q\n' \ + "/$relative" "$existing" "$safe_target" >>"$log" + rm -f -- "$sorted" + return 1 + elif [[ -e $destination ]]; then + printf 'RPM symlink conflicts with existing path: %q\n' "/$relative" >>"$log" + rm -f -- "$sorted" + return 1 + fi + if ! ln -s -- "$safe_target" "$destination"; then + printf 'unable to create RPM symlink: %q -> %q\n' "/$relative" "$safe_target" >>"$log" + rm -f -- "$sorted" + return 1 + fi + done <"$sorted" + + rm -f -- "$sorted" +} diff --git a/tests/test_compare_sisyphus_set_versions.py b/tests/test_compare_sisyphus_set_versions.py new file mode 100644 index 0000000..de2a704 --- /dev/null +++ b/tests/test_compare_sisyphus_set_versions.py @@ -0,0 +1,319 @@ +from __future__ import annotations + +import os +import subprocess +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +SCRIPT = REPO_ROOT / "scripts" / "compare_sisyphus_set_versions.sh" +PACKAGE_PARSER = REPO_ROOT / "scripts" / "parse_sisyphus_packages.awk" +RESUME_PARSER = REPO_ROOT / "scripts" / "completed_set_versions.awk" +PAYLOAD_HELPERS = REPO_ROOT / "scripts" / "rpm_payload_safety.sh" + + +def run_script(*arguments: str, input_text: str | None = None) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["bash", str(SCRIPT), *arguments], + cwd=REPO_ROOT, + text=True, + input=input_text, + capture_output=True, + ) + + +def test_requires_explicit_scope_before_touching_repository() -> None: + completed = run_script() + + assert completed.returncode == 2 + assert "use --all, --limit, or --package" in completed.stderr + + +def test_rejects_source_list_and_package_name_injection_before_network() -> None: + bad_mirror = run_script("--package", "strace", "--mirror", "https://example.test/\nrpm evil") + assert bad_mirror.returncode == 2 + assert "mirror must not contain whitespace" in bad_mirror.stderr + + bad_package = run_script("--package", "--option") + assert bad_package.returncode == 2 + assert "invalid package name" in bad_package.stderr + + +def test_build_mkset_wraps_newset_and_matches_known_alt_payload(tmp_path: Path) -> None: + output = tmp_path / "new-mkset" + completed = run_script("--build-mkset", str(output)) + + assert completed.returncode == 0, completed.stderr + assert output.is_file() + assert os.access(output, os.X_OK) + + encoded = subprocess.run( + [str(output), "16"], + input="sym0\nsym1\nsym2\nsym3\nsym0\n", + text=True, + capture_output=True, + check=True, + ) + assert encoded.stdout == "set:jg2ZwFjdTD2B0\n" + + bad_bpp = subprocess.run( + [str(output), "16junk"], + input="sym0\n", + text=True, + capture_output=True, + ) + assert bad_bpp.returncode == 2 + assert "invalid BPP" in bad_bpp.stderr + + empty = subprocess.run( + [str(output), "16"], + input="", + text=True, + capture_output=True, + ) + assert empty.returncode == 2 + assert "no symbols" in empty.stderr + + +def test_package_parser_preserves_empty_md5_field() -> None: + metadata = """\ +Package: libexample +Architecture: x86_64 +Version: 1.2-alt1 +Filename: libexample-1.2-alt1.x86_64.rpm +Provides: libexample.so.1 = set:abc +Depends: libc.so.6 + libc.so.6(GLIBC_2.34)(64bit) >= set:def + +""" + completed = subprocess.run( + ["awk", "-f", str(PACKAGE_PARSER)], + input=metadata, + text=True, + capture_output=True, + check=True, + ) + + assert completed.stdout.rstrip("\n").split("\x1c") == [ + "libexample", + "x86_64", + "1.2-alt1", + "libexample-1.2-alt1.x86_64.rpm", + "", + "1", + "1", + ] + + +def test_resume_parser_ignores_partial_summary_rows() -> None: + report = "\n".join( + [ + "timestamp\trecord\tpackage\tarchitecture\tstatus\tside\tcapability\toperator\texpected\tgenerated\tdetail", + "t\tSTART\tbroken\tx86_64\tprocessing\t-\t-\t-\t-\t-\t1-alt1", + "t\tSUMMARY\tbroken\tx86_64\tmatch", + "t\tSTART\tcomplete\tnoarch\tprocessing\t-\t-\t-\t-\t-\t2-alt1", + "t\tSUMMARY\tcomplete\tnoarch\tmatch\t-\t-\t-\t-\t-\tdone; complete=1", + "", + ] + ) + completed = subprocess.run( + ["awk", "-f", str(RESUME_PARSER)], + input=report, + text=True, + capture_output=True, + check=True, + ) + + assert completed.stdout == "complete\tnoarch\t2-alt1\n" + + +def test_absolute_rpm_symlink_is_rewritten_inside_temporary_root(tmp_path: Path) -> None: + root = tmp_path / "root" + (root / "usr" / "sbin").mkdir(parents=True) + completed = subprocess.run( + [ + "bash", + "-c", + 'source "$1"; rpm_safe_symlink_target "$2" "$3" "$4"', + "bash", + str(PAYLOAD_HELPERS), + str(root), + "usr/sbin/update-alternatives", + "/bin/true", + ], + text=True, + capture_output=True, + ) + + assert completed.returncode == 0, completed.stderr + assert completed.stdout == "../../bin/true\n" + + +def test_escaping_relative_rpm_symlink_is_rejected(tmp_path: Path) -> None: + root = tmp_path / "root" + root.mkdir() + completed = subprocess.run( + [ + "bash", + "-c", + 'source "$1"; rpm_safe_symlink_target "$2" "$3" "$4"', + "bash", + str(PAYLOAD_HELPERS), + str(root), + "usr/link", + "../../../outside", + ], + text=True, + capture_output=True, + ) + + assert completed.returncode != 0 + + +def test_cpio_member_pattern_quotes_glob_characters() -> None: + completed = subprocess.run( + [ + "bash", + "-c", + 'source "$1"; rpm_cpio_literal_pattern "$2"', + "bash", + str(PAYLOAD_HELPERS), + "./usr/share/doc/a[1]*?.txt", + ], + text=True, + capture_output=True, + ) + + assert completed.returncode == 0, completed.stderr + assert completed.stdout == "./usr/share/doc/a[[]1[]][*][?].txt\n" + + +def test_directory_symlinks_are_installed_before_children(tmp_path: Path) -> None: + root = tmp_path / "root" + root.mkdir() + manifest = tmp_path / "symlinks.tsv" + manifest.write_text("bin/sh\tbash\nbin\tusr/bin\n") + completed = subprocess.run( + [ + "bash", + "-c", + 'source "$1"; rpm_install_symlink_manifest "$2" "$3" "$4"', + "bash", + str(PAYLOAD_HELPERS), + str(root), + str(manifest), + str(tmp_path / "extract.log"), + ], + text=True, + capture_output=True, + ) + + assert completed.returncode == 0, completed.stderr + assert (root / "bin").is_symlink() + assert os.readlink(root / "bin") == "usr/bin" + assert (root / "usr" / "bin" / "sh").is_symlink() + assert os.readlink(root / "usr" / "bin" / "sh") == "bash" + + +def test_child_target_is_revalidated_after_parent_alias(tmp_path: Path) -> None: + root = tmp_path / "root" + root.mkdir() + manifest = tmp_path / "symlinks.tsv" + manifest.write_text("deep/a\t..\ndeep/a/link\t../outside\n") + completed = subprocess.run( + [ + "bash", + "-c", + 'source "$1"; rpm_install_symlink_manifest "$2" "$3" "$4"', + "bash", + str(PAYLOAD_HELPERS), + str(root), + str(manifest), + str(tmp_path / "extract.log"), + ], + text=True, + capture_output=True, + ) + + assert completed.returncode != 0 + assert not (tmp_path / "outside").exists() + + +def test_absolute_target_is_rewritten_after_parent_alias(tmp_path: Path) -> None: + root = tmp_path / "root" + root.mkdir() + manifest = tmp_path / "symlinks.tsv" + manifest.write_text("bin\tusr/bin\nbin/tool\t/opt/tool\n") + completed = subprocess.run( + [ + "bash", + "-c", + 'source "$1"; rpm_install_symlink_manifest "$2" "$3" "$4"', + "bash", + str(PAYLOAD_HELPERS), + str(root), + str(manifest), + str(tmp_path / "extract.log"), + ], + text=True, + capture_output=True, + ) + + assert completed.returncode == 0, completed.stderr + assert os.readlink(root / "usr" / "bin" / "tool") == "../../opt/tool" + assert (root / "usr" / "bin" / "tool").resolve(strict=False) == root / "opt" / "tool" + + +def test_symlinked_temporary_root_is_canonicalized(tmp_path: Path) -> None: + real_root = tmp_path / "real-root" + real_root.mkdir() + root_alias = tmp_path / "root-alias" + root_alias.symlink_to(real_root, target_is_directory=True) + manifest = tmp_path / "symlinks.tsv" + manifest.write_text("bin\tusr/bin\n") + completed = subprocess.run( + [ + "bash", + "-c", + 'source "$1"; rpm_install_symlink_manifest "$2" "$3" "$4"', + "bash", + str(PAYLOAD_HELPERS), + str(root_alias), + str(manifest), + str(tmp_path / "extract.log"), + ], + text=True, + capture_output=True, + ) + + assert completed.returncode == 0, completed.stderr + assert (real_root / "bin").is_symlink() + + +def test_rpm_file_record_with_tab_in_filename_is_rejected() -> None: + record = "/name\tpart\x1c" + completed = subprocess.run( + [ + "bash", + "-c", + 'source "$1"; rpm_split_file_record "$2"', + "bash", + str(PAYLOAD_HELPERS), + record, + ], + text=True, + capture_output=True, + ) + + assert completed.returncode != 0 + + +def test_help_documents_streaming_report_and_test_scope() -> None: + completed = run_script("--help") + + assert completed.returncode == 0 + assert "--report FILE" in completed.stdout + assert "--package NAME" in completed.stdout + assert "--limit N" in completed.stdout + assert "--all" in completed.stdout + assert "appended after every package" in completed.stdout diff --git a/tt b/tt new file mode 100644 index 0000000..10fdfa9 --- /dev/null +++ b/tt @@ -0,0 +1,17 @@ +2026-07-19T03:40:59Z START ORBit2 x86_64 processing - - - - - 2.14.20-alt0.4:sisyphus+361606.200.1.1@1730563927 +2026-07-19T03:41:09Z DEPENDENCY ORBit2 x86_64 extra_generated requires libORBit-2.so.0()(64bit) >= set:niDtqCAHBjYjaJDKEoB0hQrUcD1RWQ8Y9dT4Ll8IwTKKrUFvFOcPooPnZzZqghuX9GWYZi3 - +2026-07-19T03:41:09Z DEPENDENCY ORBit2 x86_64 match requires libglib-2.0.so.0()(64bit) >= set:ojfwe09N7dwsn61JFrFyGiav6VPWMh6BXOl0ykouiiezwiOhIbG0 set:ojfwe09N7dwsn61JFrFyGiav6VPWMh6BXOl0ykouiiezwiOhIbG0 - +2026-07-19T03:41:09Z SUMMARY ORBit2 x86_64 mismatch - - - - - requires expected=1 generated=2 differences=0 extras=1; complete=1 + + +2026-07-19T03:48:54Z DEPENDENCY SILLY x86_64 mismatch requires libjpeg.so.62()(64bit) >= set:jfvZkgcUQqm1WcNFYdT6Zh set:kgvLPFlp5EP4DOJVCPR1WC6 - +2026-07-19T03:48:54Z DEPENDENCY SILLY x86_64 match requires libpng16.so.16()(64bit) >= set:lhkC1fmC6EFQ5Klh4dezRCyvQFOXIZ3uwhXdUhBK49 set:lhkC1fmC6EFQ5Klh4dezRCyvQFOXIZ3uwhXdUhBK49 - + +2026-07-19T03:58:11Z START abuse x86_64 processing - - - - - 0.8-alt2@1308659965 +2026-07-19T03:58:33Z DEPENDENCY abuse x86_64 match requires libSDL-1.2.so.0()(64bit) >= set:lgkZiCZcFvTDDX6Y1PaRgCdFuVAwizcZfaTezfxZFUxcDOoVbbKRxY6ahoMjPQkxX5fdZtjAKGEY4 set:lgkZiCZcFvTDDX6Y1PaRgCdFuVAwizcZfaTezfxZFUxcDOoVbbKRxY6ahoMjPQkxX5fdZtjAKGEY4 - +2026-07-19T03:58:33Z DEPENDENCY abuse x86_64 mismatch requires libSDL_mixer-1.2.so.0()(64bit) >= set:lgIdCOqw7JFqcojADYwCZ7pQY3CJ1LnAqVzI3Z0QXq0pLy1 set:kfMSoaHVyGBhV7CJ1YuVyx95uAdQPsq0b5zW6rUlsY3 - +2026-07-19T03:58:33Z SUMMARY abuse x86_64 mismatch - - - - - requires expected=2 generated=2 differences=1 extras=0; complete=1 + +2026-07-19T04:08:20Z DEPENDENCY airstrike x86_64 mismatch requires libSDL_mixer-1.2.so.0()(64bit) >= set:lirzppdVhrMiF4ChTZdaR8 set:khS6jpdPZrxpQlg2V9Bq4 - + +