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