Home | History | Annotate | Download | only in posix
      1 /* patch.c - Apply a "universal" diff.
      2  *
      3  * Copyright 2007 Rob Landley <rob (at) landley.net>
      4  *
      5  * see http://opengroup.org/onlinepubs/9699919799/utilities/patch.html
      6  * (But only does -u, because who still cares about "ed"?)
      7  *
      8  * TODO:
      9  * -b backup
     10  * -N ignore already applied
     11  * -d chdir first
     12  * -D define wrap #ifdef and #ifndef around changes
     13  * -o outfile output here instead of in place
     14  * -r rejectfile write rejected hunks to this file
     15  *
     16  * -E remove empty files --remove-empty-files
     17  * -f force (no questions asked)
     18  * -F fuzz (number, default 2)
     19  * [file] which file to patch
     20 
     21 USE_PATCH(NEWTOY(patch, USE_TOYBOX_DEBUG("x")"ulp#i:R", TOYFLAG_USR|TOYFLAG_BIN))
     22 
     23 config PATCH
     24   bool "patch"
     25   default y
     26   help
     27     usage: patch [-i file] [-p depth] [-Ru]
     28 
     29     Apply a unified diff to one or more files.
     30 
     31     -i	Input file (defaults=stdin)
     32     -l	Loose match (ignore whitespace)
     33     -p	Number of '/' to strip from start of file paths (default=all)
     34     -R	Reverse patch.
     35     -u	Ignored (only handles "unified" diffs)
     36 
     37     This version of patch only handles unified diffs, and only modifies
     38     a file when all all hunks to that file apply.  Patch prints failed
     39     hunks to stderr, and exits with nonzero status if any hunks fail.
     40 
     41     A file compared against /dev/null (or with a date <= the epoch) is
     42     created/deleted as appropriate.
     43 */
     44 
     45 #define FOR_patch
     46 #include "toys.h"
     47 
     48 GLOBALS(
     49   char *infile;
     50   long prefix;
     51 
     52   struct double_list *current_hunk;
     53   long oldline, oldlen, newline, newlen;
     54   long linenum;
     55   int context, state, filein, fileout, filepatch, hunknum;
     56   char *tempname;
     57 )
     58 
     59 // Dispose of a line of input, either by writing it out or discarding it.
     60 
     61 // state < 2: just free
     62 // state = 2: write whole line to stderr
     63 // state = 3: write whole line to fileout
     64 // state > 3: write line+1 to fileout when *line != state
     65 
     66 static void do_line(void *data)
     67 {
     68   struct double_list *dlist = (struct double_list *)data;
     69 
     70   if (TT.state>1 && *dlist->data != TT.state) {
     71     char *s = dlist->data+(TT.state>3 ? 1 : 0);
     72     int i = TT.state == 2 ? 2 : TT.fileout;
     73 
     74     xwrite(i, s, strlen(s));
     75     xwrite(i, "\n", 1);
     76   }
     77 
     78   if (toys.optflags & FLAG_x)
     79     fprintf(stderr, "DO %d: %s\n", TT.state, dlist->data);
     80 
     81   free(dlist->data);
     82   free(data);
     83 }
     84 
     85 static void finish_oldfile(void)
     86 {
     87   if (TT.tempname) replace_tempfile(TT.filein, TT.fileout, &TT.tempname);
     88   TT.fileout = TT.filein = -1;
     89 }
     90 
     91 static void fail_hunk(void)
     92 {
     93   if (!TT.current_hunk) return;
     94 
     95   fprintf(stderr, "Hunk %d FAILED %ld/%ld.\n",
     96       TT.hunknum, TT.oldline, TT.newline);
     97   toys.exitval = 1;
     98 
     99   // If we got to this point, we've seeked to the end.  Discard changes to
    100   // this file and advance to next file.
    101 
    102   TT.state = 2;
    103   llist_traverse(TT.current_hunk, do_line);
    104   TT.current_hunk = NULL;
    105   delete_tempfile(TT.filein, TT.fileout, &TT.tempname);
    106   TT.state = 0;
    107 }
    108 
    109 // Compare ignoring whitespace. Just returns 0/1, no > or <
    110 static int loosecmp(char *aa, char *bb)
    111 {
    112   int a = 0, b = 0;
    113 
    114   for (;;) {
    115     while (isspace(aa[a])) a++;
    116     while (isspace(bb[b])) b++;
    117     if (aa[a] != bb[b]) return 1;
    118     if (!aa[a]) return 0;
    119     a++, b++;
    120   }
    121 }
    122 
    123 // Given a hunk of a unified diff, make the appropriate change to the file.
    124 // This does not use the location information, but instead treats a hunk
    125 // as a sort of regex.  Copies data from input to output until it finds
    126 // the change to be made, then outputs the changed data and returns.
    127 // (Finding EOF first is an error.)  This is a single pass operation, so
    128 // multiple hunks must occur in order in the file.
    129 
    130 static int apply_one_hunk(void)
    131 {
    132   struct double_list *plist, *buf = NULL, *check;
    133   int matcheof, trailing = 0, reverse = toys.optflags & FLAG_R, backwarn = 0;
    134   int (*lcmp)(char *aa, char *bb);
    135 
    136   lcmp = (toys.optflags & FLAG_l) ? (void *)loosecmp : (void *)strcmp;
    137   dlist_terminate(TT.current_hunk);
    138 
    139   // Match EOF if there aren't as many ending context lines as beginning
    140   for (plist = TT.current_hunk; plist; plist = plist->next) {
    141     if (plist->data[0]==' ') trailing++;
    142     else trailing = 0;
    143     if (toys.optflags & FLAG_x) fprintf(stderr, "HUNK:%s\n", plist->data);
    144   }
    145   matcheof = !trailing || trailing < TT.context;
    146 
    147   if (toys.optflags & FLAG_x)
    148     fprintf(stderr,"MATCHEOF=%c\n", matcheof ? 'Y' : 'N');
    149 
    150   // Loop through input data searching for this hunk.  Match all context
    151   // lines and all lines to be removed until we've found the end of a
    152   // complete hunk.
    153   plist = TT.current_hunk;
    154   buf = NULL;
    155 
    156   for (;;) {
    157     char *data = get_line(TT.filein);
    158 
    159     TT.linenum++;
    160     // Figure out which line of hunk to compare with next.  (Skip lines
    161     // of the hunk we'd be adding.)
    162     while (plist && *plist->data == "+-"[reverse]) {
    163       if (data && !lcmp(data, plist->data+1)) {
    164         if (!backwarn) backwarn = TT.linenum;
    165       }
    166       plist = plist->next;
    167     }
    168 
    169     // Is this EOF?
    170     if (!data) {
    171       if (toys.optflags & FLAG_x) fprintf(stderr, "INEOF\n");
    172 
    173       // Does this hunk need to match EOF?
    174       if (!plist && matcheof) break;
    175 
    176       if (backwarn)
    177         fprintf(stderr, "Possibly reversed hunk %d at %ld\n",
    178             TT.hunknum, TT.linenum);
    179 
    180       // File ended before we found a place for this hunk.
    181       fail_hunk();
    182       goto done;
    183     } else if (toys.optflags & FLAG_x) fprintf(stderr, "IN: %s\n", data);
    184     check = dlist_add(&buf, data);
    185 
    186     // Compare this line with next expected line of hunk.
    187 
    188     // A match can fail because the next line doesn't match, or because
    189     // we hit the end of a hunk that needed EOF, and this isn't EOF.
    190 
    191     // If match failed, flush first line of buffered data and
    192     // recheck buffered data for a new match until we find one or run
    193     // out of buffer.
    194 
    195     for (;;) {
    196       if (!plist || lcmp(check->data, plist->data+1)) {
    197         // Match failed.  Write out first line of buffered data and
    198         // recheck remaining buffered data for a new match.
    199 
    200         if (toys.optflags & FLAG_x) {
    201           int bug = 0;
    202 
    203           if (!plist) fprintf(stderr, "NULL plist\n");
    204           else {
    205             while (plist->data[bug] == check->data[bug]) bug++;
    206             fprintf(stderr, "NOT(%d:%d!=%d): %s\n", bug, plist->data[bug],
    207               check->data[bug], plist->data);
    208           }
    209         }
    210 
    211         // If this hunk must match start of file, fail if it didn't.
    212         if (!TT.context || trailing>TT.context) {
    213           fail_hunk();
    214           goto done;
    215         }
    216 
    217         TT.state = 3;
    218         do_line(check = dlist_pop(&buf));
    219         plist = TT.current_hunk;
    220 
    221         // If we've reached the end of the buffer without confirming a
    222         // match, read more lines.
    223         if (!buf) break;
    224         check = buf;
    225       } else {
    226         if (toys.optflags & FLAG_x) fprintf(stderr, "MAYBE: %s\n", plist->data);
    227         // This line matches.  Advance plist, detect successful match.
    228         plist = plist->next;
    229         if (!plist && !matcheof) goto out;
    230         check = check->next;
    231         if (check == buf) break;
    232       }
    233     }
    234   }
    235 out:
    236   // We have a match.  Emit changed data.
    237   TT.state = "-+"[reverse];
    238   llist_traverse(TT.current_hunk, do_line);
    239   TT.current_hunk = NULL;
    240   TT.state = 1;
    241 done:
    242   if (buf) {
    243     dlist_terminate(buf);
    244     llist_traverse(buf, do_line);
    245   }
    246 
    247   return TT.state;
    248 }
    249 
    250 // Read a patch file and find hunks, opening/creating/deleting files.
    251 // Call apply_one_hunk() on each hunk.
    252 
    253 // state 0: Not in a hunk, look for +++.
    254 // state 1: Found +++ file indicator, look for @@
    255 // state 2: In hunk: counting initial context lines
    256 // state 3: In hunk: getting body
    257 
    258 void patch_main(void)
    259 {
    260   int reverse = toys.optflags&FLAG_R, state = 0, patchlinenum = 0,
    261     strip = 0;
    262   char *oldname = NULL, *newname = NULL;
    263 
    264   if (TT.infile) TT.filepatch = xopen(TT.infile, O_RDONLY);
    265   TT.filein = TT.fileout = -1;
    266 
    267   // Loop through the lines in the patch
    268   for (;;) {
    269     char *patchline;
    270 
    271     patchline = get_line(TT.filepatch);
    272     if (!patchline) break;
    273 
    274     // Other versions of patch accept damaged patches,
    275     // so we need to also.
    276     if (strip || !patchlinenum++) {
    277       int len = strlen(patchline);
    278       if (patchline[len-1] == '\r') {
    279         if (!strip) fprintf(stderr, "Removing DOS newlines\n");
    280         strip = 1;
    281         patchline[len-1]=0;
    282       }
    283     }
    284     if (!*patchline) {
    285       free(patchline);
    286       patchline = xstrdup(" ");
    287     }
    288 
    289     // Are we assembling a hunk?
    290     if (state >= 2) {
    291       if (*patchline==' ' || *patchline=='+' || *patchline=='-') {
    292         dlist_add(&TT.current_hunk, patchline);
    293 
    294         if (*patchline != '+') TT.oldlen--;
    295         if (*patchline != '-') TT.newlen--;
    296 
    297         // Context line?
    298         if (*patchline==' ' && state==2) TT.context++;
    299         else state=3;
    300 
    301         // If we've consumed all expected hunk lines, apply the hunk.
    302 
    303         if (!TT.oldlen && !TT.newlen) state = apply_one_hunk();
    304         continue;
    305       }
    306       dlist_terminate(TT.current_hunk);
    307       fail_hunk();
    308       state = 0;
    309       continue;
    310     }
    311 
    312     // Open a new file?
    313     if (!strncmp("--- ", patchline, 4) || !strncmp("+++ ", patchline, 4)) {
    314       char *s, **name = &oldname;
    315       int i;
    316 
    317       if (*patchline == '+') {
    318         name = &newname;
    319         state = 1;
    320       }
    321 
    322       free(*name);
    323       finish_oldfile();
    324 
    325       // Trim date from end of filename (if any).  We don't care.
    326       for (s = patchline+4; *s && *s!='\t'; s++)
    327         if (*s=='\\' && s[1]) s++;
    328       i = atoi(s);
    329       if (i>1900 && i<=1970) *name = xstrdup("/dev/null");
    330       else {
    331         *s = 0;
    332         *name = xstrdup(patchline+4);
    333       }
    334 
    335       // We defer actually opening the file because svn produces broken
    336       // patches that don't signal they want to create a new file the
    337       // way the patch man page says, so you have to read the first hunk
    338       // and _guess_.
    339 
    340     // Start a new hunk?  Usually @@ -oldline,oldlen +newline,newlen @@
    341     // but a missing ,value means the value is 1.
    342     } else if (state == 1 && !strncmp("@@ -", patchline, 4)) {
    343       int i;
    344       char *s = patchline+4;
    345 
    346       // Read oldline[,oldlen] +newline[,newlen]
    347 
    348       TT.oldlen = TT.newlen = 1;
    349       TT.oldline = strtol(s, &s, 10);
    350       if (*s == ',') TT.oldlen=strtol(s+1, &s, 10);
    351       TT.newline = strtol(s+2, &s, 10);
    352       if (*s == ',') TT.newlen = strtol(s+1, &s, 10);
    353 
    354       TT.context = 0;
    355       state = 2;
    356 
    357       // If this is the first hunk, open the file.
    358       if (TT.filein == -1) {
    359         int oldsum, newsum, del = 0;
    360         char *name;
    361 
    362         oldsum = TT.oldline + TT.oldlen;
    363         newsum = TT.newline + TT.newlen;
    364 
    365         name = reverse ? oldname : newname;
    366 
    367         // We're deleting oldname if new file is /dev/null (before -p)
    368         // or if new hunk is empty (zero context) after patching
    369         if (!strcmp(name, "/dev/null") || !(reverse ? oldsum : newsum))
    370         {
    371           name = reverse ? newname : oldname;
    372           del++;
    373         }
    374 
    375         // handle -p path truncation.
    376         for (i = 0, s = name; *s;) {
    377           if ((toys.optflags & FLAG_p) && TT.prefix == i) break;
    378           if (*s++ != '/') continue;
    379           while (*s == '/') s++;
    380           name = s;
    381           i++;
    382         }
    383 
    384         if (del) {
    385           printf("removing %s\n", name);
    386           xunlink(name);
    387           state = 0;
    388         // If we've got a file to open, do so.
    389         } else if (!(toys.optflags & FLAG_p) || i <= TT.prefix) {
    390           // If the old file was null, we're creating a new one.
    391           if ((!strcmp(oldname, "/dev/null") || !oldsum) && access(name, F_OK))
    392           {
    393             printf("creating %s\n", name);
    394             if (mkpathat(AT_FDCWD, name, 0, 2))
    395               perror_exit("mkpath %s", name);
    396             TT.filein = xcreate(name, O_CREAT|O_EXCL|O_RDWR, 0666);
    397           } else {
    398             printf("patching %s\n", name);
    399             TT.filein = xopen(name, O_RDONLY);
    400           }
    401           TT.fileout = copy_tempfile(TT.filein, name, &TT.tempname);
    402           TT.linenum = 0;
    403           TT.hunknum = 0;
    404         }
    405       }
    406 
    407       TT.hunknum++;
    408 
    409       continue;
    410     }
    411 
    412     // If we didn't continue above, discard this line.
    413     free(patchline);
    414   }
    415 
    416   finish_oldfile();
    417 
    418   if (CFG_TOYBOX_FREE) {
    419     close(TT.filepatch);
    420     free(oldname);
    421     free(newname);
    422   }
    423 }
    424