xref: /aosp_15_r20/external/toybox/toys/posix/patch.c (revision cf5a6c84e2b8763fc1a7db14496fd4742913b199)
1 /* patch.c - Apply a "universal" diff.
2  *
3  * Copyright 2007 Rob Landley <[email protected]>
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 (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 = 0, *newname = 0;
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 file (-i or stdin) collecting hunks
320   for (;;) {
321     char *patchline;
322 
323     if (!(patchline = get_line(TT.filepatch))) break;
324 
325     // Other versions of patch accept damaged patches, so we need to also.
326     if (strip || !patchlinenum++) {
327       int len = strlen(patchline);
328       if (len && patchline[len-1] == '\r') {
329         if (!strip && !FLAG(s)) fprintf(stderr, "Removing DOS newlines\n");
330         strip = 1;
331         patchline[len-1] = 0;
332       }
333     }
334     if (!*patchline) {
335       free(patchline);
336       patchline = xstrdup(" ");
337     }
338 
339     // Are we assembling a hunk?
340     if (state >= 2) {
341       if (*patchline==' ' || *patchline=='+' || *patchline=='-') {
342         dlist_add((void *)&TT.current_hunk, patchline);
343 
344         if (*patchline != '+') TT.oldlen--;
345         if (*patchline != '-') TT.newlen--;
346 
347         // Context line?
348         if (*patchline==' ' && state==2) TT.context++;
349         else state=3;
350 
351         // If we've consumed all expected hunk lines, apply the hunk.
352         if (!TT.oldlen && !TT.newlen) state = apply_one_hunk();
353       } else {
354         dlist_terminate(TT.current_hunk);
355         fail_hunk();
356         state = 0;
357       }
358       continue;
359     }
360 
361     // Open a new file?
362     if (!strncmp("--- ", patchline, 4) || !strncmp("+++ ", patchline, 4)) {
363       char *s, **name = &oldname;
364       int i;
365 
366       if (*patchline == '+') {
367         name = &newname;
368         state = 1;
369       }
370 
371       free(*name);
372       finish_oldfile();
373 
374       // Trim date from end of filename (if any). Date<=epoch means delete.
375       for (s = patchline+4; *s && *s!='\t'; s++);
376       i = atoi(s);
377       if (i>1900 && i<=1970) *name = xstrdup("/dev/null");
378       else {
379         *s = 0;
380         *name = unquote_file(patchline+4);
381       }
382 
383       // We defer actually opening the file because svn produces broken
384       // patches that don't signal they want to create a new file the
385       // way the patch man page says, so you have to read the first hunk
386       // and _guess_.
387 
388     // Start a new hunk?  Usually @@ -oldline,oldlen +newline,newlen @@
389     // but a missing ,value means the value is 1.
390     } else if (state == 1 && !strncmp("@@ -", patchline, 4)) {
391       int i;
392       char *s = patchline+4;
393 
394       // Read oldline[,oldlen] +newline[,newlen]
395 
396       TT.oldlen = TT.newlen = 1;
397       TT.oldline = strtol(s, &s, 10);
398       if (*s == ',') TT.oldlen=strtol(s+1, &s, 10);
399       TT.newline = strtol(s+2, &s, 10);
400       if (*s == ',') TT.newlen = strtol(s+1, &s, 10);
401 
402       TT.context = 0;
403       state = 2;
404 
405       // If this is the first hunk, open the file.
406       if (TT.filein == -1) {
407         int oldsum, newsum, del = 0;
408         char *name;
409 
410         oldsum = TT.oldline + TT.oldlen;
411         newsum = TT.newline + TT.newlen;
412 
413         // If an original file was provided on the command line, it overrides
414         // *all* files mentioned in the patch, not just the first.
415         if (toys.optc) {
416           char **which = FLAG(R) ? &oldname : &newname;
417 
418           free(*which);
419           *which = xstrdup(toys.optargs[0]);
420           // The supplied path should be taken literally with or without -p.
421           toys.optflags |= FLAG_p;
422           TT.p = 0;
423         }
424 
425         name = FLAG(R) ? oldname : newname;
426 
427         // We're deleting oldname if new file is /dev/null (before -p)
428         // or if new hunk is empty (zero context) after patching
429         if (!strcmp(name, "/dev/null") || !(FLAG(R) ? oldsum : newsum)) {
430           name = FLAG(R) ? newname : oldname;
431           del++;
432         }
433 
434         // handle -p path truncation.
435         for (i = 0, s = name; *s;) {
436           if (FLAG(p) && TT.p == i) break;
437           if (*s++ != '/') continue;
438           while (*s == '/') s++;
439           name = s;
440           i++;
441         }
442 
443         if (del) {
444           if (!FLAG(s)) printf("removing %s\n", name);
445           if (!FLAG(dry_run)) xunlink(name);
446           state = 0;
447         // If we've got a file to open, do so.
448         } else if (!FLAG(p) || i <= TT.p) {
449           // If the old file was null, we're creating a new one.
450           if ((!strcmp(oldname, "/dev/null") || !oldsum) && access(name, F_OK))
451           {
452             if (!FLAG(s)) printf("creating %s\n", name);
453             if (FLAG(dry_run)) TT.filein = xopen("/dev/null", O_RDWR);
454             else {
455               if (mkpath(name)) perror_exit("mkpath %s", name);
456               TT.filein = xcreate(name, O_CREAT|O_EXCL|O_RDWR, 0666);
457             }
458           } else {
459             if (!FLAG(s)) printf("patching %s\n", name);
460             TT.filein = xopenro(name);
461           }
462           if (FLAG(dry_run)) TT.fileout = xopen("/dev/null", O_RDWR);
463           else TT.fileout = copy_tempfile(TT.filein, name, &TT.tempname);
464           TT.linenum = TT.outnum = TT.hunknum = 0;
465         }
466       }
467 
468       TT.hunknum++;
469 
470       continue;
471     }
472 
473     // If we didn't continue above, discard this line.
474     free(patchline);
475   }
476 
477   finish_oldfile();
478 
479   if (CFG_TOYBOX_FREE) {
480     close(TT.filepatch);
481     free(oldname);
482     free(newname);
483   }
484 }
485