ctm_rmail.c revision 6290
1/*
2 * Accept one (or more) ASCII encoded chunks that together make a compressed
3 * CTM delta.  Decode them and reconstruct the deltas.  Any completed
4 * deltas may be passed to ctm for unpacking.
5 *
6 * Author: Stephen McKay
7 *
8 * NOTICE: This is free software.  I hope you get some use from this program.
9 * In return you should think about all the nice people who give away software.
10 * Maybe you should write some free software too.
11 */
12
13#include <stdio.h>
14#include <stdlib.h>
15#include <unistd.h>
16#include <string.h>
17#include <errno.h>
18#include <unistd.h>
19#include <sys/types.h>
20#include <sys/stat.h>
21#include "error.h"
22#include "options.h"
23
24#define CTM_STATUS	".ctm_status"
25
26char *piece_dir = NULL;		/* Where to store pieces of deltas. */
27char *delta_dir = NULL;		/* Where to store completed deltas. */
28char *base_dir = NULL;		/* The tree to apply deltas to. */
29int delete_after = 0;		/* Delete deltas after ctm applies them. */
30
31void apply_complete(void);
32int read_piece(char *input_file);
33int combine_if_complete(char *delta, int pce, int npieces);
34int decode_line(char *line, char *out_buf);
35
36/*
37 * If given a '-p' flag, read encoded delta pieces from stdin or file
38 * arguments, decode them and assemble any completed deltas.  If given
39 * a '-b' flag, pass any completed deltas to 'ctm' for application to
40 * the source tree.  The '-d' flag is mandatory, but either of '-p' or
41 * '-b' can be omitted.  If given the '-l' flag, notes and errors will
42 * be timestamped and written to the given file.
43 *
44 * Exit status is 0 for success or 1 for indigestible input.  That is,
45 * 0 means the encode input pieces were decoded and stored, and 1 means
46 * some input was discarded.  If a delta fails to apply, this won't be
47 * reflected in the exit status.  In this case, the delta is left in
48 * 'deltadir'.
49 */
50
51int
52main(int argc, char **argv)
53    {
54    char *log_file = NULL;
55    int status = 0;
56
57    err_prog_name(argv[0]);
58
59    OPTIONS("[-D] [-p piecedir] [-d deltadir] [-b basedir] [-l log] [file ...]")
60	FLAG('D', delete_after)
61	STRING('p', piece_dir)
62	STRING('d', delta_dir)
63	STRING('b', base_dir)
64	STRING('l', log_file)
65    ENDOPTS
66
67    if (delta_dir == NULL)
68	usage();
69
70    if (piece_dir == NULL && (base_dir == NULL || argc>1))
71	usage();
72
73    if (log_file != NULL)
74	err_set_log(log_file);
75
76    if (argc <= 1)
77	{
78	if (piece_dir != NULL)
79	    status = read_piece(NULL);
80	}
81    else
82	{
83	while (*++argv != NULL)
84	    status |= read_piece(*argv);
85	}
86
87    if (base_dir != NULL)
88	apply_complete();
89
90    return status;
91    }
92
93
94/*
95 * Construct the file name of a piece of a delta.
96 */
97#define mk_piece_name(fn,d,p,n)	\
98    sprintf((fn), "%s/%s+%d-%d", piece_dir, (d), (p), (n))
99
100/*
101 * Construct the file name of an assembled delta.
102 */
103#define mk_delta_name(fn,d)	\
104    sprintf((fn), "%s/%s", delta_dir, (d))
105
106/*
107 * If the next required delta is now present, let ctm lunch on it and any
108 * contiguous deltas.
109 */
110void
111apply_complete()
112    {
113    int i, dn;
114    FILE *fp, *ctm;
115    struct stat sb;
116    char class[20];
117    char delta[30];
118    char fname[1000];
119    char buf[2000];
120    char junk[2];
121    char here[1000];
122
123    sprintf(fname, "%s/%s", base_dir, CTM_STATUS);
124    if ((fp = fopen(fname, "r")) == NULL)
125	return;
126
127    i = fscanf(fp, "%s %d %c", class, &dn, junk);
128    fclose(fp);
129    if (i != 2)
130	return;
131
132    /*
133     * We might need to convert the delta filename to an absolute pathname.
134     */
135    here[0] = '\0';
136    if (delta_dir[0] != '/')
137	{
138	getcwd(here, sizeof(here)-1);
139	i = strlen(here) - 1;
140	if (i >= 0 && here[i] != '/')
141	    {
142	    here[++i] = '/';
143	    here[++i] = '\0';
144	    }
145	}
146
147    /*
148     * Keep applying deltas until we run out or something bad happens.
149     */
150    for (;;)
151	{
152	sprintf(delta, "%s.%04d.gz", class, ++dn);
153	mk_delta_name(fname, delta);
154
155	if (stat(fname, &sb) < 0)
156	    return;
157
158	sprintf(buf, "(cd %s && ctm %s%s) 2>&1", base_dir, here, fname);
159	if ((ctm = popen(buf, "r")) == NULL)
160	    {
161	    err("ctm failed to apply %s", delta);
162	    return;
163	    }
164
165	while (fgets(buf, sizeof(buf), ctm) != NULL)
166	    {
167	    i = strlen(buf) - 1;
168	    if (i >= 0 && buf[i] == '\n')
169		buf[i] = '\0';
170	    err("ctm: %s", buf);
171	    }
172
173	if (pclose(ctm) != 0)
174	    {
175	    err("ctm failed to apply %s", delta);
176	    return;
177	    }
178
179	if (delete_after)
180	    unlink(fname);
181
182	err("%s applied%s", delta, delete_after ? " and deleted" : "");
183	}
184    }
185
186
187/*
188 * This cheap plastic checksum effectively rotates our checksum-so-far
189 * left one, then adds the character.  We only want 16 bits of it, and
190 * don't care what happens to the rest.  It ain't much, but it's small.
191 */
192#define add_ck(sum,x)	\
193    ((sum) += ((x)&0xff) + (sum) + (((sum)&0x8000) ? 1 : 0))
194
195
196/*
197 * Decode the data between BEGIN and END, and stash it in the staging area.
198 * Multiple pieces can be present in a single file, bracketed by BEGIN/END.
199 * If we have all pieces of a delta, combine them.  Returns 0 on success,
200 * and 1 for any sort of failure.
201 */
202int
203read_piece(char *input_file)
204    {
205    int status = 0;
206    FILE *ifp, *ofp = 0;
207    int decoding = 0;
208    int line_no = 0;
209    int i, n;
210    int pce, npieces;
211    unsigned claimed_cksum;
212    unsigned short cksum = 0;
213    char out_buf[200];
214    char line[200];
215    char delta[30];
216    char pname[1000];
217    char junk[2];
218
219    ifp = stdin;
220    if (input_file != NULL && (ifp = fopen(input_file, "r")) == NULL)
221	{
222	err("cannot open '%s' for reading", input_file);
223	return 1;
224	}
225
226    while (fgets(line, sizeof(line), ifp) != NULL)
227	{
228	line_no++;
229
230	/*
231	 * Look for the beginning of an encoded piece.
232	 */
233	if (!decoding)
234	    {
235	    char *s;
236
237	    if (sscanf(line, "CTM_MAIL BEGIN %s %d %d %c",
238		    delta, &pce, &npieces, junk) != 3)
239		continue;
240
241	    while ((s = strchr(delta, '/')) != NULL)
242		*s = '_';
243
244	    mk_piece_name(pname, delta, pce, npieces);
245	    if ((ofp = fopen(pname, "w")) == NULL)
246		{
247		err("cannot open '%s' for writing", pname);
248		status++;
249		continue;
250		}
251
252	    cksum = 0xffff;
253	    decoding++;
254	    continue;
255	    }
256
257	/*
258	 * We are decoding.  Stop if we see the end flag.
259	 */
260	if (sscanf(line, "CTM_MAIL END %d %c", &claimed_cksum, junk) == 1)
261	    {
262	    int e;
263
264	    decoding = 0;
265
266	    fflush(ofp);
267	    e = ferror(ofp);
268	    fclose(ofp);
269
270	    if (e)
271		err("error writing %s", pname);
272
273	    if (cksum != claimed_cksum)
274		err("checksum: read %d, calculated %d", claimed_cksum, cksum);
275
276	    if (e || cksum != claimed_cksum)
277		{
278		err("%s %d/%d discarded", delta, pce, npieces);
279		unlink(pname);
280		status++;
281		continue;
282		}
283
284	    err("%s %d/%d stored", delta, pce, npieces);
285
286	    if (!combine_if_complete(delta, pce, npieces))
287		status++;
288	    continue;
289	    }
290
291	/*
292	 * Must be a line of encoded data.  Decode it, sum it, and save it.
293	 */
294	n = decode_line(line, out_buf);
295	if (n < 0)
296	    {
297	    err("line %d: illegal character: '%c'", line_no, line[-n]);
298	    err("%s %d/%d discarded", delta, pce, npieces);
299
300	    fclose(ofp);
301	    unlink(pname);
302
303	    status++;
304	    decoding = 0;
305	    continue;
306	    }
307
308	for (i = 0; i < n; i++)
309	    add_ck(cksum, out_buf[i]);
310
311	fwrite(out_buf, sizeof(char), n, ofp);
312	}
313
314    if (decoding)
315	{
316	err("truncated file");
317	err("%s %d/%d discarded", delta, pce, npieces);
318
319	fclose(ofp);
320	unlink(pname);
321
322	status++;
323	}
324
325    if (ferror(ifp))
326	{
327	err("error reading %s", input_file == NULL ? "stdin" : input_file);
328	status++;
329	}
330
331    if (input_file != NULL)
332	fclose(ifp);
333
334    return (status != 0);
335    }
336
337
338/*
339 * Put the pieces together to form a delta, if they are all present.
340 * Returns 1 on success (even if we didn't do anything), and 0 on failure.
341 */
342int
343combine_if_complete(char *delta, int pce, int npieces)
344    {
345    int i;
346    FILE *dfp, *pfp;
347    int c;
348    struct stat sb;
349    char pname[1000];
350    char dname[1000];
351
352    /*
353     * All here?
354     */
355    for (i = 1; i <= npieces; i++)
356	{
357	if (i == pce)
358	    continue;
359	mk_piece_name(pname, delta, i, npieces);
360	if (stat(pname, &sb) < 0)
361	    return 1;
362	}
363
364    mk_delta_name(dname, delta);
365
366    /*
367     * We can probably just rename() it in to place if it is a small delta.
368     */
369    if (npieces == 1)
370	{
371	mk_piece_name(pname, delta, 1, 1);
372	if (rename(pname, dname) == 0)
373	    {
374	    err("%s complete", delta);
375	    return 1;
376	    }
377	}
378
379    if ((dfp = fopen(dname, "w")) == NULL)
380	{
381	err("cannot open '%s' for writing", dname);
382	return 0;
383	}
384
385    /*
386     * Ok, the hard way.  Reconstruct the delta by reading each piece in order.
387     */
388    for (i = 1; i <= npieces; i++)
389	{
390	mk_piece_name(pname, delta, i, npieces);
391	if ((pfp = fopen(pname, "r")) == NULL)
392	    {
393	    err("cannot open '%s' for reading", pname);
394	    fclose(dfp);
395	    unlink(dname);
396	    return 0;
397	    }
398	while ((c = getc(pfp)) != EOF)
399	    putc(c, dfp);
400	fclose(pfp);
401	}
402    fflush(dfp);
403    if (ferror(dfp))
404	{
405	err("error writing '%s'", dname);
406	fclose(dfp);
407	unlink(dname);
408	return 0;
409	}
410    fclose(dfp);
411
412    /*
413     * Throw the pieces away.
414     */
415    for (i = 1; i <= npieces; i++)
416	{
417	mk_piece_name(pname, delta, i, npieces);
418	unlink(pname);
419	}
420
421    err("%s complete", delta);
422    return 1;
423    }
424
425
426/*
427 * MIME BASE64 decode table.
428 */
429static unsigned char from_b64[0x80] =
430    {
431    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
432    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
433    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
434    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
435    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
436    0xff, 0xff, 0xff, 0x3e, 0xff, 0xff, 0xff, 0x3f,
437    0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b,
438    0x3c, 0x3d, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
439    0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
440    0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
441    0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
442    0x17, 0x18, 0x19, 0xff, 0xff, 0xff, 0xff, 0xff,
443    0xff, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
444    0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
445    0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30,
446    0x31, 0x32, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff
447    };
448
449
450/*
451 * Decode a line of ASCII into binary.  Returns the number of bytes in
452 * the output buffer, or < 0 on indigestable input.  Error output is
453 * the negative of the index of the inedible character.
454 */
455int
456decode_line(char *line, char *out_buf)
457    {
458    unsigned char *ip = (unsigned char *)line;
459    unsigned char *op = (unsigned char *)out_buf;
460    unsigned long bits;
461    unsigned x;
462
463    for (;;)
464	{
465	if (*ip >= 0x80 || (x = from_b64[*ip]) >= 0x40)
466	    break;
467	bits = x << 18;
468	ip++;
469	if (*ip < 0x80 && (x = from_b64[*ip]) < 0x40)
470	    {
471	    bits |= x << 12;
472	    *op++ = bits >> 16;
473	    ip++;
474	    if (*ip < 0x80 && (x = from_b64[*ip]) < 0x40)
475		{
476		bits |= x << 6;
477		*op++ = bits >> 8;
478		ip++;
479		if (*ip < 0x80 && (x = from_b64[*ip]) < 0x40)
480		    {
481		    bits |= x;
482		    *op++ = bits;
483		    ip++;
484		    }
485		}
486	    }
487	}
488
489    if (*ip == '\0' || *ip == '\n')
490	return op - (unsigned char *)out_buf;
491    else
492	return -(ip - (unsigned char *)line);
493    }
494