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