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