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