#!/usr/bin/env bash # # Wrapper to apply binary patches without git. # # Copyright (C) 2014 Sebastian Lackner # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA # # Setup parser variables nogit=0 lineno=0 verbose=0 patch_mode=0 patch_tmpfile="" # Macros abort() { if [ ! -z "$patch_tmpfile" ]; then rm "$patch_tmpfile" patch_tmpfile="" fi echo "[PATCH:$lineno] ERR: $1" >&2 exit 1 } invalid_parser_state() { abort "Invalid parser state (patch_mode=$patch_mode)!" } warning() { echo "[PATCH:$lineno] WRN: $1" >&2 } usage() { echo "" echo "Usage: ./gitapply [--nogit] [-v] [-d DIRECTORY]" echo "" echo "Reads patch data from stdin and applies the patch to the current" echo "directory or the directory given via commandline." echo "" echo "The patch file can contain both unified text patches as well as" echo "git binary patches." echo "" } gitsha1() { echo -en "blob $(du -b "$1" | cut -f1)\x00" | cat - "$1" | sha1sum | cut -d' ' -f1 } # Parse environment variables while [[ $# > 0 ]]; do cmd="$1"; shift case "$cmd" in --nogit) nogit=1 ;; -v) verbose=1 ;; --directory=*) cd "${cmd#*=}" ;; -d) cd "$1"; shift ;; -R) echo "Reverse applying patches not supported yet with this tool." >&2 exit 1 ;; --help) usage exit 0 ;; *) warning "Unknown argument $cmd." ;; esac done # Redirect to git apply if available if [ "$nogit" -eq 0 ] && command -v git >/dev/null 2>&1; then exec git apply --whitespace=nowarn "$@" exit 1 fi # Check for missing depdencies for dependency in awk chmod cut dd du gzip hexdump patch sha1sum; do if ! command -v "$dependency" >/dev/null 2>&1; then echo "Missing dependency: $dependency - please install this program and try again." >&2 exit 1 fi done # Detect BSD if gzip -V 2>&1 | grep "BSD" &> /dev/null; then echo "This script is not compatible with *BSD utilities. Please install git," >&2 echo "which provides the same functionality and will be used instead." >&2 exit 1 fi # Decode base85 git data, prepend with a gzip header awk_b85=' BEGIN{ git="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; b85="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~"; printf("\x1f\x8b\x08\x00\x00\x00\x00\x00"); while (getline > 0){ l = index(git, substr($0, 1, 1)); if (l == 0){ exit 1; }; p=2; while (l > 0 && p <= length($0)){ a = index(b85, substr($0, p++, 1)); b = index(b85, substr($0, p++, 1)); c = index(b85, substr($0, p++, 1)); d = index(b85, substr($0, p++, 1)); e = index(b85, substr($0, p++, 1)); if (a-- == 0 || b-- == 0 || c-- == 0 || d-- == 0 || e-- == 0){ exit 1; } n = (((a * 85 + b) * 85 + c) * 85 + d) * 85 + e; if (n > 4294967295){ exit 1; } a = n % 256; n /= 256; b = n % 256; n /= 256; c = n % 256; n /= 256; d = n % 256; if (l-- > 0) printf("%c", d); if (l-- > 0) printf("%c", c); if (l-- > 0) printf("%c", b); if (l-- > 0) printf("%c", a); } if (p != length($0) + 1 || l != 0){ exit 1; } } }' # Decodes the information from a git delta patch passed in as hex encoded awk_gitpatch=' function get_byte(a, b){ # usage: get_byte() if (length(__buffer) == 0){ if(getline __buffer <= 0){ exit 1; } } a = index(hex, substr(__buffer, 1, 1)); b = index(hex, substr(__buffer, 2, 1)); if (a-- == 0 || b-- == 0){ exit 1; } __buffer = substr(__buffer, 3); __pos++; return a * 16 + b; } function skip_bytes(n, m){ # usage: skip_bytes(n) if (length(__buffer) == 0){ if(getline __buffer <= 0){ exit 1; } } while (n >= (length(__buffer) / 2)){ m = length(__buffer) / 2; n -= m; __pos += m; if(getline __buffer <= 0){ exit 1; } } if (n > 0){ __buffer = substr(__buffer, 1 + 2 * n); __pos += n; } } function get_delta_hdr_size(){ # usage: get_delta_hdr_size() cmd = get_byte(); size = and(cmd, 0x7f); i = 7; while (and(cmd, 0x80)){ cmd = get_byte(); size += lshift(and(cmd, 0x7f), i); i += 7; } return size; } BEGIN{ hex="0123456789ABCDEF" src_size = get_delta_hdr_size(); dst_size = get_delta_hdr_size(); printf("S %d %d\n", src_size, dst_size); while (dst_size > 0){ cmd = get_byte(); if (and(cmd, 0x80)){ cp_offs = 0; cp_size = 0; if (and(cmd, 0x01)){ cp_offs = get_byte(); } if (and(cmd, 0x02)){ cp_offs += lshift(get_byte(), 8); } if (and(cmd, 0x04)){ cp_offs += lshift(get_byte(), 16); } if (and(cmd, 0x08)){ cp_offs += lshift(get_byte(), 24); } if (and(cmd, 0x10)){ cp_size = get_byte(); } if (and(cmd, 0x20)){ cp_size += lshift(get_byte(), 8); } if (and(cmd, 0x40)){ cp_size += lshift(get_byte(), 16); } if (cp_size == 0) cp_size = 0x10000; if (cp_offs + cp_size > src_size || cp_size > dst_size){ exit 1; } printf("1 %d %d\n", cp_offs, cp_size); dst_size -= cp_size; }else if (cmd){ if (cmd > dst_size){ exit 1; } printf("2 %d %d\n", __pos, cmd); skip_bytes(cmd); dst_size -= cmd; }else{ exit 1; } } printf("E 0 0\n"); }' # Parse lines of the patch while IFS= read -r line; do (( lineno++ )) # In verbose mode we print each line of the patch to stdout if [ "$verbose" -ne 0 ]; then echo "$lineno: $line" fi # MODE 1: Parse header # Fall-through to 2 if [ "$patch_mode" -eq 1 ]; then if [[ "$line" =~ ^---\ (.*)$ ]]; then patch_oldname="${BASH_REMATCH[1]}" echo "$line" >> "$patch_tmpfile" continue elif [[ "$line" =~ ^\+\+\+\ (.*)$ ]]; then patch_newname="${BASH_REMATCH[1]}" echo "$line" >> "$patch_tmpfile" continue elif [ "${line:0:8}" == "old mode" ] || [ "${line:0:17}" == "deleted file mode" ]; then # ignore echo "$line" >> "$patch_tmpfile" continue elif [[ "$line" =~ ^new\ mode\ ([0-9]*)$ ]] || [[ "$line" =~ ^new\ file\ mode\ ([0-9]*)$ ]]; then patch_filemode="${BASH_REMATCH[1]}" echo "$line" >> "$patch_tmpfile" continue elif [ "${line:0:8}" == "new mode" ] || [ "${line:0:13}" == "new file mode" ]; then patch_errors+=("$lineno: Unable to parse header line '$line'.") patch_invalid=1 echo "$line" >> "$patch_tmpfile" continue elif [ "${line:0:9}" == "copy from" ] || [ "${line:0:7}" == "copy to" ]; then patch_errors+=("$lineno: Copy header not implemented yet.") patch_invalid=1 echo "$line" >> "$patch_tmpfile" continue elif [ "${line:0:7}" == "rename " ]; then patch_errors+=("$lineno: Patch rename header not implemented yet.") patch_invalid=1 echo "$line" >> "$patch_tmpfile" continue elif [ "${line:0:16}" == "similarity index" ] || [ "${line:0:19}" == "dissimilarity index" ]; then # ignore echo "$line" >> "$patch_tmpfile" continue elif [[ "$line" =~ ^index\ ([a-fA-F0-9]*)\.\.([a-fA-F0-9]*) ]]; then patch_oldsha1="${BASH_REMATCH[1]}" patch_newsha1="${BASH_REMATCH[2]}" echo "$line" >> "$patch_tmpfile" continue elif [ "${line:0:6}" == "index " ]; then patch_errors+=("$lineno: Unable to parse header line '$line'.") patch_invalid=1 echo "$line" >> "$patch_tmpfile" continue else # Remove first path components, which are always a/ and b/ for git patches if [[ "$patch_oldname" =~ ^a/(.*)$ ]]; then patch_oldname="${BASH_REMATCH[1]}" elif [ "$patch_oldname" != "/dev/null" ]; then abort "Old name doesn't start with a/." fi if [[ "$patch_newname" =~ ^b/(.*)$ ]]; then patch_newname="${BASH_REMATCH[1]}" elif [ "$patch_newname" != "/dev/null" ]; then abort "New name doesn't start with b/." fi patch_mode=2 # fall-through fi fi # MODE 2: Decide between binary and textual patch data # Fall-through to 200, 0 if [ "$patch_mode" -eq 2 ]; then if [[ "$line" == "GIT binary patch" ]]; then if [ -z "$patch_oldsha1" ] || [ -z "$patch_newsha1" ]; then patch_errors+=("$lineno: Missing index header, sha1 sums required for binary patch.") patch_invalid=1 fi if [ "$patch_oldname" != "$patch_newname" ]; then patch_errors+=("$lineno: Stripped old- and new name doesn't match for binary patch.") patch_invalid=1 fi if [ "$patch_invalid" -ne 0 ]; then for error in "${patch_errors[@]}"; do echo "$error" >&2; done abort "Unable to continue." fi patch_mode=100 continue elif [ "${line:0:4}" == "@@ -" ]; then # We count the number of lines added/removed for informational purposes patch_total_add=0 patch_total_rem=0 patch_mode=200 # fall-through elif [ "${line:0:11}" == "diff --git " ]; then if [ "$patch_oldname" != "$patch_newname" ]; then patch_errors+=("$lineno: Stripped old- and new name doesn't match.") patch_invalid=1 fi if [ "$patch_invalid" -ne 0 ]; then for error in "${patch_errors[@]}"; do echo "$error" >&2; done abort "Unable to continue." fi if [ ! -z "$patch_filemode" ]; then echo "patching $patch_newname" chmod "${patch_filemode: -3}" "$patch_oldname" # we ignore failures for now fi patch_mode=0 # fall-through elif [ ! -z "$line" ]; then abort "Unknown patch format." fi fi # MODE 100: Decide between binary literal/delta patch if [ "$patch_mode" -eq 100 ]; then if [[ "$line" =~ ^(literal|delta)\ ([0-9]+)$ ]]; then binary_patch_type="${BASH_REMATCH[1]}" binary_patch_size="${BASH_REMATCH[2]}" # Check shasum if its not a patch creating a new file if [ "$patch_oldsha1" != "0000000000000000000000000000000000000000" ] || [ "$binary_patch_type" == "delta" ] || [ -f "$patch_oldname" ]; then if [ -f "$patch_oldname" ]; then sha=$(gitsha1 "$patch_oldname") else sha="0000000000000000000000000000000000000000" fi if [ "$patch_oldsha1" != "$sha" ]; then echo "$lineno: Expected $patch_oldsha1" >&2 echo "$lineno: Got $sha" >&2 abort "Unable to continue because of sha1 mismatch of original file." fi fi # Is it a patch deleting this file if [ "$patch_newsha1" == "0000000000000000000000000000000000000000" ] && [ "$binary_patch_size" -eq 0 ] && [ "$binary_patch_type" == "literal" ]; then echo "patching $patch_newname" # Apply the patch: Just delete the file if [ -f "$patch_oldname" ] && ! rm "$patch_oldname"; then abort "Unable to delete file $patch_oldname." fi # We are ready with this one patch_mode=0 continue fi # Clear temporary file, we will use it to decode the binary block echo -en "" > "$patch_tmpfile" patch_mode=101 continue else abort "Only literal/delta patches are supported." fi fi # MODE 101: Decode data if [ "$patch_mode" -eq 101 ]; then if [ ! -z "$line" ]; then # Append this binary chunk in the temp file echo "$line" >> "$patch_tmpfile" else echo "patching $patch_newname" decoded_tmpfile=$(mktemp) if [ ! -f "$decoded_tmpfile" ]; then abort "Unable to create temporary file for patch." fi # Decode base85 and add a gzip header awk "$awk_b85" < "$patch_tmpfile" | gzip -dc > "$decoded_tmpfile" 2>/dev/null # The new temp file replaces the old one rm "$patch_tmpfile" patch_tmpfile="$decoded_tmpfile" # Ensure that resulting binary patch has the correct size if [ "$binary_patch_size" -ne "$(du -b "$patch_tmpfile" | cut -f 1)" ]; then abort "Uncompressed binary patch has wrong size." fi # Apply git delta path if [ "$binary_patch_type" == "delta" ]; then decoded_tmpfile=$(mktemp) if [ ! -f "$decoded_tmpfile" ]; then abort "Unable to create temporary file for patch." fi binary_patch_complete=0 binary_patch_destsize="" while read cmd arg1 arg2; do if [ "$cmd" == "S" ]; then binary_patch_destsize="$arg2" [ "$arg1" -eq "$(du -b "$patch_oldname" | cut -f 1)" ] || break elif [ "$cmd" == "1" ]; then dd if="$patch_oldname" bs=1 skip="$arg1" count="$arg2" >> "$decoded_tmpfile" 2>/dev/null || break elif [ "$cmd" == "2" ]; then dd if="$patch_tmpfile" bs=1 skip="$arg1" count="$arg2" >> "$decoded_tmpfile" 2>/dev/null || break elif [ "$cmd" == "E" ]; then binary_patch_complete=1 else break; fi done < <(hexdump -v -e '32/1 "%02X" "\n"' "$patch_tmpfile" | awk "$awk_gitpatch") # The new temp file replaces the old one rm "$patch_tmpfile" patch_tmpfile="$decoded_tmpfile" if [ "$binary_patch_complete" -ne 1 ]; then abort "Unable to parse full patch." elif [ "$binary_patch_destsize" -ne "$(du -b "$patch_tmpfile" | cut -f 1)" ]; then abort "Unpacked delta patch has wrong size." fi elif [ "$binary_patch_type" != "literal" ]; then invalid_parser_state fi # Check shasum if its not a patch creating a new file sha=$(gitsha1 "$patch_tmpfile") if [ "$patch_newsha1" != "$sha" ]; then echo "$lineno: Expected $patch_newsha1" >&2 echo "$lineno: Got $sha" >&2 abort "Unable to continue because of sha1 mismatch after applying the patch." fi if ! cp "$patch_tmpfile" "$patch_oldname"; then abort "Unable to replace original file." fi if [ ! -z "$patch_filemode" ]; then chmod "${patch_filemode: -3}" "$patch_oldname" # we ignore failures for now fi # We're ready with this patch patch_mode=0 continue fi fi # MODE 200: Text patch # Fall-through to 0 if [ "$patch_mode" -eq 200 ]; then hunk="^@@\ -(([0-9]+),)?([0-9]+)\ \+(([0-9]+),)?([0-9]+)\ @@" if [[ "$line" =~ $hunk ]]; then # ${BASH_REMATCH[2]} - source line # ${BASH_REMATCH[5]} - end line hunk_src_lines="${BASH_REMATCH[3]}" hunk_dst_lines="${BASH_REMATCH[6]}" if [ "$hunk_src_lines" -eq 0 ] && [ "$hunk_dst_lines" -eq 0 ]; then abort "Empty hunk doesn't make sense." fi # Start of a new hunk, append it echo "$line" >> "$patch_tmpfile" patch_mode=201 continue elif [ "${line:0:2}" == "\\ " ]; then # ignore echo "$line" >> "$patch_tmpfile" continue else echo "patching $patch_newname" # Try to apply the patch using 'patch' if ! patch -p1 -s -f < "$patch_tmpfile"; then abort "Patch did not apply, aborting." fi patch_mode=0 # fall-through fi fi # MODE 201: Wait until we reach the end of a hunk if [ "$patch_mode" -eq 201 ]; then # These lines are part of a hunk, append it echo "$line" >> "$patch_tmpfile" if [ "${line:0:1}" == " " ] && [ "$hunk_src_lines" -gt 0 ] && [ "$hunk_dst_lines" -gt 0 ]; then (( hunk_src_lines-- )) (( hunk_dst_lines-- )) elif [ "${line:0:1}" == "-" ] && [ "$hunk_src_lines" -gt 0 ]; then (( hunk_src_lines-- )) (( patch_total_rem++ )) elif [ "${line:0:1}" == "+" ] && [ "$hunk_dst_lines" -gt 0 ]; then (( hunk_dst_lines-- )) (( patch_total_add++ )) elif [ "${line:0:2}" == "\\ " ]; then continue # ignore "\\ No newline ..." else abort "Unexpected line in hunk." fi # If it was the last line of this hunk then go back to mode 200 if [ "$hunk_src_lines" -eq 0 ] && [ "$hunk_dst_lines" -eq 0 ]; then patch_mode=200 continue fi fi # MODE 0: Search for patch header if [ "$patch_mode" -eq 0 ]; then if [[ "$line" =~ ^diff\ --git\ ([^ ]*)\ ([^ ]*)$ ]]; then # Is this patch valid? The array will contain a list of detected errors patch_invalid=0 patch_errors=() # Setup name and sha1 sum variables patch_oldname="${BASH_REMATCH[1]}" patch_newname="${BASH_REMATCH[2]}" patch_oldsha1="" patch_newsha1="" # Filemode patch_filemode="" if [ ! -z "$patch_tmpfile" ]; then rm "$patch_tmpfile" fi patch_tmpfile=$(mktemp) if [ ! -f "$patch_tmpfile" ]; then abort "Unable to create temporary file for patch." fi echo "$line" >> "$patch_tmpfile" patch_mode=1 continue elif [ "${line:0:4}" == "@@ -" ] || [ "${line:0:4}" == "--- " ] || [ "${line:0:4}" == "+++ " ]; then abort "Patch corrupted or not created with git." fi fi done # Apply last text patch (if any) if [ "$patch_mode" -eq 200 ]; then echo "patching $patch_newname" # Try to apply the patch using 'patch' if ! patch -p1 -s -f < "$patch_tmpfile"; then abort "Patch did not apply, aborting." fi patch_mode=0 fi # Make sure we're not just parsing a patch if [ "$patch_mode" -ne 0 ]; then abort "File ended in the middle of a patch!" fi # Clean up temp files if any if [ ! -z "$patch_tmpfile" ]; then rm "$patch_tmpfile" patch_tmpfile="" fi # Success exit 0