1/* vi: set sw=4 ts=4: */
2/*
3 * busybox patch applet to handle the unified diff format.
4 * Copyright (C) 2003 Glenn McGrath
5 *
6 * Licensed under GPLv2 or later, see file LICENSE in this source tree.
7 *
8 * This applet is written to work with patches generated by GNU diff,
9 * where there is equivalent functionality busybox patch shall behave
10 * as per GNU patch.
11 *
12 * There is a SUSv3 specification for patch, however it looks to be
13 * incomplete, it doesnt even mention unified diff format.
14 * http://www.opengroup.org/onlinepubs/007904975/utilities/patch.html
15 *
16 * Issues
17 * - Non-interactive
18 * - Patches must apply cleanly or patch (not just one hunk) will fail.
19 * - Reject file isnt saved
20 */
21
22#include "libbb.h"
23
24static unsigned copy_lines(FILE *src_stream, FILE *dst_stream, unsigned lines_count)
25{
26	while (src_stream && lines_count) {
27		char *line;
28		line = xmalloc_fgets(src_stream);
29		if (line == NULL) {
30			break;
31		}
32		if (fputs(line, dst_stream) == EOF) {
33			bb_perror_msg_and_die("error writing to new file");
34		}
35		free(line);
36		lines_count--;
37	}
38	return lines_count;
39}
40
41/* If patch_level is -1 it will remove all directory names
42 * char *line must be greater than 4 chars
43 * returns NULL if the file doesnt exist or error
44 * returns malloc'ed filename
45 * NB: frees 1st argument!
46 */
47static char *extract_filename(char *line, int patch_level, const char *pat)
48{
49	char *temp = NULL, *filename_start_ptr = line + 4;
50
51	if (strncmp(line, pat, 4) == 0) {
52		/* Terminate string at end of source filename */
53		line[strcspn(line, "\t\n\r")] = '\0';
54
55		/* Skip over (patch_level) number of leading directories */
56		while (patch_level--) {
57			temp = strchr(filename_start_ptr, '/');
58			if (!temp)
59				break;
60			filename_start_ptr = temp + 1;
61		}
62		temp = xstrdup(filename_start_ptr);
63	}
64	free(line);
65	return temp;
66}
67
68int patch_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
69int patch_main(int argc UNUSED_PARAM, char **argv)
70{
71	struct stat saved_stat;
72	char *patch_line;
73	FILE *patch_file;
74	int patch_level;
75	int ret = 0;
76	char plus = '+';
77	unsigned opt;
78	enum {
79		OPT_R = (1 << 2),
80		OPT_N = (1 << 3),
81		/*OPT_f = (1 << 4), ignored */
82		/*OPT_E = (1 << 5), ignored, this is the default */
83		/*OPT_g = (1 << 6), ignored */
84		OPT_dry_run = (1 << 7) * ENABLE_LONG_OPTS,
85	};
86
87	xfunc_error_retval = 2;
88	{
89		const char *p = "-1";
90		const char *i = "-"; /* compat */
91#if ENABLE_LONG_OPTS
92		static const char patch_longopts[] ALIGN1 =
93			"strip\0"                 Required_argument "p"
94			"input\0"                 Required_argument "i"
95			"reverse\0"               No_argument       "R"
96			"forward\0"               No_argument       "N"
97		/* "Assume user knows what [s]he is doing, do not ask any questions": */
98			"force\0"                 No_argument       "f" /*ignored*/
99# if ENABLE_DESKTOP
100			"remove-empty-files\0"    No_argument       "E" /*ignored*/
101		/* "Controls actions when a file is under RCS or SCCS control,
102		 * and does not exist or is read-only and matches the default version,
103		 * or when a file is under ClearCase control and does not exist..."
104		 * IOW: rather obscure option.
105		 * But Gentoo's portage does use -g0 */
106			"get\0"                   Required_argument "g" /*ignored*/
107# endif
108			"dry-run\0"               No_argument       "\xfd"
109# if ENABLE_DESKTOP
110			"backup-if-mismatch\0"    No_argument       "\xfe" /*ignored*/
111			"no-backup-if-mismatch\0" No_argument       "\xff" /*ignored*/
112# endif
113			;
114		applet_long_options = patch_longopts;
115#endif
116		/* -f,-E,-g are ignored */
117		opt = getopt32(argv, "p:i:RN""fEg:", &p, &i, NULL);
118		if (opt & OPT_R)
119			plus = '-';
120		patch_level = xatoi(p); /* can be negative! */
121		patch_file = xfopen_stdin(i);
122	}
123
124	patch_line = xmalloc_fgetline(patch_file);
125	while (patch_line) {
126		FILE *src_stream;
127		FILE *dst_stream;
128		//char *old_filename;
129		char *new_filename;
130		char *backup_filename = NULL;
131		unsigned src_cur_line = 1;
132		unsigned dst_cur_line = 0;
133		unsigned dst_beg_line;
134		unsigned bad_hunk_count = 0;
135		unsigned hunk_count = 0;
136		smallint copy_trailing_lines_flag = 0;
137
138		/* Skip everything upto the "---" marker
139		 * No need to parse the lines "Only in <dir>", and "diff <args>"
140		 */
141		do {
142			/* Extract the filename used before the patch was generated */
143			new_filename = extract_filename(patch_line, patch_level, "--- ");
144			// was old_filename above
145			patch_line = xmalloc_fgetline(patch_file);
146			if (!patch_line) goto quit;
147		} while (!new_filename);
148		free(new_filename); // "source" filename is irrelevant
149
150		new_filename = extract_filename(patch_line, patch_level, "+++ ");
151		if (!new_filename) {
152			bb_error_msg_and_die("invalid patch");
153		}
154
155		/* Get access rights from the file to be patched */
156		if (stat(new_filename, &saved_stat) != 0) {
157			char *slash = strrchr(new_filename, '/');
158			if (slash) {
159				/* Create leading directories */
160				*slash = '\0';
161				bb_make_directory(new_filename, -1, FILEUTILS_RECUR);
162				*slash = '/';
163			}
164			src_stream = NULL;
165			saved_stat.st_mode = 0644;
166		} else if (!(opt & OPT_dry_run)) {
167			backup_filename = xasprintf("%s.orig", new_filename);
168			xrename(new_filename, backup_filename);
169			src_stream = xfopen_for_read(backup_filename);
170		} else
171			src_stream = xfopen_for_read(new_filename);
172
173		if (opt & OPT_dry_run) {
174			dst_stream = xfopen_for_write("/dev/null");
175		} else {
176			dst_stream = xfopen_for_write(new_filename);
177			fchmod(fileno(dst_stream), saved_stat.st_mode);
178		}
179
180		printf("patching file %s\n", new_filename);
181
182		/* Handle all hunks for this file */
183		patch_line = xmalloc_fgets(patch_file);
184		while (patch_line) {
185			unsigned count;
186			unsigned src_beg_line;
187			unsigned hunk_offset_start;
188			unsigned src_last_line = 1;
189			unsigned dst_last_line = 1;
190
191			if ((sscanf(patch_line, "@@ -%d,%d +%d,%d", &src_beg_line, &src_last_line, &dst_beg_line, &dst_last_line) < 3)
192			 && (sscanf(patch_line, "@@ -%d +%d,%d", &src_beg_line, &dst_beg_line, &dst_last_line) < 2)
193			) {
194				/* No more hunks for this file */
195				break;
196			}
197			if (plus != '+') {
198				/* reverse patch */
199				unsigned tmp = src_last_line;
200				src_last_line = dst_last_line;
201				dst_last_line = tmp;
202				tmp = src_beg_line;
203				src_beg_line = dst_beg_line;
204				dst_beg_line = tmp;
205			}
206			hunk_count++;
207
208			if (src_beg_line && dst_beg_line) {
209				/* Copy unmodified lines upto start of hunk */
210				/* src_beg_line will be 0 if it's a new file */
211				count = src_beg_line - src_cur_line;
212				if (copy_lines(src_stream, dst_stream, count)) {
213					bb_error_msg_and_die("bad src file");
214				}
215				src_cur_line += count;
216				dst_cur_line += count;
217				copy_trailing_lines_flag = 1;
218			}
219			src_last_line += hunk_offset_start = src_cur_line;
220			dst_last_line += dst_cur_line;
221
222			while (1) {
223				free(patch_line);
224				patch_line = xmalloc_fgets(patch_file);
225				if (patch_line == NULL)
226					break; /* EOF */
227				if (!*patch_line) {
228					/* whitespace-damaged patch with "" lines */
229					free(patch_line);
230					patch_line = xstrdup(" ");
231				}
232				if ((*patch_line != '-') && (*patch_line != '+')
233				 && (*patch_line != ' ')
234				) {
235					break; /* End of hunk */
236				}
237				if (*patch_line != plus) { /* '-' or ' ' */
238					char *src_line = NULL;
239					if (src_cur_line == src_last_line)
240						break;
241					if (src_stream) {
242						src_line = xmalloc_fgets(src_stream);
243						if (src_line) {
244							int diff = strcmp(src_line, patch_line + 1);
245							src_cur_line++;
246							free(src_line);
247							if (diff)
248								src_line = NULL;
249						}
250					}
251					/* Do not patch an already patched hunk with -N */
252					if (src_line == 0 && (opt & OPT_N)) {
253						continue;
254					}
255					if (!src_line) {
256						bb_error_msg("hunk #%u FAILED at %u", hunk_count, hunk_offset_start);
257						bad_hunk_count++;
258						break;
259					}
260					if (*patch_line != ' ') { /* '-' */
261						continue;
262					}
263				}
264				if (dst_cur_line == dst_last_line)
265					break;
266				fputs(patch_line + 1, dst_stream);
267				dst_cur_line++;
268			} /* end of while loop handling one hunk */
269		} /* end of while loop handling one file */
270
271		/* Cleanup last patched file */
272		if (copy_trailing_lines_flag) {
273			copy_lines(src_stream, dst_stream, (unsigned)(-1));
274		}
275		if (src_stream) {
276			fclose(src_stream);
277		}
278		fclose(dst_stream);
279		if (bad_hunk_count) {
280			ret = 1;
281			bb_error_msg("%u out of %u hunk FAILED", bad_hunk_count, hunk_count);
282		} else {
283			/* It worked, we can remove the backup */
284			if (backup_filename) {
285				unlink(backup_filename);
286			}
287			if (!(opt & OPT_dry_run)
288			 && ((dst_cur_line == 0) || (dst_beg_line == 0))
289			) {
290				/* The new patched file is empty, remove it */
291				xunlink(new_filename);
292				// /* old_filename and new_filename may be the same file */
293				// unlink(old_filename);
294			}
295		}
296		free(backup_filename);
297		//free(old_filename);
298		free(new_filename);
299	} /* end of "while there are patch lines" */
300 quit:
301	/* 0 = SUCCESS
302	 * 1 = Some hunks failed
303	 * 2 = More serious problems (exited earlier)
304	 */
305	return ret;
306}
307