Annotation of src/lib/libintl/gettext.c, Revision 1.13
1.13 ! yamt 1: /* $NetBSD: gettext.c,v 1.12 2001/12/29 05:54:36 yamt Exp $ */
1.1 itojun 2:
3: /*-
1.9 minoura 4: * Copyright (c) 2000, 2001 Citrus Project,
1.1 itojun 5: * All rights reserved.
6: *
7: * Redistribution and use in source and binary forms, with or without
8: * modification, are permitted provided that the following conditions
9: * are met:
10: * 1. Redistributions of source code must retain the above copyright
11: * notice, this list of conditions and the following disclaimer.
12: * 2. Redistributions in binary form must reproduce the above copyright
13: * notice, this list of conditions and the following disclaimer in the
14: * documentation and/or other materials provided with the distribution.
15: *
16: * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
17: * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18: * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19: * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
20: * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21: * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
22: * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
23: * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
24: * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
25: * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
26: * SUCH DAMAGE.
1.10 yamt 27: *
28: * $Citrus: xpg4dl/FreeBSD/lib/libintl/gettext.c,v 1.31 2001/09/27 15:18:45 yamt Exp $
1.1 itojun 29: */
30:
31: #include <sys/cdefs.h>
32: #if defined(LIBC_SCCS) && !defined(lint)
1.13 ! yamt 33: __RCSID("$NetBSD: gettext.c,v 1.12 2001/12/29 05:54:36 yamt Exp $");
1.1 itojun 34: #endif /* LIBC_SCCS and not lint */
35:
36: #include <sys/types.h>
37: #include <sys/param.h>
38: #include <sys/stat.h>
39: #include <sys/mman.h>
40: #include <sys/uio.h>
41:
42: #include <fcntl.h>
43: #include <stdio.h>
44: #include <stdlib.h>
45: #include <unistd.h>
46: #include <string.h>
47: #if 0
48: #include <util.h>
49: #endif
50: #include <libintl.h>
51: #include <locale.h>
52: #include "libintl_local.h"
53: #include "pathnames.h"
54:
55: static const char *lookup_category __P((int));
56: static const char *split_locale __P((const char *));
57: static const char *lookup_mofile __P((char *, size_t, const char *,
1.10 yamt 58: const char *, const char *, const char *, struct domainbinding *));
1.1 itojun 59: static u_int32_t flip __P((u_int32_t, u_int32_t));
1.9 minoura 60: static int validate __P((void *, struct mohandle *));
61: static int mapit __P((const char *, struct domainbinding *));
62: static int unmapit __P((struct domainbinding *));
63: static const char *lookup_hash __P((const char *, struct domainbinding *));
64: static const char *lookup_bsearch __P((const char *, struct domainbinding *));
65: static const char *lookup __P((const char *, struct domainbinding *));
1.10 yamt 66: static const char *get_lang_env(const char *);
1.1 itojun 67:
68: /*
69: * shortcut functions. the main implementation resides in dcngettext().
70: */
71: char *
72: gettext(msgid)
73: const char *msgid;
74: {
75:
76: return dcngettext(NULL, msgid, NULL, 1UL, LC_MESSAGES);
77: }
78:
79: char *
80: dgettext(domainname, msgid)
81: const char *domainname;
82: const char *msgid;
83: {
84:
85: return dcngettext(domainname, msgid, NULL, 1UL, LC_MESSAGES);
86: }
87:
88: char *
89: dcgettext(domainname, msgid, category)
90: const char *domainname;
91: const char *msgid;
92: int category;
93: {
94:
95: return dcngettext(domainname, msgid, NULL, 1UL, category);
96: }
97:
98: char *
99: ngettext(msgid1, msgid2, n)
100: const char *msgid1;
101: const char *msgid2;
102: unsigned long int n;
103: {
104:
105: return dcngettext(NULL, msgid1, msgid2, n, LC_MESSAGES);
106: }
107:
108: char *
109: dngettext(domainname, msgid1, msgid2, n)
110: const char *domainname;
111: const char *msgid1;
112: const char *msgid2;
113: unsigned long int n;
114: {
115:
116: return dcngettext(domainname, msgid1, msgid2, n, LC_MESSAGES);
117: }
118:
119: /*
120: * dcngettext() -
121: * lookup internationalized message on database locale/category/domainname
122: * (like ja_JP.eucJP/LC_MESSAGES/domainname).
123: * if n equals to 1, internationalized message will be looked up for msgid1.
124: * otherwise, message will be looked up for msgid2.
125: * if the lookup fails, the function will return msgid1 or msgid2 as is.
126: *
127: * Even though the return type is "char *", caller should not rewrite the
128: * region pointed to by the return value (should be "const char *", but can't
129: * change it for compatibility with other implementations).
130: *
131: * by default (if domainname == NULL), domainname is taken from the value set
132: * by textdomain(). usually name of the application (like "ls") is used as
133: * domainname. category is usually LC_MESSAGES.
134: *
135: * the code reads in *.mo files generated by GNU gettext. *.mo is a host-
136: * endian encoded file. both endians are supported here, as the files are in
137: * /usr/share/locale! (or we should move those files into /usr/libdata)
138: */
139:
140: static const char *
141: lookup_category(category)
142: int category;
143: {
144:
145: switch (category) {
146: case LC_COLLATE: return "LC_COLLATE";
147: case LC_CTYPE: return "LC_CTYPE";
148: case LC_MONETARY: return "LC_MONETARY";
149: case LC_NUMERIC: return "LC_NUMERIC";
150: case LC_TIME: return "LC_TIME";
151: case LC_MESSAGES: return "LC_MESSAGES";
152: }
153: return NULL;
154: }
155:
156: /*
157: * XPG syntax: language[_territory[.codeset]][@modifier]
158: * XXX boundary check on "result" is lacking
159: */
160: static const char *
161: split_locale(lname)
162: const char *lname;
163: {
164: char buf[BUFSIZ], tmp[BUFSIZ];
165: char *l, *t, *c, *m;
166: static char result[BUFSIZ];
167:
168: memset(result, 0, sizeof(result));
169:
170: if (strlen(lname) + 1 > sizeof(buf)) {
171: fail:
172: return lname;
173: }
174:
175: strlcpy(buf, lname, sizeof(buf));
176: m = strrchr(buf, '@');
177: if (m)
178: *m++ = '\0';
179: c = strrchr(buf, '.');
180: if (c)
181: *c++ = '\0';
182: t = strrchr(buf, '_');
183: if (t)
184: *t++ = '\0';
185: l = buf;
186: if (strlen(l) == 0)
187: goto fail;
188: if (c && !t)
189: goto fail;
190:
191: if (m) {
192: if (t) {
193: if (c) {
194: snprintf(tmp, sizeof(tmp), "%s_%s.%s@%s",
195: l, t, c, m);
196: strlcat(result, tmp, sizeof(result));
197: strlcat(result, ":", sizeof(result));
198: }
199: snprintf(tmp, sizeof(tmp), "%s_%s@%s", l, t, m);
200: strlcat(result, tmp, sizeof(result));
201: strlcat(result, ":", sizeof(result));
202: }
203: snprintf(tmp, sizeof(tmp), "%s@%s", l, m);
204: strlcat(result, tmp, sizeof(result));
205: strlcat(result, ":", sizeof(result));
206: }
207: if (t) {
208: if (c) {
209: snprintf(tmp, sizeof(tmp), "%s_%s.%s", l, t, c);
210: strlcat(result, tmp, sizeof(result));
211: strlcat(result, ":", sizeof(result));
212: }
1.12 yamt 213: snprintf(tmp, sizeof(tmp), "%s_%s", l, t);
1.1 itojun 214: strlcat(result, tmp, sizeof(result));
215: strlcat(result, ":", sizeof(result));
216: }
217: strlcat(result, l, sizeof(result));
218:
219: return result;
220: }
221:
222: static const char *
1.9 minoura 223: lookup_mofile(buf, len, dir, lpath, category, domainname, db)
1.1 itojun 224: char *buf;
225: size_t len;
226: const char *dir;
1.10 yamt 227: const char *lpath; /* list of locales to be tried */
1.1 itojun 228: const char *category;
229: const char *domainname;
1.9 minoura 230: struct domainbinding *db;
1.1 itojun 231: {
232: struct stat st;
233: char *p, *q;
1.10 yamt 234: char lpath_tmp[BUFSIZ];
1.1 itojun 235:
1.10 yamt 236: strlcpy(lpath_tmp, lpath, sizeof(lpath_tmp));
237: q = lpath_tmp;
1.9 minoura 238: /* CONSTCOND */
1.1 itojun 239: while (1) {
240: p = strsep(&q, ":");
241: if (!p)
242: break;
243: if (!*p)
244: continue;
245:
246: /* don't mess with default locales */
247: if (strcmp(p, "C") == 0 || strcmp(p, "POSIX") == 0)
248: return NULL;
249:
250: /* validate pathname */
251: if (strchr(p, '/') || strchr(category, '/'))
252: continue;
253: #if 1 /*?*/
254: if (strchr(domainname, '/'))
255: continue;
256: #endif
257:
258: snprintf(buf, len, "%s/%s/%s/%s.mo", dir, p,
259: category, domainname);
260: if (stat(buf, &st) < 0)
261: continue;
262: if ((st.st_mode & S_IFMT) != S_IFREG)
263: continue;
264:
1.9 minoura 265: if (mapit(buf, db) == 0)
1.1 itojun 266: return buf;
267: }
268:
269: return NULL;
270: }
271:
272: static u_int32_t
273: flip(v, magic)
274: u_int32_t v;
275: u_int32_t magic;
276: {
277:
278: if (magic == MO_MAGIC)
279: return v;
280: else if (magic == MO_MAGIC_SWAPPED) {
281: v = ((v >> 24) & 0xff) | ((v >> 8) & 0xff00) |
282: ((v << 8) & 0xff0000) | ((v << 24) & 0xff000000);
283: return v;
284: } else {
285: abort();
286: /*NOTREACHED*/
287: }
288: }
289:
290: static int
1.9 minoura 291: validate(arg, mohandle)
1.1 itojun 292: void *arg;
1.9 minoura 293: struct mohandle *mohandle;
1.1 itojun 294: {
295: char *p;
296:
297: p = (char *)arg;
1.9 minoura 298: if (p < (char *)mohandle->addr ||
299: p > (char *)mohandle->addr + mohandle->len)
1.1 itojun 300: return 0;
301: else
302: return 1;
303: }
304:
305: int
1.9 minoura 306: mapit(path, db)
1.1 itojun 307: const char *path;
1.9 minoura 308: struct domainbinding *db;
1.1 itojun 309: {
310: int fd;
311: struct stat st;
312: char *base;
313: u_int32_t magic, revision;
314: struct moentry *otable, *ttable;
315: struct moentry_h *p;
316: struct mo *mo;
317: size_t l;
318: int i;
319: char *v;
1.9 minoura 320: struct mohandle *mohandle = &db->mohandle;
1.1 itojun 321:
1.9 minoura 322: if (mohandle->addr && mohandle->addr != MAP_FAILED &&
323: mohandle->mo.mo_magic)
1.1 itojun 324: return 0; /*already opened*/
325:
1.9 minoura 326: unmapit(db);
1.1 itojun 327:
328: #if 0
329: if (secure_path(path) != 0)
330: goto fail;
331: #endif
332: if (stat(path, &st) < 0)
333: goto fail;
334: if ((st.st_mode & S_IFMT) != S_IFREG || st.st_size > GETTEXT_MMAP_MAX)
335: goto fail;
336: fd = open(path, O_RDONLY);
337: if (fd < 0)
338: goto fail;
1.2 itojun 339: if (read(fd, &magic, sizeof(magic)) != sizeof(magic) ||
1.1 itojun 340: (magic != MO_MAGIC && magic != MO_MAGIC_SWAPPED)) {
341: close(fd);
342: goto fail;
343: }
1.2 itojun 344: if (read(fd, &revision, sizeof(revision)) != sizeof(revision) ||
1.1 itojun 345: flip(revision, magic) != MO_REVISION) {
346: close(fd);
347: goto fail;
348: }
1.9 minoura 349: mohandle->addr = mmap(NULL, (size_t)st.st_size, PROT_READ,
1.4 itojun 350: MAP_FILE | MAP_SHARED, fd, (off_t)0);
1.9 minoura 351: if (!mohandle->addr || mohandle->addr == MAP_FAILED) {
1.1 itojun 352: close(fd);
353: goto fail;
354: }
355: close(fd);
1.9 minoura 356: mohandle->len = (size_t)st.st_size;
1.1 itojun 357:
1.9 minoura 358: base = mohandle->addr;
359: mo = (struct mo *)mohandle->addr;
1.1 itojun 360:
361: /* flip endian. do not flip magic number! */
1.9 minoura 362: mohandle->mo.mo_magic = mo->mo_magic;
363: mohandle->mo.mo_revision = flip(mo->mo_revision, magic);
364: mohandle->mo.mo_nstring = flip(mo->mo_nstring, magic);
1.1 itojun 365:
366: /* validate otable/ttable */
367: otable = (struct moentry *)(base + flip(mo->mo_otable, magic));
368: ttable = (struct moentry *)(base + flip(mo->mo_ttable, magic));
1.9 minoura 369: if (!validate(otable, mohandle) ||
370: !validate(&otable[mohandle->mo.mo_nstring], mohandle)) {
371: unmapit(db);
1.1 itojun 372: goto fail;
373: }
1.9 minoura 374: if (!validate(ttable, mohandle) ||
375: !validate(&ttable[mohandle->mo.mo_nstring], mohandle)) {
376: unmapit(db);
1.1 itojun 377: goto fail;
378: }
379:
380: /* allocate [ot]table, and convert to normal pointer representation. */
1.9 minoura 381: l = sizeof(struct moentry_h) * mohandle->mo.mo_nstring;
382: mohandle->mo.mo_otable = (struct moentry_h *)malloc(l);
383: if (!mohandle->mo.mo_otable) {
384: unmapit(db);
1.1 itojun 385: goto fail;
386: }
1.9 minoura 387: mohandle->mo.mo_ttable = (struct moentry_h *)malloc(l);
388: if (!mohandle->mo.mo_ttable) {
389: unmapit(db);
1.1 itojun 390: goto fail;
391: }
1.9 minoura 392: p = mohandle->mo.mo_otable;
393: for (i = 0; i < mohandle->mo.mo_nstring; i++) {
1.1 itojun 394: p[i].len = flip(otable[i].len, magic);
395: p[i].off = base + flip(otable[i].off, magic);
396:
1.9 minoura 397: if (!validate(p[i].off, mohandle) ||
398: !validate(p[i].off + p[i].len + 1, mohandle)) {
399: unmapit(db);
1.1 itojun 400: goto fail;
401: }
402: }
1.9 minoura 403: p = mohandle->mo.mo_ttable;
404: for (i = 0; i < mohandle->mo.mo_nstring; i++) {
1.1 itojun 405: p[i].len = flip(ttable[i].len, magic);
406: p[i].off = base + flip(ttable[i].off, magic);
407:
1.9 minoura 408: if (!validate(p[i].off, mohandle) ||
409: !validate(p[i].off + p[i].len + 1, mohandle)) {
410: unmapit(db);
1.1 itojun 411: goto fail;
412: }
413: }
414:
415: /* grab MIME-header and charset field */
1.9 minoura 416: mohandle->mo.mo_header = lookup("", db);
417: if (mohandle->mo.mo_header)
418: v = strstr(mohandle->mo.mo_header, "charset=");
1.1 itojun 419: else
420: v = NULL;
421: if (v) {
1.9 minoura 422: mohandle->mo.mo_charset = strdup(v + 8);
423: if (!mohandle->mo.mo_charset)
1.6 itojun 424: goto fail;
1.9 minoura 425: v = strchr(mohandle->mo.mo_charset, '\n');
1.1 itojun 426: if (v)
427: *v = '\0';
428: }
429:
430: /*
431: * XXX check charset, reject it if we are unable to support the charset
432: * with the current locale.
433: * for example, if we are using euc-jp locale and we are looking at
434: * *.mo file encoded by euc-kr (charset=euc-kr), we should reject
435: * the *.mo file as we cannot support it.
436: */
437:
438: return 0;
439:
440: fail:
441: return -1;
442: }
443:
444: static int
1.9 minoura 445: unmapit(db)
446: struct domainbinding *db;
1.1 itojun 447: {
1.9 minoura 448: struct mohandle *mohandle = &db->mohandle;
1.1 itojun 449:
450: /* unmap if there's already mapped region */
1.9 minoura 451: if (mohandle->addr && mohandle->addr != MAP_FAILED)
452: munmap(mohandle->addr, mohandle->len);
453: mohandle->addr = NULL;
454: if (mohandle->mo.mo_otable)
455: free(mohandle->mo.mo_otable);
456: if (mohandle->mo.mo_ttable)
457: free(mohandle->mo.mo_ttable);
458: if (mohandle->mo.mo_charset)
459: free(mohandle->mo.mo_charset);
460: memset(&mohandle->mo, 0, sizeof(mohandle->mo));
1.1 itojun 461: return 0;
462: }
463:
1.9 minoura 464: /* ARGSUSED */
1.1 itojun 465: static const char *
1.9 minoura 466: lookup_hash(msgid, db)
1.1 itojun 467: const char *msgid;
1.9 minoura 468: struct domainbinding *db;
1.1 itojun 469: {
470:
471: /*
472: * XXX should try a hashed lookup here, but to do so, we need to
473: * look inside the GPL'ed *.c and re-implement...
474: */
475: return NULL;
476: }
477:
478: static const char *
1.9 minoura 479: lookup_bsearch(msgid, db)
1.1 itojun 480: const char *msgid;
1.9 minoura 481: struct domainbinding *db;
1.1 itojun 482: {
483: int top, bottom, middle, omiddle;
484: int n;
1.9 minoura 485: struct mohandle *mohandle = &db->mohandle;
1.1 itojun 486:
487: top = 0;
1.9 minoura 488: bottom = mohandle->mo.mo_nstring;
1.1 itojun 489: omiddle = -1;
1.9 minoura 490: /* CONSTCOND */
1.1 itojun 491: while (1) {
492: if (top > bottom)
1.4 itojun 493: break;
1.1 itojun 494: middle = (top + bottom) / 2;
495: /* avoid possible infinite loop, when the data is not sorted */
496: if (omiddle == middle)
1.4 itojun 497: break;
1.9 minoura 498: if (middle < 0 || middle >= mohandle->mo.mo_nstring)
1.4 itojun 499: break;
1.1 itojun 500:
1.9 minoura 501: n = strcmp(msgid, mohandle->mo.mo_otable[middle].off);
1.1 itojun 502: if (n == 0)
1.9 minoura 503: return (const char *)mohandle->mo.mo_ttable[middle].off;
1.1 itojun 504: else if (n < 0)
505: bottom = middle;
506: else
507: top = middle;
508: omiddle = middle;
509: }
510:
511: return NULL;
512: }
513:
514: static const char *
1.9 minoura 515: lookup(msgid, db)
1.1 itojun 516: const char *msgid;
1.9 minoura 517: struct domainbinding *db;
1.1 itojun 518: {
519: const char *v;
520:
1.9 minoura 521: v = lookup_hash(msgid, db);
1.1 itojun 522: if (v)
523: return v;
524:
1.9 minoura 525: return lookup_bsearch(msgid, db);
1.1 itojun 526: }
527:
1.10 yamt 528: static const char *get_lang_env(const char *category_name)
529: {
530: const char *lang;
531:
532: /* 1. see LANGUAGE variable first. */
533: lang = getenv("LANGUAGE");
534: if (lang)
535: return lang;
536:
537: /* 2. if LANGUAGE isn't set, see LC_ALL, LC_xxx, LANG. */
1.13 ! yamt 538: lang = getenv("LC_ALL");
1.10 yamt 539: if (!lang)
1.13 ! yamt 540: lang = getenv(category_name);
1.10 yamt 541: if (!lang)
542: lang = getenv("LANG");
543:
544: if (!lang)
545: return 0; /* error */
546:
547: return split_locale(lang);
548: }
549:
1.1 itojun 550: char *
551: dcngettext(domainname, msgid1, msgid2, n, category)
552: const char *domainname;
553: const char *msgid1;
554: const char *msgid2;
555: unsigned long int n;
556: int category;
557: {
558: const char *msgid;
559: char path[PATH_MAX];
1.10 yamt 560: const char *lpath;
1.1 itojun 561: static char olpath[PATH_MAX];
1.6 itojun 562: const char *cname = NULL;
1.1 itojun 563: const char *v;
1.6 itojun 564: static char *ocname = NULL;
565: static char *odomainname = NULL;
1.5 itojun 566: struct domainbinding *db;
1.1 itojun 567:
568: msgid = (n == 1) ? msgid1 : msgid2;
1.8 minoura 569: if (msgid == NULL)
570: return NULL;
1.1 itojun 571:
572: if (!domainname)
1.9 minoura 573: domainname = __current_domainname;
1.1 itojun 574: cname = lookup_category(category);
575: if (!domainname || !cname)
576: goto fail;
577:
1.10 yamt 578: lpath = get_lang_env(cname);
579: if (!lpath)
1.1 itojun 580: goto fail;
1.10 yamt 581:
1.9 minoura 582: for (db = __bindings; db; db = db->next)
1.5 itojun 583: if (strcmp(db->domainname, domainname) == 0)
584: break;
1.9 minoura 585: if (!db) {
586: if (!bindtextdomain(domainname, _PATH_TEXTDOMAIN))
587: goto fail;
588: db = __bindings;
1.11 yamt 589: }
590:
591: /* resolve relative path */
592: /* XXX not necessary? */
593: if (db->path[0] != '/') {
594: char buf[PATH_MAX];
595:
596: if (getcwd(buf, sizeof(buf)) == 0)
597: goto fail;
598: if (strlcat(buf, "/", sizeof(buf)) >= sizeof(buf))
599: goto fail;
600: if (strlcat(buf, db->path, sizeof(buf)) >= sizeof(buf))
601: goto fail;
602: strcpy(db->path, buf);
1.9 minoura 603: }
1.5 itojun 604:
1.1 itojun 605: /* don't bother looking it up if the values are the same */
1.5 itojun 606: if (odomainname && strcmp(domainname, odomainname) == 0 &&
1.9 minoura 607: ocname && strcmp(cname, ocname) == 0 && strcmp(lpath, olpath) == 0 &&
608: db->mohandle.mo.mo_magic)
1.1 itojun 609: goto found;
610:
611: /* try to find appropriate file, from $LANGUAGE */
1.5 itojun 612: if (lookup_mofile(path, sizeof(path), db->path, lpath, cname,
1.9 minoura 613: domainname, db) == NULL)
1.3 itojun 614: goto fail;
1.5 itojun 615:
616: if (odomainname)
617: free(odomainname);
618: if (ocname)
619: free(ocname);
1.6 itojun 620: odomainname = strdup(domainname);
1.5 itojun 621: ocname = strdup(cname);
1.6 itojun 622: if (!odomainname || !ocname) {
623: if (odomainname)
624: free(odomainname);
625: if (ocname)
626: free(ocname);
627: odomainname = ocname = NULL;
628: }
1.10 yamt 629: else
630: strlcpy(olpath, lpath, sizeof(olpath));
1.1 itojun 631:
632: found:
1.9 minoura 633: v = lookup(msgid, db);
1.1 itojun 634: if (v) {
635: /*
636: * XXX call iconv() here, if translated text is encoded
637: * differently from currently-selected encoding (locale).
638: * look at Content-type header in *.mo file, in string obtained
639: * by gettext("").
640: */
641:
642: /*
643: * Given the amount of printf-format security issues, it may
644: * be a good idea to validate if the original msgid and the
645: * translated message format string carry the same printf-like
646: * format identifiers.
647: */
648:
649: msgid = v;
650: }
651:
652: fail:
653: /* LINTED const cast */
654: return (char *)msgid;
655: }
CVSweb <webmaster@jp.NetBSD.org>