/* vi: set sw=4 ts=4: * * Apply a "universal" diff. * Adapted from toybox's patch implementation. * * Copyright 2007 Rob Landley <rob@landley.net> * * see http://www.opengroup.org/onlinepubs/009695399/utilities/patch.html * (But only does -u, because who still cares about "ed"?) * * TODO: * -b backup * -l treat all whitespace as a single space * -d chdir first * -D define wrap #ifdef and #ifndef around changes * -o outfile output here instead of in place * -r rejectfile write rejected hunks to this file * --dry-run (regression!) * * -f force (no questions asked) * -F fuzz (number, default 2) * [file] which file to patch */ //config:config PATCH //config: bool "patch" //config: default y //config: help //config: Apply a unified diff formatted patch. //applet:IF_PATCH(APPLET(patch, BB_DIR_USR_BIN, BB_SUID_DROP)) //kbuild:lib-$(CONFIG_PATCH) += patch.o //usage:#define patch_trivial_usage //usage: "[OPTIONS] [ORIGFILE [PATCHFILE]]" //usage:#define patch_full_usage "\n\n" //usage: IF_LONG_OPTS( //usage: " -p,--strip N Strip N leading components from file names" //usage: "\n -i,--input DIFF Read DIFF instead of stdin" //usage: "\n -R,--reverse Reverse patch" //usage: "\n -N,--forward Ignore already applied patches" /*usage: "\n --dry-run Don't actually change files" - TODO */ //usage: "\n -E,--remove-empty-files Remove output files if they become empty" //usage: ) //usage: IF_NOT_LONG_OPTS( //usage: " -p N Strip N leading components from file names" //usage: "\n -i DIFF Read DIFF instead of stdin" //usage: "\n -R Reverse patch" //usage: "\n -N Ignore already applied patches" //usage: "\n -E Remove output files if they become empty" //usage: ) /* -u "interpret as unified diff" is supported but not documented: this info is not useful for --help */ /* -x "debug" is supported but does nothing */ //usage: //usage:#define patch_example_usage //usage: "$ patch -p1 < example.diff\n" //usage: "$ patch -p0 -i example.diff" #include "libbb.h" // libbb candidate? struct double_list { struct double_list *next; struct double_list *prev; char *data; }; // Free all the elements of a linked list // Call freeit() on each element before freeing it. static void dlist_free(struct double_list *list, void (*freeit)(void *data)) { while (list) { void *pop = list; list = list->next; freeit(pop); // Bail out also if list is circular. if (list == pop) break; } } // Add an entry before "list" element in (circular) doubly linked list static struct double_list *dlist_add(struct double_list **list, char *data) { struct double_list *llist; struct double_list *line = xmalloc(sizeof(*line)); line->data = data; llist = *list; if (llist) { struct double_list *p; line->next = llist; p = line->prev = llist->prev; // (list is circular, we assume p is never NULL) p->next = line; llist->prev = line; } else *list = line->next = line->prev = line; return line; } struct globals { char *infile; long prefix; struct double_list *current_hunk; long oldline, oldlen, newline, newlen; long linenum; int context, state, hunknum; int filein, fileout; char *tempname; int exitval; }; #define TT (*ptr_to_globals) #define INIT_TT() do { \ SET_PTR_TO_GLOBALS(xzalloc(sizeof(TT))); \ } while (0) #define FLAG_STR "Rup:i:NEx" /* FLAG_REVERSE must be == 1! Code uses this fact. */ #define FLAG_REVERSE (1 << 0) #define FLAG_u (1 << 1) #define FLAG_PATHLEN (1 << 2) #define FLAG_INPUT (1 << 3) #define FLAG_IGNORE (1 << 4) #define FLAG_RMEMPTY (1 << 5) /* Enable this bit and use -x for debug output: */ #define FLAG_DEBUG (0 << 6) // Dispose of a line of input, either by writing it out or discarding it. // state < 2: just free // state = 2: write whole line to stderr // state = 3: write whole line to fileout // state > 3: write line+1 to fileout when *line != state #define PATCH_DEBUG (option_mask32 & FLAG_DEBUG) static void do_line(void *data) { struct double_list *dlist = data; if (TT.state>1 && *dlist->data != TT.state) fdprintf(TT.state == 2 ? 2 : TT.fileout, "%s\n", dlist->data+(TT.state>3 ? 1 : 0)); if (PATCH_DEBUG) fdprintf(2, "DO %d: %s\n", TT.state, dlist->data); free(dlist->data); free(dlist); } static void finish_oldfile(void) { if (TT.tempname) { // Copy the rest of the data and replace the original with the copy. char *temp; if (TT.filein != -1) { bb_copyfd_eof(TT.filein, TT.fileout); xclose(TT.filein); } xclose(TT.fileout); temp = xstrdup(TT.tempname); temp[strlen(temp) - 6] = '\0'; rename(TT.tempname, temp); free(temp); free(TT.tempname); TT.tempname = NULL; } TT.fileout = TT.filein = -1; } static void fail_hunk(void) { if (!TT.current_hunk) return; fdprintf(2, "Hunk %d FAILED %ld/%ld.\n", TT.hunknum, TT.oldline, TT.newline); TT.exitval = 1; // If we got to this point, we've seeked to the end. Discard changes to // this file and advance to next file. TT.state = 2; TT.current_hunk->prev->next = NULL; dlist_free(TT.current_hunk, do_line); TT.current_hunk = NULL; // Abort the copy and delete the temporary file. close(TT.filein); close(TT.fileout); unlink(TT.tempname); free(TT.tempname); TT.tempname = NULL; TT.state = 0; } // Given a hunk of a unified diff, make the appropriate change to the file. // This does not use the location information, but instead treats a hunk // as a sort of regex. Copies data from input to output until it finds // the change to be made, then outputs the changed data and returns. // (Finding EOF first is an error.) This is a single pass operation, so // multiple hunks must occur in order in the file. static int apply_one_hunk(void) { struct double_list *plist, *buf = NULL, *check; int matcheof = 0, reverse = option_mask32 & FLAG_REVERSE, backwarn = 0; /* Do we try "dummy" revert to check whether * to silently skip this hunk? Used to implement -N. */ int dummy_revert = 0; // Break doubly linked list so we can use singly linked traversal function. TT.current_hunk->prev->next = NULL; // Match EOF if there aren't as many ending context lines as beginning for (plist = TT.current_hunk; plist; plist = plist->next) { if (plist->data[0]==' ') matcheof++; else matcheof = 0; if (PATCH_DEBUG) fdprintf(2, "HUNK:%s\n", plist->data); } matcheof = !matcheof || matcheof < TT.context; if (PATCH_DEBUG) fdprintf(2,"MATCHEOF=%c\n", matcheof ? 'Y' : 'N'); // Loop through input data searching for this hunk. Match all context // lines and all lines to be removed until we've found the end of a // complete hunk. plist = TT.current_hunk; buf = NULL; if (reverse ? TT.oldlen : TT.newlen) for (;;) { char *data = xmalloc_reads(TT.filein, NULL); TT.linenum++; // Figure out which line of hunk to compare with next. (Skip lines // of the hunk we'd be adding.) while (plist && *plist->data == "+-"[reverse]) { if (data && !strcmp(data, plist->data+1)) { if (!backwarn) { backwarn = TT.linenum; if (option_mask32 & FLAG_IGNORE) { dummy_revert = 1; reverse ^= 1; continue; } } } plist = plist->next; } // Is this EOF? if (!data) { if (PATCH_DEBUG) fdprintf(2, "INEOF\n"); // Does this hunk need to match EOF? if (!plist && matcheof) break; if (backwarn) fdprintf(2,"Possibly reversed hunk %d at %ld\n", TT.hunknum, TT.linenum); // File ended before we found a place for this hunk. fail_hunk(); goto done; } if (PATCH_DEBUG) fdprintf(2, "IN: %s\n", data); check = dlist_add(&buf, data); // Compare this line with next expected line of hunk. // todo: teach the strcmp() to ignore whitespace. // A match can fail because the next line doesn't match, or because // we hit the end of a hunk that needed EOF, and this isn't EOF. // If match failed, flush first line of buffered data and // recheck buffered data for a new match until we find one or run // out of buffer. for (;;) { if (!plist || strcmp(check->data, plist->data+1)) { // Match failed. Write out first line of buffered data and // recheck remaining buffered data for a new match. if (PATCH_DEBUG) fdprintf(2, "NOT: %s\n", plist->data); TT.state = 3; check = buf; buf = buf->next; check->prev->next = buf; buf->prev = check->prev; do_line(check); plist = TT.current_hunk; // If we've reached the end of the buffer without confirming a // match, read more lines. if (check == buf) { buf = NULL; break; } check = buf; } else { if (PATCH_DEBUG) fdprintf(2, "MAYBE: %s\n", plist->data); // This line matches. Advance plist, detect successful match. plist = plist->next; if (!plist && !matcheof) goto out; check = check->next; if (check == buf) break; } } } out: // We have a match. Emit changed data. TT.state = "-+"[reverse ^ dummy_revert]; dlist_free(TT.current_hunk, do_line); TT.current_hunk = NULL; TT.state = 1; done: if (buf) { buf->prev->next = NULL; dlist_free(buf, do_line); } return TT.state; } // Read a patch file and find hunks, opening/creating/deleting files. // Call apply_one_hunk() on each hunk. // state 0: Not in a hunk, look for +++. // state 1: Found +++ file indicator, look for @@ // state 2: In hunk: counting initial context lines // state 3: In hunk: getting body int patch_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; int patch_main(int argc UNUSED_PARAM, char **argv) { int opts; int reverse, state = 0; char *oldname = NULL, *newname = NULL; char *opt_p, *opt_i; long oldlen = oldlen; /* for compiler */ long newlen = newlen; /* for compiler */ INIT_TT(); opts = getopt32(argv, FLAG_STR, &opt_p, &opt_i); argv += optind; reverse = opts & FLAG_REVERSE; TT.prefix = (opts & FLAG_PATHLEN) ? xatoi(opt_p) : 0; // can be negative! TT.filein = TT.fileout = -1; if (opts & FLAG_INPUT) { xmove_fd(xopen_stdin(opt_i), STDIN_FILENO); } else { if (argv[0] && argv[1]) { xmove_fd(xopen_stdin(argv[1]), STDIN_FILENO); } } if (argv[0]) { oldname = xstrdup(argv[0]); newname = xstrdup(argv[0]); } // Loop through the lines in the patch for(;;) { char *patchline; patchline = xmalloc_fgetline(stdin); if (!patchline) break; // Other versions of patch accept damaged patches, // so we need to also. if (!*patchline) { free(patchline); patchline = xstrdup(" "); } // Are we assembling a hunk? if (state >= 2) { if (*patchline==' ' || *patchline=='+' || *patchline=='-') { dlist_add(&TT.current_hunk, patchline); if (*patchline != '+') oldlen--; if (*patchline != '-') newlen--; // Context line? if (*patchline==' ' && state==2) TT.context++; else state=3; // If we've consumed all expected hunk lines, apply the hunk. if (!oldlen && !newlen) state = apply_one_hunk(); continue; } fail_hunk(); state = 0; continue; } // Open a new file? if (!strncmp("--- ", patchline, 4) || !strncmp("+++ ", patchline, 4)) { char *s, **name = reverse ? &newname : &oldname; int i; if (*patchline == '+') { name = reverse ? &oldname : &newname; state = 1; } finish_oldfile(); if (!argv[0]) { free(*name); // Trim date from end of filename (if any). We don't care. for (s = patchline+4; *s && *s!='\t'; s++) if (*s=='\\' && s[1]) s++; i = atoi(s); if (i>1900 && i<=1970) *name = xstrdup("/dev/null"); else { *s = 0; *name = xstrdup(patchline+4); } } // We defer actually opening the file because svn produces broken // patches that don't signal they want to create a new file the // way the patch man page says, so you have to read the first hunk // and _guess_. // Start a new hunk? Usually @@ -oldline,oldlen +newline,newlen @@ // but a missing ,value means the value is 1. } else if (state == 1 && !strncmp("@@ -", patchline, 4)) { int i; char *s = patchline+4; // Read oldline[,oldlen] +newline[,newlen] TT.oldlen = oldlen = TT.newlen = newlen = 1; TT.oldline = strtol(s, &s, 10); if (*s == ',') TT.oldlen = oldlen = strtol(s+1, &s, 10); TT.newline = strtol(s+2, &s, 10); if (*s == ',') TT.newlen = newlen = strtol(s+1, &s, 10); if (oldlen < 1 && newlen < 1) bb_error_msg_and_die("Really? %s", patchline); TT.context = 0; state = 2; // If this is the first hunk, open the file. if (TT.filein == -1) { int oldsum, newsum, empty = 0; char *name; oldsum = TT.oldline + oldlen; newsum = TT.newline + newlen; name = reverse ? oldname : newname; // We're deleting oldname if new file is /dev/null (before -p) // or if new hunk is empty (zero context) after patching if (!strcmp(name, "/dev/null") || !(reverse ? oldsum : newsum)) { name = reverse ? newname : oldname; empty++; } // handle -p path truncation. for (i = 0, s = name; *s;) { if ((option_mask32 & FLAG_PATHLEN) && TT.prefix == i) break; if (*s++ != '/') continue; while (*s == '/') s++; i++; name = s; } if (empty) { // File is empty after the patches have been applied state = 0; if (option_mask32 & FLAG_RMEMPTY) { // If flag -E or --remove-empty-files is set printf("removing %s\n", name); xunlink(name); } else { printf("patching file %s\n", name); xclose(xopen(name, O_WRONLY | O_TRUNC)); } // If we've got a file to open, do so. } else if (!(option_mask32 & FLAG_PATHLEN) || i <= TT.prefix) { struct stat statbuf; // If the old file was null, we're creating a new one. if (!strcmp(oldname, "/dev/null") || !oldsum) { printf("creating %s\n", name); s = strrchr(name, '/'); if (s) { *s = 0; bb_make_directory(name, -1, FILEUTILS_RECUR); *s = '/'; } TT.filein = xopen(name, O_CREAT|O_EXCL|O_RDWR); } else { printf("patching file %s\n", name); TT.filein = xopen(name, O_RDONLY); } TT.tempname = xasprintf("%sXXXXXX", name); TT.fileout = xmkstemp(TT.tempname); // Set permissions of output file fstat(TT.filein, &statbuf); fchmod(TT.fileout, statbuf.st_mode); TT.linenum = 0; TT.hunknum = 0; } } TT.hunknum++; continue; } // If we didn't continue above, discard this line. free(patchline); } finish_oldfile(); if (ENABLE_FEATURE_CLEAN_UP) { free(oldname); free(newname); } return TT.exitval; }