• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /* patch.c - Apply a "universal" diff.
2  *
3  * Copyright 2007 Rob Landley <rob@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 define wrap #ifdef and #ifndef around changes
12  * -o outfile output here instead of in place
13  * -r rejectfile write rejected hunks to this file
14  * -E remove empty files --remove-empty-files
15  * git syntax (rename, etc)
16 
17 USE_PATCH(NEWTOY(patch, ">2(no-backup-if-mismatch)(dry-run)F#g#fulp#v(verbose)@d:i:Rs(quiet)[!sv]", TOYFLAG_USR|TOYFLAG_BIN))
18 
19 config PATCH
20   bool "patch"
21   default y
22   help
23     usage: patch [-Rlsuv] [-d DIR] [-i FILE] [-p DEPTH] [-F FUZZ] [--dry-run] [FILE [PATCH]]
24 
25     Apply a unified diff to one or more files.
26 
27     -d	Modify files in DIR
28     -F	Fuzz factor (number of non-matching context lines allowed per hunk)
29     -i	Input patch from FILE (default=stdin)
30     -l	Loose match (ignore whitespace)
31     -p	Number of '/' to strip from start of file paths (default=all)
32     -R	Reverse patch
33     -s	Silent except for errors
34     -v	Verbose (-vv to see decisions)
35     --dry-run Don't change files, just confirm patch applies
36 
37     Only handles "unified" diff format (-u is assumed and ignored). Only
38     modifies files when all hunks to that file apply. Prints failed hunks
39     to stderr, and exits with nonzero status if any hunks fail.
40 
41     Files compared against /dev/null (or with a date <= the unix epoch) are
42     created/deleted as appropriate. Default -F value is the number of
43     leading/trailing context lines minus one (usually 2).
44 */
45 
46 #define FOR_patch
47 #include "toys.h"
48 
49 GLOBALS(
50   char *i, *d;
51   long v, p, g, F;
52 
53   void *current_hunk;
54   long oldline, oldlen, newline, newlen, linenum, outnum;
55   int context, state, filein, fileout, filepatch, hunknum;
56   char *tempname;
57 )
58 
59 // TODO xgetline() instead, but replace_tempfile() wants fd...
get_line(int fd)60 char *get_line(int fd)
61 {
62   char c, *buf = NULL;
63   long len = 0;
64 
65   for (;;) {
66     if (1>read(fd, &c, 1)) break;
67     if (!(len & 63)) buf=xrealloc(buf, len+65);
68     if ((buf[len++]=c) == '\n') break;
69   }
70   if (buf) {
71     buf[len]=0;
72     if (buf[--len]=='\n') buf[len]=0;
73   }
74 
75   return buf;
76 }
77 
78 // Dispose of a line of input, either by writing it out or discarding it.
79 
80 // state < 2: just free
81 // state = 2: write whole line to stderr
82 // state = 3: write whole line to fileout
83 // state > 3: write line+1 to fileout when *line != state
84 
do_line(void * data)85 static void do_line(void *data)
86 {
87   struct double_list *dlist = data;
88 
89   TT.outnum++;
90   if (TT.state>1)
91     if (0>dprintf(TT.state==2 ? 2 : TT.fileout,"%s\n",dlist->data+(TT.state>3)))
92       perror_exit("write");
93 
94   llist_free_double(data);
95 }
96 
finish_oldfile(void)97 static void finish_oldfile(void)
98 {
99   if (TT.tempname) replace_tempfile(TT.filein, TT.fileout, &TT.tempname);
100   TT.fileout = TT.filein = -1;
101 }
102 
fail_hunk(void)103 static void fail_hunk(void)
104 {
105   if (!TT.current_hunk) return;
106 
107   fprintf(stderr, "Hunk %d FAILED %ld/%ld.\n",
108       TT.hunknum, TT.oldline, TT.newline);
109   toys.exitval = 1;
110 
111   // If we got to this point, we've seeked to the end.  Discard changes to
112   // this file and advance to next file.
113 
114   TT.state = 2;
115   llist_traverse(TT.current_hunk, do_line);
116   TT.current_hunk = NULL;
117   if (!FLAG(dry_run)) delete_tempfile(TT.filein, TT.fileout, &TT.tempname);
118   TT.state = 0;
119 }
120 
121 // Compare ignoring whitespace. Just returns 0/1, no > or <
loosecmp(char * aa,char * bb)122 static int loosecmp(char *aa, char *bb)
123 {
124   int a = 0, b = 0;
125 
126   for (;;) {
127     while (isspace(aa[a])) a++;
128     while (isspace(bb[b])) b++;
129     if (aa[a] != bb[b]) return 1;
130     if (!aa[a]) return 0;
131     a++, b++;
132   }
133 }
134 
135 // Given a hunk of a unified diff, make the appropriate change to the file.
136 // This does not use the location information, but instead treats a hunk
137 // as a sort of regex. Copies data from input to output until it finds
138 // the change to be made, then outputs the changed data and returns.
139 // (Finding EOF first is an error.) This is a single pass operation, so
140 // multiple hunks must occur in order in the file.
141 
apply_one_hunk(void)142 static int apply_one_hunk(void)
143 {
144   struct double_list *plist, *buf = 0, *check = 0;
145   int matcheof, trail = 0, allfuzz = 0, fuzz, ii;
146   int (*lcmp)(char *aa, char *bb) = FLAG(l) ? (void *)loosecmp : (void *)strcmp;
147   long backwarn = 0;
148   char *data = toybuf;
149 
150   if (TT.v>1) printf("START %d\n", TT.hunknum);
151 
152   // Match EOF if there aren't as many ending context lines as beginning
153   dlist_terminate(TT.current_hunk);
154   for (fuzz = 0, plist = TT.current_hunk; plist; plist = plist->next) {
155     char *s = plist->data, c = *s;
156 
157     if (c==' ') trail++;
158     else trail = 0;
159 
160     // Only allow fuzz if 2 context lines have multiple nonwhitespace chars.
161     // avoids the "all context was blank or } lines" issue. Removed lines
162     // count as context since they're matched.
163     if (c==' ' || c=="-+"[FLAG(R)]) {
164       while (isspace(*++s));
165       if (*s && s[1] && !isspace(s[1])) fuzz++;
166     }
167   }
168   matcheof = !trail || trail < TT.context;
169   if (fuzz>1) allfuzz = TT.F ? : TT.context ? TT.context-1 : 0;
170 
171   // Loop through input data searching for this hunk. Match all context
172   // lines and lines to be removed until we've found end of complete hunk.
173   plist = TT.current_hunk;
174   fuzz = 0;
175   for (;;) {
176     if (data) {
177       data = get_line(TT.filein);
178       check = data ? dlist_add(&buf, data) : 0;
179       TT.linenum++;
180     }
181     if (TT.v>1) printf("READ[%ld] %s\n", TT.linenum, data ? : "(NULL)");
182 
183     // Compare buffered line(s) with expected lines of hunk. Match can fail
184     // because next line doesn't match, or because we hit end of a hunk that
185     // needed EOF and this isn't EOF.
186     for (;;) {
187       // Find hunk line to match (skip added lines) and detect reverse matches
188       while (plist && *plist->data == "+-"[FLAG(R)]) {
189         // TODO: proper backwarn = full hunk applies in reverse, not just 1 line
190         if (data) {
191           ii = strcspn(data, " \t");
192           if (data[ii+!!data[ii]] && !lcmp(data, plist->data+1))
193             backwarn = TT.linenum;
194         }
195         plist = plist->next;
196       }
197       if (TT.v>1 && plist)
198         printf("HUNK %s\nLINE %s\n", plist->data+1, check ? check->data : "");
199 
200       // End of hunk?
201       if (!plist) {
202         if (TT.v>1) printf("END OF HUNK\n");
203         if (matcheof == !data) goto out;
204 
205       // Compare line and handle match
206       } else if (check && !lcmp(check->data, plist->data+1)) {
207         if (TT.v>1) printf("MATCH\n");
208 handle_match:
209         plist = plist->next;
210         if ((check = check->next) == buf) {
211           if (plist || matcheof) break;
212           goto out;
213         } else continue;
214       }
215 
216       // Did we hit EOF?
217       if (!data) {
218         if (TT.v>1) printf("EOF\n");
219         if (backwarn && !FLAG(s))
220           fprintf(stderr, "Possibly reversed hunk %d at %ld\n",
221               TT.hunknum, backwarn);
222 
223         // File ended before we found a place for this hunk.
224         fail_hunk();
225         goto done;
226       }
227       if (TT.v>1) printf("NOT MATCH\n");
228 
229       // Match failed: can we fuzz it?
230       if (plist && *plist->data == ' ' && fuzz<allfuzz) {
231         fuzz++;
232         if (TT.v>1) printf("FUZZ %d %s\n", fuzz, check->data);
233         goto handle_match;
234       }
235 
236       // If this hunk must match start of file, fail if it didn't.
237       if (!TT.context || trail>TT.context) {
238         fail_hunk();
239         goto done;
240       }
241 
242       // Write out first line of buffer and recheck rest for new match.
243       TT.state = 3;
244       if (TT.v>1) printf("WRITE %s\n", buf->data);
245       do_line(check = dlist_pop(&buf));
246       plist = TT.current_hunk;
247       fuzz = 0;
248 
249       // If end of the buffer without finishing a match, read more lines.
250       if (!buf) break;
251       check = buf;
252     }
253   }
254 out:
255   if (TT.v) xprintf("Hunk #%d succeeded at %ld.\n", TT.hunknum, TT.linenum);
256   // We have a match.  Emit changed data.
257   TT.state = "-+"[FLAG(R)];
258   while ((plist = dlist_pop(&TT.current_hunk))) {
259     if (TT.state == *plist->data || *plist->data == ' ') {
260       if (*plist->data == ' ') dprintf(TT.fileout, "%s\n", buf->data);
261       llist_free_double(dlist_pop(&buf));
262     } else dprintf(TT.fileout, "%s\n", plist->data+1);
263     llist_free_double(plist);
264   }
265   TT.current_hunk = 0;
266   TT.state = 1;
267 done:
268   llist_traverse(buf, do_line);
269 
270   return TT.state;
271 }
272 
273 // read a filename that has been quoted or escaped
unquote_file(char * filename)274 static char *unquote_file(char *filename)
275 {
276   char *s = filename, *t, *newfile;
277 
278   // Return copy of file that wasn't quoted
279   if (*s++ != '"' || !*s) return xstrdup(filename);
280 
281   // quoted and escaped filenames are larger than the original
282   for (t = newfile = xmalloc(strlen(s) + 1); *s != '"'; s++) {
283     if (!s[1]) error_exit("bad %s", filename);
284 
285     // don't accept escape sequences unless the filename is quoted
286     if (*s != '\\') *t++ = *s;
287     else if (*++s >= '0' && *s < '8') {
288       *t++ = strtoul(s, &s, 8);
289       s--;
290     } else {
291       if (!(*t = unescape(*s))) *t = *s;;
292       t++;
293     }
294   }
295   *t = 0;
296 
297   return newfile;
298 }
299 
300 // Read a patch file and find hunks, opening/creating/deleting files.
301 // Call apply_one_hunk() on each hunk.
302 
303 // state 0: Not in a hunk, look for +++.
304 // state 1: Found +++ file indicator, look for @@
305 // state 2: In hunk: counting initial context lines
306 // state 3: In hunk: getting body
307 
patch_main(void)308 void patch_main(void)
309 {
310   int state = 0, patchlinenum = 0, strip = 0;
311   char *oldname = NULL, *newname = NULL;
312 
313   if (toys.optc == 2) TT.i = toys.optargs[1];
314   if (TT.i) TT.filepatch = xopenro(TT.i);
315   TT.filein = TT.fileout = -1;
316 
317   if (TT.d) xchdir(TT.d);
318 
319   // Loop through the lines in the patch
320   for (;;) {
321     char *patchline;
322 
323     patchline = get_line(TT.filepatch);
324     if (!patchline) break;
325 
326     // Other versions of patch accept damaged patches, so we need to also.
327     if (strip || !patchlinenum++) {
328       int len = strlen(patchline);
329       if (len && patchline[len-1] == '\r') {
330         if (!strip && !FLAG(s)) fprintf(stderr, "Removing DOS newlines\n");
331         strip = 1;
332         patchline[len-1]=0;
333       }
334     }
335     if (!*patchline) {
336       free(patchline);
337       patchline = xstrdup(" ");
338     }
339 
340     // Are we assembling a hunk?
341     if (state >= 2) {
342       if (*patchline==' ' || *patchline=='+' || *patchline=='-') {
343         dlist_add((void *)&TT.current_hunk, patchline);
344 
345         if (*patchline != '+') TT.oldlen--;
346         if (*patchline != '-') TT.newlen--;
347 
348         // Context line?
349         if (*patchline==' ' && state==2) TT.context++;
350         else state=3;
351 
352         // If we've consumed all expected hunk lines, apply the hunk.
353         if (!TT.oldlen && !TT.newlen) state = apply_one_hunk();
354         continue;
355       }
356       dlist_terminate(TT.current_hunk);
357       fail_hunk();
358       state = 0;
359       continue;
360     }
361 
362     // Open a new file?
363     if (!strncmp("--- ", patchline, 4) || !strncmp("+++ ", patchline, 4)) {
364       char *s, **name = &oldname;
365       int i;
366 
367       if (*patchline == '+') {
368         name = &newname;
369         state = 1;
370       }
371 
372       free(*name);
373       finish_oldfile();
374 
375       // Trim date from end of filename (if any).  We don't care.
376       for (s = patchline+4; *s && *s!='\t'; s++);
377       i = atoi(s);
378       if (i>1900 && i<=1970) *name = xstrdup("/dev/null");
379       else {
380         *s = 0;
381         *name = unquote_file(patchline+4);
382       }
383 
384       // We defer actually opening the file because svn produces broken
385       // patches that don't signal they want to create a new file the
386       // way the patch man page says, so you have to read the first hunk
387       // and _guess_.
388 
389     // Start a new hunk?  Usually @@ -oldline,oldlen +newline,newlen @@
390     // but a missing ,value means the value is 1.
391     } else if (state == 1 && !strncmp("@@ -", patchline, 4)) {
392       int i;
393       char *s = patchline+4;
394 
395       // Read oldline[,oldlen] +newline[,newlen]
396 
397       TT.oldlen = TT.newlen = 1;
398       TT.oldline = strtol(s, &s, 10);
399       if (*s == ',') TT.oldlen=strtol(s+1, &s, 10);
400       TT.newline = strtol(s+2, &s, 10);
401       if (*s == ',') TT.newlen = strtol(s+1, &s, 10);
402 
403       TT.context = 0;
404       state = 2;
405 
406       // If this is the first hunk, open the file.
407       if (TT.filein == -1) {
408         int oldsum, newsum, del = 0;
409         char *name;
410 
411         oldsum = TT.oldline + TT.oldlen;
412         newsum = TT.newline + TT.newlen;
413 
414         // If an original file was provided on the command line, it overrides
415         // *all* files mentioned in the patch, not just the first.
416         if (toys.optc) {
417           char **which = FLAG(R) ? &oldname : &newname;
418 
419           free(*which);
420           *which = xstrdup(toys.optargs[0]);
421           // The supplied path should be taken literally with or without -p.
422           toys.optflags |= FLAG_p;
423           TT.p = 0;
424         }
425 
426         name = FLAG(R) ? oldname : newname;
427 
428         // We're deleting oldname if new file is /dev/null (before -p)
429         // or if new hunk is empty (zero context) after patching
430         if (!strcmp(name, "/dev/null") || !(FLAG(R) ? oldsum : newsum)) {
431           name = FLAG(R) ? newname : oldname;
432           del++;
433         }
434 
435         // handle -p path truncation.
436         for (i = 0, s = name; *s;) {
437           if (FLAG(p) && TT.p == i) break;
438           if (*s++ != '/') continue;
439           while (*s == '/') s++;
440           name = s;
441           i++;
442         }
443 
444         if (del) {
445           if (!FLAG(s)) printf("removing %s\n", name);
446           if (!FLAG(dry_run)) xunlink(name);
447           state = 0;
448         // If we've got a file to open, do so.
449         } else if (!FLAG(p) || i <= TT.p) {
450           // If the old file was null, we're creating a new one.
451           if ((!strcmp(oldname, "/dev/null") || !oldsum) && access(name, F_OK))
452           {
453             if (!FLAG(s)) printf("creating %s\n", name);
454             if (FLAG(dry_run)) TT.filein = xopen("/dev/null", O_RDWR);
455             else {
456               if (mkpath(name)) perror_exit("mkpath %s", name);
457               TT.filein = xcreate(name, O_CREAT|O_EXCL|O_RDWR, 0666);
458             }
459           } else {
460             if (!FLAG(s)) printf("patching %s\n", name);
461             TT.filein = xopenro(name);
462           }
463           if (FLAG(dry_run)) TT.fileout = xopen("/dev/null", O_RDWR);
464           else TT.fileout = copy_tempfile(TT.filein, name, &TT.tempname);
465           TT.linenum = TT.outnum = TT.hunknum = 0;
466         }
467       }
468 
469       TT.hunknum++;
470 
471       continue;
472     }
473 
474     // If we didn't continue above, discard this line.
475     free(patchline);
476   }
477 
478   finish_oldfile();
479 
480   if (CFG_TOYBOX_FREE) {
481     close(TT.filepatch);
482     free(oldname);
483     free(newname);
484   }
485 }
486