-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrcs-text.c
496 lines (429 loc) · 13.7 KB
/
rcs-text.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
/*
* Copyright (c) 2017, 2019-2020 Tuxera US Inc
* SPDX-License-Identifier: GPL-2.0-or-later
*
* Read revision data from plain-text MKSSI RCS files.
*/
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "interfaces.h"
/* buffer an RCS patch in a structured list of such patches */
struct rcs_patch_buffer {
/*
* Parent patch. This is the subsequent revision whose contents are
* derived from this revision. For trunk revisions, this is a lower
* revision number; for branch revisions, a higher one.
*/
struct rcs_patch_buffer *parent;
/*
* List of branches based on this revision. For example, say this is
* the patch for revision 1.2. branches would point at the first
* branch, say 1.2.1.1; that branch would have parent pointers leading
* to 1.2.1.2, 1.2.1.3, etc. If there is more than one branch based
* on revision 1.2, the branch_next will be used. So for revision 1.2,
* its branches->branch_next might lead to rev. 1.2.2.1 (which might
* also have its own parent revisions).
*/
struct rcs_patch_buffer *branches;
struct rcs_patch_buffer *branch_next;
/* RCS version and patch structures (for convenience) */
struct rcs_version *ver;
const struct rcs_patch *patch;
/* Raw text of the patch. The lines buffer points into this. */
char *text;
/* Lines buffer for the patch */
struct rcs_line *lines;
};
/* parse line number and line count from an RCS patch line */
static bool
get_lineno_and_count(const char *s, unsigned int *lineno, unsigned int *count)
{
char *end;
/* Expected format: number, space, number, newline/NUL */
errno = 0;
*lineno = (unsigned int)strtoul(s, &end, 10);
if (end == s || *end != ' ' || errno)
return false;
s = end + 1;
errno = 0;
*count = (unsigned int)strtoul(s, &end, 10);
if (end == s || (*end && *end != '\n') || errno)
return false;
return true;
}
/* patch the preceding revision to yield the new revision */
static struct rcs_line *
apply_patch(const struct rcs_file *file, const struct rcs_number *revnum,
struct rcs_line *data_lines, struct rcs_line *patch_lines)
{
struct rcs_line *pln;
unsigned int ln, ct, i;
char cmd;
for (pln = patch_lines; pln;) {
cmd = pln->line[0];
/* Skip blank lines */
if (cmd == '\n' || cmd == '\0') {
pln = pln->next;
continue;
}
/*
* RCS patches only have two commands: 'a' for insert and 'd'
* for delete.
*/
if (cmd != 'a' && cmd != 'd') {
fprintf(stderr, "unrecognized patch command '%c' "
"(0x%02x)\n", cmd, (unsigned int)cmd);
goto error;
}
/*
* Both 'a' and 'd' have a line number and line count. The line
* number is where the insert/delete starts. Importantly, this
* line number is the *original* line number, prior to applying
* any changes from the patch. The line count is the number of
* lines to insert/delete.
*/
if (!get_lineno_and_count(&pln->line[1], &ln, &ct)) {
fprintf(stderr, "cannot parse line number and count\n");
goto error;
}
if (cmd == 'a') {
if (!lines_insert(&data_lines, pln->next, ln, ct)) {
fprintf(stderr, "cannot insert lines\n");
goto error;
}
/* Move past insert command */
pln = pln->next;
/* Move past inserted lines */
for (i = 0; i < ct; ++i)
pln = pln->next;
} else if (cmd == 'd') {
if (!lines_delete(data_lines, ln, ct)) {
fprintf(stderr, "cannot delete lines\n");
goto error;
}
/* Move past delete command */
pln = pln->next;
}
}
/*
* Once the patch is completely applied, we can remove deleted lines
* from the line buffer and renumber the lines.
*/
lines_reset(&data_lines);
return data_lines;
error:
fprintf(stderr, "cannot patch to \"%s\" rev. %s\n", file->name,
rcs_number_string_sb(revnum));
fprintf(stderr, "bad patch line %u: \"", pln->lineno);
line_fprint(stderr, pln->line);
fprintf(stderr, "\"\n");
fatal_error("bad RCS patch");
return NULL; /* unreachable */
}
/* read the text of an RCS patch from disk */
static char *
read_patch_text(const struct rcs_file *file, const struct rcs_patch *patch)
{
ssize_t len;
char *text;
int fd;
/*
* patch->text.length includes the opening/closing @ characters, which
* we do not want to read.
*/
len = patch->text.length - 2;
text = xmalloc(len + 1, __func__);
if ((fd = open(file->master_name, O_RDONLY)) == -1)
fatal_system_error("cannot open \"%s\"", file->master_name);
errno = 0;
if (pread(fd, text, len, patch->text.offset + 1) != len)
fatal_system_error("cannot read from \"%s\"",
file->master_name);
close(fd);
text[len] = '\0';
return text;
}
/* instantiate a patch buffer */
static struct rcs_patch_buffer *new_patch_buf(const struct rcs_file *file,
const struct rcs_number *revnum)
{
struct rcs_patch_buffer *pbuf;
pbuf = xcalloc(1, sizeof *pbuf, __func__);
pbuf->ver = rcs_file_find_version(file, revnum, true);
pbuf->patch = rcs_file_find_patch(file, revnum, true);
if (!pbuf->patch->missing) {
pbuf->text = read_patch_text(file, pbuf->patch);
pbuf->lines = string_to_lines(pbuf->text);
}
return pbuf;
}
/* read a file's patches from a given starting revision into patch buffers */
static struct rcs_patch_buffer *
read_patches_from_rev(const struct rcs_file *file,
const struct rcs_number *startrev)
{
struct rcs_number rev;
struct rcs_patch_buffer *head, *pbuf, *br_pbuf;
struct rcs_patch_buffer **parent_prev_next, **br_prev_next;
const struct rcs_branch *b;
parent_prev_next = &head;
for (rev = *startrev; rev.c; rev = pbuf->ver->parent) {
/* Read this patch into a patch buffer */
pbuf = new_patch_buf(file, &rev);
/* Recursively read any branch patch chains which start here */
br_prev_next = &pbuf->branches;
for (b = pbuf->ver->branches; b; b = b->next) {
br_pbuf = read_patches_from_rev(file, &b->number);
*br_prev_next = br_pbuf;
br_prev_next = &br_pbuf->branch_next;
}
*br_prev_next = NULL;
*parent_prev_next = pbuf;
parent_prev_next = &pbuf->parent;
}
*parent_prev_next = NULL;
return head;
}
/* read all of a file's patches into a list of patch buffers */
static struct rcs_patch_buffer *
read_patches(const struct rcs_file *file)
{
/* Start reading from the head revision */
return read_patches_from_rev(file, &file->head);
}
/* free a list of patch buffers */
static void
free_patch_buffers(struct rcs_patch_buffer *patches)
{
struct rcs_patch_buffer *p, *pparent, *bp, *bpnext;
/* Walk the list of revisions */
for (p = patches; p; p = pparent) {
/* Recursively free each branch which starts here */
for (bp = p->branches; bp; bp = bpnext) {
bpnext = bp->branch_next;
free_patch_buffers(bp);
}
/* Free the patch text and lines */
lines_free(p->lines);
if (p->text)
free(p->text);
pparent = p->parent;
free(p);
}
}
/* pass file revision data to the callback*/
static void
emit_revision_data(rcs_revision_data_handler_t *callback,
struct rcs_file *file, struct rcs_version *ver,
const struct rcs_patch *patch, const struct rcs_line *data_lines,
bool has_member_type_other)
{
struct rcs_line *data_lines_expanded;
char *data;
/*
* If the patch (or any of its antecedants) was missing from the RCS
* file, emit an empty revision. This emulates how MKSSI handles
* RCS files that are corrupt in this manner.
*/
if (patch->missing) {
callback(file, &ver->number, "", has_member_type_other);
return;
}
/*
* Need to do RCS keyword expansion. The provided data_lines may still
* be needed in their original form to patch to the subsequent revision,
* so make a copy for the expansion.
*/
data_lines_expanded = lines_copy(data_lines);
/*
* No keyword expansion for "other" member types, but it is assumed (not
* much data) that "@@" characters still need to be un-escaped.
*/
if (has_member_type_other)
rcs_data_unescape_ats(data_lines_expanded);
else
rcs_data_keyword_expansion(file, ver, patch,
data_lines_expanded);
/* Convert the data lines into a string and pass to the callback */
data = lines_to_string(data_lines_expanded);
callback(file, &ver->number, data, has_member_type_other);
free(data);
/* Free the copied data lines */
lines_free(data_lines_expanded);
}
/* pass file revision data(s) to the callback */
static void
emit_revision(rcs_revision_data_handler_t *callback,
struct rcs_file *file, struct rcs_version *ver,
const struct rcs_patch *patch, const struct rcs_line *data_lines)
{
/*
* Rare special case: for text files with member type "other", MKSSI
* seems to grab rev. 1.1 without doing keyword expansion, so we need
* to export a special version without expanding keywords.
*
* Still need to export this rev. 1.1 with keyword expansion afterward,
* because it might also be needed as a normal member type "archive".
*/
if (file->has_member_type_other && !file->binary && ver->number.c == 2
&& ver->number.n[0] == 1 && ver->number.n[1] == 1)
emit_revision_data(callback, file, ver, patch, data_lines,
true);
emit_revision_data(callback, file, ver, patch, data_lines, false);
}
/* apply patches and pass the resulting revision data to the callback */
static struct rcs_line *
apply_patches_and_emit(rcs_revision_data_handler_t *callback,
struct rcs_file *file, struct rcs_line *prev_data_lines,
struct rcs_patch_buffer *patches)
{
struct rcs_patch_buffer *p, *bp;
struct rcs_line *branch_data_lines, *data_lines;
data_lines = NULL;
for (p = patches; p; p = p->parent) {
if (prev_data_lines)
/*
* Apply the patch to transmute the previous revision
* data lines into the data lines for the current
* revision.
*/
data_lines = apply_patch(file, &p->ver->number,
prev_data_lines, p->lines);
else
/*
* Patch for the head revision is the data for that
* revision.
*/
data_lines = p->lines;
/* Pass the revision data to the callback */
emit_revision(callback, file, p->ver, p->patch, data_lines);
/* Iterate through all branches which start at this revision */
for (bp = p->branches; bp; bp = bp->branch_next) {
/*
* Branch patches apply against data_lines, but since
* we still need that data for subsequent revisions on
* this level, make a copy.
*/
branch_data_lines = lines_copy(data_lines);
/*
* Recursively apply patches and emit revision data for
* this chain of branch patches.
*/
branch_data_lines = apply_patches_and_emit(callback,
file, branch_data_lines, bp);
/* Free the copied data lines */
lines_free(branch_data_lines);
}
prev_data_lines = data_lines;
}
/*
* data_lines will have changed from prev_data_lines if a) the latter
* was NULL; or b) if applying a patch changed the first line. Return
* the possibly changed pointer so that it can be freed correctly.
*/
return data_lines;
}
/* read every RCS revision for a file, passing the data to the callback */
void
rcs_file_read_all_revisions(struct rcs_file *file,
rcs_revision_data_handler_t *callback)
{
struct rcs_patch_buffer *patches;
/*
* The "store-by-reference" option is intended for binary files, but
* nothing prevents it from being used with text files. Not currently
* implemented.
*/
if (file->reference_subdir)
fatal_error("unsupported feature: \"%s\" is a text file stored "
"by reference", file->name);
/*
* Read every patch. These must remain in memory until we are done
* with the file, since portions of a patch (the inserted lines) are
* incorporated into the text of subsequent revisions.
*/
patches = read_patches(file);
/*
* Apply the patches in sequence and emit the resulting revision data
* to the callback.
*/
patches->lines = apply_patches_and_emit(callback, file, NULL, patches);
/* Free the patch buffers */
free_patch_buffers(patches);
}
/* read a given revision from an RCS file */
char *
rcs_file_read_revision(struct rcs_file *file, const struct rcs_number *revnum)
{
struct rcs_line *data_lines, *patch_lines;
char *patch_text, *data_text;
const struct rcs_patch *patch;
struct rcs_version *ver;
const struct rcs_branch *b;
struct rcs_number getrev, brrev;
int cmp;
data_lines = NULL;
getrev = file->head;
loop:
ver = rcs_file_find_version(file, &getrev, true);
patch = rcs_file_find_patch(file, &getrev, true);
if (patch->missing)
/*
* Currently, this function shouldn't be called for file
* revisions that depend upon missing patches.
*/
fatal_error("missing patch for file %s rev. %s\n",
file->name, rcs_number_string_sb(&getrev));
if (data_lines) {
patch_text = read_patch_text(file, patch);
patch_lines = string_to_lines(patch_text);
data_lines = apply_patch(file, &ver->number, data_lines,
patch_lines);
lines_free(patch_lines);
lines_reset(&data_lines);
} else {
patch_text = read_patch_text(file, patch);
data_lines = string_to_lines(patch_text);
}
if (rcs_number_equal(&getrev, revnum)) {
rcs_data_keyword_expansion(file, ver, patch, data_lines);
data_text = lines_to_string(data_lines);
lines_free(data_lines);
free(patch_text);
return data_text;
}
if (rcs_number_partial_match(revnum, &getrev)) {
for (b = ver->branches; b; b = b->next) {
/*
* We want a branchier rev like 1.145.1.82.1.1 to match
* a less branchy rev that leads to it like 1.145.1.1.
*/
brrev = *revnum;
while (brrev.c > b->number.c)
brrev.c -= 2;
if (rcs_number_same_branch(&brrev, &b->number)) {
getrev = b->number;
goto loop;
}
}
} else if (ver->parent.c) {
/*
* Get the parent revision next as long as it brings us closer
* to the request revision. Remember that trunk revisions are
* descending and branch revisions are ascending.
*/
cmp = rcs_number_compare(&ver->parent, &getrev);
if (rcs_number_is_trunk(&ver->parent) ? cmp <= 0 : cmp >= 0) {
getrev = ver->parent;
goto loop;
}
}
fatal_error("cannot find \"%s\" rev. %s", file->name,
rcs_number_string_sb(revnum));
return NULL; /* unreachable */
}