Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
n_diff.c
Go to the documentation of this file.
1/*
2 * Nilorea Library
3 * Copyright (C) 2005-2026 Castagnier Mickael
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14 * implied. See the License for the specific language governing
15 * permissions and limitations under the License.
16 *
17 * SPDX-License-Identifier: Apache-2.0
18 */
19
27
28#include "nilorea/n_diff.h"
29#include "nilorea/n_common.h"
30#include "nilorea/n_log.h"
31
32#include <limits.h>
33#include <stdio.h>
34#include <string.h>
35
37#define DIFF_OUT_PADDING 256
39#define DIFF_HEADER_BUF 128
40
42typedef struct DIFF_TEXT {
44 char* buf;
46 char** line;
48 size_t count;
49} DIFF_TEXT;
50
52static void _text_free(DIFF_TEXT* t) {
53 FreeNoLog(t->line);
54 FreeNoLog(t->buf);
55 t->count = 0;
56}
57
60static int _text_load(DIFF_TEXT* t, const char* text) {
61 size_t len = 0, nb = 1, idx = 0;
62 char* start = NULL;
63
64 t->buf = NULL;
65 t->line = NULL;
66 t->count = 0;
67
68 if (!text || !text[0]) return TRUE;
69
70 len = strlen(text);
71 for (size_t i = 0; i < len; i++) {
72 if (text[i] == '\n') nb++;
73 }
74 if (text[len - 1] == '\n') nb--;
75 if (nb == 0) return TRUE;
76
77 Malloc(t->buf, char, len + 1);
78 if (!t->buf) return FALSE;
79 memcpy(t->buf, text, len + 1);
80
81 Malloc(t->line, char*, nb);
82 if (!t->line) {
83 FreeNoLog(t->buf);
84 return FALSE;
85 }
86
87 start = t->buf;
88 for (size_t i = 0; i <= len && idx < nb; i++) {
89 if (t->buf[i] == '\n' || t->buf[i] == '\0') {
90 t->buf[i] = '\0';
91 t->line[idx] = start;
92 idx++;
93 start = t->buf + i + 1;
94 }
95 }
96 t->count = idx;
97 return TRUE;
98}
99
101static void _line_free(void* ptr) {
103 if (!line) return;
104 FreeNoLog(line->text);
106}
107
110static int _emit(N_DIFF_RESULT* result, int op, const char* text, size_t line_a, size_t line_b) {
111 N_DIFF_LINE* line = NULL;
112
114 if (!line) return FALSE;
115
116 line->op = op;
117 line->line_a = line_a;
118 line->line_b = line_b;
119 line->text = strdup(text ? text : "");
120 if (!line->text) {
122 return FALSE;
123 }
124 if (list_unshift(result->lines, line, _line_free) != TRUE) {
126 return FALSE;
127 }
128
129 if (op == N_DIFF_COMMON) {
130 result->nb_common++;
131 } else if (op == N_DIFF_DELETE) {
132 result->nb_deleted++;
133 } else {
134 result->nb_inserted++;
135 }
136 return TRUE;
137}
138
144static int _myers(N_DIFF_RESULT* result, char** a, int n, char** b, int m, size_t base_a, size_t base_b, int max_edits) {
145 int* v = NULL;
146 int** trace = NULL;
147 int maxd = n + m;
148 int voff = 0, found = -1, ret = -1;
149
150 if (maxd > max_edits) maxd = max_edits;
151 voff = maxd + 1;
152
153 Malloc(v, int, (size_t)(2 * maxd + 3));
154 if (!v) return -1;
155 Malloc(trace, int*, (size_t)(maxd + 1));
156 if (!trace) {
157 FreeNoLog(v);
158 return -1;
159 }
160
161 for (int d = 0; d <= maxd; d++) {
162 int* snap = NULL;
163
164 /* the backtrack reads the state as it was before this step */
165 Malloc(snap, int, (size_t)(2 * d + 3));
166 if (!snap) goto cleanup;
167 for (int k = -d - 1; k <= d + 1; k++) {
168 snap[k + d + 1] = v[k + voff];
169 }
170 trace[d] = snap;
171
172 for (int k = -d; k <= d; k += 2) {
173 int x = 0, y = 0;
174 if (k == -d || (k != d && v[k - 1 + voff] < v[k + 1 + voff])) {
175 /* move down: b contributes a line */
176 x = v[k + 1 + voff];
177 } else {
178 /* move right: a loses a line */
179 x = v[k - 1 + voff] + 1;
180 }
181 y = x - k;
182 while (x < n && y < m && strcmp(a[x], b[y]) == 0) {
183 x++;
184 y++;
185 }
186 v[k + voff] = x;
187 if (x >= n && y >= m) {
188 found = d;
189 break;
190 }
191 }
192 if (found >= 0) break;
193 }
194
195 if (found < 0) {
196 ret = 0;
197 goto cleanup;
198 }
199
200 {
201 int x = n, y = m;
202 for (int d = found; d >= 0; d--) {
203 const int* snap = trace[d];
204 int k = x - y;
205 int prev_k = 0, prev_x = 0, prev_y = 0;
206
207 if (d > 0) {
208 if (k == -d || (k != d && snap[k - 1 + d + 1] < snap[k + 1 + d + 1])) {
209 prev_k = k + 1;
210 } else {
211 prev_k = k - 1;
212 }
213 prev_x = snap[prev_k + d + 1];
214 prev_y = prev_x - prev_k;
215 if (prev_x < 0 || prev_x > n || prev_y < 0 || prev_y > m) {
216 n_log(LOG_ERR, "n_diff: inconsistent trace at d %d, giving up", d);
217 goto cleanup;
218 }
219 }
220 /* d 0 is the leading snake: it runs diagonally from the origin,
221 so walking back to (0,0) is all that is left */
222
223 while (x > prev_x && y > prev_y) {
224 x--;
225 y--;
226 if (!_emit(result, N_DIFF_COMMON, a[x], base_a + (size_t)x, base_b + (size_t)y)) goto cleanup;
227 }
228 if (d > 0) {
229 if (x == prev_x) {
230 y--;
231 if (!_emit(result, N_DIFF_INSERT, b[y], N_DIFF_NO_LINE, base_b + (size_t)y)) goto cleanup;
232 } else {
233 x--;
234 if (!_emit(result, N_DIFF_DELETE, a[x], base_a + (size_t)x, N_DIFF_NO_LINE)) goto cleanup;
235 }
236 }
237 }
238 }
239 ret = 1;
240
241cleanup:
242 if (trace) {
243 for (int d = 0; d <= maxd; d++) {
244 FreeNoLog(trace[d]);
245 }
246 FreeNoLog(trace);
247 }
248 FreeNoLog(v);
249 return ret;
250}
251
252N_DIFF_RESULT* n_diff_lines(const char* text_a, const char* text_b, size_t max_edits) {
253 DIFF_TEXT ta, tb;
254 N_DIFF_RESULT* result = NULL;
255 size_t pre = 0, suf = 0, mid_n = 0, mid_m = 0;
256 int rc = 0;
257
258 if (max_edits == 0) max_edits = N_DIFF_MAX_EDITS_DEFAULT;
259 if (max_edits > N_DIFF_MAX_EDITS_CEILING) max_edits = N_DIFF_MAX_EDITS_CEILING;
260
261 if (!_text_load(&ta, text_a)) return NULL;
262 if (!_text_load(&tb, text_b)) {
263 _text_free(&ta);
264 return NULL;
265 }
266
267 Malloc(result, N_DIFF_RESULT, 1);
268 if (!result) goto fail;
270 if (!result->lines) goto fail;
271
272 /* identical head and tail cost nothing to match and shrink the region the
273 bounded search has to cover, which is what keeps the common case (two
274 runs of the same request) far below the edit ceiling */
275 while (pre < ta.count && pre < tb.count && strcmp(ta.line[pre], tb.line[pre]) == 0) {
276 pre++;
277 }
278 // cppcheck-suppress knownConditionTrueFalse ; cppcheck only follows the head loop exit where it ran to exhaustion, where nothing is left to match
279 while (suf < ta.count - pre && suf < tb.count - pre &&
280 strcmp(ta.line[ta.count - 1 - suf], tb.line[tb.count - 1 - suf]) == 0) {
281 suf++;
282 }
283 mid_n = ta.count - pre - suf;
284 mid_m = tb.count - pre - suf;
285
286 /* the script is built back to front: tail, then the middle, then head */
287 for (size_t i = 0; i < suf; i++) {
288 size_t ia = ta.count - 1 - i;
289 size_t ib = tb.count - 1 - i;
290 if (!_emit(result, N_DIFF_COMMON, ta.line[ia], ia, ib)) goto fail;
291 }
292
293 if (mid_n == 0 && mid_m == 0) {
294 rc = 1;
295 } else if (mid_n > INT_MAX / 2 || mid_m > INT_MAX / 2) {
296 /* more lines than the trace arithmetic can index */
297 rc = 0;
298 } else {
299 rc = _myers(result, ta.line + pre, (int)mid_n, tb.line + pre, (int)mid_m, pre, pre, (int)max_edits);
300 }
301 if (rc < 0) goto fail;
302
303 if (rc == 0) {
304 /* too far apart to search: report the whole differing region as one
305 delete run followed by one insert run, which is coarse but never
306 misleading, and say so through truncated */
307 result->truncated = 1;
308 for (size_t i = mid_m; i > 0; i--) {
309 size_t ib = pre + i - 1;
310 if (!_emit(result, N_DIFF_INSERT, tb.line[ib], N_DIFF_NO_LINE, ib)) goto fail;
311 }
312 for (size_t i = mid_n; i > 0; i--) {
313 size_t ia = pre + i - 1;
314 if (!_emit(result, N_DIFF_DELETE, ta.line[ia], ia, N_DIFF_NO_LINE)) goto fail;
315 }
316 }
317
318 for (size_t i = pre; i > 0; i--) {
319 if (!_emit(result, N_DIFF_COMMON, ta.line[i - 1], i - 1, i - 1)) goto fail;
320 }
321
322 _text_free(&ta);
323 _text_free(&tb);
324 return result;
325
326fail:
327 n_diff_result_free(&result);
328 _text_free(&ta);
329 _text_free(&tb);
330 return NULL;
331}
332
334 if (!result || !*result) return;
335 if ((*result)->lines) {
336 list_destroy(&(*result)->lines);
337 }
338 FreeNoLog(*result);
339}
340
350
352static int _out_mem(DIFF_OUT* o, const char* s, size_t n) {
353 if (n == 0) return TRUE;
354 return write_and_fit_ex(&o->buf, &o->size, &o->written, s, (NSTRBYTE)n, DIFF_OUT_PADDING);
355}
356
358static int _out_str(DIFF_OUT* o, const char* s) {
359 return _out_mem(o, s, strlen(s));
360}
361
364 N_STR* out = NULL;
365 if (!o->buf) return char_to_nstr("");
366 out = char_to_nstr_nocopy(o->buf);
367 if (!out) {
368 FreeNoLog(o->buf);
369 }
370 o->buf = NULL;
371 o->size = 0;
372 o->written = 0;
373 return out;
374}
375
377static char _op_char(int op) {
378 if (op == N_DIFF_DELETE) return '-';
379 if (op == N_DIFF_INSERT) return '+';
380 return ' ';
381}
382
384static int _out_line(DIFF_OUT* o, const N_DIFF_LINE* line) {
385 char marker = _op_char(line->op);
386 if (!_out_mem(o, &marker, 1)) return FALSE;
387 if (!_out_str(o, line->text ? line->text : "")) return FALSE;
388 return _out_mem(o, "\n", 1);
389}
390
391N_STR* n_diff_to_unified(const N_DIFF_RESULT* result, size_t context) {
392 DIFF_OUT out;
393 N_DIFF_LINE** line = NULL;
394 size_t* a_before = NULL;
395 size_t* b_before = NULL;
396 size_t count = 0, idx = 0, seen_a = 0, seen_b = 0;
397
398 __n_assert(result, return NULL);
399 __n_assert(result->lines, return NULL);
400
401 memset(&out, 0, sizeof(out));
402
403 if (context == N_DIFF_CONTEXT_FULL) {
404 list_foreach(node, result->lines) {
405 const N_DIFF_LINE* cur = (const N_DIFF_LINE*)node->ptr;
406 if (!cur) continue;
407 if (!_out_line(&out, cur)) goto fail;
408 }
409 return _out_finish(&out);
410 }
411
412 count = result->lines->nb_items;
413 if (count == 0) return _out_finish(&out);
414
415 /* indexed access, plus how many lines of each side precede every
416 position, which is what the hunk headers are counted from */
417 Malloc(line, N_DIFF_LINE*, count);
418 if (!line) goto fail;
419 Malloc(a_before, size_t, count);
420 if (!a_before) goto fail;
421 Malloc(b_before, size_t, count);
422 if (!b_before) goto fail;
423
424 list_foreach(node, result->lines) {
425 N_DIFF_LINE* cur = (N_DIFF_LINE*)node->ptr;
426 /* skipping empties rather than storing them keeps every entry of line[]
427 non-NULL, which is what the hunk scan below relies on */
428 if (!cur) continue;
429 line[idx] = cur;
430 a_before[idx] = seen_a;
431 b_before[idx] = seen_b;
432 if (cur->op != N_DIFF_INSERT) seen_a++;
433 if (cur->op != N_DIFF_DELETE) seen_b++;
434 idx++;
435 }
436 count = idx;
437 if (count == 0) {
438 FreeNoLog(b_before);
439 FreeNoLog(a_before);
441 return _out_finish(&out);
442 }
443
444 idx = 0;
445 while (idx < count) {
446 size_t start = 0, end = 0, last = 0, scan = 0;
447 size_t len_a = 0, len_b = 0, start_a = 0, start_b = 0;
448 char header[DIFF_HEADER_BUF];
449
450 if (line[idx]->op == N_DIFF_COMMON) {
451 idx++;
452 continue;
453 }
454
455 start = (idx > context) ? (idx - context) : 0;
456 last = idx;
457 for (scan = idx; scan < count; scan++) {
458 if (line[scan]->op != N_DIFF_COMMON) {
459 last = scan;
460 } else if (scan - last > 2 * context) {
461 /* far enough from the last change to close the hunk; a
462 nearer one would have been merged into it */
463 break;
464 }
465 }
466 end = last + context;
467 if (end >= count) end = count - 1;
468
469 for (scan = start; scan <= end; scan++) {
470 if (line[scan]->op != N_DIFF_INSERT) len_a++;
471 if (line[scan]->op != N_DIFF_DELETE) len_b++;
472 }
473 start_a = len_a ? a_before[start] + 1 : a_before[start];
474 start_b = len_b ? b_before[start] + 1 : b_before[start];
475
476 snprintf(header, sizeof(header), "@@ -%zu,%zu +%zu,%zu @@\n", start_a, len_a, start_b, len_b);
477 if (!_out_str(&out, header)) goto fail;
478 for (scan = start; scan <= end; scan++) {
479 if (!_out_line(&out, line[scan])) goto fail;
480 }
481 idx = end + 1;
482 }
483
484 FreeNoLog(b_before);
485 FreeNoLog(a_before);
487 return _out_finish(&out);
488
489fail:
490 FreeNoLog(b_before);
491 FreeNoLog(a_before);
493 FreeNoLog(out.buf);
494 return NULL;
495}
static N_GUI_MOUSE_EVENT last
the last event the widget was told of, and how many
void line(BITMAP *bmp, int x1, int y1, int x2, int y2, int color)
draw a line
#define FreeNoLog(__ptr)
Free Handler without log.
Definition n_common.h:272
#define Malloc(__ptr, __struct, __size)
Malloc Handler to get errors and set to 0.
Definition n_common.h:204
#define __n_assert(__ptr, __ret)
macro to assert things
Definition n_common.h:279
size_t nb_items
number of item currently in the list
Definition n_list.h:61
#define list_foreach(__ITEM_, __LIST_)
ForEach macro helper, safe for node removal during iteration.
Definition n_list.h:89
int list_unshift(LIST *list, void *ptr, void(*destructor)(void *ptr))
Add a pointer at the start of the list.
Definition n_list.c:317
int list_destroy(LIST **list)
Empty and Free a list container.
Definition n_list.c:548
LIST * new_generic_list(size_t max_items)
Initialiaze a generic list container to max_items pointers.
Definition n_list.c:37
#define MAX_LIST_ITEMS
flag to pass to new_generic_list for the maximum possible number of item in a list
Definition n_list.h:75
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:113
#define LOG_ERR
error conditions
Definition n_log.h:100
int truncated
1 when the edit limit was hit and the differing region was reported as one delete run followed by one...
Definition n_diff.h:108
size_t nb_deleted
number of N_DIFF_DELETE lines
Definition n_diff.h:103
size_t nb_inserted
number of N_DIFF_INSERT lines
Definition n_diff.h:105
size_t nb_common
number of N_DIFF_COMMON lines
Definition n_diff.h:101
int op
N_DIFF_COMMON, N_DIFF_DELETE or N_DIFF_INSERT.
Definition n_diff.h:93
LIST * lines
LIST of N_DIFF_LINE*, in output order.
Definition n_diff.h:99
void n_diff_result_free(N_DIFF_RESULT **result)
Free an edit script and every line it holds.
Definition n_diff.c:333
#define N_DIFF_MAX_EDITS_CEILING
hard ceiling n_diff_lines() clamps max_edits to, keeping the worst case trace under about 64 MB
Definition n_diff.h:82
#define N_DIFF_NO_LINE
N_DIFF_LINE line_a / line_b value when the line has no counterpart there.
Definition n_diff.h:72
#define N_DIFF_DELETE
the line is only in the first text
Definition n_diff.h:67
#define N_DIFF_COMMON
the line is present in both texts
Definition n_diff.h:65
#define N_DIFF_MAX_EDITS_DEFAULT
n_diff_lines() default ceiling on the number of differing lines searched
Definition n_diff.h:78
#define N_DIFF_INSERT
the line is only in the second text
Definition n_diff.h:69
#define N_DIFF_CONTEXT_FULL
n_diff_to_unified() context asking for every common line, without hunk headers
Definition n_diff.h:75
N_DIFF_RESULT * n_diff_lines(const char *text_a, const char *text_b, size_t max_edits)
Compare two texts line by line.
Definition n_diff.c:252
N_STR * n_diff_to_unified(const N_DIFF_RESULT *result, size_t context)
Render an edit script as unified diff text.
Definition n_diff.c:391
one line of an edit script
Definition n_diff.h:85
edit script turning one text into another
Definition n_diff.h:97
size_t NSTRBYTE
N_STR base unit.
Definition n_str.h:58
N_STR * char_to_nstr(const char *src)
Convert a char into a N_STR, short version.
Definition n_str.c:262
N_STR * char_to_nstr_nocopy(char *src)
Convert a char into a N_STR, direct use of linked source pointer.
Definition n_str.c:275
int write_and_fit_ex(char **dest, NSTRBYTE *size, NSTRBYTE *written, const char *src, NSTRBYTE src_size, NSTRBYTE additional_padding)
Append src_size bytes from src to *dest, updating size/written, growing the buffer if needed.
Definition n_str.c:1194
A box including a string and his lenght.
Definition n_str.h:61
Common headers and low-level functions & define.
static int _out_mem(DIFF_OUT *o, const char *s, size_t n)
append n bytes of s to the output, TRUE on success
Definition n_diff.c:352
static void _text_free(DIFF_TEXT *t)
release a DIFF_TEXT, leaving it empty
Definition n_diff.c:52
static int _myers(N_DIFF_RESULT *result, char **a, int n, char **b, int m, size_t base_a, size_t base_b, int max_edits)
Myers greedy forward pass with a stored trace, then a backtrack emitting the script in reverse.
Definition n_diff.c:144
#define DIFF_OUT_PADDING
growth headroom so consecutive short appends share one realloc
Definition n_diff.c:37
static int _text_load(DIFF_TEXT *t, const char *text)
split text into lines.
Definition n_diff.c:60
static void _line_free(void *ptr)
destructor for the N_DIFF_LINE entries of N_DIFF_RESULT.lines
Definition n_diff.c:101
static N_STR * _out_finish(DIFF_OUT *o)
hand the buffer over to a new N_STR (no copy); the DIFF_OUT is emptied
Definition n_diff.c:363
static int _emit(N_DIFF_RESULT *result, int op, const char *text, size_t line_a, size_t line_b)
prepend one line to the script, which is built back to front.
Definition n_diff.c:110
char ** line
start of each line inside buf
Definition n_diff.c:46
static int _out_line(DIFF_OUT *o, const N_DIFF_LINE *line)
append one script line as "<marker>text\n", TRUE on success
Definition n_diff.c:384
NSTRBYTE written
meaningful bytes, excluding the terminating '\0'
Definition n_diff.c:348
char * buf
buffer, reallocated on growth
Definition n_diff.c:344
static char _op_char(int op)
unified diff marker for an operation
Definition n_diff.c:377
#define DIFF_HEADER_BUF
widest "@@ -a,b +c,d @@" header we can produce
Definition n_diff.c:39
size_t count
number of lines
Definition n_diff.c:48
char * buf
copy of the input with every ' ' turned into '\0'
Definition n_diff.c:44
NSTRBYTE size
total allocation in bytes
Definition n_diff.c:346
static int _out_str(DIFF_OUT *o, const char *s)
append a NULL terminated string to the output, TRUE on success
Definition n_diff.c:358
growing output buffer fed by write_and_fit_ex
Definition n_diff.c:342
a text split into lines, all pointing into one owned buffer
Definition n_diff.c:42
Line oriented text comparison (Myers shortest edit script).
Generic log system.