1 /*
2  * Verify that translations in the .po file have the same number and type of
3  * printf-style format strings.
4  *
5  * Copyright 2007-2017 by Apple Inc.
6  * Copyright 1997-2007 by Easy Software Products, all rights reserved.
7  *
8  * Licensed under Apache License v2.0.  See the file "LICENSE" for more information.
9  *
10  * Usage:
11  *
12  *   checkpo filename.{po,strings} [... filenameN.{po,strings}]
13  *
14  * Compile with:
15  *
16  *   gcc -o checkpo checkpo.c `cups-config --libs`
17  */
18 
19 #include <cups/cups-private.h>
20 
21 
22 /*
23  * Local functions...
24  */
25 
26 static char		*abbreviate(const char *s, char *buf, int bufsize);
27 static cups_array_t	*collect_formats(const char *id);
28 static void		free_formats(cups_array_t *fmts);
29 
30 
31 /*
32  * 'main()' - Validate .po and .strings files.
33  */
34 
35 int					/* O - Exit code */
main(int argc,char * argv[])36 main(int  argc,				/* I - Number of command-line args */
37      char *argv[])			/* I - Command-line arguments */
38 {
39   int			i;		/* Looping var */
40   cups_array_t		*po;		/* .po file */
41   _cups_message_t	*msg;		/* Current message */
42   cups_array_t		*idfmts,	/* Format strings in msgid */
43 			*strfmts;	/* Format strings in msgstr */
44   char			*idfmt,		/* Current msgid format string */
45 			*strfmt;	/* Current msgstr format string */
46   int			fmtidx;		/* Format index */
47   int			status,		/* Exit status */
48 			pass,		/* Pass/fail status */
49 			untranslated;	/* Untranslated messages */
50   char			idbuf[80],	/* Abbreviated msgid */
51 			strbuf[80];	/* Abbreviated msgstr */
52 
53 
54   if (argc < 2)
55   {
56     puts("Usage: checkpo filename.{po,strings} [... filenameN.{po,strings}]");
57     return (1);
58   }
59 
60  /*
61   * Check every .po or .strings file on the command-line...
62   */
63 
64   for (i = 1, status = 0; i < argc; i ++)
65   {
66    /*
67     * Use the CUPS .po loader to get the message strings...
68     */
69 
70     if (strstr(argv[i], ".strings"))
71       po = _cupsMessageLoad(argv[i], _CUPS_MESSAGE_STRINGS);
72     else
73       po = _cupsMessageLoad(argv[i], _CUPS_MESSAGE_PO | _CUPS_MESSAGE_EMPTY);
74 
75     if (!po)
76     {
77       perror(argv[i]);
78       return (1);
79     }
80 
81     if (i > 1)
82       putchar('\n');
83     printf("%s: ", argv[i]);
84     fflush(stdout);
85 
86    /*
87     * Scan every message for a % string and then match them up with
88     * the corresponding string in the translation...
89     */
90 
91     pass         = 1;
92     untranslated = 0;
93 
94     for (msg = (_cups_message_t *)cupsArrayFirst(po);
95          msg;
96 	 msg = (_cups_message_t *)cupsArrayNext(po))
97     {
98      /*
99       * Make sure filter message prefixes are not translated...
100       */
101 
102       if (!strncmp(msg->msg, "ALERT:", 6) || !strncmp(msg->msg, "CRIT:", 5) ||
103           !strncmp(msg->msg, "DEBUG:", 6) || !strncmp(msg->msg, "DEBUG2:", 7) ||
104           !strncmp(msg->msg, "EMERG:", 6) || !strncmp(msg->msg, "ERROR:", 6) ||
105           !strncmp(msg->msg, "INFO:", 5) || !strncmp(msg->msg, "NOTICE:", 7) ||
106           !strncmp(msg->msg, "WARNING:", 8))
107       {
108         if (pass)
109 	{
110 	  pass = 0;
111 	  puts("FAIL");
112 	}
113 
114 	printf("    Bad prefix on filter message \"%s\"\n",
115 	       abbreviate(msg->msg, idbuf, sizeof(idbuf)));
116       }
117 
118       idfmt = msg->msg + strlen(msg->msg) - 1;
119       if (idfmt >= msg->msg && *idfmt == '\n')
120       {
121         if (pass)
122 	{
123 	  pass = 0;
124 	  puts("FAIL");
125 	}
126 
127 	printf("    Trailing newline in message \"%s\"\n",
128 	       abbreviate(msg->msg, idbuf, sizeof(idbuf)));
129       }
130 
131       for (; idfmt >= msg->msg; idfmt --)
132         if (!isspace(*idfmt & 255))
133 	  break;
134 
135       if (idfmt >= msg->msg && *idfmt == '!')
136       {
137         if (pass)
138 	{
139 	  pass = 0;
140 	  puts("FAIL");
141 	}
142 
143 	printf("    Exclamation in message \"%s\"\n",
144 	       abbreviate(msg->msg, idbuf, sizeof(idbuf)));
145       }
146 
147       if ((idfmt - 2) >= msg->msg && !strncmp(idfmt - 2, "...", 3))
148       {
149         if (pass)
150 	{
151 	  pass = 0;
152 	  puts("FAIL");
153 	}
154 
155 	printf("    Ellipsis in message \"%s\"\n",
156 	       abbreviate(msg->msg, idbuf, sizeof(idbuf)));
157       }
158 
159       if (!msg->str || !msg->str[0])
160       {
161         untranslated ++;
162 	continue;
163       }
164       else if (strchr(msg->msg, '%'))
165       {
166         idfmts  = collect_formats(msg->msg);
167 	strfmts = collect_formats(msg->str);
168 	fmtidx  = 0;
169 
170         for (strfmt = (char *)cupsArrayFirst(strfmts);
171 	     strfmt;
172 	     strfmt = (char *)cupsArrayNext(strfmts))
173 	{
174 	  if (isdigit(strfmt[1] & 255) && strfmt[2] == '$')
175 	  {
176 	   /*
177 	    * Handle positioned format stuff...
178 	    */
179 
180             fmtidx = strfmt[1] - '1';
181             strfmt += 3;
182 	    if ((idfmt = (char *)cupsArrayIndex(idfmts, fmtidx)) != NULL)
183 	      idfmt ++;
184 	  }
185 	  else
186 	  {
187 	   /*
188 	    * Compare against the current format...
189 	    */
190 
191 	    idfmt = (char *)cupsArrayIndex(idfmts, fmtidx);
192           }
193 
194 	  fmtidx ++;
195 
196 	  if (!idfmt || strcmp(strfmt, idfmt))
197 	    break;
198 	}
199 
200         if (cupsArrayCount(strfmts) != cupsArrayCount(idfmts) || strfmt)
201 	{
202 	  if (pass)
203 	  {
204 	    pass = 0;
205 	    puts("FAIL");
206 	  }
207 
208 	  printf("    Bad translation string \"%s\"\n        for \"%s\"\n",
209 	         abbreviate(msg->str, strbuf, sizeof(strbuf)),
210 		 abbreviate(msg->msg, idbuf, sizeof(idbuf)));
211           fputs("    Translation formats:", stdout);
212 	  for (strfmt = (char *)cupsArrayFirst(strfmts);
213 	       strfmt;
214 	       strfmt = (char *)cupsArrayNext(strfmts))
215 	    printf(" %s", strfmt);
216           fputs("\n    Original formats:", stdout);
217 	  for (idfmt = (char *)cupsArrayFirst(idfmts);
218 	       idfmt;
219 	       idfmt = (char *)cupsArrayNext(idfmts))
220 	    printf(" %s", idfmt);
221           putchar('\n');
222           putchar('\n');
223 	}
224 
225 	free_formats(idfmts);
226 	free_formats(strfmts);
227       }
228 
229      /*
230       * Only allow \\, \n, \r, \t, \", and \### character escapes...
231       */
232 
233       for (strfmt = msg->str; *strfmt; strfmt ++)
234       {
235         if (*strfmt == '\\')
236         {
237           strfmt ++;
238 
239           if (*strfmt != '\\' && *strfmt != 'n' && *strfmt != 'r' && *strfmt != 't' && *strfmt != '\"' && !isdigit(*strfmt & 255))
240 	  {
241 	    if (pass)
242 	    {
243 	      pass = 0;
244 	      puts("FAIL");
245 	    }
246 
247 	    printf("    Bad escape \\%c in filter message \"%s\"\n"
248 		   "      for \"%s\"\n", strfmt[1],
249 		   abbreviate(msg->str, strbuf, sizeof(strbuf)),
250 		   abbreviate(msg->msg, idbuf, sizeof(idbuf)));
251 	    break;
252 	  }
253 	}
254       }
255     }
256 
257     if (pass)
258     {
259       int count = cupsArrayCount(po);	/* Total number of messages */
260 
261       if (untranslated >= (count / 10) && strcmp(argv[i], "cups.pot"))
262       {
263        /*
264         * Only allow 10% of messages to be untranslated before we fail...
265 	*/
266 
267         pass = 0;
268         puts("FAIL");
269 	printf("    Too many untranslated messages (%d of %d or %.1f%% are translated)\n", count - untranslated, count, 100.0 - 100.0 * untranslated / count);
270       }
271       else if (untranslated > 0)
272         printf("PASS (%d of %d or %.1f%% are translated)\n", count - untranslated, count, 100.0 - 100.0 * untranslated / count);
273       else
274         puts("PASS");
275     }
276 
277     if (!pass)
278       status = 1;
279 
280     _cupsMessageFree(po);
281   }
282 
283   return (status);
284 }
285 
286 
287 /*
288  * 'abbreviate()' - Abbreviate a message string as needed.
289  */
290 
291 static char *				/* O - Abbreviated string */
abbreviate(const char * s,char * buf,int bufsize)292 abbreviate(const char *s,		/* I - String to abbreviate */
293            char       *buf,		/* I - Buffer */
294 	   int        bufsize)		/* I - Size of buffer */
295 {
296   char	*bufptr;			/* Pointer into buffer */
297 
298 
299   for (bufptr = buf, bufsize -= 4; *s && bufsize > 0; s ++)
300   {
301     if (*s == '\n')
302     {
303       if (bufsize < 2)
304         break;
305 
306       *bufptr++ = '\\';
307       *bufptr++ = 'n';
308       bufsize -= 2;
309     }
310     else if (*s == '\t')
311     {
312       if (bufsize < 2)
313         break;
314 
315       *bufptr++ = '\\';
316       *bufptr++ = 't';
317       bufsize -= 2;
318     }
319     else if (*s >= 0 && *s < ' ')
320     {
321       if (bufsize < 4)
322         break;
323 
324       sprintf(bufptr, "\\%03o", *s);
325       bufptr += 4;
326       bufsize -= 4;
327     }
328     else
329     {
330       *bufptr++ = *s;
331       bufsize --;
332     }
333   }
334 
335   if (*s)
336     memcpy(bufptr, "...", 4);
337   else
338     *bufptr = '\0';
339 
340   return (buf);
341 }
342 
343 
344 /*
345  * 'collect_formats()' - Collect all of the format strings in the msgid.
346  */
347 
348 static cups_array_t *			/* O - Array of format strings */
collect_formats(const char * id)349 collect_formats(const char *id)		/* I - msgid string */
350 {
351   cups_array_t	*fmts;			/* Array of format strings */
352   char		buf[255],		/* Format string buffer */
353 		*bufptr;		/* Pointer into format string */
354 
355 
356   fmts = cupsArrayNew(NULL, NULL);
357 
358   while ((id = strchr(id, '%')) != NULL)
359   {
360     if (id[1] == '%')
361     {
362      /*
363       * Skip %%...
364       */
365 
366       id += 2;
367       continue;
368     }
369 
370     for (bufptr = buf; *id && bufptr < (buf + sizeof(buf) - 1); id ++)
371     {
372       *bufptr++ = *id;
373 
374       if (strchr("CDEFGIOSUXcdeifgopsux", *id))
375       {
376         id ++;
377         break;
378       }
379     }
380 
381     *bufptr = '\0';
382     cupsArrayAdd(fmts, strdup(buf));
383   }
384 
385   return (fmts);
386 }
387 
388 
389 /*
390  * 'free_formats()' - Free all of the format strings.
391  */
392 
393 static void
free_formats(cups_array_t * fmts)394 free_formats(cups_array_t *fmts)	/* I - Array of format strings */
395 {
396   char	*s;				/* Current string */
397 
398 
399   for (s = (char *)cupsArrayFirst(fmts); s; s = (char *)cupsArrayNext(fmts))
400     free(s);
401 
402   cupsArrayDelete(fmts);
403 }
404