/* Produce a unidiff output from a diff_result. */
/*
* Copyright (c) 2020 Neels Hofmeyr <neels@hofmeyr.de>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <arraylist.h>
#include <diff_main.h>
#include <diff_output.h>
#include "diff_internal.h"
#include "diff_debug.h"
bool
diff_chunk_context_empty(const struct diff_chunk_context *cc)
{
return diff_range_empty(&cc->chunk);
}
int
diff_chunk_get_left_start(const struct diff_chunk *c,
const struct diff_result *r, int context_lines)
{
int left_start = diff_atom_root_idx(r->left, c->left_start);
return MAX(0, left_start - context_lines);
}
int
diff_chunk_get_left_end(const struct diff_chunk *c,
const struct diff_result *r, int context_lines)
{
int left_start = diff_chunk_get_left_start(c, r, 0);
return MIN(r->left->atoms.len,
left_start + c->left_count + context_lines);
}
int
diff_chunk_get_right_start(const struct diff_chunk *c,
const struct diff_result *r, int context_lines)
{
int right_start = diff_atom_root_idx(r->right, c->right_start);
return MAX(0, right_start - context_lines);
}
int
diff_chunk_get_right_end(const struct diff_chunk *c,
const struct diff_result *r, int context_lines)
{
int right_start = diff_chunk_get_right_start(c, r, 0);
return MIN(r->right->atoms.len,
right_start + c->right_count + context_lines);
}
struct diff_chunk *
diff_chunk_get(const struct diff_result *r, int chunk_idx)
{
return &r->chunks.head[chunk_idx];
}
int
diff_chunk_get_left_count(struct diff_chunk *c)
{
return c->left_count;
}
int
diff_chunk_get_right_count(struct diff_chunk *c)
{
return c->right_count;
}
void
diff_chunk_context_get(struct diff_chunk_context *cc, const struct diff_result *r,
int chunk_idx, int context_lines)
{
const struct diff_chunk *c = &r->chunks.head[chunk_idx];
int left_start = diff_chunk_get_left_start(c, r, context_lines);
int left_end = diff_chunk_get_left_end(c, r, context_lines);
int right_start = diff_chunk_get_right_start(c, r, context_lines);
int right_end = diff_chunk_get_right_end(c, r, context_lines);
*cc = (struct diff_chunk_context){
.chunk = {
.start = chunk_idx,
.end = chunk_idx + 1,
},
.left = {
.start = left_start,
.end = left_end,
},
.right = {
.start = right_start,
.end = right_end,
},
};
}
bool
diff_chunk_contexts_touch(const struct diff_chunk_context *cc,
const struct diff_chunk_context *other)
{
return diff_ranges_touch(&cc->chunk, &other->chunk)
|| diff_ranges_touch(&cc->left, &other->left)
|| diff_ranges_touch(&cc->right, &other->right);
}
void
diff_chunk_contexts_merge(struct diff_chunk_context *cc,
const struct diff_chunk_context *other)
{
diff_ranges_merge(&cc->chunk, &other->chunk);
diff_ranges_merge(&cc->left, &other->left);
diff_ranges_merge(&cc->right, &other->right);
}
void
diff_chunk_context_load_change(struct diff_chunk_context *cc,
int *nchunks_used,
struct diff_result *result,
int start_chunk_idx,
int context_lines)
{
int i;
int seen_minus = 0, seen_plus = 0;
if (nchunks_used)
*nchunks_used = 0;
for (i = start_chunk_idx; i < result->chunks.len; i++) {
struct diff_chunk *chunk = &result->chunks.head[i];
enum diff_chunk_type t = diff_chunk_type(chunk);
struct diff_chunk_context next;
if (t != CHUNK_MINUS && t != CHUNK_PLUS) {
if (nchunks_used)
(*nchunks_used)++;
if (seen_minus || seen_plus)
break;
else
continue;
} else if (t == CHUNK_MINUS)
seen_minus = 1;
else if (t == CHUNK_PLUS)
seen_plus = 1;
if (diff_chunk_context_empty(cc)) {
/* Note down the start point, any number of subsequent
* chunks may be joined up to this chunk by being
* directly adjacent. */
diff_chunk_context_get(cc, result, i, context_lines);
if (nchunks_used)
(*nchunks_used)++;
continue;
}
/* There already is a previous chunk noted down for being
* printed. Does it join up with this one? */
diff_chunk_context_get(&next, result, i, context_lines);
if (diff_chunk_contexts_touch(cc, &next)) {
/* This next context touches or overlaps the previous
* one, join. */
diff_chunk_contexts_merge(cc, &next);
if (nchunks_used)
(*nchunks_used)++;
continue;
} else
break;
}
}
struct diff_output_unidiff_state {
bool header_printed;
char prototype[DIFF_FUNCTION_CONTEXT_SIZE];
int last_prototype_idx;
};
struct diff_output_unidiff_state *
diff_output_unidiff_state_alloc(void)
{
struct diff_output_unidiff_state *state;
state = calloc(1, sizeof(struct diff_output_unidiff_state));
if (state != NULL)
diff_output_unidiff_state_reset(state);
return state;
}
void
diff_output_unidiff_state_reset(struct diff_output_unidiff_state *state)
{
state->header_printed = false;
memset(state->prototype, 0, sizeof(state->prototype));
state->last_prototype_idx = 0;
}
void
diff_output_unidiff_state_free(struct diff_output_unidiff_state *state)
{
free(state);
}
static int
output_unidiff_chunk(struct diff_output_info *outinfo, FILE *dest,
struct diff_output_unidiff_state *state,
const struct diff_input_info *info,
const struct diff_result *result,
bool print_header, bool show_function_prototypes,
const struct diff_chunk_context *cc)
{
int rc, left_start, left_len, right_start, right_len;
off_t outoff = 0, *offp;
if (diff_range_empty(&cc->left) && diff_range_empty(&cc->right))
return DIFF_RC_OK;
if (outinfo && outinfo->line_offsets.len > 0) {
unsigned int idx = outinfo->line_offsets.len - 1;
outoff = outinfo->line_offsets.head[idx];
}
if (print_header && !(state->header_printed)) {
rc = fprintf(dest, "--- %s\n",
diff_output_get_label_left(info));
if (rc < 0)
return errno;
if (outinfo) {
ARRAYLIST_ADD(offp, outinfo->line_offsets);
if (offp == NULL)
return ENOMEM;
outoff += rc;
*offp = outoff;
}
rc = fprintf(dest, "+++ %s\n",
diff_output_get_label_right(info));
if (rc < 0)
return errno;
if (outinfo) {
ARRAYLIST_ADD(offp, outinfo->line_offsets);
if (offp == NULL)
return ENOMEM;
outoff += rc;
*offp = outoff;
}
state->header_printed = true;
}
left_len = cc->left.end - cc->left.start;
if (result->left->atoms.len == 0)
left_start = 0;
else if (left_len == 0 && cc->left.start > 0)
left_start = cc->left.start;
else
left_start = cc->left.start + 1;
right_len = cc->right.end - cc->right.start;
if (result->right->atoms.len == 0)
right_start = 0;
else if (right_len == 0 && cc->right.start > 0)
right_start = cc->right.start;
else
right_start = cc->right.start + 1;
if (show_function_prototypes) {
rc = diff_output_match_function_prototype(state->prototype,
sizeof(state->prototype), &state->last_prototype_idx,
result, cc);
if (rc)
return rc;
}
if (left_len == 1 && right_len == 1) {
rc = fprintf(dest, "@@ -%d +%d @@%s%s\n",
left_start, right_start,
state->prototype[0] ? " " : "",
state->prototype[0] ? state->prototype : "");
} else if (left_len == 1 && right_len != 1) {
rc = fprintf(dest, "@@ -%d +%d,%d @@%s%s\n",
left_start, right_start, right_len,
state->prototype[0] ? " " : "",
state->prototype[0] ? state->prototype : "");
} else if (left_len != 1 && right_len == 1) {
rc = fprintf(dest, "@@ -%d,%d +%d @@%s%s\n",
left_start, left_len, right_start,
state->prototype[0] ? " " : "",
state->prototype[0] ? state->prototype : "");
} else {
rc = fprintf(dest, "@@ -%d,%d +%d,%d @@%s%s\n",
left_start, left_len, right_start, right_len,
state->prototype[0] ? " " : "",
state->prototype[0] ? state->prototype : "");
}
if (rc < 0)
return errno;
if (outinfo) {
ARRAYLIST_ADD(offp, outinfo->line_offsets);
if (offp == NULL)
return ENOMEM;
outoff += rc;
*offp = outoff;
}
/* Got the absolute line numbers where to start printing, and the index
* of the interesting (non-context) chunk.
* To print context lines above the interesting chunk, nipping on the
* previous chunk index may be necessary.
* It is guaranteed to be only context lines where left == right, so it
* suffices to look on the left. */
const struct diff_chunk *first_chunk;
int chunk_start_line;
first_chunk = &result->chunks.head[cc->chunk.start];
chunk_start_line = diff_atom_root_idx(result->left,
first_chunk->left_start);
if (cc->left.start < chunk_start_line) {
rc = diff_output_lines(outinfo, dest, " ",
&result->left->atoms.head[cc->left.start],
chunk_start_line - cc->left.start);
if (rc)
return rc;
}
/* Now write out all the joined chunks and contexts between them */
int c_idx;
for (c_idx = cc->chunk.start; c_idx < cc->chunk.end; c_idx++) {
const struct diff_chunk *c = &result->chunks.head[c_idx];
if (c->left_count && c->right_count)
rc = diff_output_lines(outinfo, dest,
c->solved ? " " : "?",
c->left_start, c->left_count);
else if (c->left_count && !c->right_count)
rc = diff_output_lines(outinfo, dest,
c->solved ? "-" : "?",
c->left_start, c->left_count);
else if (c->right_count && !c->left_count)
rc = diff_output_lines(outinfo, dest,
c->solved ? "+" : "?",
c->right_start, c->right_count);
if (rc)
return rc;
if (cc->chunk.end == result->chunks.len) {
rc = diff_output_trailing_newline_msg(outinfo, dest, c);
if (rc != DIFF_RC_OK)
return rc;
}
}
/* Trailing context? */
const struct diff_chunk *last_chunk;
int chunk_end_line;
last_chunk = &result->chunks.head[cc->chunk.end - 1];
chunk_end_line = diff_atom_root_idx(result->left,
last_chunk->left_start
+ last_chunk->left_count);
if (cc->left.end > chunk_end_line) {
rc = diff_output_lines(outinfo, dest, " ",
&result->left->atoms.head[chunk_end_line],
cc->left.end - chunk_end_line);
if (rc)
return rc;
}
return DIFF_RC_OK;
}
int
diff_output_unidiff_chunk(struct diff_output_info **output_info, FILE *dest,
struct diff_output_unidiff_state *state,
const struct diff_input_info *info,
const struct diff_result *result,
const struct diff_chunk_context *cc)
{
struct diff_output_info *outinfo = NULL;
int flags = (result->left->root->diff_flags |
result->right->root->diff_flags);
bool show_function_prototypes = (flags & DIFF_FLAG_SHOW_PROTOTYPES);
if (output_info) {
*output_info = diff_output_info_alloc();
if (*output_info == NULL)
return ENOMEM;
outinfo = *output_info;
}
return output_unidiff_chunk(outinfo, dest, state, info,
result, false, show_function_prototypes, cc);
}
int
diff_output_unidiff(struct diff_output_info **output_info,
FILE *dest, const struct diff_input_info *info,
const struct diff_result *result,
unsigned int context_lines)
{
struct diff_output_unidiff_state *state;
struct diff_chunk_context cc = {};
struct diff_output_info *outinfo = NULL;
int atomizer_flags = (result->left->atomizer_flags|
result->right->atomizer_flags);
int flags = (result->left->root->diff_flags |
result->right->root->diff_flags);
bool show_function_prototypes = (flags & DIFF_FLAG_SHOW_PROTOTYPES);
bool force_text = (flags & DIFF_FLAG_FORCE_TEXT_DATA);
bool have_binary = (atomizer_flags & DIFF_ATOMIZER_FOUND_BINARY_DATA);
int i;
if (!result)
return EINVAL;
if (result->rc != DIFF_RC_OK)
return result->rc;
if (output_info) {
*output_info = diff_output_info_alloc();
if (*output_info == NULL)
return ENOMEM;
outinfo = *output_info;
}
if (have_binary && !force_text) {
for (i = 0; i < result->chunks.len; i++) {
struct diff_chunk *c = &result->chunks.head[i];
enum diff_chunk_type t = diff_chunk_type(c);
if (t != CHUNK_MINUS && t != CHUNK_PLUS)
continue;
fprintf(dest, "Binary files %s and %s differ\n",
diff_output_get_label_left(info),
diff_output_get_label_right(info));
break;
}
return DIFF_RC_OK;
}
state = diff_output_unidiff_state_alloc();
if (state == NULL) {
if (output_info) {
diff_output_info_free(*output_info);
*output_info = NULL;
}
return ENOMEM;
}
#if DEBUG
unsigned int check_left_pos, check_right_pos;
check_left_pos = 0;
check_right_pos = 0;
for (i = 0; i < result->chunks.len; i++) {
struct diff_chunk *c = &result->chunks.head[i];
enum diff_chunk_type t = diff_chunk_type(c);
debug("[%d] %s lines L%d R%d @L %d @R %d\n",
i, (t == CHUNK_MINUS ? "minus" :
(t == CHUNK_PLUS ? "plus" :
(t == CHUNK_SAME ? "same" : "?"))),
c->left_count,
c->right_count,
c->left_start ? diff_atom_root_idx(result->left, c->left_start) : -1,
c->right_start ? diff_atom_root_idx(result->right, c->right_start) : -1);
assert(check_left_pos == diff_atom_root_idx(result->left, c->left_start));
assert(check_right_pos == diff_atom_root_idx(result->right, c->right_start));
check_left_pos += c->left_count;
check_right_pos += c->right_count;
}
assert(check_left_pos == result->left->atoms.len);
assert(check_right_pos == result->right->atoms.len);
#endif
for (i = 0; i < result->chunks.len; i++) {
struct diff_chunk *c = &result->chunks.head[i];
enum diff_chunk_type t = diff_chunk_type(c);
struct diff_chunk_context next;
if (t != CHUNK_MINUS && t != CHUNK_PLUS)
continue;
if (diff_chunk_context_empty(&cc)) {
/* These are the first lines being printed.
* Note down the start point, any number of subsequent
* chunks may be joined up to this unidiff chunk by
* context lines or by being directly adjacent. */
diff_chunk_context_get(&cc, result, i, context_lines);
debug("new chunk to be printed:"
" chunk %d-%d left %d-%d right %d-%d\n",
cc.chunk.start, cc.chunk.end,
cc.left.start, cc.left.end,
cc.right.start, cc.right.end);
continue;
}
/* There already is a previous chunk noted down for being
* printed. Does it join up with this one? */
diff_chunk_context_get(&next, result, i, context_lines);
debug("new chunk to be printed:"
" chunk %d-%d left %d-%d right %d-%d\n",
next.chunk.start, next.chunk.end,
next.left.start, next.left.end,
next.right.start, next.right.end);
if (diff_chunk_contexts_touch(&cc, &next)) {
/* This next context touches or overlaps the previous
* one, join. */
diff_chunk_contexts_merge(&cc, &next);
debug("new chunk to be printed touches previous chunk,"
" now: left %d-%d right %d-%d\n",
cc.left.start, cc.left.end,
cc.right.start, cc.right.end);
continue;
}
/* No touching, so the previous context is complete with a gap
* between it and this next one. Print the previous one and
* start fresh here. */
debug("new chunk to be printed does not touch previous chunk;"
" print left %d-%d right %d-%d\n",
cc.left.start, cc.left.end, cc.right.start, cc.right.end);
output_unidiff_chunk(outinfo, dest, state, info, result,
true, show_function_prototypes, &cc);
cc = next;
debug("new unprinted chunk is left %d-%d right %d-%d\n",
cc.left.start, cc.left.end, cc.right.start, cc.right.end);
}
if (!diff_chunk_context_empty(&cc))
output_unidiff_chunk(outinfo, dest, state, info, result,
true, show_function_prototypes, &cc);
diff_output_unidiff_state_free(state);
return DIFF_RC_OK;
}