#!/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 # nogit=0 tmpfile="" # Show usage information about gitapply script usage() { echo "" echo "Usage: ./gitapply.sh [--nogit] [-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 "" } # Critical error, abort abort() { if [ ! -z "$tmpfile" ]; then rm "$tmpfile" tmpfile="" fi echo "[PATCH] ERR: $1" >&2 exit 1 } # Show a warning warning() { echo "[PATCH] WRN: $1" >&2 } # Calculate git sha1 hash gitsha1() { if [ -f "$1" ]; then echo -en "blob $(du -b "$1" | cut -f1)\x00" | cat - "$1" | sha1sum | cut -d' ' -f1 else echo "0000000000000000000000000000000000000000" fi } # Determine size of a file (or zero, if it doesn't exist) filesize() { local size=$(du -b "$1" | cut -f1) if [ -z "$size" ]; then size="0" fi echo "$size" } # Parse environment variables while [[ $# > 0 ]]; do cmd="$1"; shift case "$cmd" in --nogit) nogit=1 ;; -v) ;; --directory=*) cd "${cmd#*=}" ;; -d) cd "$1"; shift ;; -R) abort "Reverse applying patches not supported yet with this tool." ;; --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 # Detect BSD - we check this first to error out as early as possible 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 # Check for missing depdencies for dependency in awk cut dd du grep 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 # Decode base85 git data, prepend with a gzip header awk_decode_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_decode_binarypatch=' 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"); }' # Find end of patch header awk_eof_header=' BEGIN{ ofs=1; } !/^(--- |\+\+\+ |old |new |copy |rename |similarity |index |GIT |literal |delta )/{ ofs=0; exit 0; } END{ print FNR+ofs; }' # Find end of text patch awk_eof_textpatch=' BEGIN{ ofs=1; } !/^(@| |+|-|\\)/{ ofs=0; exit 0; } END{ print FNR+ofs; }' # Find end of git binary patch awk_eof_binarypatch=' BEGIN{ ofs=1; } !/^[A-Za-z]/{ ofs=0; exit 0; } END{ print FNR+ofs; }' # Create a temporary file containing the patch - NOTE: even if the user # provided a filename it still makes sense to work with a temporary file, # to avoid changes of the content while this script is active. tmpfile=$(mktemp) if [ ! -f "$tmpfile" ]; then tmpfile="" abort "Unable to create temporary file for patch." elif ! cat > "$tmpfile"; then abort "Patch truncated." fi # Go through the different patch sections lastoffset=1 for offset in $(awk '/^diff --git /{ print FNR; }' "$tmpfile"); do # Check part between end of last patch and start of current patch if [ "$lastoffset" -gt "$offset" ]; then abort "Unable to split patch. Is this a proper git patch?" elif [ "$lastoffset" -lt "$offset" ]; then tmpoffset=$((offset - 1)) if sed -n "$lastoffset,$tmpoffset p" "$tmpfile" | grep -q '^\(@@ -\|--- \|+++ \)'; then abort "Patch corrupted or not created with git." fi fi # Find out the size of the patch header tmpoffset=$((offset + 1)) tmpoffset=$(sed -n "$tmpoffset,\$ p" "$tmpfile" | awk "$awk_eof_header") hdroffset=$((offset + tmpoffset)) # Parse all important fields of the header patch_oldname="" patch_newname="" patch_oldsha1="" patch_newsha1="" patch_is_binary=0 patch_binary_type="" patch_binary_size="" tmpoffset=$((hdroffset - 1)) while IFS= read -r line; do if [ "$line" == "GIT binary patch" ]; then patch_is_binary=1 elif [[ "$line" =~ ^diff\ --git\ ([^ ]*)\ ([^ ]*)$ ]]; then patch_oldname="${BASH_REMATCH[1]}" patch_newname="${BASH_REMATCH[2]}" elif [[ "$line" =~ ^---\ (.*)$ ]]; then patch_oldname="${BASH_REMATCH[1]}" elif [[ "$line" =~ ^\+\+\+\ (.*)$ ]]; then patch_newname="${BASH_REMATCH[1]}" elif [[ "$line" =~ ^index\ ([a-fA-F0-9]*)\.\.([a-fA-F0-9]*) ]]; then patch_oldsha1="${BASH_REMATCH[1]}" patch_newsha1="${BASH_REMATCH[2]}" elif [[ "$line" =~ ^(literal|delta)\ ([0-9]+)$ ]]; then patch_binary_type="${BASH_REMATCH[1]}" patch_binary_size="${BASH_REMATCH[2]}" fi done < <(sed -n "$offset,$tmpoffset p" "$tmpfile") # 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 # Short progress message echo "patching $patch_newname" # If its a textual patch, then use 'patch' to apply it. if [ "$patch_is_binary" -eq 0 ]; then # Find end of textual patch tmpoffset=$(sed -n "$hdroffset,\$ p" "$tmpfile" | awk "$awk_eof_textpatch") lastoffset=$((hdroffset + tmpoffset - 1)) # Apply textual patch tmpoffset=$((lastoffset - 1)) if ! sed -n "$offset,$tmpoffset p" "$tmpfile" | patch -p1 -s -f; then abort "Textual patch did not apply, aborting." fi continue fi # It is a binary patch - check that requirements are fulfilled if [ "$patch_binary_type" != "literal" ] && [ "$patch_binary_type" != "delta" ]; then abort "Unknown binary patch type." elif [ -z "$patch_oldsha1" ] || [ -z "$patch_newsha1" ]; then abort "Missing index header, sha1 sums required for binary patch." elif [ "$patch_oldname" != "$patch_newname" ]; then abort "Stripped old and new name doesn't match for binary patch." fi # Ensure that checksum of old file matches sha=$(gitsha1 "$patch_oldname") if [ "$patch_oldsha1" != "$sha" ]; then abort "Checksum mismatch for $patch_oldname (expected $patch_oldsha1, got $sha)." fi # Find end of binary patch tmpoffset=$(sed -n "$hdroffset,\$ p" "$tmpfile" | awk "$awk_eof_binarypatch") lastoffset=$((hdroffset + tmpoffset - 1)) # Special case - deleting the whole file if [ "$patch_newsha1" == "0000000000000000000000000000000000000000" ] && [ "$patch_binary_size" -eq 0 ] && [ "$patch_binary_type" == "literal" ]; then # Applying the patch just means deleting the file if [ -f "$patch_oldname" ] && ! rm "$patch_oldname"; then abort "Unable to delete file $patch_oldname." fi continue fi # Create temporary file for literal patch literal_tmpfile=$(mktemp) if [ ! -f "$literal_tmpfile" ]; then abort "Unable to create temporary file for binary patch." fi # Decode base85 and gzip compression tmpoffset=$((lastoffset - 1)) sed -n "$hdroffset,$tmpoffset p" "$tmpfile" | awk "$awk_decode_b85" | gzip -dc > "$literal_tmpfile" 2>/dev/null if [ "$patch_binary_size" -ne "$(filesize "$literal_tmpfile")" ]; then rm "$literal_tmpfile" abort "Uncompressed binary patch has wrong size." fi # Convert delta to literal patch if [ "$patch_binary_type" == "delta" ]; then # Create new temporary file for literal patch delta_tmpfile="$literal_tmpfile" literal_tmpfile=$(mktemp) if [ ! -f "$literal_tmpfile" ]; then rm "$delta_tmpfile" abort "Unable to create temporary file for binary patch." fi patch_binary_complete=0 patch_binary_destsize=0 while read cmd arg1 arg2; do if [ "$cmd" == "S" ]; then [ "$arg1" -eq "$(filesize "$patch_oldname")" ] || break patch_binary_destsize="$arg2" elif [ "$cmd" == "1" ]; then dd if="$patch_oldname" bs=1 skip="$arg1" count="$arg2" >> "$literal_tmpfile" 2>/dev/null || break elif [ "$cmd" == "2" ]; then dd if="$delta_tmpfile" bs=1 skip="$arg1" count="$arg2" >> "$literal_tmpfile" 2>/dev/null || break elif [ "$cmd" == "E" ]; then patch_binary_complete=1 else break; fi done < <(hexdump -v -e '32/1 "%02X" "\n"' "$delta_tmpfile" | awk "$awk_decode_binarypatch") rm "$delta_tmpfile" if [ "$patch_binary_complete" -eq 0 ]; then rm "$literal_tmpfile" abort "Unable to parse full patch." elif [ "$patch_binary_destsize" -ne "$(filesize "$literal_tmpfile")" ]; then rm "$literal_tmpfile" abort "Unpacked delta patch has wrong size." fi fi # Ensure that checksum of literal patch matches sha=$(gitsha1 "$literal_tmpfile") if [ "$patch_newsha1" != "$sha" ]; then rm "$literal_tmpfile" abort "Checksum mismatch for patched $patch_newname (expected $patch_newsha1, got $sha)." fi # Apply the patch - copy literal patch to destination path if ! cp "$literal_tmpfile" "$patch_newname"; then rm "$literal_tmpfile" abort "Unable to replace $patch_newname with patched file." fi rm "$literal_tmpfile" done # Check last remaining part for unparsed patches if sed -n "$lastoffset,\$ p" "$tmpfile" | grep -q '^\(@@ -\|--- \|+++ \)'; then abort "Patch corrupted or not created with git." fi # Delete temp file (if any) if [ ! -z "$tmpfile" ]; then rm "$tmpfile" tmpfile="" fi # Success exit 0