xref: /freebsd/contrib/libdiff/lib/diff_output_unidiff.c (revision db33c6f3ae9d1231087710068ee4ea5398aacca7)
1 /* Produce a unidiff output from a diff_result. */
2 /*
3  * Copyright (c) 2020 Neels Hofmeyr <neels@hofmeyr.de>
4  *
5  * Permission to use, copy, modify, and distribute this software for any
6  * purpose with or without fee is hereby granted, provided that the above
7  * copyright notice and this permission notice appear in all copies.
8  *
9  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16  */
17 
18 #include <errno.h>
19 #include <stdbool.h>
20 #include <stdint.h>
21 #include <stdio.h>
22 #include <stdlib.h>
23 #include <string.h>
24 #include <assert.h>
25 
26 #include <arraylist.h>
27 #include <diff_main.h>
28 #include <diff_output.h>
29 
30 #include "diff_internal.h"
31 #include "diff_debug.h"
32 
33 off_t
34 diff_chunk_get_left_start_pos(const struct diff_chunk *c)
35 {
36 	return c->left_start->pos;
37 }
38 
39 off_t
40 diff_chunk_get_right_start_pos(const struct diff_chunk *c)
41 {
42 	return c->right_start->pos;
43 }
44 
45 bool
46 diff_chunk_context_empty(const struct diff_chunk_context *cc)
47 {
48 	return diff_range_empty(&cc->chunk);
49 }
50 
51 int
52 diff_chunk_get_left_start(const struct diff_chunk *c,
53     const struct diff_result *r, int context_lines)
54 {
55 	int left_start = diff_atom_root_idx(r->left, c->left_start);
56 	return MAX(0, left_start - context_lines);
57 }
58 
59 int
60 diff_chunk_get_left_end(const struct diff_chunk *c,
61     const struct diff_result *r, int context_lines)
62 {
63 	int left_start = diff_chunk_get_left_start(c, r, 0);
64 	return MIN(r->left->atoms.len,
65 	    left_start + c->left_count + context_lines);
66 }
67 
68 int
69 diff_chunk_get_right_start(const struct diff_chunk *c,
70     const struct diff_result *r, int context_lines)
71 {
72 	int right_start = diff_atom_root_idx(r->right, c->right_start);
73 	return MAX(0, right_start - context_lines);
74 }
75 
76 int
77 diff_chunk_get_right_end(const struct diff_chunk *c,
78     const struct diff_result *r, int context_lines)
79 {
80 	int right_start = diff_chunk_get_right_start(c, r, 0);
81 	return MIN(r->right->atoms.len,
82 	    right_start + c->right_count + context_lines);
83 }
84 
85 struct diff_chunk *
86 diff_chunk_get(const struct diff_result *r, int chunk_idx)
87 {
88 	return &r->chunks.head[chunk_idx];
89 }
90 
91 int
92 diff_chunk_get_left_count(struct diff_chunk *c)
93 {
94 	return c->left_count;
95 }
96 
97 int
98 diff_chunk_get_right_count(struct diff_chunk *c)
99 {
100 	return c->right_count;
101 }
102 
103 void
104 diff_chunk_context_get(struct diff_chunk_context *cc, const struct diff_result *r,
105 		  int chunk_idx, int context_lines)
106 {
107 	const struct diff_chunk *c = &r->chunks.head[chunk_idx];
108 	int left_start = diff_chunk_get_left_start(c, r, context_lines);
109 	int left_end = diff_chunk_get_left_end(c, r, context_lines);
110 	int right_start = diff_chunk_get_right_start(c, r, context_lines);
111 	int right_end = diff_chunk_get_right_end(c, r,  context_lines);
112 
113 	*cc = (struct diff_chunk_context){
114 		.chunk = {
115 			.start = chunk_idx,
116 			.end = chunk_idx + 1,
117 		},
118 		.left = {
119 			.start = left_start,
120 			.end = left_end,
121 		},
122 		.right = {
123 			.start = right_start,
124 			.end = right_end,
125 		},
126 	};
127 }
128 
129 bool
130 diff_chunk_contexts_touch(const struct diff_chunk_context *cc,
131 			  const struct diff_chunk_context *other)
132 {
133 	return diff_ranges_touch(&cc->chunk, &other->chunk)
134 		|| diff_ranges_touch(&cc->left, &other->left)
135 		|| diff_ranges_touch(&cc->right, &other->right);
136 }
137 
138 void
139 diff_chunk_contexts_merge(struct diff_chunk_context *cc,
140 			  const struct diff_chunk_context *other)
141 {
142 	diff_ranges_merge(&cc->chunk, &other->chunk);
143 	diff_ranges_merge(&cc->left, &other->left);
144 	diff_ranges_merge(&cc->right, &other->right);
145 }
146 
147 void
148 diff_chunk_context_load_change(struct diff_chunk_context *cc,
149 			       int *nchunks_used,
150 			       struct diff_result *result,
151 			       int start_chunk_idx,
152 			       int context_lines)
153 {
154 	int i;
155 	int seen_minus = 0, seen_plus = 0;
156 
157 	if (nchunks_used)
158 		*nchunks_used = 0;
159 
160 	for (i = start_chunk_idx; i < result->chunks.len; i++) {
161 		struct diff_chunk *chunk = &result->chunks.head[i];
162 		enum diff_chunk_type t = diff_chunk_type(chunk);
163 		struct diff_chunk_context next;
164 
165 		if (t != CHUNK_MINUS && t != CHUNK_PLUS) {
166 			if (nchunks_used)
167 				(*nchunks_used)++;
168 			if (seen_minus || seen_plus)
169 				break;
170 			else
171 				continue;
172 		} else if (t == CHUNK_MINUS)
173 			seen_minus = 1;
174 		else if (t == CHUNK_PLUS)
175 			seen_plus = 1;
176 
177 		if (diff_chunk_context_empty(cc)) {
178 			/* Note down the start point, any number of subsequent
179 			 * chunks may be joined up to this chunk by being
180 			 * directly adjacent. */
181 			diff_chunk_context_get(cc, result, i, context_lines);
182 			if (nchunks_used)
183 				(*nchunks_used)++;
184 			continue;
185 		}
186 
187 		/* There already is a previous chunk noted down for being
188 		 * printed. Does it join up with this one? */
189 		diff_chunk_context_get(&next, result, i, context_lines);
190 
191 		if (diff_chunk_contexts_touch(cc, &next)) {
192 			/* This next context touches or overlaps the previous
193 			 * one, join. */
194 			diff_chunk_contexts_merge(cc, &next);
195 			if (nchunks_used)
196 				(*nchunks_used)++;
197 			continue;
198 		} else
199 			break;
200 	}
201 }
202 
203 struct diff_output_unidiff_state {
204 	bool header_printed;
205 	char prototype[DIFF_FUNCTION_CONTEXT_SIZE];
206 	int last_prototype_idx;
207 };
208 
209 struct diff_output_unidiff_state *
210 diff_output_unidiff_state_alloc(void)
211 {
212 	struct diff_output_unidiff_state *state;
213 
214 	state = calloc(1, sizeof(struct diff_output_unidiff_state));
215 	if (state != NULL)
216 		diff_output_unidiff_state_reset(state);
217 	return state;
218 }
219 
220 void
221 diff_output_unidiff_state_reset(struct diff_output_unidiff_state *state)
222 {
223 	state->header_printed = false;
224 	memset(state->prototype, 0, sizeof(state->prototype));
225 	state->last_prototype_idx = 0;
226 }
227 
228 void
229 diff_output_unidiff_state_free(struct diff_output_unidiff_state *state)
230 {
231 	free(state);
232 }
233 
234 static int
235 output_unidiff_chunk(struct diff_output_info *outinfo, FILE *dest,
236 		     struct diff_output_unidiff_state *state,
237 		     const struct diff_input_info *info,
238 		     const struct diff_result *result,
239 		     bool print_header, bool show_function_prototypes,
240 		     const struct diff_chunk_context *cc)
241 {
242 	int rc, left_start, left_len, right_start, right_len;
243 	off_t outoff = 0, *offp;
244 	uint8_t *typep;
245 
246 	if (diff_range_empty(&cc->left) && diff_range_empty(&cc->right))
247 		return DIFF_RC_OK;
248 
249 	if (outinfo && outinfo->line_offsets.len > 0) {
250 		unsigned int idx = outinfo->line_offsets.len - 1;
251 		outoff = outinfo->line_offsets.head[idx];
252 	}
253 
254 	if (print_header && !(state->header_printed)) {
255 		rc = fprintf(dest, "--- %s\n",
256 		    diff_output_get_label_left(info));
257 		if (rc < 0)
258 			return errno;
259 		if (outinfo) {
260 			ARRAYLIST_ADD(offp, outinfo->line_offsets);
261 			if (offp == NULL)
262 				return ENOMEM;
263 			outoff += rc;
264 			*offp = outoff;
265 			ARRAYLIST_ADD(typep, outinfo->line_types);
266 			if (typep == NULL)
267 				return ENOMEM;
268 			*typep = DIFF_LINE_MINUS;
269 		}
270 		rc = fprintf(dest, "+++ %s\n",
271 		    diff_output_get_label_right(info));
272 		if (rc < 0)
273 			return errno;
274 		if (outinfo) {
275 			ARRAYLIST_ADD(offp, outinfo->line_offsets);
276 			if (offp == NULL)
277 				return ENOMEM;
278 			outoff += rc;
279 			*offp = outoff;
280 			ARRAYLIST_ADD(typep, outinfo->line_types);
281 			if (typep == NULL)
282 				return ENOMEM;
283 			*typep = DIFF_LINE_PLUS;
284 		}
285 		state->header_printed = true;
286 	}
287 
288 	left_len = cc->left.end - cc->left.start;
289 	if (result->left->atoms.len == 0)
290 		left_start = 0;
291 	else if (left_len == 0 && cc->left.start > 0)
292 		left_start = cc->left.start;
293 	else
294 		left_start = cc->left.start + 1;
295 
296 	right_len = cc->right.end - cc->right.start;
297 	if (result->right->atoms.len == 0)
298 		right_start = 0;
299 	else if (right_len == 0 && cc->right.start > 0)
300 		right_start = cc->right.start;
301 	else
302 		right_start = cc->right.start + 1;
303 
304 	/* Got the absolute line numbers where to start printing, and the index
305 	 * of the interesting (non-context) chunk.
306 	 * To print context lines above the interesting chunk, nipping on the
307 	 * previous chunk index may be necessary.
308 	 * It is guaranteed to be only context lines where left == right, so it
309 	 * suffices to look on the left. */
310 	const struct diff_chunk *first_chunk;
311 	int chunk_start_line;
312 	first_chunk = &result->chunks.head[cc->chunk.start];
313 	chunk_start_line = diff_atom_root_idx(result->left,
314 					      first_chunk->left_start);
315 	if (show_function_prototypes) {
316 		rc = diff_output_match_function_prototype(state->prototype,
317 		    sizeof(state->prototype), &state->last_prototype_idx,
318 		    result, chunk_start_line);
319 		if (rc)
320 			return rc;
321 	}
322 
323 	if (left_len == 1 && right_len == 1) {
324 		rc = fprintf(dest, "@@ -%d +%d @@%s%s\n",
325 			left_start, right_start,
326 			state->prototype[0] ? " " : "",
327 			state->prototype[0] ? state->prototype : "");
328 	} else if (left_len == 1 && right_len != 1) {
329 		rc = fprintf(dest, "@@ -%d +%d,%d @@%s%s\n",
330 			left_start, right_start, right_len,
331 			state->prototype[0] ? " " : "",
332 			state->prototype[0] ? state->prototype : "");
333 	} else if (left_len != 1 && right_len == 1) {
334 		rc = fprintf(dest, "@@ -%d,%d +%d @@%s%s\n",
335 			left_start, left_len, right_start,
336 			state->prototype[0] ? " " : "",
337 			state->prototype[0] ? state->prototype : "");
338 	} else {
339 		rc = fprintf(dest, "@@ -%d,%d +%d,%d @@%s%s\n",
340 			left_start, left_len, right_start, right_len,
341 			state->prototype[0] ? " " : "",
342 			state->prototype[0] ? state->prototype : "");
343 	}
344 	if (rc < 0)
345 		return errno;
346 	if (outinfo) {
347 		ARRAYLIST_ADD(offp, outinfo->line_offsets);
348 		if (offp == NULL)
349 			return ENOMEM;
350 		outoff += rc;
351 		*offp = outoff;
352 		ARRAYLIST_ADD(typep, outinfo->line_types);
353 		if (typep == NULL)
354 			return ENOMEM;
355 		*typep = DIFF_LINE_HUNK;
356 	}
357 
358 	if (cc->left.start < chunk_start_line) {
359 		rc = diff_output_lines(outinfo, dest, " ",
360 				  &result->left->atoms.head[cc->left.start],
361 				  chunk_start_line - cc->left.start);
362 		if (rc)
363 			return rc;
364 	}
365 
366 	/* Now write out all the joined chunks and contexts between them */
367 	int c_idx;
368 	for (c_idx = cc->chunk.start; c_idx < cc->chunk.end; c_idx++) {
369 		const struct diff_chunk *c = &result->chunks.head[c_idx];
370 
371 		if (c->left_count && c->right_count)
372 			rc = diff_output_lines(outinfo, dest,
373 					  c->solved ? " " : "?",
374 					  c->left_start, c->left_count);
375 		else if (c->left_count && !c->right_count)
376 			rc = diff_output_lines(outinfo, dest,
377 					  c->solved ? "-" : "?",
378 					  c->left_start, c->left_count);
379 		else if (c->right_count && !c->left_count)
380 			rc = diff_output_lines(outinfo, dest,
381 					  c->solved ? "+" : "?",
382 					  c->right_start, c->right_count);
383 		if (rc)
384 			return rc;
385 
386 		if (cc->chunk.end == result->chunks.len) {
387 			rc = diff_output_trailing_newline_msg(outinfo, dest, c);
388 			if (rc != DIFF_RC_OK)
389 				return rc;
390 		}
391 	}
392 
393 	/* Trailing context? */
394 	const struct diff_chunk *last_chunk;
395 	int chunk_end_line;
396 	last_chunk = &result->chunks.head[cc->chunk.end - 1];
397 	chunk_end_line = diff_atom_root_idx(result->left,
398 					    last_chunk->left_start
399 					    + last_chunk->left_count);
400 	if (cc->left.end > chunk_end_line) {
401 		rc = diff_output_lines(outinfo, dest, " ",
402 				  &result->left->atoms.head[chunk_end_line],
403 				  cc->left.end - chunk_end_line);
404 		if (rc)
405 			return rc;
406 
407 		if (cc->left.end == result->left->atoms.len) {
408 			rc = diff_output_trailing_newline_msg(outinfo, dest,
409 			    &result->chunks.head[result->chunks.len - 1]);
410 			if (rc != DIFF_RC_OK)
411 				return rc;
412 		}
413 	}
414 
415 	return DIFF_RC_OK;
416 }
417 
418 int
419 diff_output_unidiff_chunk(struct diff_output_info **output_info, FILE *dest,
420 			  struct diff_output_unidiff_state *state,
421 			  const struct diff_input_info *info,
422 			  const struct diff_result *result,
423 			  const struct diff_chunk_context *cc)
424 {
425 	struct diff_output_info *outinfo = NULL;
426 	int flags = (result->left->root->diff_flags |
427 	    result->right->root->diff_flags);
428 	bool show_function_prototypes = (flags & DIFF_FLAG_SHOW_PROTOTYPES);
429 
430 	if (output_info) {
431 		*output_info = diff_output_info_alloc();
432 		if (*output_info == NULL)
433 			return ENOMEM;
434 		outinfo = *output_info;
435 	}
436 
437 	return output_unidiff_chunk(outinfo, dest, state, info,
438 	    result, false, show_function_prototypes, cc);
439 }
440 
441 int
442 diff_output_unidiff(struct diff_output_info **output_info,
443 		    FILE *dest, const struct diff_input_info *info,
444 		    const struct diff_result *result,
445 		    unsigned int context_lines)
446 {
447 	struct diff_output_unidiff_state *state;
448 	struct diff_chunk_context cc = {};
449 	struct diff_output_info *outinfo = NULL;
450 	int atomizer_flags = (result->left->atomizer_flags|
451 	    result->right->atomizer_flags);
452 	int flags = (result->left->root->diff_flags |
453 	    result->right->root->diff_flags);
454 	bool show_function_prototypes = (flags & DIFF_FLAG_SHOW_PROTOTYPES);
455 	bool force_text = (flags & DIFF_FLAG_FORCE_TEXT_DATA);
456 	bool have_binary = (atomizer_flags & DIFF_ATOMIZER_FOUND_BINARY_DATA);
457 	off_t outoff = 0, *offp;
458 	uint8_t *typep;
459 	int rc, i;
460 
461 	if (!result)
462 		return EINVAL;
463 	if (result->rc != DIFF_RC_OK)
464 		return result->rc;
465 
466 	if (output_info) {
467 		*output_info = diff_output_info_alloc();
468 		if (*output_info == NULL)
469 			return ENOMEM;
470 		outinfo = *output_info;
471 	}
472 
473 	if (have_binary && !force_text) {
474 		for (i = 0; i < result->chunks.len; i++) {
475 			struct diff_chunk *c = &result->chunks.head[i];
476 			enum diff_chunk_type t = diff_chunk_type(c);
477 
478 			if (t != CHUNK_MINUS && t != CHUNK_PLUS)
479 				continue;
480 
481 			if (outinfo && outinfo->line_offsets.len > 0) {
482 				unsigned int idx =
483 				    outinfo->line_offsets.len - 1;
484 				outoff = outinfo->line_offsets.head[idx];
485 			}
486 
487 			rc = fprintf(dest, "Binary files %s and %s differ\n",
488 			    diff_output_get_label_left(info),
489 			    diff_output_get_label_right(info));
490 			if (outinfo) {
491 				ARRAYLIST_ADD(offp, outinfo->line_offsets);
492 				if (offp == NULL)
493 					return ENOMEM;
494 				outoff += rc;
495 				*offp = outoff;
496 				ARRAYLIST_ADD(typep, outinfo->line_types);
497 				if (typep == NULL)
498 					return ENOMEM;
499 				*typep = DIFF_LINE_NONE;
500 			}
501 			break;
502 		}
503 
504 		return DIFF_RC_OK;
505 	}
506 
507 	state = diff_output_unidiff_state_alloc();
508 	if (state == NULL) {
509 		if (output_info) {
510 			diff_output_info_free(*output_info);
511 			*output_info = NULL;
512 		}
513 		return ENOMEM;
514 	}
515 
516 #if DEBUG
517 	unsigned int check_left_pos, check_right_pos;
518 	check_left_pos = 0;
519 	check_right_pos = 0;
520 	for (i = 0; i < result->chunks.len; i++) {
521 		struct diff_chunk *c = &result->chunks.head[i];
522 		enum diff_chunk_type t = diff_chunk_type(c);
523 
524 		debug("[%d] %s lines L%d R%d @L %d @R %d\n",
525 		      i, (t == CHUNK_MINUS ? "minus" :
526 			  (t == CHUNK_PLUS ? "plus" :
527 			   (t == CHUNK_SAME ? "same" : "?"))),
528 		      c->left_count,
529 		      c->right_count,
530 		      c->left_start ? diff_atom_root_idx(result->left, c->left_start) : -1,
531 		      c->right_start ? diff_atom_root_idx(result->right, c->right_start) : -1);
532 		assert(check_left_pos == diff_atom_root_idx(result->left, c->left_start));
533 		assert(check_right_pos == diff_atom_root_idx(result->right, c->right_start));
534 		check_left_pos += c->left_count;
535 		check_right_pos += c->right_count;
536 
537 	}
538 	assert(check_left_pos == result->left->atoms.len);
539 	assert(check_right_pos == result->right->atoms.len);
540 #endif
541 
542 	for (i = 0; i < result->chunks.len; i++) {
543 		struct diff_chunk *c = &result->chunks.head[i];
544 		enum diff_chunk_type t = diff_chunk_type(c);
545 		struct diff_chunk_context next;
546 
547 		if (t != CHUNK_MINUS && t != CHUNK_PLUS)
548 			continue;
549 
550 		if (diff_chunk_context_empty(&cc)) {
551 			/* These are the first lines being printed.
552 			 * Note down the start point, any number of subsequent
553 			 * chunks may be joined up to this unidiff chunk by
554 			 * context lines or by being directly adjacent. */
555 			diff_chunk_context_get(&cc, result, i, context_lines);
556 			debug("new chunk to be printed:"
557 			      " chunk %d-%d left %d-%d right %d-%d\n",
558 			      cc.chunk.start, cc.chunk.end,
559 			      cc.left.start, cc.left.end,
560 			      cc.right.start, cc.right.end);
561 			continue;
562 		}
563 
564 		/* There already is a previous chunk noted down for being
565 		 * printed. Does it join up with this one? */
566 		diff_chunk_context_get(&next, result, i, context_lines);
567 		debug("new chunk to be printed:"
568 		      " chunk %d-%d left %d-%d right %d-%d\n",
569 		      next.chunk.start, next.chunk.end,
570 		      next.left.start, next.left.end,
571 		      next.right.start, next.right.end);
572 
573 		if (diff_chunk_contexts_touch(&cc, &next)) {
574 			/* This next context touches or overlaps the previous
575 			 * one, join. */
576 			diff_chunk_contexts_merge(&cc, &next);
577 			debug("new chunk to be printed touches previous chunk,"
578 			      " now: left %d-%d right %d-%d\n",
579 			      cc.left.start, cc.left.end,
580 			      cc.right.start, cc.right.end);
581 			continue;
582 		}
583 
584 		/* No touching, so the previous context is complete with a gap
585 		 * between it and this next one. Print the previous one and
586 		 * start fresh here. */
587 		debug("new chunk to be printed does not touch previous chunk;"
588 		      " print left %d-%d right %d-%d\n",
589 		      cc.left.start, cc.left.end, cc.right.start, cc.right.end);
590 		output_unidiff_chunk(outinfo, dest, state, info, result,
591 		    true, show_function_prototypes, &cc);
592 		cc = next;
593 		debug("new unprinted chunk is left %d-%d right %d-%d\n",
594 		      cc.left.start, cc.left.end, cc.right.start, cc.right.end);
595 	}
596 
597 	if (!diff_chunk_context_empty(&cc))
598 		output_unidiff_chunk(outinfo, dest, state, info, result,
599 		    true, show_function_prototypes, &cc);
600 	diff_output_unidiff_state_free(state);
601 	return DIFF_RC_OK;
602 }
603