1/* vi: set sw=4 ts=4: */
2/*
3 *  busybox patch applet to handle the unified diff format.
4 *  Copyright (C) 2003 Glenn McGrath <bug1@iinet.net.au>
5 *
6 *  Licensed under the GPL v2 or later, see the file LICENSE in this tarball.
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 <getopt.h>
23
24#include "libbb.h"
25
26static unsigned int copy_lines(FILE *src_stream, FILE *dest_stream, const unsigned int lines_count)
27{
28	unsigned int i = 0;
29
30	while (src_stream && (i < lines_count)) {
31		char *line;
32		line = xmalloc_fgets(src_stream);
33		if (line == NULL) {
34			break;
35		}
36		if (fputs(line, dest_stream) == EOF) {
37			bb_perror_msg_and_die("error writing to new file");
38		}
39		free(line);
40
41		i++;
42	}
43	return i;
44}
45
46/* If patch_level is -1 it will remove all directory names
47 * char *line must be greater than 4 chars
48 * returns NULL if the file doesnt exist or error
49 * returns malloc'ed filename
50 */
51
52static char *extract_filename(char *line, int patch_level)
53{
54	char *temp, *filename_start_ptr = line + 4;
55	int i;
56
57	/* Terminate string at end of source filename */
58	temp = strchrnul(filename_start_ptr, '\t');
59	*temp = '\0';
60
61	/* Skip over (patch_level) number of leading directories */
62	if (patch_level == -1)
63		patch_level = INT_MAX;
64	for (i = 0; i < patch_level; i++) {
65		temp = strchr(filename_start_ptr, '/');
66		if (!temp)
67			break;
68		filename_start_ptr = temp + 1;
69	}
70
71	return xstrdup(filename_start_ptr);
72}
73
74static int file_doesnt_exist(const char *filename)
75{
76	struct stat statbuf;
77	return stat(filename, &statbuf);
78}
79
80int patch_main(int argc, char **argv);
81int patch_main(int argc, char **argv)
82{
83	int patch_level = -1;
84	char *patch_line;
85	int ret;
86	FILE *patch_file = NULL;
87
88	{
89		char *p, *i;
90		ret = getopt32(argv, "p:i:", &p, &i);
91		if (ret & 1)
92			patch_level = xatol_range(p, -1, USHRT_MAX);
93		if (ret & 2) {
94			patch_file = xfopen(i, "r");
95		} else {
96			patch_file = stdin;
97		}
98		ret = 0;
99	}
100
101	patch_line = xmalloc_getline(patch_file);
102	while (patch_line) {
103		FILE *src_stream;
104		FILE *dst_stream;
105		char *original_filename;
106		char *new_filename;
107		char *backup_filename;
108		unsigned int src_cur_line = 1;
109		unsigned int dest_cur_line = 0;
110		unsigned int dest_beg_line;
111		unsigned int bad_hunk_count = 0;
112		unsigned int hunk_count = 0;
113		char copy_trailing_lines_flag = 0;
114
115		/* Skip everything upto the "---" marker
116		 * No need to parse the lines "Only in <dir>", and "diff <args>"
117		 */
118		while (patch_line && strncmp(patch_line, "--- ", 4) != 0) {
119			free(patch_line);
120			patch_line = xmalloc_getline(patch_file);
121		}
122
123		/* Extract the filename used before the patch was generated */
124		original_filename = extract_filename(patch_line, patch_level);
125		free(patch_line);
126
127		patch_line = xmalloc_getline(patch_file);
128		if (strncmp(patch_line, "+++ ", 4) != 0) {
129			ret = 2;
130			bb_error_msg("invalid patch");
131			continue;
132		}
133		new_filename = extract_filename(patch_line, patch_level);
134		free(patch_line);
135
136		if (file_doesnt_exist(new_filename)) {
137			char *line_ptr;
138			/* Create leading directories */
139			line_ptr = strrchr(new_filename, '/');
140			if (line_ptr) {
141				*line_ptr = '\0';
142				bb_make_directory(new_filename, -1, FILEUTILS_RECUR);
143				*line_ptr = '/';
144			}
145			dst_stream = xfopen(new_filename, "w+");
146			backup_filename = NULL;
147		} else {
148			backup_filename = xmalloc(strlen(new_filename) + 6);
149			strcpy(backup_filename, new_filename);
150			strcat(backup_filename, ".orig");
151			if (rename(new_filename, backup_filename) == -1) {
152				bb_perror_msg_and_die("cannot create file %s",
153						backup_filename);
154			}
155			dst_stream = xfopen(new_filename, "w");
156		}
157
158		if ((backup_filename == NULL) || file_doesnt_exist(original_filename)) {
159			src_stream = NULL;
160		} else {
161			if (strcmp(original_filename, new_filename) == 0) {
162				src_stream = xfopen(backup_filename, "r");
163			} else {
164				src_stream = xfopen(original_filename, "r");
165			}
166		}
167
168		printf("patching file %s\n", new_filename);
169
170		/* Handle each hunk */
171		patch_line = xmalloc_fgets(patch_file);
172		while (patch_line) {
173			unsigned int count;
174			unsigned int src_beg_line;
175			unsigned int unused;
176			unsigned int hunk_offset_start = 0;
177			int hunk_error = 0;
178
179			/* This bit should be improved */
180			if ((sscanf(patch_line, "@@ -%d,%d +%d,%d @@", &src_beg_line, &unused, &dest_beg_line, &unused) != 4) &&
181				(sscanf(patch_line, "@@ -%d,%d +%d @@", &src_beg_line, &unused, &dest_beg_line) != 3) &&
182				(sscanf(patch_line, "@@ -%d +%d,%d @@", &src_beg_line, &dest_beg_line, &unused) != 3)) {
183				/* No more hunks for this file */
184				break;
185			}
186			free(patch_line);
187			hunk_count++;
188
189			if (src_beg_line && dest_beg_line) {
190				/* Copy unmodified lines upto start of hunk */
191				/* src_beg_line will be 0 if its a new file */
192				count = src_beg_line - src_cur_line;
193				if (copy_lines(src_stream, dst_stream, count) != count) {
194					bb_error_msg_and_die("bad src file");
195				}
196				src_cur_line += count;
197				dest_cur_line += count;
198				copy_trailing_lines_flag = 1;
199			}
200			hunk_offset_start = src_cur_line;
201
202			while ((patch_line = xmalloc_fgets(patch_file)) != NULL) {
203				if ((*patch_line == '-') || (*patch_line == ' ')) {
204					char *src_line = NULL;
205					if (src_stream) {
206						src_line = xmalloc_fgets(src_stream);
207						if (!src_line) {
208							hunk_error++;
209							break;
210						} else {
211							src_cur_line++;
212						}
213						if (strcmp(src_line, patch_line + 1) != 0) {
214							bb_error_msg("hunk #%d FAILED at %d", hunk_count, hunk_offset_start);
215							hunk_error++;
216							free(patch_line);
217							/* Probably need to find next hunk, etc... */
218							/* but for now we just bail out */
219							patch_line = NULL;
220							break;
221						}
222						free(src_line);
223					}
224					if (*patch_line == ' ') {
225						fputs(patch_line + 1, dst_stream);
226						dest_cur_line++;
227					}
228				} else if (*patch_line == '+') {
229					fputs(patch_line + 1, dst_stream);
230					dest_cur_line++;
231				} else {
232					break;
233				}
234				free(patch_line);
235			}
236			if (hunk_error) {
237				bad_hunk_count++;
238			}
239		}
240
241		/* Cleanup last patched file */
242		if (copy_trailing_lines_flag) {
243			copy_lines(src_stream, dst_stream, -1);
244		}
245		if (src_stream) {
246			fclose(src_stream);
247		}
248		if (dst_stream) {
249			fclose(dst_stream);
250		}
251		if (bad_hunk_count) {
252			if (!ret) {
253				ret = 1;
254			}
255			bb_error_msg("%d out of %d hunk FAILED", bad_hunk_count, hunk_count);
256		} else {
257			/* It worked, we can remove the backup */
258			if (backup_filename) {
259				unlink(backup_filename);
260			}
261			if ((dest_cur_line == 0) || (dest_beg_line == 0)) {
262				/* The new patched file is empty, remove it */
263				xunlink(new_filename);
264				if (strcmp(new_filename, original_filename) != 0)
265					xunlink(original_filename);
266			}
267		}
268	}
269
270	/* 0 = SUCCESS
271	 * 1 = Some hunks failed
272	 * 2 = More serious problems
273	 */
274	return ret;
275}
276