1 /* Reading Desktop Entry files.
2 Copyright (C) 1995-1998, 2000-2003, 2005-2006, 2008-2009, 2014-2019, 2023 Free Software Foundation, Inc.
3 This file was written by Daiki Ueno <ueno@gnu.org>.
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 3 of the License, or
8 (at your option) 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, see <https://www.gnu.org/licenses/>. */
17
18 #ifdef HAVE_CONFIG_H
19 # include <config.h>
20 #endif
21
22 /* Specification. */
23 #include "read-desktop.h"
24
25 #include "xalloc.h"
26
27 #include <assert.h>
28 #include <errno.h>
29 #include <stdbool.h>
30 #include <stdio.h>
31 #include <stdlib.h>
32 #include <string.h>
33
34 #include "error.h"
35 #include "error-progname.h"
36 #include "xalloc.h"
37 #include "xvasprintf.h"
38 #include "c-ctype.h"
39 #include "po-lex.h"
40 #include "po-xerror.h"
41 #include "gettext.h"
42
43 #define _(str) gettext (str)
44
45 /* The syntax of a Desktop Entry file is defined at
46 https://standards.freedesktop.org/desktop-entry-spec/latest/index.html. */
47
48 desktop_reader_ty *
49 desktop_reader_alloc (desktop_reader_class_ty *method_table)
50 {
51 desktop_reader_ty *reader;
52
53 reader = (desktop_reader_ty *) xmalloc (method_table->size);
54 reader->methods = method_table;
55 if (method_table->constructor)
56 method_table->constructor (reader);
57 return reader;
58 }
59
60 void
61 desktop_reader_free (desktop_reader_ty *reader)
62 {
63 if (reader->methods->destructor)
64 reader->methods->destructor (reader);
65 free (reader);
66 }
67
68 void
69 desktop_reader_handle_group (desktop_reader_ty *reader, const char *group)
70 {
71 if (reader->methods->handle_group)
72 reader->methods->handle_group (reader, group);
73 }
74
75 void
76 desktop_reader_handle_pair (desktop_reader_ty *reader,
77 lex_pos_ty *key_pos,
78 const char *key,
79 const char *locale,
80 const char *value)
81 {
82 if (reader->methods->handle_pair)
83 reader->methods->handle_pair (reader, key_pos, key, locale, value);
84 }
85
86 void
87 desktop_reader_handle_comment (desktop_reader_ty *reader, const char *s)
88 {
89 if (reader->methods->handle_comment)
90 reader->methods->handle_comment (reader, s);
91 }
92
93 void
94 desktop_reader_handle_blank (desktop_reader_ty *reader, const char *s)
95 {
96 if (reader->methods->handle_blank)
97 reader->methods->handle_blank (reader, s);
98 }
99
100 /* Real filename, used in error messages about the input file. */
101 static const char *real_file_name;
102
103 /* File name and line number. */
104 extern lex_pos_ty gram_pos;
105
106 /* The input file stream. */
107 static FILE *fp;
108
109
110 static int
111 phase1_getc ()
112 {
113 int c;
114
115 c = getc (fp);
116
117 if (c == EOF)
118 {
119 if (ferror (fp))
120 {
121 const char *errno_description = strerror (errno);
122 po_xerror (PO_SEVERITY_FATAL_ERROR, NULL, NULL, 0, 0, false,
123 xasprintf ("%s: %s",
124 xasprintf (_("error while reading \"%s\""),
125 real_file_name),
126 errno_description));
127 }
128 return EOF;
129 }
130
131 return c;
132 }
133
134 static inline void
135 phase1_ungetc (int c)
136 {
137 if (c != EOF)
138 ungetc (c, fp);
139 }
140
141
142 static unsigned char phase2_pushback[2];
143 static int phase2_pushback_length;
144
145 static int
146 phase2_getc ()
147 {
148 int c;
149
150 if (phase2_pushback_length)
151 c = phase2_pushback[--phase2_pushback_length];
152 else
153 {
154 c = phase1_getc ();
155
156 if (c == '\r')
157 {
158 int c2 = phase1_getc ();
159 if (c2 == '\n')
160 c = c2;
161 else
162 phase1_ungetc (c2);
163 }
164 }
165
166 if (c == '\n')
167 gram_pos.line_number++;
168
169 return c;
170 }
171
172 static void
173 phase2_ungetc (int c)
174 {
175 if (c == '\n')
176 --gram_pos.line_number;
177 if (c != EOF)
178 phase2_pushback[phase2_pushback_length++] = c;
179 }
180
181 enum token_type_ty
182 {
183 token_type_eof,
184 token_type_group,
185 token_type_pair,
186 /* Unlike other scanners, preserve comments and blank lines for
187 merging translations back into a desktop file, with msgfmt. */
188 token_type_comment,
189 token_type_blank,
190 token_type_other
191 };
192 typedef enum token_type_ty token_type_ty;
193
194 typedef struct token_ty token_ty;
195 struct token_ty
196 {
197 token_type_ty type;
198 char *string;
199 const char *value;
200 const char *locale;
201 };
202
203 /* Free the memory pointed to by a 'struct token_ty'. */
204 static inline void
205 free_token (token_ty *tp)
206 {
207 if (tp->type == token_type_group || tp->type == token_type_pair
208 || tp->type == token_type_comment || tp->type == token_type_blank)
209 free (tp->string);
210 }
211
212 static void
213 desktop_lex (token_ty *tp)
214 {
215 static char *buffer;
216 static size_t bufmax;
217 size_t bufpos;
218
219 #undef APPEND
220 #define APPEND(c) \
221 do \
222 { \
223 if (bufpos >= bufmax) \
224 { \
225 bufmax += 100; \
226 buffer = xrealloc (buffer, bufmax); \
227 } \
228 buffer[bufpos++] = c; \
229 } \
230 while (0)
231
232 bufpos = 0;
233 for (;;)
234 {
235 int c;
236
237 c = phase2_getc ();
238
239 switch (c)
240 {
241 case EOF:
242 tp->type = token_type_eof;
243 return;
244
245 case '[':
246 {
247 bool non_blank = false;
248
249 for (;;)
250 {
251 c = phase2_getc ();
252 if (c == EOF || c == ']')
253 break;
254 if (c == '\n')
255 {
256 po_xerror (PO_SEVERITY_WARNING, NULL,
257 real_file_name, gram_pos.line_number, 0, false,
258 _("unterminated group name"));
259 break;
260 }
261 /* Group names may contain all ASCII characters
262 except for '[' and ']' and control characters. */
263 if (!(c_isascii (c) && c != '[' && !c_iscntrl (c)))
264 break;
265 APPEND (c);
266 }
267 /* Skip until newline. */
268 while (c != '\n' && c != EOF)
269 {
270 c = phase2_getc ();
271 if (c == EOF)
272 break;
273 if (!c_isspace (c))
274 non_blank = true;
275 }
276 if (non_blank)
277 po_xerror (PO_SEVERITY_WARNING, NULL,
278 real_file_name, gram_pos.line_number, 0, false,
279 _("invalid non-blank character"));
280 APPEND (0);
281 tp->type = token_type_group;
282 tp->string = xstrdup (buffer);
283 return;
284 }
285
286 case '#':
287 {
288 /* Read until newline. */
289 for (;;)
290 {
291 c = phase2_getc ();
292 if (c == EOF || c == '\n')
293 break;
294 APPEND (c);
295 }
296 APPEND (0);
297 tp->type = token_type_comment;
298 tp->string = xstrdup (buffer);
299 return;
300 }
301
302 case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
303 case 'G': case 'H': case 'I': case 'J': case 'K': case 'L':
304 case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R':
305 case 'S': case 'T': case 'U': case 'V': case 'W': case 'X':
306 case 'Y': case 'Z':
307 case '-':
308 case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
309 case 'g': case 'h': case 'i': case 'j': case 'k': case 'l':
310 case 'm': case 'n': case 'o': case 'p': case 'q': case 'r':
311 case 's': case 't': case 'u': case 'v': case 'w': case 'x':
312 case 'y': case 'z':
313 case '0': case '1': case '2': case '3': case '4':
314 case '5': case '6': case '7': case '8': case '9':
315 {
316 size_t locale_start;
317 bool found_locale = false;
318 size_t value_start;
319 for (;;)
320 {
321 APPEND (c);
322
323 c = phase2_getc ();
324 switch (c)
325 {
326 case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
327 case 'G': case 'H': case 'I': case 'J': case 'K': case 'L':
328 case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R':
329 case 'S': case 'T': case 'U': case 'V': case 'W': case 'X':
330 case 'Y': case 'Z':
331 case '-':
332 case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
333 case 'g': case 'h': case 'i': case 'j': case 'k': case 'l':
334 case 'm': case 'n': case 'o': case 'p': case 'q': case 'r':
335 case 's': case 't': case 'u': case 'v': case 'w': case 'x':
336 case 'y': case 'z':
337 case '0': case '1': case '2': case '3': case '4':
338 case '5': case '6': case '7': case '8': case '9':
339 continue;
340
341 case '[':
342 /* Finish the key part and start the locale part. */
343 APPEND (0);
344 found_locale = true;
345 locale_start = bufpos;
346
347 for (;;)
348 {
349 int c2 = phase2_getc ();
350 if (c2 == EOF || c2 == ']')
351 break;
352 APPEND (c2);
353 }
354 break;
355
356 default:
357 phase2_ungetc (c);
358 break;
359 }
360 break;
361 }
362 APPEND (0);
363
364 /* Skip any space before '='. */
365 for (;;)
366 {
367 c = phase2_getc ();
368 switch (c)
369 {
370 case ' ':
371 continue;
372 default:
373 phase2_ungetc (c);
374 break;
375 case EOF: case '\n':
376 break;
377 }
378 break;
379 }
380
381 c = phase2_getc ();
382 if (c != '=')
383 {
384 po_xerror (PO_SEVERITY_WARNING, NULL,
385 real_file_name, gram_pos.line_number, 0, false,
386 xasprintf (_("missing '=' after \"%s\""), buffer));
387 for (;;)
388 {
389 c = phase2_getc ();
390 if (c == EOF || c == '\n')
391 break;
392 }
393 tp->type = token_type_other;
394 return;
395 }
396
397 /* Skip any space after '='. */
398 for (;;)
399 {
400 c = phase2_getc ();
401 switch (c)
402 {
403 case ' ':
404 continue;
405 default:
406 phase2_ungetc (c);
407 break;
408 case EOF:
409 break;
410 }
411 break;
412 }
413
414 value_start = bufpos;
415 for (;;)
416 {
417 c = phase2_getc ();
418 if (c == EOF || c == '\n')
419 break;
420 APPEND (c);
421 }
422 APPEND (0);
423 tp->type = token_type_pair;
424 tp->string = xmemdup (buffer, bufpos);
425 tp->locale = found_locale ? &buffer[locale_start] : NULL;
426 tp->value = &buffer[value_start];
427 return;
428 }
429 default:
430 {
431 bool non_blank = false;
432
433 for (;;)
434 {
435 if (c == '\n' || c == EOF)
436 break;
437
438 if (!c_isspace (c))
439 non_blank = true;
440 else
441 APPEND (c);
442
443 c = phase2_getc ();
444 }
445 if (non_blank)
446 {
447 po_xerror (PO_SEVERITY_WARNING, NULL,
448 real_file_name, gram_pos.line_number, 0, false,
449 _("invalid non-blank line"));
450 tp->type = token_type_other;
451 return;
452 }
453 APPEND (0);
454 tp->type = token_type_blank;
455 tp->string = xstrdup (buffer);
456 return;
457 }
458 }
459 }
460 #undef APPEND
461 }
462
463 void
464 desktop_parse (desktop_reader_ty *reader, FILE *file,
465 const char *real_filename, const char *logical_filename)
466 {
467 fp = file;
468 real_file_name = real_filename;
469 gram_pos.file_name = xstrdup (logical_filename);
470 gram_pos.line_number = 1;
471
472 for (;;)
473 {
474 struct token_ty token;
475 desktop_lex (&token);
476 switch (token.type)
477 {
478 case token_type_eof:
479 goto out;
480 case token_type_group:
481 desktop_reader_handle_group (reader, token.string);
482 break;
483 case token_type_comment:
484 desktop_reader_handle_comment (reader, token.string);
485 break;
486 case token_type_pair:
487 desktop_reader_handle_pair (reader, &gram_pos,
488 token.string, token.locale, token.value);
489 break;
490 case token_type_blank:
491 desktop_reader_handle_blank (reader, token.string);
492 break;
493 case token_type_other:
494 break;
495 }
496 free_token (&token);
497 }
498
499 out:
500 fp = NULL;
501 real_file_name = NULL;
502 gram_pos.line_number = 0;
503 }
504
505 char *
506 desktop_escape_string (const char *s, bool is_list)
507 {
508 char *buffer, *p;
509
510 p = buffer = XNMALLOC (strlen (s) * 2 + 1, char);
511
512 /* The first character must not be a whitespace. */
513 if (*s == ' ')
514 {
515 p = stpcpy (p, "\\s");
516 s++;
517 }
518 else if (*s == '\t')
519 {
520 p = stpcpy (p, "\\t");
521 s++;
522 }
523
524 for (;; s++)
525 {
526 if (*s == '\0')
527 {
528 *p = '\0';
529 break;
530 }
531
532 switch (*s)
533 {
534 case '\n':
535 p = stpcpy (p, "\\n");
536 break;
537 case '\r':
538 p = stpcpy (p, "\\r");
539 break;
540 case '\\':
541 if (is_list && *(s + 1) == ';')
542 {
543 p = stpcpy (p, "\\;");
544 s++;
545 }
546 else
547 p = stpcpy (p, "\\\\");
548 break;
549 default:
550 *p++ = *s;
551 break;
552 }
553 }
554
555 return buffer;
556 }
557
558 char *
559 desktop_unescape_string (const char *s, bool is_list)
560 {
561 char *buffer, *p;
562
563 p = buffer = XNMALLOC (strlen (s) + 1, char);
564 for (;; s++)
565 {
566 if (*s == '\0')
567 {
568 *p = '\0';
569 break;
570 }
571
572 if (*s == '\\')
573 {
574 s++;
575
576 if (*s == '\0')
577 {
578 *p = '\0';
579 break;
580 }
581
582 switch (*s)
583 {
584 case 's':
585 *p++ = ' ';
586 break;
587 case 'n':
588 *p++ = '\n';
589 break;
590 case 't':
591 *p++ = '\t';
592 break;
593 case 'r':
594 *p++ = '\r';
595 break;
596 case ';':
597 p = stpcpy (p, "\\;");
598 break;
599 default:
600 *p++ = *s;
601 break;
602 }
603 }
604 else
605 *p++ = *s;
606 }
607 return buffer;
608 }
609
610 void
611 desktop_add_keyword (hash_table *keywords, const char *name, bool is_list)
612 {
613 hash_insert_entry (keywords, name, strlen (name), (void *) is_list);
614 }
615
616 void
617 desktop_add_default_keywords (hash_table *keywords)
618 {
619 /* When adding new keywords here, also update the documentation in
620 xgettext.texi! */
621 desktop_add_keyword (keywords, "Name", false);
622 desktop_add_keyword (keywords, "GenericName", false);
623 desktop_add_keyword (keywords, "Comment", false);
624 #if 0 /* Icon values are localizable, but not supported by xgettext. */
625 desktop_add_keyword (keywords, "Icon", false);
626 #endif
627 desktop_add_keyword (keywords, "Keywords", true);
628 }