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