X-Git-Url: https://git.librecmc.org/?a=blobdiff_plain;f=editors%2Fpatch.c;h=a90252a0324a11252737dee93799115964eb6e72;hb=0bfb9c2cf0e8b711634014e06d39b5f47c5a95c2;hp=2a2b130b973cea81fbc77ffc806239ac1895ac46;hpb=cc2965fd236a85e5cac4fffa1c34057997780385;p=oweals%2Fbusybox.git diff --git a/editors/patch.c b/editors/patch.c index 2a2b130b9..a90252a03 100644 --- a/editors/patch.c +++ b/editors/patch.c @@ -1,238 +1,549 @@ -/* vi: set sw=4 ts=4: */ -/* - * busybox patch applet to handle the unified diff format. - * Copyright (C) 2003 Glenn McGrath +/* vi: set sw=4 ts=4: * - * Licensed under the GPL v2 or later, see the file LICENSE in this tarball. + * Apply a "universal" diff. + * Adapted from toybox's patch implementation. * - * This applet is written to work with patches generated by GNU diff, - * where there is equivalent functionality busybox patch shall behave - * as per GNU patch. + * Copyright 2007 Rob Landley * - * There is a SUSv3 specification for patch, however it looks to be - * incomplete, it doesnt even mention unified diff format. - * http://www.opengroup.org/onlinepubs/007904975/utilities/patch.html + * see http://www.opengroup.org/onlinepubs/009695399/utilities/patch.html + * (But only does -u, because who still cares about "ed"?) * - * Issues - * - Non-interactive - * - Patches must apply cleanly or patch (not just one hunk) will fail. - * - Reject file isnt saved + * 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 + * + * -f force (no questions asked) + * -F fuzz (number, default 2) + * [file] which file to patch */ +//applet:IF_PATCH(APPLET(patch, BB_DIR_USR_BIN, BB_SUID_DROP)) + +//kbuild:lib-$(CONFIG_PATCH) += patch.o + +//config:config PATCH +//config: bool "patch" +//config: default y +//config: help +//config: Apply a unified diff formatted patch. + +//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" +//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: ) +//usage: +//usage:#define patch_example_usage +//usage: "$ patch -p1 < example.diff\n" +//usage: "$ patch -p0 -i example.diff" + #include "libbb.h" -static unsigned copy_lines(FILE *src_stream, FILE *dest_stream, unsigned lines_count) + +// 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 (src_stream && lines_count) { - char *line; - line = xmalloc_fgets(src_stream); - if (line == NULL) { - break; - } - if (fputs(line, dest_stream) == EOF) { - bb_perror_msg_and_die("error writing to new file"); + 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) +//non-standard: +#define FLAG_DEBUG (1 << 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); } - free(line); - lines_count--; + xclose(TT.fileout); + + temp = xstrdup(TT.tempname); + temp[strlen(temp) - 6] = '\0'; + rename(TT.tempname, temp); + free(temp); + + free(TT.tempname); + TT.tempname = NULL; } - return lines_count; + TT.fileout = TT.filein = -1; } -/* If patch_level is -1 it will remove all directory names - * char *line must be greater than 4 chars - * returns NULL if the file doesnt exist or error - * returns malloc'ed filename - * NB: frees 1st argument! - */ -static char *extract_filename(char *line, int patch_level, const char *pat) +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) { - char *temp = NULL, *filename_start_ptr = line + 4; - - if (strncmp(line, pat, 4) == 0) { - /* Terminate string at end of source filename */ - line[strcspn(line,"\t\n\r")] = '\0'; - - /* Skip over (patch_level) number of leading directories */ - while (patch_level--) { - temp = strchr(filename_start_ptr, '/'); - if (!temp) - break; - filename_start_ptr = temp + 1; + 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 < 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, 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; + } } - temp = xstrdup(filename_start_ptr); } - free(line); - return temp; +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 ATTRIBUTE_UNUSED, char **argv) +int patch_main(int argc UNUSED_PARAM, char **argv) { - struct stat saved_stat; - char *patch_line; - FILE *patch_file; - int patch_level; - int ret = 0; - - xfunc_error_retval = 2; - { - const char *p = "-1"; - const char *i = "-"; /* compat */ - getopt32(argv, "p:i:", &p, &i); - patch_level = xatoi(p); /* can be negative! */ - patch_file = xfopen_stdin(i); + 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; - patch_line = xmalloc_getline(patch_file); - while (patch_line) { - FILE *src_stream; - FILE *dst_stream; - //char *old_filename; - char *new_filename; - char *backup_filename; - unsigned src_cur_line = 1; - unsigned dest_cur_line = 0; - unsigned dest_beg_line; - unsigned bad_hunk_count = 0; - unsigned hunk_count = 0; - smallint copy_trailing_lines_flag = 0; - - /* Skip everything upto the "---" marker - * No need to parse the lines "Only in ", and "diff " - */ - do { - /* Extract the filename used before the patch was generated */ - new_filename = extract_filename(patch_line, patch_level, "--- "); - // was old_filename above - patch_line = xmalloc_getline(patch_file); - if (!patch_line) goto quit; - } while (!new_filename); - free(new_filename); // "source" filename is irrelevant - - new_filename = extract_filename(patch_line, patch_level, "+++ "); - if (!new_filename) { - bb_error_msg_and_die("invalid patch"); + 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(" "); } - /* Get access rights from the file to be patched */ - if (stat(new_filename, &saved_stat) != 0) { - char *slash = strrchr(new_filename, '/'); - if (slash) { - /* Create leading directories */ - *slash = '\0'; - bb_make_directory(new_filename, -1, FILEUTILS_RECUR); - *slash = '/'; + // 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; } - backup_filename = NULL; - src_stream = NULL; - saved_stat.st_mode = 0644; - } else { - backup_filename = xasprintf("%s.orig", new_filename); - xrename(new_filename, backup_filename); - src_stream = xfopen(backup_filename, "r"); + fail_hunk(); + state = 0; + continue; } - dst_stream = xfopen(new_filename, "w"); - fchmod(fileno(dst_stream), saved_stat.st_mode); - - printf("patching file %s\n", new_filename); - - /* Handle all hunks for this file */ - patch_line = xmalloc_fgets(patch_file); - while (patch_line) { - unsigned count; - unsigned src_beg_line; - unsigned hunk_offset_start; - unsigned src_last_line = 1; - - if ((sscanf(patch_line, "@@ -%d,%d +%d", &src_beg_line, &src_last_line, &dest_beg_line) != 3) - && (sscanf(patch_line, "@@ -%d +%d", &src_beg_line, &dest_beg_line) != 2) - ) { /* No more hunks for this file */ - break; + + // 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; } - hunk_count++; - - if (src_beg_line && dest_beg_line) { - /* Copy unmodified lines upto start of hunk */ - /* src_beg_line will be 0 if it's a new file */ - count = src_beg_line - src_cur_line; - if (copy_lines(src_stream, dst_stream, count)) { - bb_error_msg_and_die("bad src file"); + + 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); } - src_cur_line += count; - dest_cur_line += count; - copy_trailing_lines_flag = 1; } - src_last_line += hunk_offset_start = src_cur_line; - - while (1) { - free(patch_line); - patch_line = xmalloc_fgets(patch_file); - if (patch_line == NULL) - break; /* EOF */ - if ((*patch_line != '-') && (*patch_line != '+') - && (*patch_line != ' ') - ) { - break; /* End of hunk */ + + // 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++; } - if (*patch_line != '+') { /* '-', ' ' or '\n' */ - char *src_line = NULL; - if (src_cur_line == src_last_line) - break; - if (src_stream) { - src_line = xmalloc_fgets(src_stream); - if (src_line) { - int diff = strcmp(src_line, patch_line + 1); - src_cur_line++; - free(src_line); - if (diff) src_line = NULL; - } + + // handle -p path truncation. + for (i=0, s = name; *s;) { + if ((option_mask32 & FLAG_PATHLEN) && TT.prefix == i) break; + if (*(s++)=='/') { + name = s; + i++; } - if (!src_line) { - bb_error_msg("hunk #%u FAILED at %u", hunk_count, hunk_offset_start); - bad_hunk_count++; - break; + } + + 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 (*patch_line == '-') { - continue; + // 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; } - fputs(patch_line + 1, dst_stream); - dest_cur_line++; - } /* end of while loop handling one hunk */ - } /* end of while loop handling one file */ - - /* Cleanup last patched file */ - if (copy_trailing_lines_flag) { - copy_lines(src_stream, dst_stream, (unsigned)(-1)); - } - if (src_stream) { - fclose(src_stream); - } - fclose(dst_stream); - if (bad_hunk_count) { - ret = 1; - bb_error_msg("%u out of %u hunk FAILED", bad_hunk_count, hunk_count); - } else { - /* It worked, we can remove the backup */ - if (backup_filename) { - unlink(backup_filename); - } - if ((dest_cur_line == 0) || (dest_beg_line == 0)) { - /* The new patched file is empty, remove it */ - xunlink(new_filename); - // /* old_filename and new_filename may be the same file */ - // unlink(old_filename); } + + TT.hunknum++; + + continue; } - free(backup_filename); - //free(old_filename); - free(new_filename); - } /* end of "while there are patch lines" */ -quit: - - /* 0 = SUCCESS - * 1 = Some hunks failed - * 2 = More serious problems (exited earlier) - */ - return ret; + + // 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; }