1/***********************************************************************
2*                                                                      *
3*               This software is part of the ast package               *
4*          Copyright (c) 2000-2009 AT&T Intellectual Property          *
5*                      and is licensed under the                       *
6*                  Common Public License, Version 1.0                  *
7*                    by AT&T Intellectual Property                     *
8*                                                                      *
9*                A copy of the License is available at                 *
10*            http://www.opensource.org/licenses/cpl1.0.txt             *
11*         (with md5 checksum 059e8cd6165cb4c31e351f2b69388fd9)         *
12*                                                                      *
13*              Information and Software Systems Research               *
14*                            AT&T Research                             *
15*                           Florham Park NJ                            *
16*                                                                      *
17*                 Glenn Fowler <gsf@research.att.com>                  *
18*                                                                      *
19***********************************************************************/
20#pragma prototyped
21/*
22 * Glenn Fowler
23 * AT&T Research
24 */
25
26static const char usage[] =
27"[-?\n@(#)$Id: msggen (AT&T Research) 2002-03-11 $\n]"
28USAGE_LICENSE
29"[+NAME?msggen - generate a machine independent formatted message catalog]"
30"[+DESCRIPTION?\bmsggen\b merges the message text source files \amsgfile\a"
31"	into a machine independent formatted message catalog \acatfile\a."
32"	The file \acatfile\a will be created if it does not already exist."
33"	If \acatfile\a does exist, its messages will be included in the new"
34"	\acatfile\a. If set and message numbers collide, the new message"
35"	text defined in \amsgfile\a will replace the old message text"
36"	currently contained in \acatfile\a. Non-ASCII characters must be"
37"	UTF-8 encoded. \biconv\b(1) can be used to convert to/from UTF-8.]"
38"[f:format?List the \bprintf\b(3) format signature for each message in"
39"	\acatfile\a. A format signature is one line containing one character"
40"	per format specification:]{"
41"		[c?char]"
42"		[d?double]"
43"		[D?long double]"
44"		[f?float]"
45"		[h?short]"
46"		[i?int]"
47"		[j?long long]"
48"		[l?long]"
49"		[p?void*]"
50"		[s?string]"
51"		[t?ptrdiff_t]"
52"		[z?size_t]"
53"		[???unknown]"
54"}"
55"[l:list?List \acatfile\a in UTF-8 \amsgfile\a form.]"
56"[s:set?Convert the \acatfile\a operand to a message set number and"
57"	print the number on the standard output.]"
58"[+EXTENDED DESCRIPTION?Message text source files are in \bgencat\b(1)"
59"	format, defined as follows. Note that the fields of a message text"
60"	source line are separated by a single blank character. Any other"
61"	blank characters are considered as being part of the subsequent"
62"	field. The \bNL_*\b constants are defined in one or both of"
63"	\b<limits.h>\b and \b<nl_types.h>\b.]{"
64"		[+$ \acomment\a?A line beginning with \b$\b followed by a"
65"			blank character is treated as a comment.]"
66"		[+$delset \an\a \acomment\a?This line deletes message set"
67"			\an\a from an existing message catalog. \an\a"
68"			denotes the set number [1, \bNL_SETMAX\b]]. Any"
69"			text following the set number is treated as a"
70"			comment.]"
71"		[+$quote \ac\a?This line specifies an optional quote"
72"			character \ac\a, which can be used to surround"
73"			\amessage-text\a so that trailing spaces or"
74"			empty messages are visible in a message source"
75"			line. By default, or if an empty \b$quote\b"
76"			directive is supplied, no quoting of \amessage-text\a"
77"			will be recognized.]"
78"		[+$set \an\a \acomment\a?This line specifies the set"
79"			identifier of the following messages until the next"
80"			\b$set\b or end-of-file appears. \an\a denotes the set"
81"			identifier, which is defined as a number in the range"
82"			[1, \bNL_SETMAX\b]]. Set numbers need not be"
83"			contiguous. Any text following the set identifier is"
84"			treated as a comment. If no \b$set\b directive is"
85"			specified in a 	message text source file, all messages"
86"			will be located in message set \b1\b.]"
87"		[+$translation \aidentification\a \aYYYY-MM-DD\a[,...]]?Append"
88"			translation info to the message catalog header. Only"
89"			the newest date for a given \aidentification\a"
90"			is retained in the catalog. Multiple translation lines"
91"			are combined into a single \b,\b separated list.]"
92"		[+\am\a \amessage-text\a?\am\a denotes the message identifier,"
93"			which is defined as a number in the range"
94"			[1, \bNL_MSGMAX\b]]. The message-text is stored in the"
95"			message catalogue with the set identifier specified by"
96"			the last \b$set\b directive, and with message"
97"			identifier \am\a. If the \amessage-text\a is empty,"
98"			and a blank character field separator is present, an"
99"			empty string is stored in the message catalogue. If a"
100"			message source line has a message number, but neither"
101"			a field separator nor \amessage-text\a, the existing"
102"			message with that number (if any) is deleted from the"
103"			catalogue. Message identifiers need not be contiguous."
104"			There are no \amessage-text\a length restrictions.]"
105"}"
106
107"\n"
108"\ncatfile [ msgfile ]\n"
109"\n"
110
111"[+SEE ALSO?\bgencat\b(1), \biconv\b(1), \bmsgcc\b(1), \btranslate\b(1),"
112"	\bfmtfmt\b(3)]"
113;
114
115#include <ast.h>
116#include <ctype.h>
117#include <ccode.h>
118#include <error.h>
119#include <mc.h>
120
121typedef struct Xl_s
122{
123	struct Xl_s*	next;
124	char*		date;
125	char		name[1];
126} Xl_t;
127
128/*
129 * append s to the translation list
130 */
131
132static Xl_t*
133translation(Xl_t* xp, register char* s)
134{
135	register Xl_t*	px;
136	register char*	t;
137	char*		d;
138	char*		e;
139
140	do
141	{
142		for (; isspace(*s); s++);
143		for (d = e = 0, t = s; *t; t++)
144			if (*t == ',')
145			{
146				e = t;
147				*e++ = 0;
148				break;
149			}
150			else if (isspace(*t))
151				d = t;
152		if (d)
153		{
154			*d++ = 0;
155			for (px = xp; px; px = px->next)
156				if (streq(px->name, s))
157				{
158					if (strcoll(px->date, d) < 0)
159					{
160						free(px->date);
161						if (!(px->date = strdup(d)))
162							error(ERROR_SYSTEM|3, "out of space [translation]");
163					}
164					break;
165				}
166			if (!px)
167			{
168				if (!(px = newof(0, Xl_t, 1, strlen(s))) || !(px->date = strdup(d)))
169					error(ERROR_SYSTEM|3, "out of space [translation]");
170				strcpy(px->name, s);
171				px->next = xp;
172				xp = px;
173			}
174		}
175	} while (s = e);
176	return xp;
177}
178
179/*
180 * sfprintf() with ccmaps(from,to)
181 */
182
183static int
184ccsfprintf(int from, int to, Sfio_t* sp, const char* format, ...)
185{
186	va_list		ap;
187	Sfio_t*		tp;
188	char*		s;
189	int		n;
190
191	va_start(ap, format);
192	if (from == to)
193		n = sfvprintf(sp, format, ap);
194	else if (tp = sfstropen())
195	{
196		n = sfvprintf(tp, format, ap);
197		s = sfstrbase(tp);
198		ccmaps(s, n, from, to);
199		n = sfwrite(sp, s, n);
200		sfstrclose(tp);
201	}
202	else
203		n = -1;
204	return n;
205}
206
207int
208main(int argc, char** argv)
209{
210	register Mc_t*	mc;
211	register char*	s;
212	register char*	t;
213	register int	c;
214	register int	q;
215	register int	i;
216	int		num;
217	char*		b;
218	char*		e;
219	char*		catfile;
220	char*		msgfile;
221	Sfio_t*		sp;
222	Sfio_t*		mp;
223	Sfio_t*		tp;
224	Xl_t*		px;
225	Xl_t*		bp;
226
227	Xl_t*		xp = 0;
228	int		format = 0;
229	int		list = 0;
230	int		set = 0;
231
232	NoP(argc);
233	error_info.id = "msggen";
234	for (;;)
235	{
236		switch (optget(argv, usage))
237		{
238		case 'f':
239			format = list = 1;
240			continue;
241		case 'l':
242			list = 1;
243			continue;
244		case 's':
245			set = 1;
246			continue;
247		case '?':
248			error(ERROR_USAGE|4, "%s", opt_info.arg);
249			continue;
250		case ':':
251			error(2, "%s", opt_info.arg);
252			continue;
253		}
254		break;
255	}
256	argv += opt_info.index;
257	if (error_info.errors || !(catfile = *argv++))
258		error(ERROR_USAGE|4, "%s", optusage(NiL));
259
260	/*
261	 * set and list only need catfile
262	 */
263
264	if (set)
265	{
266		sfprintf(sfstdout, "%d\n", mcindex(catfile, NiL, NiL, NiL));
267		return error_info.errors != 0;
268	}
269	else if (list)
270	{
271		if (!(sp = sfopen(NiL, catfile, "r")))
272			error(ERROR_SYSTEM|3, "%s: cannot read catalog", catfile);
273		if (!(mc = mcopen(sp)))
274			error(ERROR_SYSTEM|3, "%s: catalog content error", catfile);
275		sfclose(sp);
276		if (format)
277		{
278			for (set = 1; set <= mc->num; set++)
279				if (mc->set[set].num)
280				{
281					sfprintf(sfstdout, "$set %d\n", set);
282					for (num = 1; num <= mc->set[set].num; num++)
283						if (s = mc->set[set].msg[num])
284							sfprintf(sfstdout, "%d \"%s\"\n", num, fmtfmt(s));
285				}
286		}
287		else
288		{
289			if (*mc->translation)
290			{
291				ccsfprintf(CC_NATIVE, CC_ASCII, sfstdout, "$translation ");
292				sfprintf(sfstdout, "%s", mc->translation);
293				ccsfprintf(CC_NATIVE, CC_ASCII, sfstdout, "\n");
294			}
295			ccsfprintf(CC_NATIVE, CC_ASCII, sfstdout, "$quote \"\n");
296			for (set = 1; set <= mc->num; set++)
297				if (mc->set[set].num)
298				{
299					ccsfprintf(CC_NATIVE, CC_ASCII, sfstdout, "$set %d\n", set);
300					for (num = 1; num <= mc->set[set].num; num++)
301						if (s = mc->set[set].msg[num])
302						{
303							ccsfprintf(CC_NATIVE, CC_ASCII, sfstdout, "%d \"", num);
304							while (c = *s++)
305							{
306								/*INDENT...*/
307
308			switch (c)
309			{
310			case 0x22: /* " */
311			case 0x5C: /* \ */
312				sfputc(sfstdout, 0x5C);
313				break;
314			case 0x07: /* \a */
315				c = 0x61;
316				sfputc(sfstdout, 0x5C);
317				break;
318			case 0x08: /* \b */
319				c = 0x62;
320				sfputc(sfstdout, 0x5C);
321				break;
322			case 0x0A: /* \n */
323				c = 0x6E;
324				sfputc(sfstdout, 0x5C);
325				break;
326			case 0x0B: /* \v */
327				c = 0x76;
328				sfputc(sfstdout, 0x5C);
329				break;
330			case 0x0C: /* \f */
331				c = 0x66;
332				sfputc(sfstdout, 0x5C);
333				break;
334			case 0x0D: /* \r */
335				c = 0x72;
336				sfputc(sfstdout, 0x5C);
337				break;
338			}
339
340								/*...UNDENT*/
341								sfputc(sfstdout, c);
342							}
343							ccsfprintf(CC_NATIVE, CC_ASCII, sfstdout, "\"\n");
344						}
345				}
346		}
347		mcclose(mc);
348		return error_info.errors != 0;
349	}
350	else if (!(msgfile = *argv++) || *argv)
351		error(3, "exactly one message file must be specified");
352
353	/*
354	 * open the files and handles
355	 */
356
357	if (!(tp = sfstropen()))
358		error(ERROR_SYSTEM|3, "out of space [string stream]");
359	if (!(mp = sfopen(NiL, msgfile, "r")))
360		error(ERROR_SYSTEM|3, "%s: cannot read message file", msgfile);
361	sp = sfopen(NiL, catfile, "r");
362	if (!(mc = mcopen(sp)))
363		error(ERROR_SYSTEM|3, "%s: catalog content error", catfile);
364	if (sp)
365		sfclose(sp);
366	xp = translation(xp, mc->translation);
367
368	/*
369	 * read the message file
370	 */
371
372	q = 0;
373	set = 1;
374	error_info.file = msgfile;
375	while (s = sfgetr(mp, '\n', 1))
376	{
377		error_info.line++;
378		if (!*s)
379			continue;
380		if (*s == '$')
381		{
382			if (!*++s || isspace(*s))
383				continue;
384			for (t = s; *s && !isspace(*s); s++);
385			if (*s)
386				*s++ = 0;
387			if (streq(t, "delset"))
388			{
389				while (isspace(*s))
390					s++;
391				num = (int)strtol(s, NiL, 0);
392				if (num < mc->num && mc->set[num].num)
393					for (i = 1; i <= mc->set[num].num; i++)
394						mcput(mc, num, i, NiL);
395			}
396			else if (streq(t, "quote"))
397				q = *s ? *s : 0;
398			else if (streq(t, "set"))
399			{
400				while (isspace(*s))
401					s++;
402				num = (int)strtol(s, &e, 0);
403				if (e != s)
404					set = num;
405				else
406					error(2, "set number expected");
407			}
408			else if (streq(t, "translation"))
409				xp = translation(xp, s);
410		}
411		else
412		{
413			t = s + sfvalue(mp);
414			num = (int)strtol(s, &e, 0);
415			if (e != s)
416			{
417				s = e;
418				if (!*s)
419				{
420					if (mcput(mc, set, num, NiL))
421						error(2, "(%d,%d): cannot delete message", set, num);
422				}
423				else if (isspace(*s++))
424				{
425					if (t > (s + 1) && *(t -= 2) == '\\')
426					{
427						sfwrite(tp, s, t - s);
428						while (s = sfgetr(mp, '\n', 0))
429						{
430							error_info.line++;
431							t = s + sfvalue(mp);
432							if (t <= (s + 1) || *(t -= 2) != '\\')
433								break;
434							sfwrite(tp, s, t - s);
435						}
436						if (!(s = sfstruse(tp)))
437							error(ERROR_SYSTEM|3, "out of space");
438					}
439					if (q)
440					{
441						if (*s++ != q)
442						{
443							error(2, "(%d,%d): %c quote expected", set, num, q);
444							continue;
445						}
446						b = t = s;
447						while (c = *s++)
448						{
449							if (c == '\\')
450							{
451								c = chresc(s - 1, &e);
452								s = e;
453								if (c)
454									*t++ = c;
455								else
456									error(1, "nul character ignored");
457							}
458							else if (c == q)
459								break;
460							else
461								*t++ = c;
462						}
463						if (*s)
464						{
465							error(2, "(%d,%d): characters after quote not expected", set, num);
466							continue;
467						}
468						*t = 0;
469						s = b;
470					}
471					if (mcput(mc, set, num, s))
472						error(2, "(%d,%d): cannot add message", set, num);
473				}
474				else
475					error(2, "message text expected");
476			}
477			else
478				error(2, "message number expected");
479		}
480	}
481	error_info.file = 0;
482	error_info.line = 0;
483
484	/*
485	 * fix up the translation record
486	 */
487
488	if (xp)
489	{
490		t = "";
491		for (;;)
492		{
493			for (bp = 0, px = xp; px; px = px->next)
494				if (px->date && (!bp || strcoll(bp->date, px->date) < 0))
495					bp = px;
496			if (!bp)
497				break;
498			sfprintf(tp, "%s%s %s", t, bp->name, bp->date);
499			t = ", ";
500			bp->date = 0;
501		}
502		if (!(mc->translation = sfstruse(tp)))
503			error(ERROR_SYSTEM|3, "out of space");
504	}
505
506	/*
507	 * dump the catalog to a local temporary
508	 * rename if no errors
509	 */
510
511	if (!(s = pathtemp(NiL, 0, "", error_info.id, NiL)) || !(sp = sfopen(NiL, s, "w")))
512		error(ERROR_SYSTEM|3, "%s: cannot write catalog file", catfile);
513	if (mcdump(mc, sp) || mcclose(mc) || sfclose(sp))
514	{
515		remove(s);
516		error(ERROR_SYSTEM|3, "%s: temporary catalog file write error", s);
517	}
518	remove(catfile);
519	if (rename(s, catfile))
520		error(ERROR_SYSTEM|3, "%s: cannot rename from temporary catalog file %s", catfile, s);
521	return error_info.errors != 0;
522}
523