• 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 = 0;
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 = 0;
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 (FLAG(F) && !TT.F) fuzz = 0;
170   if (fuzz>1) allfuzz = TT.F ? : TT.context ? TT.context-1 : 0;
171 
172   // Loop through input data searching for this hunk. Match all context
173   // lines and lines to be removed until we've found end of complete hunk.
174   plist = TT.current_hunk;
175   fuzz = 0;
176   for (;;) {
177     if (data) {
178       data = get_line(TT.filein);
179       check = data ? dlist_add(&buf, data) : 0;
180       TT.linenum++;
181     }
182     if (TT.v>1) printf("READ[%ld] %s\n", TT.linenum, data ? : "(NULL)");
183 
184     // Compare buffered line(s) with expected lines of hunk. Match can fail
185     // because next line doesn't match, or because we hit end of a hunk that
186     // needed EOF and this isn't EOF.
187     for (;;) {
188       // Find hunk line to match (skip added lines) and detect reverse matches
189       while (plist && *plist->data == "+-"[FLAG(R)]) {
190         // TODO: proper backwarn = full hunk applies in reverse, not just 1 line
191         if (data) {
192           ii = strcspn(data, " \t");
193           if (data[ii+!!data[ii]] && !lcmp(data, plist->data+1))
194             backwarn = TT.linenum;
195         }
196         plist = plist->next;
197       }
198       if (TT.v>1 && plist)
199         printf("HUNK %s\nLINE %s\n", plist->data+1, check ? check->data : "");
200 
201       // End of hunk?
202       if (!plist) {
203         if (TT.v>1) printf("END OF HUNK\n");
204         if (matcheof == !data) goto out;
205 
206       // Compare line and handle match
207       } else if (check && !lcmp(check->data, plist->data+1)) {
208         if (TT.v>1) printf("MATCH\n");
209 handle_match:
210         plist = plist->next;
211         if ((check = check->next) == buf) {
212           if (plist || matcheof) break;
213           goto out;
214         } else continue;
215       }
216 
217       // Did we hit EOF?
218       if (!data) {
219         if (TT.v>1) printf("EOF\n");
220         if (backwarn && !FLAG(s))
221           fprintf(stderr, "Possibly reversed hunk %d at %ld\n",
222               TT.hunknum, backwarn);
223 
224         // File ended before we found a place for this hunk.
225         fail_hunk();
226         goto done;
227       }
228       if (TT.v>1) printf("NOT MATCH\n");
229 
230       // Match failed: can we fuzz it?
231       if (plist && *plist->data == ' ' && fuzz<allfuzz) {
232         fuzz++;
233         if (TT.v>1) printf("FUZZ %d %s\n", fuzz, check->data);
234         goto handle_match;
235       }
236 
237       // If this hunk must match start of file, fail if it didn't.
238       if (!TT.context || trail>TT.context) {
239         fail_hunk();
240         goto done;
241       }
242 
243       // Write out first line of buffer and recheck rest for new match.
244       TT.state = 3;
245       if (TT.v>1) printf("WRITE %s\n", buf->data);
246       do_line(check = dlist_pop(&buf));
247       plist = TT.current_hunk;
248       fuzz = 0;
249 
250       // If end of the buffer without finishing a match, read more lines.
251       if (!buf) break;
252       check = buf;
253     }
254   }
255 out:
256   if (TT.v) xprintf("Hunk #%d succeeded at %ld.\n", TT.hunknum, TT.linenum);
257   // We have a match.  Emit changed data.
258   TT.state = "-+"[FLAG(R)];
259   while ((plist = dlist_pop(&TT.current_hunk))) {
260     if (TT.state == *plist->data || *plist->data == ' ') {
261       if (*plist->data == ' ') dprintf(TT.fileout, "%s\n", buf->data);
262       llist_free_double(dlist_pop(&buf));
263     } else dprintf(TT.fileout, "%s\n", plist->data+1);
264     llist_free_double(plist);
265   }
266   TT.current_hunk = 0;
267   TT.state = 1;
268 done:
269   llist_traverse(buf, do_line);
270 
271   return TT.state;
272 }
273 
274 // read a filename that has been quoted or escaped
unquote_file(char * filename)275 static char *unquote_file(char *filename)
276 {
277   char *s = filename, *t, *newfile;
278 
279   // Return copy of file that wasn't quoted
280   if (*s++ != '"' || !*s) return xstrdup(filename);
281 
282   // quoted and escaped filenames are larger than the original
283   for (t = newfile = xmalloc(strlen(s) + 1); *s != '"'; s++) {
284     if (!s[1]) error_exit("bad %s", filename);
285 
286     // don't accept escape sequences unless the filename is quoted
287     if (*s != '\\') *t++ = *s;
288     else if (*++s >= '0' && *s < '8') {
289       *t++ = strtoul(s, &s, 8);
290       s--;
291     } else {
292       if (!(*t = unescape(*s))) *t = *s;;
293       t++;
294     }
295   }
296   *t = 0;
297 
298   return newfile;
299 }
300 
301 // Read a patch file and find hunks, opening/creating/deleting files.
302 // Call apply_one_hunk() on each hunk.
303 
304 // state 0: Not in a hunk, look for +++.
305 // state 1: Found +++ file indicator, look for @@
306 // state 2: In hunk: counting initial context lines
307 // state 3: In hunk: getting body
308 
patch_main(void)309 void patch_main(void)
310 {
311   int state = 0, patchlinenum = 0, strip = 0;
312   char *oldname = 0, *newname = 0;
313 
314   if (toys.optc == 2) TT.i = toys.optargs[1];
315   if (TT.i) TT.filepatch = xopenro(TT.i);
316   TT.filein = TT.fileout = -1;
317 
318   if (TT.d) xchdir(TT.d);
319 
320   // Loop through the lines in the patch file (-i or stdin) collecting hunks
321   for (;;) {
322     char *patchline;
323 
324     if (!(patchline = get_line(TT.filepatch))) 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       } else {
355         dlist_terminate(TT.current_hunk);
356         fail_hunk();
357         state = 0;
358       }
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). Date<=epoch means delete.
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