1/* Reading NeXTstep/GNUstep .strings files.
2   Copyright (C) 2003, 2005-2006 Free Software Foundation, Inc.
3   Written by Bruno Haible <bruno@clisp.org>, 2003.
4
5   This program is free software; you can redistribute it and/or modify
6   it under the terms of the GNU General Public License as published by
7   the Free Software Foundation; either version 2, or (at your option)
8   any later version.
9
10   This program is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU General Public License for more details.
14
15   You should have received a copy of the GNU General Public License
16   along with this program; if not, write to the Free Software Foundation,
17   Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.  */
18
19#ifdef HAVE_CONFIG_H
20# include <config.h>
21#endif
22
23/* Specification.  */
24#include "read-stringtable.h"
25
26#include <assert.h>
27#include <errno.h>
28#include <stdbool.h>
29#include <stdio.h>
30#include <stdlib.h>
31#include <string.h>
32
33#include "error.h"
34#include "error-progname.h"
35#include "read-catalog-abstract.h"
36#include "xalloc.h"
37#include "xvasprintf.h"
38#include "po-xerror.h"
39#include "utf8-ucs4.h"
40#include "ucs4-utf8.h"
41#include "gettext.h"
42
43#define _(str) gettext (str)
44
45/* The format of NeXTstep/GNUstep .strings files is documented in
46     gnustep-base-1.8.0/Tools/make_strings/Using.txt
47   and in the comments of method propertyListFromStringsFileFormat in
48     gnustep-base-1.8.0/Source/NSString.m
49   In summary, it's a Objective-C like file with pseudo-assignments of the form
50          "key" = "value";
51   where the key is the msgid and the value is the msgstr.
52
53   The implementation of the parser of .strings files is in
54     gnustep-base-1.8.0/Source/NSString.m
55     function GSPropertyListFromStringsFormat
56     (indirectly called from NSBundle's method localizedStringForKey).
57
58   A test case is in
59     gnustep-base-1.8.0/Testing/English.lproj/NXStringTable.example
60 */
61
62/* Handling of comments: We copy all comments from the .strings file to
63   the PO file. This is not really needed; it's a service for translators
64   who don't like PO files and prefer to maintain the .strings file.  */
65
66
67/* Real filename, used in error messages about the input file.  */
68static const char *real_file_name;
69
70/* File name and line number.  */
71extern lex_pos_ty gram_pos;
72
73/* The input file stream.  */
74static FILE *fp;
75
76
77/* Phase 1: Read a byte.
78   Max. 4 pushback characters.  */
79
80static unsigned char phase1_pushback[4];
81static int phase1_pushback_length;
82
83static int
84phase1_getc ()
85{
86  int c;
87
88  if (phase1_pushback_length)
89    return phase1_pushback[--phase1_pushback_length];
90
91  c = getc (fp);
92
93  if (c == EOF)
94    {
95      if (ferror (fp))
96	{
97	  const char *errno_description = strerror (errno);
98	  po_xerror (PO_SEVERITY_FATAL_ERROR, NULL, NULL, 0, 0, false,
99		     xasprintf ("%s: %s",
100				xasprintf (_("error while reading \"%s\""),
101					   real_file_name),
102				errno_description));
103	}
104      return EOF;
105    }
106
107  return c;
108}
109
110static void
111phase1_ungetc (int c)
112{
113  if (c != EOF)
114    phase1_pushback[phase1_pushback_length++] = c;
115}
116
117
118/* Phase 2: Read an UCS-4 character.
119   Max. 2 pushback characters.  */
120
121/* End-of-file indicator for functions returning an UCS-4 character.  */
122#define UEOF -1
123
124static int phase2_pushback[4];
125static int phase2_pushback_length;
126
127/* The input file can be in Unicode encoding (UCS-2BE, UCS-2LE, UTF-8, each
128   with a BOM!), or otherwise the locale-dependent default encoding is used.
129   Since we don't want to depend on the locale here, we use ISO-8859-1
130   instead.  */
131enum enc
132{
133  enc_undetermined,
134  enc_ucs2be,
135  enc_ucs2le,
136  enc_utf8,
137  enc_iso8859_1
138};
139static enum enc encoding;
140
141static int
142phase2_getc ()
143{
144  if (phase2_pushback_length)
145    return phase2_pushback[--phase2_pushback_length];
146
147  if (encoding == enc_undetermined)
148    {
149      /* Determine the input file's encoding.  */
150      int c0, c1;
151
152      c0 = phase1_getc ();
153      if (c0 == EOF)
154	return UEOF;
155      c1 = phase1_getc ();
156      if (c1 == EOF)
157	{
158	  phase1_ungetc (c0);
159	  encoding = enc_iso8859_1;
160	}
161      else if (c0 == 0xfe && c1 == 0xff)
162	encoding = enc_ucs2be;
163      else if (c0 == 0xff && c1 == 0xfe)
164	encoding = enc_ucs2le;
165      else
166	{
167	  int c2;
168
169	  c2 = phase1_getc ();
170	  if (c2 == EOF)
171	    {
172	      phase1_ungetc (c1);
173	      phase1_ungetc (c0);
174	      encoding = enc_iso8859_1;
175	    }
176	  else if (c0 == 0xef && c1 == 0xbb && c2 == 0xbf)
177	    encoding = enc_utf8;
178	  else
179	    {
180	      phase1_ungetc (c2);
181	      phase1_ungetc (c1);
182	      phase1_ungetc (c0);
183	      encoding = enc_iso8859_1;
184	    }
185	}
186    }
187
188  switch (encoding)
189    {
190    case enc_ucs2be:
191      /* Read an UCS-2BE encoded character.  */
192      {
193	int c0, c1;
194
195	c0 = phase1_getc ();
196	if (c0 == EOF)
197	  return UEOF;
198	c1 = phase1_getc ();
199	if (c1 == EOF)
200	  return UEOF;
201	return (c0 << 8) + c1;
202      }
203
204    case enc_ucs2le:
205      /* Read an UCS-2LE encoded character.  */
206      {
207	int c0, c1;
208
209	c0 = phase1_getc ();
210	if (c0 == EOF)
211	  return UEOF;
212	c1 = phase1_getc ();
213	if (c1 == EOF)
214	  return UEOF;
215	return c0 + (c1 << 8);
216      }
217
218    case enc_utf8:
219      /* Read an UTF-8 encoded character.  */
220      {
221	unsigned char buf[6];
222	unsigned int count;
223	int c;
224	unsigned int uc;
225
226	c = phase1_getc ();
227	if (c == EOF)
228	  return UEOF;
229	buf[0] = c;
230	count = 1;
231
232	if (buf[0] >= 0xc0)
233	  {
234	    c = phase1_getc ();
235	    if (c == EOF)
236	      return UEOF;
237	    buf[1] = c;
238	    count = 2;
239
240	    if (buf[0] >= 0xe0
241		&& ((buf[1] ^ 0x80) < 0x40))
242	      {
243		c = phase1_getc ();
244		if (c == EOF)
245		  return UEOF;
246		buf[2] = c;
247		count = 3;
248
249		if (buf[0] >= 0xf0
250		    && ((buf[2] ^ 0x80) < 0x40))
251		  {
252		    c = phase1_getc ();
253		    if (c == EOF)
254		      return UEOF;
255		    buf[3] = c;
256		    count = 4;
257
258		    if (buf[0] >= 0xf8
259			&& ((buf[3] ^ 0x80) < 0x40))
260		      {
261			c = phase1_getc ();
262			if (c == EOF)
263			  return UEOF;
264			buf[4] = c;
265			count = 5;
266
267			if (buf[0] >= 0xfc
268			    && ((buf[4] ^ 0x80) < 0x40))
269			  {
270			    c = phase1_getc ();
271			    if (c == EOF)
272			      return UEOF;
273			    buf[5] = c;
274			    count = 6;
275			  }
276		      }
277		  }
278	      }
279	  }
280
281	u8_mbtouc (&uc, buf, count);
282	return uc;
283      }
284
285    case enc_iso8859_1:
286      /* Read an ISO-8859-1 encoded character.  */
287      {
288	int c = phase1_getc ();
289
290	if (c == EOF)
291	  return UEOF;
292	return c;
293      }
294
295    default:
296      abort ();
297    }
298}
299
300static void
301phase2_ungetc (int c)
302{
303  if (c != UEOF)
304    phase2_pushback[phase2_pushback_length++] = c;
305}
306
307
308/* Phase 3: Read an UCS-4 character, with line number handling.  */
309
310static int
311phase3_getc ()
312{
313  int c = phase2_getc ();
314
315  if (c == '\n')
316    gram_pos.line_number++;
317
318  return c;
319}
320
321static void
322phase3_ungetc (int c)
323{
324  if (c == '\n')
325    --gram_pos.line_number;
326  phase2_ungetc (c);
327}
328
329
330/* Convert from UCS-4 to UTF-8.  */
331static char *
332conv_from_ucs4 (const int *buffer, size_t buflen)
333{
334  unsigned char *utf8_string;
335  size_t pos;
336  unsigned char *q;
337
338  /* Each UCS-4 word needs 6 bytes at worst.  */
339  utf8_string = (unsigned char *) xmalloc (6 * buflen + 1);
340
341  for (pos = 0, q = utf8_string; pos < buflen; )
342    {
343      unsigned int uc;
344      int n;
345
346      uc = buffer[pos++];
347      n = u8_uctomb (q, uc, 6);
348      assert (n > 0);
349      q += n;
350    }
351  *q = '\0';
352  assert (q - utf8_string <= 6 * buflen);
353
354  return (char *) utf8_string;
355}
356
357
358/* Parse a string enclosed in double-quotes.  Input is UCS-4 encoded.
359   Return the string in UTF-8 encoding, or NULL if the input doesn't represent
360   a valid string enclosed in double-quotes.  */
361static char *
362parse_escaped_string (const int *string, size_t length)
363{
364  static int *buffer;
365  static size_t bufmax;
366  static size_t buflen;
367  const int *string_limit = string + length;
368  int c;
369
370  if (string == string_limit)
371    return NULL;
372  c = *string++;
373  if (c != '"')
374    return NULL;
375  buflen = 0;
376  for (;;)
377    {
378      if (string == string_limit)
379	return NULL;
380      c = *string++;
381      if (c == '"')
382	break;
383      if (c == '\\')
384	{
385	  if (string == string_limit)
386	    return NULL;
387	  c = *string++;
388	  if (c >= '0' && c <= '7')
389	    {
390	      unsigned int n = 0;
391	      int j = 0;
392	      for (;;)
393		{
394		  n = n * 8 + (c - '0');
395		  if (++j == 3)
396		    break;
397		  if (string == string_limit)
398		    break;
399		  c = *string;
400		  if (!(c >= '0' && c <= '7'))
401		    break;
402		  string++;
403		}
404	      c = n;
405	    }
406	  else if (c == 'u' || c == 'U')
407	    {
408	      unsigned int n = 0;
409	      int j;
410	      for (j = 0; j < 4; j++)
411		{
412		  if (string == string_limit)
413		    break;
414		  c = *string;
415		  if (c >= '0' && c <= '9')
416		    n = n * 16 + (c - '0');
417		  else if (c >= 'A' && c <= 'F')
418		    n = n * 16 + (c - 'A' + 10);
419		  else if (c >= 'a' && c <= 'f')
420		    n = n * 16 + (c - 'a' + 10);
421		  else
422		    break;
423		  string++;
424		}
425	      c = n;
426	    }
427	  else
428	    switch (c)
429	      {
430	      case 'a': c = '\a'; break;
431	      case 'b': c = '\b'; break;
432	      case 't': c = '\t'; break;
433	      case 'r': c = '\r'; break;
434	      case 'n': c = '\n'; break;
435	      case 'v': c = '\v'; break;
436	      case 'f': c = '\f'; break;
437	      }
438	}
439      if (buflen >= bufmax)
440	{
441	  bufmax = 2 * bufmax + 10;
442	  buffer = xrealloc (buffer, bufmax * sizeof (int));
443	}
444      buffer[buflen++] = c;
445    }
446
447  return conv_from_ucs4 (buffer, buflen);
448}
449
450
451/* Accumulating flag comments.  */
452
453static char *special_comment;
454
455static inline void
456special_comment_reset ()
457{
458  if (special_comment != NULL)
459    free (special_comment);
460  special_comment = NULL;
461}
462
463static void
464special_comment_add (const char *flag)
465{
466  if (special_comment == NULL)
467    special_comment = xstrdup (flag);
468  else
469    {
470      size_t total_len = strlen (special_comment) + 2 + strlen (flag) + 1;
471      special_comment = xrealloc (special_comment, total_len);
472      strcat (special_comment, ", ");
473      strcat (special_comment, flag);
474    }
475}
476
477static inline void
478special_comment_finish ()
479{
480  if (special_comment != NULL)
481    {
482      po_callback_comment_special (special_comment);
483      free (special_comment);
484      special_comment = NULL;
485    }
486}
487
488
489/* Accumulating comments.  */
490
491static int *buffer;
492static size_t bufmax;
493static size_t buflen;
494static bool next_is_obsolete;
495static bool next_is_fuzzy;
496static char *fuzzy_msgstr;
497static bool expect_fuzzy_msgstr_as_c_comment;
498static bool expect_fuzzy_msgstr_as_cxx_comment;
499
500static inline void
501comment_start ()
502{
503  buflen = 0;
504}
505
506static inline void
507comment_add (int c)
508{
509  if (buflen >= bufmax)
510    {
511      bufmax = 2 * bufmax + 10;
512      buffer = xrealloc (buffer, bufmax * sizeof (int));
513    }
514  buffer[buflen++] = c;
515}
516
517static inline void
518comment_line_end (size_t chars_to_remove, bool test_for_fuzzy_msgstr)
519{
520  char *line;
521
522  buflen -= chars_to_remove;
523  /* Drop trailing white space, but not EOLs.  */
524  while (buflen >= 1
525	 && (buffer[buflen - 1] == ' ' || buffer[buflen - 1] == '\t'))
526    --buflen;
527
528  /* At special positions we interpret a comment of the form
529       = "escaped string"
530     with an optional trailing semicolon as being the fuzzy msgstr, not a
531     regular comment.  */
532  if (test_for_fuzzy_msgstr
533      && buflen > 2 && buffer[0] == '=' && buffer[1] == ' '
534      && (fuzzy_msgstr =
535	  parse_escaped_string (buffer + 2,
536				buflen - (buffer[buflen - 1] == ';') - 2)))
537    return;
538
539  line = conv_from_ucs4 (buffer, buflen);
540
541  if (strcmp (line, "Flag: untranslated") == 0)
542    {
543      special_comment_add ("fuzzy");
544      next_is_fuzzy = true;
545    }
546  else if (strcmp (line, "Flag: unmatched") == 0)
547    next_is_obsolete = true;
548  else if (strlen (line) >= 6 && memcmp (line, "Flag: ", 6) == 0)
549    special_comment_add (line + 6);
550  else if (strlen (line) >= 9 && memcmp (line, "Comment: ", 9) == 0)
551    /* A comment extracted from the source.  */
552    po_callback_comment_dot (line + 9);
553  else
554    {
555      char *last_colon;
556      unsigned long number;
557      char *endp;
558
559      if (strlen (line) >= 6 && memcmp (line, "File: ", 6) == 0
560	  && (last_colon = strrchr (line + 6, ':')) != NULL
561	  && *(last_colon + 1) != '\0'
562	  && (number = strtoul (last_colon + 1, &endp, 10), *endp == '\0'))
563	{
564	  /* A "File: <filename>:<number>" type comment.  */
565	  *last_colon = '\0';
566	  po_callback_comment_filepos (line + 6, number);
567	}
568      else
569	po_callback_comment (line);
570    }
571}
572
573
574/* Phase 4: Replace each comment that is not inside a string with a space
575   character.  */
576
577static int
578phase4_getc ()
579{
580  int c;
581
582  c = phase3_getc ();
583  if (c != '/')
584    return c;
585  c = phase3_getc ();
586  switch (c)
587    {
588    default:
589      phase3_ungetc (c);
590      return '/';
591
592    case '*':
593      /* C style comment.  */
594      {
595	bool last_was_star;
596	size_t trailing_stars;
597	bool seen_newline;
598
599	comment_start ();
600	last_was_star = false;
601	trailing_stars = 0;
602	seen_newline = false;
603	/* Drop additional stars at the beginning of the comment.  */
604	for (;;)
605	  {
606	    c = phase3_getc ();
607	    if (c != '*')
608	      break;
609	    last_was_star = true;
610	  }
611	phase3_ungetc (c);
612	for (;;)
613	  {
614	    c = phase3_getc ();
615	    if (c == UEOF)
616	      break;
617	    /* We skip all leading white space, but not EOLs.  */
618	    if (!(buflen == 0 && (c == ' ' || c == '\t')))
619	      comment_add (c);
620	    switch (c)
621	      {
622	      case '\n':
623		seen_newline = true;
624		comment_line_end (1, false);
625		comment_start ();
626		last_was_star = false;
627		trailing_stars = 0;
628		continue;
629
630	      case '*':
631		last_was_star = true;
632		trailing_stars++;
633		continue;
634
635	      case '/':
636		if (last_was_star)
637		  {
638		    /* Drop additional stars at the end of the comment.  */
639		    comment_line_end (trailing_stars + 1,
640				      expect_fuzzy_msgstr_as_c_comment
641				      && !seen_newline);
642		    break;
643		  }
644		/* FALLTHROUGH */
645
646	      default:
647		last_was_star = false;
648		trailing_stars = 0;
649		continue;
650	      }
651	    break;
652	  }
653	return ' ';
654      }
655
656    case '/':
657      /* C++ style comment.  */
658      comment_start ();
659      for (;;)
660	{
661	  c = phase3_getc ();
662	  if (c == '\n' || c == UEOF)
663	    break;
664	  /* We skip all leading white space, but not EOLs.  */
665	  if (!(buflen == 0 && (c == ' ' || c == '\t')))
666	    comment_add (c);
667	}
668      comment_line_end (0, expect_fuzzy_msgstr_as_cxx_comment);
669      return '\n';
670    }
671}
672
673static inline void
674phase4_ungetc (int c)
675{
676  phase3_ungetc (c);
677}
678
679
680/* Return true if a character is considered as whitespace.  */
681static bool
682is_whitespace (int c)
683{
684  return (c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\f'
685	  || c == '\b');
686}
687
688/* Return true if a character needs quoting, i.e. cannot be used in unquoted
689   tokens.  */
690static bool
691is_quotable (int c)
692{
693  if ((c >= '0' && c <= '9')
694      || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))
695    return false;
696  switch (c)
697    {
698    case '!': case '#': case '$': case '%': case '&': case '*':
699    case '+': case '-': case '.': case '/': case ':': case '?':
700    case '@': case '|': case '~': case '_': case '^':
701      return false;
702    default:
703      return true;
704    }
705}
706
707
708/* Read a key or value string.
709   Return the string in UTF-8 encoding, or NULL if no string is seen.
710   Return the start position of the string in *pos.  */
711static char *
712read_string (lex_pos_ty *pos)
713{
714  static int *buffer;
715  static size_t bufmax;
716  static size_t buflen;
717  int c;
718
719  /* Skip whitespace before the string.  */
720  do
721    c = phase4_getc ();
722  while (is_whitespace (c));
723
724  if (c == UEOF)
725    /* No more string.  */
726    return NULL;
727
728  *pos = gram_pos;
729  buflen = 0;
730  if (c == '"')
731    {
732      /* Read a string enclosed in double-quotes.  */
733      for (;;)
734	{
735	  c = phase3_getc ();
736	  if (c == UEOF || c == '"')
737	    break;
738	  if (c == '\\')
739	    {
740	      c = phase3_getc ();
741	      if (c == UEOF)
742		break;
743	      if (c >= '0' && c <= '7')
744		{
745		  unsigned int n = 0;
746		  int j = 0;
747		  for (;;)
748		    {
749		      n = n * 8 + (c - '0');
750		      if (++j == 3)
751			break;
752		      c = phase3_getc ();
753		      if (!(c >= '0' && c <= '7'))
754			{
755			  phase3_ungetc (c);
756			  break;
757			}
758		    }
759		  c = n;
760		}
761	      else if (c == 'u' || c == 'U')
762		{
763		  unsigned int n = 0;
764		  int j;
765		  for (j = 0; j < 4; j++)
766		    {
767		      c = phase3_getc ();
768		      if (c >= '0' && c <= '9')
769			n = n * 16 + (c - '0');
770		      else if (c >= 'A' && c <= 'F')
771			n = n * 16 + (c - 'A' + 10);
772		      else if (c >= 'a' && c <= 'f')
773			n = n * 16 + (c - 'a' + 10);
774		      else
775			{
776			  phase3_ungetc (c);
777			  break;
778			}
779		    }
780		  c = n;
781		}
782	      else
783		switch (c)
784		  {
785		  case 'a': c = '\a'; break;
786		  case 'b': c = '\b'; break;
787		  case 't': c = '\t'; break;
788		  case 'r': c = '\r'; break;
789		  case 'n': c = '\n'; break;
790		  case 'v': c = '\v'; break;
791		  case 'f': c = '\f'; break;
792		  }
793	    }
794	  if (buflen >= bufmax)
795	    {
796	      bufmax = 2 * bufmax + 10;
797	      buffer = xrealloc (buffer, bufmax * sizeof (int));
798	    }
799	  buffer[buflen++] = c;
800	}
801      if (c == UEOF)
802	po_xerror (PO_SEVERITY_ERROR, NULL,
803		   real_file_name, gram_pos.line_number, (size_t)(-1), false,
804		   _("warning: unterminated string"));
805    }
806  else
807    {
808      /* Read a token outside quotes.  */
809      if (is_quotable (c))
810	po_xerror (PO_SEVERITY_ERROR, NULL,
811		   real_file_name, gram_pos.line_number, (size_t)(-1), false,
812		   _("warning: syntax error"));
813      for (; c != UEOF && !is_quotable (c); c = phase4_getc ())
814	{
815	  if (buflen >= bufmax)
816	    {
817	      bufmax = 2 * bufmax + 10;
818	      buffer = xrealloc (buffer, bufmax * sizeof (int));
819	    }
820	  buffer[buflen++] = c;
821	}
822    }
823
824  return conv_from_ucs4 (buffer, buflen);
825}
826
827
828/* Read a .strings file from a stream, and dispatch to the various
829   abstract_catalog_reader_class_ty methods.  */
830static void
831stringtable_parse (abstract_catalog_reader_ty *pop, FILE *file,
832		   const char *real_filename, const char *logical_filename)
833{
834  fp = file;
835  real_file_name = real_filename;
836  gram_pos.file_name = xstrdup (real_file_name);
837  gram_pos.line_number = 1;
838  encoding = enc_undetermined;
839  expect_fuzzy_msgstr_as_c_comment = false;
840  expect_fuzzy_msgstr_as_cxx_comment = false;
841
842  for (;;)
843    {
844      char *msgid;
845      lex_pos_ty msgid_pos;
846      char *msgstr;
847      lex_pos_ty msgstr_pos;
848      int c;
849
850      /* Prepare for next msgid/msgstr pair.  */
851      special_comment_reset ();
852      next_is_obsolete = false;
853      next_is_fuzzy = false;
854      fuzzy_msgstr = NULL;
855
856      /* Read the key and all the comments preceding it.  */
857      msgid = read_string (&msgid_pos);
858      if (msgid == NULL)
859	break;
860
861      special_comment_finish ();
862
863      /* Skip whitespace.  */
864      do
865	c = phase4_getc ();
866      while (is_whitespace (c));
867
868      /* Expect a '=' or ';'.  */
869      if (c == UEOF)
870	{
871	  po_xerror (PO_SEVERITY_ERROR, NULL,
872		     real_file_name, gram_pos.line_number, (size_t)(-1), false,
873		     _("warning: unterminated key/value pair"));
874	  break;
875	}
876      if (c == ';')
877	{
878	  /* "key"; is an abbreviation for "key"=""; and does not
879	     necessarily designate an untranslated entry.  */
880	  msgstr = xstrdup ("");
881	  msgstr_pos = msgid_pos;
882	  po_callback_message (NULL, msgid, &msgid_pos, NULL,
883			       msgstr, strlen (msgstr) + 1, &msgstr_pos,
884			       NULL, NULL, NULL,
885			       false, next_is_obsolete);
886	}
887      else if (c == '=')
888	{
889	  /* Read the value.  */
890	  msgstr = read_string (&msgstr_pos);
891	  if (msgstr == NULL)
892	    {
893	      po_xerror (PO_SEVERITY_ERROR, NULL,
894			 real_file_name, gram_pos.line_number, (size_t)(-1),
895			 false, _("warning: unterminated key/value pair"));
896	      break;
897	    }
898
899	  /* Skip whitespace.  But for fuzzy key/value pairs, look for the
900	     tentative msgstr in the form of a C style comment.  */
901	  expect_fuzzy_msgstr_as_c_comment = next_is_fuzzy;
902	  do
903	    {
904	      c = phase4_getc ();
905	      if (fuzzy_msgstr != NULL)
906		expect_fuzzy_msgstr_as_c_comment = false;
907	    }
908	  while (is_whitespace (c));
909	  expect_fuzzy_msgstr_as_c_comment = false;
910
911	  /* Expect a ';'.  */
912	  if (c == ';')
913	    {
914	      /* But for fuzzy key/value pairs, look for the tentative msgstr
915		 in the form of a C++ style comment. */
916	      if (fuzzy_msgstr == NULL && next_is_fuzzy)
917		{
918		  do
919		    c = phase3_getc ();
920		  while (c == ' ');
921		  phase3_ungetc (c);
922
923		  expect_fuzzy_msgstr_as_cxx_comment = true;
924		  c = phase4_getc ();
925		  phase4_ungetc (c);
926		  expect_fuzzy_msgstr_as_cxx_comment = false;
927		}
928	      if (fuzzy_msgstr != NULL && strcmp (msgstr, msgid) == 0)
929		msgstr = fuzzy_msgstr;
930
931	      /* A key/value pair.  */
932	      po_callback_message (NULL, msgid, &msgid_pos, NULL,
933				   msgstr, strlen (msgstr) + 1, &msgstr_pos,
934				   NULL, NULL, NULL,
935				   false, next_is_obsolete);
936	    }
937	  else
938	    {
939	      po_xerror (PO_SEVERITY_ERROR, NULL,
940			 real_file_name, gram_pos.line_number, (size_t)(-1),
941			 false, _("\
942warning: syntax error, expected ';' after string"));
943	      break;
944	    }
945	}
946      else
947	{
948	  po_xerror (PO_SEVERITY_ERROR, NULL,
949		     real_file_name, gram_pos.line_number, (size_t)(-1), false,
950		     _("\
951warning: syntax error, expected '=' or ';' after string"));
952	  break;
953	}
954    }
955
956  fp = NULL;
957  real_file_name = NULL;
958  gram_pos.line_number = 0;
959}
960
961const struct catalog_input_format input_format_stringtable =
962{
963  stringtable_parse,			/* parse */
964  true					/* produces_utf8 */
965};
966