dirpatcher.sh revision 1e9bf3e0803691d0a228da41fc608347b6db4340
1#!/bin/bash -p
2
3# Copyright (c) 2011 The Chromium Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7# usage: dirpatcher.sh old_dir patch_dir new_dir
8#
9# dirpatcher creates new_dir from patch_dir by decompressing and copying
10# files, and using goobspatch to apply binary diffs to files in old_dir.
11#
12# dirpatcher performs the inverse operation to dirdiffer. For more details,
13# consult dirdiffer.sh.
14#
15# Exit codes:
16#  0  OK
17#  1  Unknown failure
18#  2  Incorrect number of parameters
19#  3  Input directories do not exist or are not directories
20#  4  Output directory already exists
21#  5  Parent of output directory does not exist or is not a directory
22#  6  An input or output directories contains another
23#  7  Could not create output directory
24#  8  File already exists in output directory
25#  9  Found an irregular file (non-directory, file, or symbolic link) in input
26# 10  Could not create symbolic link
27# 11  Unrecognized file extension
28# 12  Attempt to patch a nonexistent or non-regular file
29# 13  Patch application failed
30# 14  File decompression failed
31# 15  File copy failed
32# 16  Could not set mode (permissions)
33# 17  Could not set modification time
34
35set -eu
36
37# Environment sanitization. Set a known-safe PATH. Clear environment variables
38# that might impact the interpreter's operation. The |bash -p| invocation
39# on the #! line takes the bite out of BASH_ENV, ENV, and SHELLOPTS (among
40# other features), but clearing them here ensures that they won't impact any
41# shell scripts used as utility programs. SHELLOPTS is read-only and can't be
42# unset, only unexported.
43export PATH="/usr/bin:/bin:/usr/sbin:/sbin"
44unset BASH_ENV CDPATH ENV GLOBIGNORE IFS POSIXLY_CORRECT
45export -n SHELLOPTS
46
47shopt -s dotglob nullglob
48
49# find_tool looks for an executable file named |tool_name|:
50#  - in the same directory as this script,
51#  - if this script is located in a Chromium source tree, at the expected
52#    Release output location in the Mac out directory,
53#  - as above, but in the Debug output location
54# If found in any of the above locations, the script's path is output.
55# Otherwise, this function outputs |tool_name| as a fallback, allowing it to
56# be found (or not) by an ordinary ${PATH} search.
57find_tool() {
58  local tool_name="${1}"
59
60  local script_dir
61  script_dir="$(dirname "${0}")"
62
63  local tool="${script_dir}/${tool_name}"
64  if [[ -f "${tool}" ]] && [[ -x "${tool}" ]]; then
65    echo "${tool}"
66    return
67  fi
68
69  local script_dir_phys
70  script_dir_phys="$(cd "${script_dir}" && pwd -P)"
71  if [[ "${script_dir_phys}" =~ ^(.*)/src/chrome/installer/mac$ ]]; then
72    tool="${BASH_REMATCH[1]}/src/out/Release/${tool_name}"
73    if [[ -f "${tool}" ]] && [[ -x "${tool}" ]]; then
74      echo "${tool}"
75      return
76    fi
77
78    tool="${BASH_REMATCH[1]}/src/out/Debug/${tool_name}"
79    if [[ -f "${tool}" ]] && [[ -x "${tool}" ]]; then
80      echo "${tool}"
81      return
82    fi
83  fi
84
85  echo "${tool_name}"
86}
87
88ME="$(basename "${0}")"
89readonly ME
90GOOBSPATCH="$(find_tool goobspatch)"
91readonly GOOBSPATCH
92readonly BUNZIP2="bunzip2"
93readonly GUNZIP="gunzip"
94XZDEC="$(find_tool xzdec)"
95readonly XZDEC
96readonly GBS_SUFFIX='$gbs'
97readonly BZ2_SUFFIX='$bz2'
98readonly GZ_SUFFIX='$gz'
99readonly XZ_SUFFIX='$xz'
100readonly PLAIN_SUFFIX='$raw'
101
102err() {
103  local error="${1}"
104
105  echo "${ME}: ${error}" >& 2
106}
107
108declare -a g_cleanup
109cleanup() {
110  local status=${?}
111
112  trap - EXIT
113  trap '' HUP INT QUIT TERM
114
115  if [[ ${status} -ge 128 ]]; then
116    err "Caught signal $((${status} - 128))"
117  fi
118
119  if [[ "${#g_cleanup[@]}" -gt 0 ]]; then
120    rm -rf "${g_cleanup[@]}"
121  fi
122
123  exit ${status}
124}
125
126copy_mode_and_time() {
127  local patch_file="${1}"
128  local new_file="${2}"
129
130  local mode
131  mode="$(stat "-f%OMp%OLp" "${patch_file}")"
132  if ! chmod -h "${mode}" "${new_file}"; then
133    exit 16
134  fi
135
136  if ! [[ -L "${new_file}" ]]; then
137    # Symbolic link modification times can't be copied because there's no
138    # shell tool that provides direct access to lutimes. Instead, the symbolic
139    # link was created with rsync, which already copied the timestamp with
140    # lutimes.
141    if ! touch -r "${patch_file}" "${new_file}"; then
142      exit 17
143    fi
144  fi
145}
146
147apply_patch() {
148  local old_file="${1}"
149  local patch_file="${2}"
150  local new_file="${3}"
151  local patcher="${4}"
152
153  if [[ -L "${old_file}" ]] || ! [[ -f "${old_file}" ]]; then
154    err "can't patch nonexistent or irregular file ${old_file}"
155    exit 12
156  fi
157
158  if ! "${patcher}" "${old_file}" "${new_file}" "${patch_file}"; then
159    err "couldn't create ${new_file} by applying ${patch_file} to ${old_file}"
160    exit 13
161  fi
162}
163
164decompress_file() {
165  local old_file="${1}"
166  local patch_file="${2}"
167  local new_file="${3}"
168  local decompressor="${4}"
169
170  if ! "${decompressor}" -c < "${patch_file}" > "${new_file}"; then
171    err "couldn't decompress ${patch_file} to ${new_file} with ${decompressor}"
172    exit 14
173  fi
174}
175
176copy_file() {
177  local old_file="${1}"
178  local patch_file="${2}"
179  local new_file="${3}"
180  local extra="${4}"
181
182  if ! cp "${patch_file}" "${new_file}"; then
183    exit 15
184  fi
185}
186
187patch_file() {
188  local old_file="${1}"
189  local patch_file="${2}"
190  local new_file="${3}"
191
192  local operation extra strip_length
193
194  if [[ "${patch_file: -${#GBS_SUFFIX}}" = "${GBS_SUFFIX}" ]]; then
195    operation="apply_patch"
196    extra="${GOOBSPATCH}"
197    strip_length=${#GBS_SUFFIX}
198  elif [[ "${patch_file: -${#BZ2_SUFFIX}}" = "${BZ2_SUFFIX}" ]]; then
199    operation="decompress_file"
200    extra="${BUNZIP2}"
201    strip_length=${#BZ2_SUFFIX}
202  elif [[ "${patch_file: -${#GZ_SUFFIX}}" = "${GZ_SUFFIX}" ]]; then
203    operation="decompress_file"
204    extra="${GUNZIP}"
205    strip_length=${#GZ_SUFFIX}
206  elif [[ "${patch_file: -${#XZ_SUFFIX}}" = "${XZ_SUFFIX}" ]]; then
207    operation="decompress_file"
208    extra="${XZDEC}"
209    strip_length=${#XZ_SUFFIX}
210  elif [[ "${patch_file: -${#PLAIN_SUFFIX}}" = "${PLAIN_SUFFIX}" ]]; then
211    operation="copy_file"
212    extra="patch"
213    strip_length=${#PLAIN_SUFFIX}
214  else
215    err "don't know how to operate on ${patch_file}"
216    exit 11
217  fi
218
219  old_file="${old_file:0:${#old_file} - ${strip_length}}"
220  new_file="${new_file:0:${#new_file} - ${strip_length}}"
221
222  if [[ -e "${new_file}" ]]; then
223    err "${new_file} already exists"
224    exit 8
225  fi
226
227  "${operation}" "${old_file}" "${patch_file}" "${new_file}" "${extra}"
228
229  copy_mode_and_time "${patch_file}" "${new_file}"
230}
231
232patch_symlink() {
233  local patch_file="${1}"
234  local new_file="${2}"
235
236  # local target
237  # target="$(readlink "${patch_file}")"
238  # ln -s "${target}" "${new_file}"
239
240  # Use rsync instead of the above, as it's the only way to preserve the
241  # timestamp of a symbolic link using shell tools.
242  if ! rsync -lt "${patch_file}" "${new_file}"; then
243    exit 10
244  fi
245
246  copy_mode_and_time "${patch_file}" "${new_file}"
247}
248
249patch_dir() {
250  local old_dir="${1}"
251  local patch_dir="${2}"
252  local new_dir="${3}"
253
254  if ! mkdir "${new_dir}"; then
255    exit 7
256  fi
257
258  local patch_file
259  for patch_file in "${patch_dir}/"*; do
260    local file="${patch_file:${#patch_dir} + 1}"
261    local old_file="${old_dir}/${file}"
262    local new_file="${new_dir}/${file}"
263
264    if [[ -e "${new_file}" ]]; then
265      err "${new_file} already exists"
266      exit 8
267    fi
268
269    if [[ -L "${patch_file}" ]]; then
270      patch_symlink "${patch_file}" "${new_file}"
271    elif [[ -d "${patch_file}" ]]; then
272      patch_dir "${old_file}" "${patch_file}" "${new_file}"
273    elif ! [[ -f "${patch_file}" ]]; then
274      err "can't handle irregular file ${patch_file}"
275      exit 9
276    else
277      patch_file "${old_file}" "${patch_file}" "${new_file}"
278    fi
279  done
280
281  copy_mode_and_time "${patch_dir}" "${new_dir}"
282}
283
284# shell_safe_path ensures that |path| is safe to pass to tools as a
285# command-line argument. If the first character in |path| is "-", "./" is
286# prepended to it. The possibly-modified |path| is output.
287shell_safe_path() {
288  local path="${1}"
289  if [[ "${path:0:1}" = "-" ]]; then
290    echo "./${path}"
291  else
292    echo "${path}"
293  fi
294}
295
296dirs_contained() {
297  local dir1="${1}/"
298  local dir2="${2}/"
299
300  if [[ "${dir1:0:${#dir2}}" = "${dir2}" ]] ||
301     [[ "${dir2:0:${#dir1}}" = "${dir1}" ]]; then
302    return 0
303  fi
304
305  return 1
306}
307
308usage() {
309  echo "usage: ${ME} old_dir patch_dir new_dir" >& 2
310}
311
312main() {
313  local old_dir patch_dir new_dir
314  old_dir="$(shell_safe_path "${1}")"
315  patch_dir="$(shell_safe_path "${2}")"
316  new_dir="$(shell_safe_path "${3}")"
317
318  trap cleanup EXIT HUP INT QUIT TERM
319
320  if ! [[ -d "${old_dir}" ]] || ! [[ -d "${patch_dir}" ]]; then
321    err "old_dir and patch_dir must exist and be directories"
322    usage
323    exit 3
324  fi
325
326  if [[ -e "${new_dir}" ]]; then
327    err "new_dir must not exist"
328    usage
329    exit 4
330  fi
331
332  local new_dir_parent
333  new_dir_parent="$(dirname "${new_dir}")"
334  if ! [[ -d "${new_dir_parent}" ]]; then
335    err "new_dir parent directory must exist and be a directory"
336    usage
337    exit 5
338  fi
339
340  local old_dir_phys patch_dir_phys new_dir_parent_phys new_dir_phys
341  old_dir_phys="$(cd "${old_dir}" && pwd -P)"
342  patch_dir_phys="$(cd "${patch_dir}" && pwd -P)"
343  new_dir_parent_phys="$(cd "${new_dir_parent}" && pwd -P)"
344  new_dir_phys="${new_dir_parent_phys}/$(basename "${new_dir}")"
345
346  if dirs_contained "${old_dir_phys}" "${patch_dir_phys}" ||
347     dirs_contained "${old_dir_phys}" "${new_dir_phys}" ||
348     dirs_contained "${patch_dir_phys}" "${new_dir_phys}"; then
349    err "directories must not contain one another"
350    usage
351    exit 6
352  fi
353
354  g_cleanup+=("${new_dir}")
355
356  patch_dir "${old_dir}" "${patch_dir}" "${new_dir}"
357
358  unset g_cleanup[${#g_cleanup[@]}]
359  trap - EXIT
360}
361
362if [[ ${#} -ne 3 ]]; then
363  usage
364  exit 2
365fi
366
367main "${@}"
368exit ${?}
369