#!/bin/sh set -eu allow_loose=0 die() { printf 'apply_patch: %s\n' "$*" >&2 exit 1 } mk_temp() { mktemp "${TMPDIR:-/tmp}/unidesk-apply-patch.XXXXXX" || exit 1 } ensure_parent() { case "$1" in */*) dir=${1%/*} [ -n "$dir" ] || dir=/ mkdir -p "$dir" ;; esac } read_text_preserve_newlines() { marker="__UNIDESK_APPLY_PATCH_EOF_$$__" text=$(cat "$1"; printf '%s' "$marker") || die "failed to read $1" printf '%s' "${text%"$marker"}" } write_file() { target=$1 source=$2 ensure_parent "$target" cp "$source" "$target" || die "failed to write $target" } line_number_for_prefix() { newline_count=$(printf '%s' "$1" | tr -cd '\n' | wc -c | tr -d ' ') printf '%s\n' $((newline_count + 1)) } replace_once_with_perl() { command -v perl >/dev/null 2>&1 || return 127 perl -0777 -e ' use strict; use warnings; sub fail { print STDERR "apply_patch: ", @_, "\n"; exit 1; } sub read_all { my ($path, $label) = @_; open my $fh, "<", $path or fail("failed to read $label"); binmode $fh; local $/; my $data = <$fh>; close $fh; return defined $data ? $data : ""; } my ($target, $search_file, $replace_file, $hunk_id, $allow_loose, $out) = @ARGV; my $old = read_all($target, $target); my $search = read_all($search_file, "hunk search"); my $replacement = read_all($replace_file, "hunk replacement"); my $new; if ($search eq "") { fail("hunk $hunk_id in $target has no context; add unchanged/deleted anchor lines or pass --allow-loose after manual review") if $allow_loose ne "1"; print STDERR "apply_patch: hunk $hunk_id matched $target:1 (loose)\n"; $new = $replacement . $old; } else { my $pos = -1; my $offset = 0; my $count = 0; while (1) { my $found = index($old, $search, $offset); last if $found < 0; $pos = $found if $count == 0; $count += 1; last if $count > 1 && $allow_loose ne "1"; $offset = $found + length($search); } fail("hunk $hunk_id context not found in $target") if $count == 0; fail("hunk $hunk_id context matched multiple locations in $target; add more unchanged context or pass --allow-loose after manual review") if $count > 1 && $allow_loose ne "1"; my $prefix = substr($old, 0, $pos); my $suffix = substr($old, $pos + length($search)); my $line = ($prefix =~ tr/\n//) + 1; print STDERR "apply_patch: hunk $hunk_id matched $target:$line\n"; $new = $prefix . $replacement . $suffix; } open my $ofh, ">", $out or fail("failed to render patched file"); binmode $ofh; print $ofh $new or fail("failed to render patched file"); close $ofh or fail("failed to render patched file"); ' "$@" } replace_once() { target=$1 search_file=$2 replace_file=$3 hunk_id=$4 [ -e "$target" ] || die "file not found: $target" fast_out=$(mk_temp) if replace_once_with_perl "$target" "$search_file" "$replace_file" "$hunk_id" "$allow_loose" "$fast_out"; then write_file "$target" "$fast_out" rm -f "$fast_out" return 0 else status=$? fi rm -f "$fast_out" [ "$status" = 127 ] || exit "$status" marker="__UNIDESK_APPLY_PATCH_EOF_$$__" old=$(cat "$target"; printf '%s' "$marker") || die "failed to read $target" old=${old%"$marker"} search=$(cat "$search_file"; printf '%s' "$marker") || die "failed to read hunk search" search=${search%"$marker"} replacement=$(cat "$replace_file"; printf '%s' "$marker") || die "failed to read hunk replacement" replacement=${replacement%"$marker"} if [ -z "$search" ]; then [ "$allow_loose" = 1 ] || die "hunk $hunk_id in $target has no context; add unchanged/deleted anchor lines or pass --allow-loose after manual review" printf 'apply_patch: hunk %s matched %s:1 (loose)\n' "$hunk_id" "$target" >&2 new=$replacement$old else case "$old" in *"$search"*) prefix=${old%%"$search"*} suffix=${old#*"$search"} case "$suffix" in *"$search"*) [ "$allow_loose" = 1 ] || die "hunk $hunk_id context matched multiple locations in $target; add more unchanged context or pass --allow-loose after manual review" ;; esac printf 'apply_patch: hunk %s matched %s:%s\n' "$hunk_id" "$target" "$(line_number_for_prefix "$prefix")" >&2 new=$prefix$replacement$suffix ;; *) die "hunk $hunk_id context not found in $target" ;; esac fi out=$(mk_temp) printf '%s' "$new" > "$out" || die "failed to render patched file" write_file "$target" "$out" rm -f "$out" } apply_update() { target=$1 body=$2 in_hunk=0 search_file= replace_file= changed=0 hunk_number=0 has_add=0 has_delete=0 seen_add=0 anchor_before_add=0 anchor_after_add=0 explicit_eof=0 finish_hunk() { [ "$in_hunk" = 1 ] || return 0 if [ "$allow_loose" != 1 ]; then [ -s "$search_file" ] || die "hunk $hunk_number in $target has no context; add unchanged/deleted anchor lines or pass --allow-loose after manual review" if [ "$has_add" = 1 ] && [ "$has_delete" = 0 ]; then if [ "$anchor_before_add" != 1 ] || { [ "$anchor_after_add" != 1 ] && [ "$explicit_eof" != 1 ]; }; then die "hunk $hunk_number in $target is insert-only without both leading and trailing context; include nearby unchanged lines after the insertion or pass --allow-loose after manual review" fi fi fi replace_once "$target" "$search_file" "$replace_file" "$hunk_number" rm -f "$search_file" "$replace_file" search_file= replace_file= in_hunk=0 changed=1 } while IFS= read -r line || [ -n "$line" ]; do case "$line" in "*** End of File"*) continue ;; "@@"*) finish_hunk search_file=$(mk_temp) replace_file=$(mk_temp) hunk_number=$((hunk_number + 1)) has_add=0 has_delete=0 seen_add=0 anchor_before_add=0 anchor_after_add=0 explicit_eof=0 in_hunk=1 continue ;; esac [ "$in_hunk" = 1 ] || die "expected hunk header in $target" case "$line" in " "*) text=${line#?} printf '%s\n' "$text" >> "$search_file" printf '%s\n' "$text" >> "$replace_file" if [ "$seen_add" = 1 ]; then anchor_after_add=1; else anchor_before_add=1; fi ;; "-"*) text=${line#?} printf '%s\n' "$text" >> "$search_file" has_delete=1 if [ "$seen_add" = 1 ]; then anchor_after_add=1; else anchor_before_add=1; fi ;; "+"*) text=${line#?} printf '%s\n' "$text" >> "$replace_file" has_add=1 seen_add=1 ;; "\\ No newline at end of file") ;; "*** End of File"*) explicit_eof=1 ;; *) die "bad hunk line in $target: $line" ;; esac done < "$body" finish_hunk [ "$changed" = 1 ] || [ -e "$target" ] || die "file not found: $target" } is_top_header() { case "$1" in "*** Add File: "*|"*** Update File: "*|"*** Delete File: "*|"*** End Patch") return 0 ;; *) return 1 ;; esac } pushed=0 pushed_line= next_patch_line() { if [ "$pushed" = 1 ]; then line=$pushed_line pushed=0 pushed_line= return 0 fi if IFS= read -r line; then return 0 fi [ -n "${line:-}" ] } push_patch_line() { pushed_line=$1 pushed=1 } parse_add_file() { target=$1 [ ! -e "$target" ] || die "file already exists: $target" tmp=$(mk_temp) while next_patch_line; do if is_top_header "$line"; then push_patch_line "$line" break fi case "$line" in "+"*) printf '%s\n' "${line#?}" >> "$tmp" ;; *) rm -f "$tmp" die "add file lines must start with + for $target" ;; esac done write_file "$target" "$tmp" rm -f "$tmp" } parse_update_file() { target=$1 move_to= body=$(mk_temp) if next_patch_line; then case "$line" in "*** Move to: "*) move_to=${line#"*** Move to: "} ;; *) push_patch_line "$line" ;; esac fi while next_patch_line; do if is_top_header "$line"; then push_patch_line "$line" break fi case "$line" in "*** Move to: "*) rm -f "$body" die "move marker must appear before update hunks" ;; *) printf '%s\n' "$line" >> "$body" ;; esac done if [ -s "$body" ]; then apply_update "$target" "$body" elif [ -z "$move_to" ] && [ ! -e "$target" ]; then rm -f "$body" die "file not found: $target" fi rm -f "$body" if [ -n "$move_to" ]; then [ -e "$target" ] || die "file not found: $target" [ ! -e "$move_to" ] || die "target file already exists: $move_to" ensure_parent "$move_to" mv "$target" "$move_to" || die "failed to move $target to $move_to" fi } main() { while [ "$#" -gt 0 ]; do case "$1" in -h|--help) printf 'apply_patch: sh-only helper; reads *** Begin Patch format from stdin; --allow-loose bypasses low-context hunk guards\n' return 0 ;; --allow-loose) allow_loose=1 shift ;; *) die "unsupported option: $1" ;; esac done next_patch_line || die "patch must start with *** Begin Patch" [ "$line" = "*** Begin Patch" ] || die "patch must start with *** Begin Patch" while next_patch_line; do case "$line" in "*** End Patch") printf 'Done!\n' return 0 ;; "*** Add File: "*) parse_add_file "${line#"*** Add File: "}" ;; "*** Delete File: "*) target=${line#"*** Delete File: "} rm -f "$target" || die "failed to delete $target" ;; "*** Update File: "*) parse_update_file "${line#"*** Update File: "}" ;; *) die "unexpected patch line: $line" ;; esac done die "missing *** End Patch" } main "$@"