diff --git a/tix/Makefile b/tix/Makefile index 1323091c..9e9b6fdd 100644 --- a/tix/Makefile +++ b/tix/Makefile @@ -26,6 +26,7 @@ tix-vars \ PROGRAMS:=\ $(BINARIES) \ +tix-check \ tix-clean \ tix-eradicate-libtool-la \ tix-fetch \ diff --git a/tix/tix-check b/tix/tix-check new file mode 100755 index 00000000..2522117b --- /dev/null +++ b/tix/tix-check @@ -0,0 +1,219 @@ +#!/bin/sh +# Copyright (c) 2017 Jonas 'Sortie' Termansen. +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# +# tix-check +# Check the operating system installation. + +set -e + +collection="" +sysroot="" +existence=false +owner=false +permissions=false +unknown_files=false + +dashdash= +previous_option= +for argument do + if test -n "$previous_option"; then + eval $previous_option=\$argument + previous_option= + shift + continue + fi + + case $argument in + *=?*) parameter=$(expr "X$argument" : '[^=]*=\(.*\)' || true) ;; + *=) parameter= ;; + *) parameter=yes ;; + esac + + case $dashdash$argument in + --) dashdash=yes ;; + --collection=*) collection=$parameter ;; + --collection) previous_option=collection ;; + --existence) existence=true ;; + --owner) owner=true ;; + --permissions) permissions=true ;; + --sysroot) previous_option=sysroot ;; + --sysroot=*) sysroot=$parameter ;; + --unknown-files) unknown_files=true ;; + -*) echo "$0: unrecognized option $argument" >&2 + exit 1 ;; + *) break ;; + esac + + shift +done + +if test -n "$previous_option"; then + echo "$0: option '$argument' requires an argument" >&2 + exit 1 +fi + +# TODO: Reject additional operands. + +if [ -z "$collection" ]; then + collection="$sysroot" +fi + +if [ -n "$collection" ]; then + collection=$(cd "$collection" && pwd) +fi + +tmp= +trap '[ -n "$tmp" ] && rm -rf "$tmp"' EXIT HUP INT QUIT TERM +tmp=$(mktemp -dt tix-check.XXXXXX) +mkdir -p "$tmp/tmp" +export TMPDIR="$tmp/tmp" + +escape_extended_regex_sed() { + printf "%s\n" "$1" | sed -E -e 's/[[$()*?\+.^{|}'"$2"']/\\\0/g' +} + +# TODO: More portable way of doing this. +getuid() { + stat -- "$1" | grep -E '^UID: ' | sed -E -e 's/^UID: //' -e 's,/.*,,' +} + +# TODO: More portable way of doing this. +getgid() { + stat -- "$1" | grep -E '^GID: ' | sed -E -e 's/^GID: //' -e 's,/.*,,' +} + +# TODO: More portable way of doing this. +getmode() { + stat -- "$1" | grep -E '^Mode: ' | sed -E -e 's,^Mode: [^/]*/,,' +} + +# TODO: Rewrite in C with the following model. +# - Every file must be in one state of being tracked: +# - Be tracked by exactly one manifest. +# - (Other states?) +# - Be a local file inside system directories (unknown file). +# - Be a local file outside system directories. + +exit_code=0 + +# TODO: Store sha256sum of each file in metadata so we can do file +# consistency checks. +if $existence || $owner || $permissions; then + for manifest in "$collection/tix/manifest/"*; do + manifest_name=$(basename -- "$manifest") + # TODO: Verify absolute paths. + cat -- "$manifest" | \ + while read -r path; do + # TODO: Handle symbolic links that are intentionally dangling like + # /share/vim/vimrc. + if [ ! -e "$collection$path" ]; then + if $existence; then + printf "%s: %s: %s%s: Missing file\n" "$0" "$manifest_name" "$collection" "$path" >&2 + exit_code=1 + fi + continue + fi + if $owner; then + expected_uid=0 + expected_gid=0 + uid=$(getuid "$collection$path") + gid=$(getgid "$collection$path") + if [ "$uid" != "$expected_uid" ]; then + printf "%s: %s: %s%s: Owned by uid %s instead of expected uid %s\n" "$0" "$manifest_name" "$collection" "$path" "$uid" "$expected_uid" >&2 + exit_code=1 + fi + if [ "$gid" != "$expected_gid" ]; then + printf "%s: %s: %s%s: Owned by gid %s instead of expected gid %s\n" "$0" "$manifest_name" "$collection" "$path" "$gid" "$expected_gid" >&2 + exit_code=1 + fi + fi + if $permissions; then + if [ -L "$collection$path" ]; then + expected_mode=lrwxrwxrwx + elif [ -d "$collection$path" ]; then + expected_mode=drwxr-xr-x + else + # TODO: Store the intended file permissions in tix metadata. + case "$path" in + /bin/*) expected_mode=-rwxr-xr-x ;; + /boot/sortix.bin) expected_mode=-rwxr-xr-x ;; + /etc/grub.d/README) expected_mode=-rw-r--r-- ;; + /etc/grub.d/*) expected_mode=-rwxr-xr-x ;; + /libexec/*) expected_mode=-rwxr-xr-x ;; + /lib/grub/i386-pc/*.exec) expected_mode=-rwxr-xr-x ;; + /lib/grub/i386-pc/*.image) expected_mode=-rwxr-xr-x ;; + /lib/grub/i386-pc/*.module) expected_mode=-rwxr-xr-x ;; + /sbin/*) expected_mode=-rwxr-xr-x ;; + /src/*) expected_mode= ;; # TODO. + *) expected_mode=-rw-r--r-- ;; + esac + fi + mode=$(getmode "$collection$path") + if [ -n "$expected_mode" ] && [ "$mode" != "$expected_mode" ]; then + printf "%s: %s: %s%s: Mode is %s instead of expected mode %s\n" "$0" "$manifest_name" "$collection" "$path" "$mode" "$expected_mode" >&2 + exit_code=1 + fi + fi + done + done +fi + +if $unknown_files; then + # TODO: More exclusion patterns. + exclude_directories="/dev|/home|/mnt|/sysmerge|/tix|/tmp|/var/cache|/var/log|/var/run|/var/www" + # Find all known directories. + for manifest in "$collection/tix/manifest/"*; do + # TODO: Verify absolute paths. + cat -- "$manifest" | \ + grep -Ev '^('"$exclude_directories"')($|/)' | + while read -r path; do + if [ -L "$collection$path" ]; then + : + elif [ -d "$collection$path" ]; then + printf "%s\n" "$path" + fi + done + # TODO: pipefail + done | \ + LC_ALL=C sort -u > "$tmp/directories" + + # Find all known files. + for manifest in "$collection/tix/manifest/"*; do + # TODO: Verify absolute paths. + grep -Ev '^('"$exclude_directories"')/' -- "$manifest" + # TODO: pipefail + done | \ + LC_ALL=C sort -u > "$tmp/expected" + + # List the contents of each system directory. + cat -- "$tmp/directories" | while read -r directory; do + (cd "${collection:-/}" && + printf '%s\n' "$directory" && + ls -1A -- ".$directory" | + sed -E -e "s,^,$(escape_extended_regex_sed "$directory" ,)/," -e 's,//,/,') + # TODO: pipefail + done | \ + LC_ALL=C sort -u \ + > "$tmp/actual" + + # TODO: This requires diffutils in the minimal ports. + diff -- "$tmp/expected" "$tmp/actual" || true + + cp -- "$tmp/directories" /tmp/directories + cp -- "$tmp/expected" /tmp/expected + cp -- "$tmp/actual" /tmp/actual +fi + +exit $exit_code