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)"USE_TOYBOX_DEBUG("x")"F#g#fulp#d:i:Rs(quiet)", TOYFLAG_USR|TOYFLAG_BIN))
18
19 config PATCH
20 bool "patch"
21 default y
22 help
23 usage: patch [-Rlsu] [-d DIR] [-i PATCH] [-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 -i Input patch file (default=stdin)
29 -l Loose match (ignore whitespace)
30 -p Number of '/' to strip from start of file paths (default=all)
31 -R Reverse patch
32 -s Silent except for errors
33 -u Ignored (only handles "unified" diffs)
34 --dry-run Don't change files, just confirm patch applies
35
36 This version of patch only handles unified diffs, and only modifies
37 a file when all hunks to that file apply. Patch prints failed hunks
38 to stderr, and exits with nonzero status if any hunks fail.
39
40 A file compared against /dev/null (or with a date <= the epoch) is
41 created/deleted as appropriate.
42 */
43
44 #define FOR_patch
45 #include "toys.h"
46
47 GLOBALS(
48 char *i, *d;
49 long p, g, F;
50
51 void *current_hunk;
52 long oldline, oldlen, newline, newlen, linenum, outnum;
53 int context, state, filein, fileout, filepatch, hunknum;
54 char *tempname;
55 )
56
57 // TODO xgetline() instead, but replace_tempfile() wants fd...
get_line(int fd)58 char *get_line(int fd)
59 {
60 char c, *buf = NULL;
61 long len = 0;
62
63 for (;;) {
64 if (1>read(fd, &c, 1)) break;
65 if (!(len & 63)) buf=xrealloc(buf, len+65);
66 if ((buf[len++]=c) == '\n') break;
67 }
68 if (buf) {
69 buf[len]=0;
70 if (buf[--len]=='\n') buf[len]=0;
71 }
72
73 return buf;
74 }
75
76 // Dispose of a line of input, either by writing it out or discarding it.
77
78 // state < 2: just free
79 // state = 2: write whole line to stderr
80 // state = 3: write whole line to fileout
81 // state > 3: write line+1 to fileout when *line != state
82
do_line(void * data)83 static void do_line(void *data)
84 {
85 struct double_list *dlist = data;
86
87 TT.outnum++;
88 if (TT.state>1)
89 if (0>dprintf(TT.state==2 ? 2 : TT.fileout,"%s\n",dlist->data+(TT.state>3)))
90 perror_exit("write");
91
92 if (FLAG(x))
93 fprintf(stderr, "DO %d %ld: %s\n", TT.state, TT.outnum, dlist->data);
94
95 llist_free_double(data);
96 }
97
finish_oldfile(void)98 static void finish_oldfile(void)
99 {
100 if (TT.tempname) replace_tempfile(TT.filein, TT.fileout, &TT.tempname);
101 TT.fileout = TT.filein = -1;
102 }
103
fail_hunk(void)104 static void fail_hunk(void)
105 {
106 if (!TT.current_hunk) return;
107
108 fprintf(stderr, "Hunk %d FAILED %ld/%ld.\n",
109 TT.hunknum, TT.oldline, TT.newline);
110 toys.exitval = 1;
111
112 // If we got to this point, we've seeked to the end. Discard changes to
113 // this file and advance to next file.
114
115 TT.state = 2;
116 llist_traverse(TT.current_hunk, do_line);
117 TT.current_hunk = NULL;
118 if (!FLAG(dry_run)) delete_tempfile(TT.filein, TT.fileout, &TT.tempname);
119 TT.state = 0;
120 }
121
122 // Compare ignoring whitespace. Just returns 0/1, no > or <
loosecmp(char * aa,char * bb)123 static int loosecmp(char *aa, char *bb)
124 {
125 int a = 0, b = 0;
126
127 for (;;) {
128 while (isspace(aa[a])) a++;
129 while (isspace(bb[b])) b++;
130 if (aa[a] != bb[b]) return 1;
131 if (!aa[a]) return 0;
132 a++, b++;
133 }
134 }
135
136 // Given a hunk of a unified diff, make the appropriate change to the file.
137 // This does not use the location information, but instead treats a hunk
138 // as a sort of regex. Copies data from input to output until it finds
139 // the change to be made, then outputs the changed data and returns.
140 // (Finding EOF first is an error.) This is a single pass operation, so
141 // multiple hunks must occur in order in the file.
142
apply_one_hunk(void)143 static int apply_one_hunk(void)
144 {
145 struct double_list *plist, *buf = 0, *check;
146 int matcheof, trail = 0, reverse = FLAG(R), backwarn = 0, allfuzz, fuzz, i;
147 int (*lcmp)(char *aa, char *bb) = FLAG(l) ? (void *)loosecmp : (void *)strcmp;
148
149 // Match EOF if there aren't as many ending context lines as beginning
150 dlist_terminate(TT.current_hunk);
151 for (fuzz = 0, plist = TT.current_hunk; plist; plist = plist->next) {
152 char c = *plist->data, *s;
153
154 if (c==' ') trail++;
155 else trail = 0;
156
157 // Only allow fuzz if 2 context lines have multiple nonwhitespace chars.
158 // avoids the "all context was blank or } lines" issue. Removed lines
159 // count as context since they're matched.
160 if (c==' ' || c=="-+"[reverse]) {
161 s = plist->data+1;
162 while (isspace(*s)) s++;
163 if (*s && s[1] && !isspace(s[1])) fuzz++;
164 }
165
166 if (FLAG(x)) fprintf(stderr, "HUNK:%s\n", plist->data);
167 }
168 matcheof = !trail || trail < TT.context;
169 if (fuzz<2) allfuzz = 0;
170 else allfuzz = FLAG(F) ? TT.F : (TT.context ? TT.context-1 : 0);
171
172 if (FLAG(x)) fprintf(stderr,"MATCHEOF=%c\n", matcheof ? 'Y' : 'N');
173
174 // Loop through input data searching for this hunk. Match all context
175 // lines and lines to be removed until we've found end of complete hunk.
176 plist = TT.current_hunk;
177 fuzz = 0;
178 for (;;) {
179 char *data = get_line(TT.filein);
180
181 // Figure out which line of hunk to compare with next. (Skip lines
182 // of the hunk we'd be adding.)
183 while (plist && *plist->data == "+-"[reverse]) {
184 if (data && !lcmp(data, plist->data+1))
185 if (!backwarn) backwarn = TT.linenum;
186 plist = plist->next;
187 }
188
189 // Is this EOF?
190 if (!data) {
191 if (FLAG(x)) fprintf(stderr, "INEOF\n");
192
193 // Does this hunk need to match EOF?
194 if (!plist && matcheof) break;
195
196 if (backwarn && !FLAG(s))
197 fprintf(stderr, "Possibly reversed hunk %d at %ld\n",
198 TT.hunknum, TT.linenum);
199
200 // File ended before we found a place for this hunk.
201 fail_hunk();
202 goto done;
203 } else {
204 TT.linenum++;
205 if (FLAG(x)) fprintf(stderr, "IN: %s\n", data);
206 }
207 check = dlist_add(&buf, data);
208
209 // Compare this line with next expected line of hunk. Match can fail
210 // because next line doesn't match, or because we hit end of a hunk that
211 // needed EOF and this isn't EOF.
212 for (i = 0;; i++) {
213 if (!plist || lcmp(check->data, plist->data+1)) {
214
215 // Match failed: can we fuzz it?
216 if (plist && *plist->data == ' ' && fuzz<allfuzz) {
217 if (FLAG(x))
218 fprintf(stderr, "FUZZED: %ld %s\n", TT.linenum, plist->data);
219 fuzz++;
220
221 goto fuzzed;
222 }
223
224 if (FLAG(x)) {
225 int bug = 0;
226
227 if (!plist) fprintf(stderr, "NULL plist\n");
228 else {
229 while (plist->data[bug] == check->data[bug]) bug++;
230 fprintf(stderr, "NOT(%d:%d!=%d): %s\n", bug, plist->data[bug],
231 check->data[bug], plist->data);
232 }
233 }
234
235 // If this hunk must match start of file, fail if it didn't.
236 if (!TT.context || trail>TT.context) {
237 fail_hunk();
238 goto done;
239 }
240
241 // Write out first line of buffer and recheck rest for new match.
242 TT.state = 3;
243 do_line(check = dlist_pop(&buf));
244 plist = TT.current_hunk;
245 fuzz = 0;
246
247 // If end of the buffer without finishing a match, read more lines.
248 if (!buf) break;
249 check = buf;
250 } else {
251 if (FLAG(x)) fprintf(stderr, "MAYBE: %s\n", plist->data);
252 fuzzed:
253 // This line matches. Advance plist, detect successful match.
254 plist = plist->next;
255 if (!plist && !matcheof) goto out;
256 check = check->next;
257 if (check == buf) break;
258 }
259 }
260 }
261 out:
262 // We have a match. Emit changed data.
263 TT.state = "-+"[reverse];
264 while ((plist = dlist_pop(&TT.current_hunk))) {
265 if (TT.state == *plist->data || *plist->data == ' ') {
266 if (*plist->data == ' ') dprintf(TT.fileout, "%s\n", buf->data);
267 llist_free_double(dlist_pop(&buf));
268 } else dprintf(TT.fileout, "%s\n", plist->data+1);
269 llist_free_double(plist);
270 }
271 TT.current_hunk = 0;
272 TT.state = 1;
273 done:
274 llist_traverse(buf, do_line);
275
276 return TT.state;
277 }
278
279 // read a filename that has been quoted or escaped
unquote_file(char * filename)280 static char *unquote_file(char *filename)
281 {
282 char *s = filename, *t;
283
284 // Return copy of file that wasn't quoted
285 if (*s++ != '"' || !*s) return xstrdup(filename);
286
287 // quoted and escaped filenames are larger than the original
288 for (t = filename = xmalloc(strlen(s) + 1); *s != '"'; s++) {
289 if (!s[1]) error_exit("bad %s", filename);
290
291 // don't accept escape sequences unless the filename is quoted
292 if (*s != '\\') *t++ = *s;
293 else if (*++s >= '0' && *s < '8') {
294 *t++ = strtoul(s, &s, 8);
295 s--;
296 } else {
297 if (!(*t = unescape(*s))) *t = *s;;
298 t++;
299 }
300 }
301 *t = 0;
302
303 return filename;
304 }
305
306 // Read a patch file and find hunks, opening/creating/deleting files.
307 // Call apply_one_hunk() on each hunk.
308
309 // state 0: Not in a hunk, look for +++.
310 // state 1: Found +++ file indicator, look for @@
311 // state 2: In hunk: counting initial context lines
312 // state 3: In hunk: getting body
313
patch_main(void)314 void patch_main(void)
315 {
316 int reverse = FLAG(R), state = 0, patchlinenum = 0, strip = 0;
317 char *oldname = NULL, *newname = NULL;
318
319 if (toys.optc == 2) TT.i = toys.optargs[1];
320 if (TT.i) TT.filepatch = xopenro(TT.i);
321 TT.filein = TT.fileout = -1;
322
323 if (TT.d) xchdir(TT.d);
324
325 // Loop through the lines in the patch
326 for (;;) {
327 char *patchline;
328
329 patchline = get_line(TT.filepatch);
330 if (!patchline) break;
331
332 // Other versions of patch accept damaged patches, so we need to also.
333 if (strip || !patchlinenum++) {
334 int len = strlen(patchline);
335 if (len && patchline[len-1] == '\r') {
336 if (!strip && !FLAG(s)) fprintf(stderr, "Removing DOS newlines\n");
337 strip = 1;
338 patchline[len-1]=0;
339 }
340 }
341 if (!*patchline) {
342 free(patchline);
343 patchline = xstrdup(" ");
344 }
345
346 // Are we assembling a hunk?
347 if (state >= 2) {
348 if (*patchline==' ' || *patchline=='+' || *patchline=='-') {
349 dlist_add((void *)&TT.current_hunk, patchline);
350
351 if (*patchline != '+') TT.oldlen--;
352 if (*patchline != '-') TT.newlen--;
353
354 // Context line?
355 if (*patchline==' ' && state==2) TT.context++;
356 else state=3;
357
358 // If we've consumed all expected hunk lines, apply the hunk.
359 if (!TT.oldlen && !TT.newlen) state = apply_one_hunk();
360 continue;
361 }
362 dlist_terminate(TT.current_hunk);
363 fail_hunk();
364 state = 0;
365 continue;
366 }
367
368 // Open a new file?
369 if (!strncmp("--- ", patchline, 4) || !strncmp("+++ ", patchline, 4)) {
370 char *s, **name = &oldname;
371 int i;
372
373 if (*patchline == '+') {
374 name = &newname;
375 state = 1;
376 }
377
378 free(*name);
379 finish_oldfile();
380
381 // Trim date from end of filename (if any). We don't care.
382 for (s = patchline+4; *s && *s!='\t'; s++);
383 i = atoi(s);
384 if (i>1900 && i<=1970) *name = xstrdup("/dev/null");
385 else {
386 *s = 0;
387 *name = unquote_file(patchline+4);
388 }
389
390 // We defer actually opening the file because svn produces broken
391 // patches that don't signal they want to create a new file the
392 // way the patch man page says, so you have to read the first hunk
393 // and _guess_.
394
395 // Start a new hunk? Usually @@ -oldline,oldlen +newline,newlen @@
396 // but a missing ,value means the value is 1.
397 } else if (state == 1 && !strncmp("@@ -", patchline, 4)) {
398 int i;
399 char *s = patchline+4;
400
401 // Read oldline[,oldlen] +newline[,newlen]
402
403 TT.oldlen = TT.newlen = 1;
404 TT.oldline = strtol(s, &s, 10);
405 if (*s == ',') TT.oldlen=strtol(s+1, &s, 10);
406 TT.newline = strtol(s+2, &s, 10);
407 if (*s == ',') TT.newlen = strtol(s+1, &s, 10);
408
409 TT.context = 0;
410 state = 2;
411
412 // If this is the first hunk, open the file.
413 if (TT.filein == -1) {
414 int oldsum, newsum, del = 0;
415 char *name;
416
417 oldsum = TT.oldline + TT.oldlen;
418 newsum = TT.newline + TT.newlen;
419
420 // If an original file was provided on the command line, it overrides
421 // *all* files mentioned in the patch, not just the first.
422 if (toys.optc) {
423 char **which = reverse ? &oldname : &newname;
424
425 free(*which);
426 *which = strdup(toys.optargs[0]);
427 // The supplied path should be taken literally with or without -p.
428 toys.optflags |= FLAG_p;
429 TT.p = 0;
430 }
431
432 name = reverse ? oldname : newname;
433
434 // We're deleting oldname if new file is /dev/null (before -p)
435 // or if new hunk is empty (zero context) after patching
436 if (!strcmp(name, "/dev/null") || !(reverse ? oldsum : newsum)) {
437 name = reverse ? newname : oldname;
438 del++;
439 }
440
441 // handle -p path truncation.
442 for (i = 0, s = name; *s;) {
443 if (FLAG(p) && TT.p == i) break;
444 if (*s++ != '/') continue;
445 while (*s == '/') s++;
446 name = s;
447 i++;
448 }
449
450 if (del) {
451 if (!FLAG(s)) printf("removing %s\n", name);
452 xunlink(name);
453 state = 0;
454 // If we've got a file to open, do so.
455 } else if (!FLAG(p) || i <= TT.p) {
456 // If the old file was null, we're creating a new one.
457 if ((!strcmp(oldname, "/dev/null") || !oldsum) && access(name, F_OK))
458 {
459 if (!FLAG(s)) printf("creating %s\n", name);
460 if (mkpath(name)) perror_exit("mkpath %s", name);
461 TT.filein = xcreate(name, O_CREAT|O_EXCL|O_RDWR, 0666);
462 } else {
463 if (!FLAG(s)) printf("patching %s\n", name);
464 TT.filein = xopenro(name);
465 }
466 if (FLAG(dry_run)) TT.fileout = xopen("/dev/null", O_RDWR);
467 else TT.fileout = copy_tempfile(TT.filein, name, &TT.tempname);
468 TT.linenum = TT.outnum = TT.hunknum = 0;
469 }
470 }
471
472 TT.hunknum++;
473
474 continue;
475 }
476
477 // If we didn't continue above, discard this line.
478 free(patchline);
479 }
480
481 finish_oldfile();
482
483 if (CFG_TOYBOX_FREE) {
484 close(TT.filepatch);
485 free(oldname);
486 free(newname);
487 }
488 }
489