#!/bin/bash # mp-check | MatterLinux package check script # MatterLinux 2023-2024 (https://matterlinux.xyz) # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . ############################# ## import common functions ## ############################# location=$(dirname "${0}") location=$(realpath "${location}") commonsh="$(echo "${location}" | sed 's/\/bin/\/lib/g')/mtsc-common.sh" source "${commonsh}" > /dev/null if [ "${?}" != "0" ]; then echo "Failed to import mtsc-common" exit 1 fi ################# ## global vars ## ################# warnc=0 tmpdir="/tmp/.mp-check" required_files=( "DATA" "HASHES" "CHANGES" "files.tar.gz" ) required_keys=( "version" "desc" "size" ) root_dirs=( "boot" "etc" "mnt" "srv" "usr" "var" "opt" "run" ) bad_vars=( '$VERSION' '$ROOTDIR' '$NAME' '$DESC' ) #################### ## util functions ## #################### # prints the help info help_cmd() { info "MatterLinux package check script (mtsc ${MTSC_VERSION})" # sourced from mtsc-common info "Usage: ${0} [archive file/source dir]" info "Options:" echo_color " $BOLD--checkpoint$RESET: specify a starting point for the check" echo_color " $BOLD--fail-warn$RESET: fail on a warning" echo_color " $BOLD--no-warn$RESET: ignore warnings" echo info "Licensed under GPLv3, see for more information" } # adds a new warning to the counter add_warning(){ [ $OPT_NO_WARN -eq 1 ] && return warn "${1}" warnc=$((warnc+1)) [ $OPT_FAIL_WARN -eq 1 ] && fail_check } # cleans up the temp directoru clean_tempdir(){ rm -rf "${tmpdir}" } # recreates the temp directory make_tempdir(){ clean_tempdir mkdir "${tmpdir}" } # returns check status fail fail_check(){ clean_tempdir unset_indent error "${BOLD}Check ${RED}FAILED${RESET}" exit 1 } # returns check status fail success_check(){ clean_tempdir unset_indent if [ "${warnc}" == "0" ]; then success "Check ${GREEN}SUCCESS${RESET}" elif [ "${warnc}" == "1" ]; then success "Check ${GREEN}SUCCESS${RESET}${BOLD} with ${YELLOW}${warnc}${RESET}${BOLD} warning${RESET}" else success "Check ${GREEN}SUCCESS${RESET}${BOLD} with ${YELLOW}${warnc}${RESET}${BOLD} warnings${RESET}" fi exit 0 } # if the last command failed, print error and fail check_ret_fail(){ if [ $? -ne 0 ]; then if [ ! -z "$1" ]; then error "$1" fi fail_check fi } check_archive(){ make_tempdir info "Extracting the archive" tar xf "${archivepath}" -C "${tmpdir}" check_ret_fail "Failed to extract the archive" info "Checking archive files" for f in "${required_files[@]}"; do if [ ! -f "${tmpdir}/${f}" ]; then error "Archive does not contain required file: ${BOLD}${f}${RESET}" fail_check fi done info "Checking DATA file" for k in "${required_keys[@]}"; do local line_1="$(grep "^${k}=" "${tmpdir}/DATA")" local line_2="$(grep "^${k} =" "${tmpdir}/DATA")" if [ -z "${line_1}" ] && [ -z "${line_2}" ]; then error "File does not contain the required key: ${BOLD}${k}${RESET}" fail_check fi if [ "${k}" == "version" ]; then if [ ! -z "${line_1}" ]; then version="$(echo "${line_1}" | sed 's/version= //g')" version="$(echo "${version}" | sed 's/version=//g')" else version="$(echo "${line_2}" | sed 's/version = //g')" version="$(echo "${version}" | sed 's/version =//g')" fi if [ -z "${version}" ]; then error "Failed to obtain package version information" fail_check fi fi if [ "${k}" == "size" ]; then if [ ! -z "${line_1}" ]; then size="$(echo "${line_1}" | sed 's/size= //g')" size="$(echo "${size}" | sed 's/size=//g')" else size="$(echo "${line_2}" | sed 's/size = //g')" size="$(echo "${size}" | sed 's/size =//g')" fi if [ -z "${size}" ]; then error "Failed to obtain package size information" fail_check fi if [ "${size}" == "0" ]; then error "Package size information is set as 0, is the package empty?" fail_check fi fi done name="$(head -n1 "${tmpdir}/DATA" | sed 's/\[//g')" name="$(echo "${name}" | sed 's/]//g')" if [ -z "${name}" ]; then error "Failed to obtain package name information" fail_check fi case "${name}" in *_*) error "Package name contains an invalid character: \"_\"" fail_check ;; *" "*) error "Package name contains an invalid character: \" \"" fail_check ;; esac case "${version}" in *_*) error "Package version contains an invalid character: \"_\"" fail_check ;; *" "*) error "Package version contains an invalid character: \" \"" fail_check ;; esac filename="${name}_${version}.mpf" info "Checking HASHES file" while read l; do if [ -z "${l}" ]; then continue fi local hash_line="$(echo "${l}" | cut -d' ' -f1)" local file_line="$(echo "${l}" | cut -d' ' -f3)" if [ -z "${hash_line}" ] || [ -z "${file_line}" ]; then error "File contains an invalid formatted line" fail_check fi if [ "${#hash_line}" != "32" ]; then error "File contains an invalid MD5 hash" fail_check fi done < "${tmpdir}/HASHES" info "Checking files.tar.gz archive" filec=0 while read p; do if [ -z "${p}" ]; then continue fi filec=$((filec + 1)) if [ "${p:0:1}" == "." ] || [ "${p:0:1}" == "/" ]; then error "Root file location is invalid (${p:0:1})" fail_check fi local path_root="$(echo "${p}" | cut -d/ -f1)" local found=0 for d in "${root_dirs[@]}"; do if [ "${path_root}" == "${d}" ]; then found=1 break fi done if [ "${found}" == "0" ]; then error "Package files contains an unknown root directory: ${path_root}" fail_check fi done < <(tar tf "${tmpdir}/files.tar.gz") if [ "${filec}" == "0" ]; then error "Package file archive is empty (no files)" fail_check fi info "Checking INSTALL file" if [ -f "${tmpdir}/INSTALL" ] && ! grep -q . "${tmpdir}/INSTALL"; then add_warning "Package contains an empty install script" fi info "Checking CHANGES file" if ! grep -q . "${tmpdir}/CHANGES"; then add_warning "Changes file is empty" fi if ! grep "${version}" "${tmpdir}/CHANGES" &> /dev/null; then add_warning "Changes potentially does not have an entry for the current version" fi info "Checking archive name" archivefile="$(basename "${archivepath}")" if [ "${archivefile}" != "${filename}" ]; then add_warning "Package archive name is not ideal (${archivefile} -> ${filename})" fi clean_tempdir } check_source(){ info "Checking the package script" if [ ! -f "${sourcepath}/pkg.sh" ]; then error "Package script does not exist" fail_check fi source "${sourcepath}/pkg.sh" check_ret_fail "Failed to source the package script" check_pkg_vars check_ret_fail if [ ${#DESC} -gt 200 ]; then error "Package description is too long (>200)" fail_check fi local desc_lower="${DESC,,}" local line_num=0 if [[ "${DESC}" == *"contains"* ]] || [[ "${DESC}" == *"provides"* ]]; then add_warning "Avoid using words such as \"contains\" or \"provides\" in the package description" fi if type INSTALL &>/dev/null; then for v in "${bad_vars[@]}"; do if type INSTALL | grep "${v}" &> /dev/null; then error "${v} used in the install script" fail_check fi done fi while read l; do line_num=$((line_num+1)) for v in "${bad_vars[@]}"; do if echo "${l}" | grep "${v}" &> /dev/null; then add_warning "${v} used without parenthesis on line ${line_num}" fi done if echo "${l}" | grep '&&' | grep -v 'cd ..' &> /dev/null; then add_warning "Unreliable use of \"&&\" on line ${line_num}" fi done < "${sourcepath}/pkg.sh" info "Checking the changes file" if [ ! -f "${sourcepath}/changes.md" ]; then error "Package does not contain a changes file" fail_check fi if ! grep -q . "${sourcepath}/changes.md"; then add_warning "Changes file is empty" fi if ! grep "${VERSION}" "${sourcepath}/changes.md" &> /dev/null; then add_warning "Changes potentially does not have an entry for the current version" fi clean_pkg_vars } ################# ## main script ## ################# OPT_CHECKPOINT="" OPT_FAIL_WARN=0 OPT_NO_WARN=0 OPT_TARGET=() for arg in "$@"; do case $arg in "--help") help_cmd exit 0 ;; "--checkpoint"*) OPT_CHECKPOINT="$(echo "${arg}" | cut -d '=' -f2)" ;; "--fail-warn") OPT_FAIL_WARN=1 ;; "--no-warn") OPT_NO_WARN=1 ;; --*) error "Unknown option: ${arg}" exit 1 ;; *) OPT_TARGET+=("${arg}") ;; esac done if [ -z "${OPT_TARGET}" ]; then error "Please specify at least one package archive or a source directory, run --help for more info" exit 1 fi if [ $OPT_FAIL_WARN -eq 1 ] && [ $OPT_NO_WARN -eq 1 ]; then error "Cannot use both of the --fail-warn and --no-warn options" exit 1 fi info "Running mp-check with the options:" if [ -z "${OPT_CHECKPOINT}" ]; then print " $BOLD CHECKPOINT = NONE" else print " $BOLD CHECKPOINT = ${OPT_CHECKPOINT}" fi print " $BOLD FAIL_WARN = $(itoyn $OPT_FAIL_WARN)" print " $BOLD NO_WARN = $(itoyn $OPT_NO_WARN)" for target in "${OPT_TARGET[@]}"; do if [ ! -f "${target}" ] && [ ! -d "${target}" ]; then error "Specified path is invalid: ${target}" exit 1 fi done tc="${#OPT_TARGET[@]}" got_checkpoint=0 ti=0 for target in "${OPT_TARGET[@]}"; do unset_indent ti=$((ti + 1)) if [ -z "${OPT_CHECKPOINT}" ] || [ "${target}" == "${OPT_CHECKPOINT}" ]; then got_checkpoint=1 fi [ $got_checkpoint -eq 0 ] && continue if [ -f "${target}" ]; then info "(${ti}/${tc}) Checking the archive: ${target}" set_indent archivepath="$(realpath "${target}")" check_archive elif [ -d "${target}" ]; then info "(${ti}/${tc}) Checking the source directory: ${target}" set_indent sourcepath="$(realpath "${target}")" check_source fi done success_check