#!/bin/bash -p # Copyright (c) 2011 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. # usage: dirpatcher.sh old_dir patch_dir new_dir # # dirpatcher creates new_dir from patch_dir by decompressing and copying # files, and using goobspatch to apply binary diffs to files in old_dir. # # dirpatcher performs the inverse operation to dirdiffer. For more details, # consult dirdiffer.sh. # # Exit codes: # 0 OK # 1 Unknown failure # 2 Incorrect number of parameters # 3 Input directories do not exist or are not directories # 4 Output directory already exists # 5 Parent of output directory does not exist or is not a directory # 6 An input or output directories contains another # 7 Could not create output directory # 8 File already exists in output directory # 9 Found an irregular file (non-directory, file, or symbolic link) in input # 10 Could not create symbolic link # 11 Unrecognized file extension # 12 Attempt to patch a nonexistent or non-regular file # 13 Patch application failed # 14 File decompression failed # 15 File copy failed # 16 Could not set mode (permissions) # 17 Could not set modification time set -eu # Environment sanitization. Set a known-safe PATH. Clear environment variables # that might impact the interpreter's operation. The |bash -p| invocation # on the #! line takes the bite out of BASH_ENV, ENV, and SHELLOPTS (among # other features), but clearing them here ensures that they won't impact any # shell scripts used as utility programs. SHELLOPTS is read-only and can't be # unset, only unexported. export PATH="/usr/bin:/bin:/usr/sbin:/sbin" unset BASH_ENV CDPATH ENV GLOBIGNORE IFS POSIXLY_CORRECT export -n SHELLOPTS shopt -s dotglob nullglob # find_tool looks for an executable file named |tool_name|: # - in the same directory as this script, # - if this script is located in a Chromium source tree, at the expected # Release output location in the Mac out directory, # - as above, but in the Debug output location # If found in any of the above locations, the script's path is output. # Otherwise, this function outputs |tool_name| as a fallback, allowing it to # be found (or not) by an ordinary ${PATH} search. find_tool() { local tool_name="${1}" local script_dir script_dir="$(dirname "${0}")" local tool="${script_dir}/${tool_name}" if [[ -f "${tool}" ]] && [[ -x "${tool}" ]]; then echo "${tool}" return fi local script_dir_phys script_dir_phys="$(cd "${script_dir}" && pwd -P)" if [[ "${script_dir_phys}" =~ ^(.*)/src/chrome/installer/mac$ ]]; then tool="${BASH_REMATCH[1]}/src/out/Release/${tool_name}" if [[ -f "${tool}" ]] && [[ -x "${tool}" ]]; then echo "${tool}" return fi tool="${BASH_REMATCH[1]}/src/out/Debug/${tool_name}" if [[ -f "${tool}" ]] && [[ -x "${tool}" ]]; then echo "${tool}" return fi fi echo "${tool_name}" } ME="$(basename "${0}")" readonly ME GOOBSPATCH="$(find_tool goobspatch)" readonly GOOBSPATCH readonly BUNZIP2="bunzip2" readonly GUNZIP="gunzip" XZDEC="$(find_tool xzdec)" readonly XZDEC readonly GBS_SUFFIX='$gbs' readonly BZ2_SUFFIX='$bz2' readonly GZ_SUFFIX='$gz' readonly XZ_SUFFIX='$xz' readonly PLAIN_SUFFIX='$raw' err() { local error="${1}" echo "${ME}: ${error}" >& 2 } declare -a g_cleanup cleanup() { local status=${?} trap - EXIT trap '' HUP INT QUIT TERM if [[ ${status} -ge 128 ]]; then err "Caught signal $((${status} - 128))" fi if [[ "${#g_cleanup[@]}" -gt 0 ]]; then rm -rf "${g_cleanup[@]}" fi exit ${status} } copy_mode_and_time() { local patch_file="${1}" local new_file="${2}" local mode mode="$(stat "-f%OMp%OLp" "${patch_file}")" if ! chmod -h "${mode}" "${new_file}"; then exit 16 fi if ! [[ -L "${new_file}" ]]; then # Symbolic link modification times can't be copied because there's no # shell tool that provides direct access to lutimes. Instead, the symbolic # link was created with rsync, which already copied the timestamp with # lutimes. if ! touch -r "${patch_file}" "${new_file}"; then exit 17 fi fi } apply_patch() { local old_file="${1}" local patch_file="${2}" local new_file="${3}" local patcher="${4}" if [[ -L "${old_file}" ]] || ! [[ -f "${old_file}" ]]; then err "can't patch nonexistent or irregular file ${old_file}" exit 12 fi if ! "${patcher}" "${old_file}" "${new_file}" "${patch_file}"; then err "couldn't create ${new_file} by applying ${patch_file} to ${old_file}" exit 13 fi } decompress_file() { local old_file="${1}" local patch_file="${2}" local new_file="${3}" local decompressor="${4}" if ! "${decompressor}" -c < "${patch_file}" > "${new_file}"; then err "couldn't decompress ${patch_file} to ${new_file} with ${decompressor}" exit 14 fi } copy_file() { local old_file="${1}" local patch_file="${2}" local new_file="${3}" local extra="${4}" if ! cp "${patch_file}" "${new_file}"; then exit 15 fi } patch_file() { local old_file="${1}" local patch_file="${2}" local new_file="${3}" local operation extra strip_length if [[ "${patch_file: -${#GBS_SUFFIX}}" = "${GBS_SUFFIX}" ]]; then operation="apply_patch" extra="${GOOBSPATCH}" strip_length=${#GBS_SUFFIX} elif [[ "${patch_file: -${#BZ2_SUFFIX}}" = "${BZ2_SUFFIX}" ]]; then operation="decompress_file" extra="${BUNZIP2}" strip_length=${#BZ2_SUFFIX} elif [[ "${patch_file: -${#GZ_SUFFIX}}" = "${GZ_SUFFIX}" ]]; then operation="decompress_file" extra="${GUNZIP}" strip_length=${#GZ_SUFFIX} elif [[ "${patch_file: -${#XZ_SUFFIX}}" = "${XZ_SUFFIX}" ]]; then operation="decompress_file" extra="${XZDEC}" strip_length=${#XZ_SUFFIX} elif [[ "${patch_file: -${#PLAIN_SUFFIX}}" = "${PLAIN_SUFFIX}" ]]; then operation="copy_file" extra="patch" strip_length=${#PLAIN_SUFFIX} else err "don't know how to operate on ${patch_file}" exit 11 fi old_file="${old_file:0:${#old_file} - ${strip_length}}" new_file="${new_file:0:${#new_file} - ${strip_length}}" if [[ -e "${new_file}" ]]; then err "${new_file} already exists" exit 8 fi "${operation}" "${old_file}" "${patch_file}" "${new_file}" "${extra}" copy_mode_and_time "${patch_file}" "${new_file}" } patch_symlink() { local patch_file="${1}" local new_file="${2}" # local target # target="$(readlink "${patch_file}")" # ln -s "${target}" "${new_file}" # Use rsync instead of the above, as it's the only way to preserve the # timestamp of a symbolic link using shell tools. if ! rsync -lt "${patch_file}" "${new_file}"; then exit 10 fi copy_mode_and_time "${patch_file}" "${new_file}" } patch_dir() { local old_dir="${1}" local patch_dir="${2}" local new_dir="${3}" if ! mkdir "${new_dir}"; then exit 7 fi local patch_file for patch_file in "${patch_dir}/"*; do local file="${patch_file:${#patch_dir} + 1}" local old_file="${old_dir}/${file}" local new_file="${new_dir}/${file}" if [[ -e "${new_file}" ]]; then err "${new_file} already exists" exit 8 fi if [[ -L "${patch_file}" ]]; then patch_symlink "${patch_file}" "${new_file}" elif [[ -d "${patch_file}" ]]; then patch_dir "${old_file}" "${patch_file}" "${new_file}" elif ! [[ -f "${patch_file}" ]]; then err "can't handle irregular file ${patch_file}" exit 9 else patch_file "${old_file}" "${patch_file}" "${new_file}" fi done copy_mode_and_time "${patch_dir}" "${new_dir}" } # shell_safe_path ensures that |path| is safe to pass to tools as a # command-line argument. If the first character in |path| is "-", "./" is # prepended to it. The possibly-modified |path| is output. shell_safe_path() { local path="${1}" if [[ "${path:0:1}" = "-" ]]; then echo "./${path}" else echo "${path}" fi } dirs_contained() { local dir1="${1}/" local dir2="${2}/" if [[ "${dir1:0:${#dir2}}" = "${dir2}" ]] || [[ "${dir2:0:${#dir1}}" = "${dir1}" ]]; then return 0 fi return 1 } usage() { echo "usage: ${ME} old_dir patch_dir new_dir" >& 2 } main() { local old_dir patch_dir new_dir old_dir="$(shell_safe_path "${1}")" patch_dir="$(shell_safe_path "${2}")" new_dir="$(shell_safe_path "${3}")" trap cleanup EXIT HUP INT QUIT TERM if ! [[ -d "${old_dir}" ]] || ! [[ -d "${patch_dir}" ]]; then err "old_dir and patch_dir must exist and be directories" usage exit 3 fi if [[ -e "${new_dir}" ]]; then err "new_dir must not exist" usage exit 4 fi local new_dir_parent new_dir_parent="$(dirname "${new_dir}")" if ! [[ -d "${new_dir_parent}" ]]; then err "new_dir parent directory must exist and be a directory" usage exit 5 fi local old_dir_phys patch_dir_phys new_dir_parent_phys new_dir_phys old_dir_phys="$(cd "${old_dir}" && pwd -P)" patch_dir_phys="$(cd "${patch_dir}" && pwd -P)" new_dir_parent_phys="$(cd "${new_dir_parent}" && pwd -P)" new_dir_phys="${new_dir_parent_phys}/$(basename "${new_dir}")" if dirs_contained "${old_dir_phys}" "${patch_dir_phys}" || dirs_contained "${old_dir_phys}" "${new_dir_phys}" || dirs_contained "${patch_dir_phys}" "${new_dir_phys}"; then err "directories must not contain one another" usage exit 6 fi g_cleanup+=("${new_dir}") patch_dir "${old_dir}" "${patch_dir}" "${new_dir}" unset g_cleanup[${#g_cleanup[@]}] trap - EXIT } if [[ ${#} -ne 3 ]]; then usage exit 2 fi main "${@}" exit ${?}