#!/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 ${?}