1 /* JavaScript format strings.
2 Copyright (C) 2001-2004, 2006-2010, 2013, 2016, 2019-2020, 2023 Free Software Foundation, Inc.
3 Written by Andreas Stricker <andy@knitter.ch>, 2010.
4 It's based on python format module from Bruno Haible.
5
6 This program is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 3 of the License, or
9 (at your option) any later version.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program. If not, see <https://www.gnu.org/licenses/>. */
18
19 #ifdef HAVE_CONFIG_H
20 # include <config.h>
21 #endif
22
23 #include <stdbool.h>
24 #include <stdlib.h>
25 #include <string.h>
26
27 #include "format.h"
28 #include "c-ctype.h"
29 #include "xalloc.h"
30 #include "xvasprintf.h"
31 #include "format-invalid.h"
32 #include "gettext.h"
33
34 #define _(str) gettext (str)
35
36 /* JavaScript format strings are not in the language specification,
37 but there are several implementations which provide the printf-like
38 feature. Here, we provide a permissive parser which at least accepts
39 format strings supported by Gjs version 1.40, where:
40
41 A directive
42 - starts with '%' or '%m$' where m is a positive integer,
43 - is optionally followed by any of the characters '0', '-', ' ',
44 or 'I', each of which acts as a flag,
45 - is optionally followed by a width specification: a nonempty digit
46 sequence,
47 - is optionally followed by '.' and a precision specification: a nonempty
48 digit sequence,
49 - is finished by a specifier
50 - 's', that needs a string argument,
51 - 'b', 'd', 'u', 'o', 'x', 'X', that need an integer argument,
52 - 'f', that need a floating-point argument,
53 - 'c', that needs a character argument.
54 - 'j', that needs an argument of any type.
55 Additionally there is the directive '%%', which takes no argument. */
56
57 enum format_arg_type
58 {
59 FAT_NONE,
60 FAT_ANY,
61 FAT_CHARACTER,
62 FAT_STRING,
63 FAT_INTEGER,
64 FAT_FLOAT
65 };
66
67 struct numbered_arg
68 {
69 unsigned int number;
70 enum format_arg_type type;
71 };
72
73 struct spec
74 {
75 unsigned int directives;
76 unsigned int numbered_arg_count;
77 struct numbered_arg *numbered;
78 };
79
80 /* Locale independent test for a decimal digit.
81 Argument can be 'char' or 'unsigned char'. (Whereas the argument of
82 <ctype.h> isdigit must be an 'unsigned char'.) */
83 #undef isdigit
84 #define isdigit(c) ((unsigned int) ((c) - '0') < 10)
85
86
87 static int
88 numbered_arg_compare (const void *p1, const void *p2)
89 {
90 unsigned int n1 = ((const struct numbered_arg *) p1)->number;
91 unsigned int n2 = ((const struct numbered_arg *) p2)->number;
92
93 return (n1 > n2 ? 1 : n1 < n2 ? -1 : 0);
94 }
95
96 static void *
97 format_parse (const char *format, bool translated, char *fdi,
98 char **invalid_reason)
99 {
100 const char *const format_start = format;
101 struct spec spec;
102 unsigned int numbered_allocated;
103 unsigned int unnumbered_arg_count;
104 struct spec *result;
105
106 spec.directives = 0;
107 spec.numbered_arg_count = 0;
108 spec.numbered = NULL;
109 numbered_allocated = 0;
110 unnumbered_arg_count = 0;
111
112 for (; *format != '\0';)
113 /* Invariant: spec.numbered_arg_count == 0 || unnumbered_arg_count == 0. */
114 if (*format++ == '%')
115 {
116 /* A directive. */
117 unsigned int number = 0;
118 enum format_arg_type type;
119
120 FDI_SET (format - 1, FMTDIR_START);
121 spec.directives++;
122
123 if (isdigit (*format))
124 {
125 const char *f = format;
126 unsigned int m = 0;
127
128 do
129 {
130 m = 10 * m + (*f - '0');
131 f++;
132 }
133 while (isdigit (*f));
134
135 if (*f == '$')
136 {
137 if (m == 0)
138 {
139 *invalid_reason = INVALID_ARGNO_0 (spec.directives);
140 FDI_SET (f, FMTDIR_ERROR);
141 goto bad_format;
142 }
143 number = m;
144 format = ++f;
145 }
146 }
147
148 /* Parse flags. */
149 while (*format == '-' || *format == '+' || *format == ' '
150 || *format == '0' || *format == 'I')
151 format++;
152
153 /* Parse width. */
154 while (isdigit (*format))
155 format++;
156
157 if (*format == '.')
158 {
159 format++;
160
161 while (isdigit (*format))
162 format++;
163 }
164
165 switch (*format)
166 {
167 case '%':
168 type = FAT_NONE;
169 break;
170 case 'c':
171 type = FAT_CHARACTER;
172 break;
173 case 's':
174 type = FAT_STRING;
175 break;
176 case 'b': case 'd': case 'o': case 'x': case 'X':
177 type = FAT_INTEGER;
178 break;
179 case 'f':
180 type = FAT_FLOAT;
181 break;
182 case 'j':
183 type = FAT_ANY;
184 break;
185 default:
186 if (*format == '\0')
187 {
188 *invalid_reason = INVALID_UNTERMINATED_DIRECTIVE ();
189 FDI_SET (format - 1, FMTDIR_ERROR);
190 }
191 else
192 {
193 *invalid_reason =
194 INVALID_CONVERSION_SPECIFIER (spec.directives, *format);
195 FDI_SET (format, FMTDIR_ERROR);
196 }
197 goto bad_format;
198 }
199
200 if (type != FAT_NONE)
201 {
202 if (number)
203 {
204 /* Numbered argument. */
205
206 /* Numbered and unnumbered specifications are exclusive. */
207 if (unnumbered_arg_count > 0)
208 {
209 *invalid_reason = INVALID_MIXES_NUMBERED_UNNUMBERED ();
210 FDI_SET (format, FMTDIR_ERROR);
211 goto bad_format;
212 }
213
214 if (numbered_allocated == spec.numbered_arg_count)
215 {
216 numbered_allocated = 2 * numbered_allocated + 1;
217 spec.numbered = (struct numbered_arg *) xrealloc (spec.numbered, numbered_allocated * sizeof (struct numbered_arg));
218 }
219 spec.numbered[spec.numbered_arg_count].number = number;
220 spec.numbered[spec.numbered_arg_count].type = type;
221 spec.numbered_arg_count++;
222 }
223 else
224 {
225 /* Unnumbered argument. */
226
227 /* Numbered and unnumbered specifications are exclusive. */
228 if (spec.numbered_arg_count > 0)
229 {
230 *invalid_reason = INVALID_MIXES_NUMBERED_UNNUMBERED ();
231 FDI_SET (format, FMTDIR_ERROR);
232 goto bad_format;
233 }
234
235 if (numbered_allocated == unnumbered_arg_count)
236 {
237 numbered_allocated = 2 * numbered_allocated + 1;
238 spec.numbered = (struct numbered_arg *) xrealloc (spec.numbered, numbered_allocated * sizeof (struct numbered_arg));
239 }
240 spec.numbered[unnumbered_arg_count].number = unnumbered_arg_count + 1;
241 spec.numbered[unnumbered_arg_count].type = type;
242 unnumbered_arg_count++;
243 }
244 }
245
246 FDI_SET (format, FMTDIR_END);
247
248 format++;
249 }
250
251 /* Convert the unnumbered argument array to numbered arguments. */
252 if (unnumbered_arg_count > 0)
253 spec.numbered_arg_count = unnumbered_arg_count;
254 /* Sort the numbered argument array, and eliminate duplicates. */
255 else if (spec.numbered_arg_count > 1)
256 {
257 unsigned int i, j;
258 bool err;
259
260 qsort (spec.numbered, spec.numbered_arg_count,
261 sizeof (struct numbered_arg), numbered_arg_compare);
262
263 /* Remove duplicates: Copy from i to j, keeping 0 <= j <= i. */
264 err = false;
265 for (i = j = 0; i < spec.numbered_arg_count; i++)
266 if (j > 0 && spec.numbered[i].number == spec.numbered[j-1].number)
267 {
268 enum format_arg_type type1 = spec.numbered[i].type;
269 enum format_arg_type type2 = spec.numbered[j-1].type;
270 enum format_arg_type type_both;
271
272 if (type1 == type2)
273 type_both = type1;
274 else
275 {
276 /* Incompatible types. */
277 type_both = FAT_NONE;
278 if (!err)
279 *invalid_reason =
280 INVALID_INCOMPATIBLE_ARG_TYPES (spec.numbered[i].number);
281 err = true;
282 }
283
284 spec.numbered[j-1].type = type_both;
285 }
286 else
287 {
288 if (j < i)
289 {
290 spec.numbered[j].number = spec.numbered[i].number;
291 spec.numbered[j].type = spec.numbered[i].type;
292 }
293 j++;
294 }
295 spec.numbered_arg_count = j;
296 if (err)
297 /* *invalid_reason has already been set above. */
298 goto bad_format;
299 }
300
301 result = XMALLOC (struct spec);
302 *result = spec;
303 return result;
304
305 bad_format:
306 if (spec.numbered != NULL)
307 free (spec.numbered);
308 return NULL;
309 }
310
311 static void
312 format_free (void *descr)
313 {
314 struct spec *spec = (struct spec *) descr;
315
316 if (spec->numbered != NULL)
317 free (spec->numbered);
318 free (spec);
319 }
320
321 static int
322 format_get_number_of_directives (void *descr)
323 {
324 struct spec *spec = (struct spec *) descr;
325
326 return spec->directives;
327 }
328
329 static bool
330 format_check (void *msgid_descr, void *msgstr_descr, bool equality,
331 formatstring_error_logger_t error_logger,
332 const char *pretty_msgid, const char *pretty_msgstr)
333 {
334 struct spec *spec1 = (struct spec *) msgid_descr;
335 struct spec *spec2 = (struct spec *) msgstr_descr;
336 bool err = false;
337
338 if (spec1->numbered_arg_count + spec2->numbered_arg_count > 0)
339 {
340 unsigned int i, j;
341 unsigned int n1 = spec1->numbered_arg_count;
342 unsigned int n2 = spec2->numbered_arg_count;
343
344 /* Check that the argument numbers are the same.
345 Both arrays are sorted. We search for the first difference. */
346 for (i = 0, j = 0; i < n1 || j < n2; )
347 {
348 int cmp = (i >= n1 ? 1 :
349 j >= n2 ? -1 :
350 spec1->numbered[i].number > spec2->numbered[j].number ? 1 :
351 spec1->numbered[i].number < spec2->numbered[j].number ? -1 :
352 0);
353 if (cmp > 0)
354 {
355 if (error_logger)
356 error_logger (_("a format specification for argument %u, as in '%s', doesn't exist in '%s'"),
357 spec2->numbered[j].number, pretty_msgstr,
358 pretty_msgid);
359 err = true;
360 break;
361 }
362 else if (cmp < 0)
363 {
364 if (equality)
365 {
366 if (error_logger)
367 error_logger (_("a format specification for argument %u doesn't exist in '%s'"),
368 spec1->numbered[i].number, pretty_msgstr);
369 err = true;
370 break;
371 }
372 else
373 i++;
374 }
375 else
376 j++, i++;
377 }
378 /* Check the argument types are the same. */
379 if (!err)
380 for (i = 0, j = 0; j < n2; )
381 {
382 if (spec1->numbered[i].number == spec2->numbered[j].number)
383 {
384 if (!(spec1->numbered[i].type == spec2->numbered[j].type
385 || (!equality
386 && (spec1->numbered[i].type == FAT_ANY
387 || spec2->numbered[i].type == FAT_ANY))))
388 {
389 if (error_logger)
390 error_logger (_("format specifications in '%s' and '%s' for argument %u are not the same"),
391 pretty_msgid, pretty_msgstr,
392 spec2->numbered[j].number);
393 err = true;
394 break;
395 }
396 j++, i++;
397 }
398 else
399 i++;
400 }
401 }
402
403 return err;
404 }
405
406
407 struct formatstring_parser formatstring_javascript =
408 {
409 format_parse,
410 format_free,
411 format_get_number_of_directives,
412 NULL,
413 format_check
414 };
415
416
417 #ifdef TEST
418
419 /* Test program: Print the argument list specification returned by
420 format_parse for strings read from standard input. */
421
422 #include <stdio.h>
423
424 static void
425 format_print (void *descr)
426 {
427 struct spec *spec = (struct spec *) descr;
428 unsigned int i;
429
430 if (spec == NULL)
431 {
432 printf ("INVALID");
433 return;
434 }
435
436 printf ("(");
437 for (i = 0; i < spec->numbered_arg_count; i++)
438 {
439 if (i > 0)
440 printf (" ");
441 switch (spec->numbered[i].type)
442 {
443 case FAT_ANY:
444 printf ("*");
445 break;
446 case FAT_CHARACTER:
447 printf ("c");
448 break;
449 case FAT_STRING:
450 printf ("s");
451 break;
452 case FAT_INTEGER:
453 printf ("i");
454 break;
455 case FAT_FLOAT:
456 printf ("f");
457 break;
458 default:
459 abort ();
460 }
461 }
462 printf (")");
463 }
464
465 int
466 main ()
467 {
468 for (;;)
469 {
470 char *line = NULL;
471 size_t line_size = 0;
472 int line_len;
473 char *invalid_reason;
474 void *descr;
475
476 line_len = getline (&line, &line_size, stdin);
477 if (line_len < 0)
478 break;
479 if (line_len > 0 && line[line_len - 1] == '\n')
480 line[--line_len] = '\0';
481
482 invalid_reason = NULL;
483 descr = format_parse (line, false, NULL, &invalid_reason);
484
485 format_print (descr);
486 printf ("\n");
487 if (descr == NULL)
488 printf ("%s\n", invalid_reason);
489
490 free (invalid_reason);
491 free (line);
492 }
493
494 return 0;
495 }
496
497 /*
498 * For Emacs M-x compile
499 * Local Variables:
500 * compile-command: "/bin/sh ../libtool --tag=CC --mode=link gcc -o a.out -static -O -g -Wall -I.. -I../gnulib-lib -I../../gettext-runtime/intl -DHAVE_CONFIG_H -DTEST format-javascript.c ../gnulib-lib/libgettextlib.la"
501 * End:
502 */
503 #endif /* TEST */