Coverage Report

Created: 2026-08-17 07:50

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gettext/gettext-tools/src/read-catalog.c
Line
Count
Source
1
/* Reading textual message catalogs (such as PO files).
2
   Copyright (C) 1995-2026 Free Software Foundation, Inc.
3
4
   This program is free software: you can redistribute it and/or modify
5
   it under the terms of the GNU General Public License as published by
6
   the Free Software Foundation; either version 3 of the License, or
7
   (at your option) any later version.
8
9
   This program is distributed in the hope that it will be useful,
10
   but WITHOUT ANY WARRANTY; without even the implied warranty of
11
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
   GNU General Public License for more details.
13
14
   You should have received a copy of the GNU General Public License
15
   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
16
17
/* Written by Peter Miller, Ulrich Drepper, and Bruno Haible.  */
18
19
#include <config.h>
20
21
/* Specification.  */
22
#include "read-catalog.h"
23
24
#include <stdbool.h>
25
#include <stdlib.h>
26
#include <string.h>
27
28
#include "po-charset.h"
29
#include "read-po-lex.h"
30
#include "xerror-handler.h"
31
#include "xalloc.h"
32
#include "read-catalog-special.h"
33
#include "gettext.h"
34
35
131k
#define _(str) gettext (str)
36
37
38
/* ========================================================================= */
39
/* Inline functions to invoke the methods.  */
40
41
static inline void
42
call_set_domain (struct default_catalog_reader_ty *dcatr,
43
                 char *name, lex_pos_ty *name_pos)
44
5.22k
{
45
5.22k
  default_catalog_reader_class_ty *methods =
46
5.22k
    (default_catalog_reader_class_ty *) dcatr->methods;
47
48
5.22k
  if (methods->set_domain)
49
5.22k
    methods->set_domain (dcatr, name, name_pos);
50
5.22k
}
51
52
static inline void
53
call_add_message (struct default_catalog_reader_ty *dcatr,
54
                  char *msgctxt,
55
                  char *msgid, lex_pos_ty *msgid_pos, char *msgid_plural,
56
                  char *msgstr, size_t msgstr_len, lex_pos_ty *msgstr_pos,
57
                  char *prev_msgctxt, char *prev_msgid, char *prev_msgid_plural,
58
                  bool force_fuzzy, bool obsolete)
59
97.2k
{
60
97.2k
  default_catalog_reader_class_ty *methods =
61
97.2k
    (default_catalog_reader_class_ty *) dcatr->methods;
62
63
97.2k
  if (methods->add_message)
64
97.2k
    methods->add_message (dcatr, msgctxt,
65
97.2k
                          msgid, msgid_pos, msgid_plural,
66
97.2k
                          msgstr, msgstr_len, msgstr_pos,
67
97.2k
                          prev_msgctxt, prev_msgid, prev_msgid_plural,
68
97.2k
                          force_fuzzy, obsolete);
69
97.2k
}
70
71
static inline void
72
call_frob_new_message (struct default_catalog_reader_ty *dcatr, message_ty *mp,
73
                       const lex_pos_ty *msgid_pos,
74
                       const lex_pos_ty *msgstr_pos)
75
31.5k
{
76
31.5k
  default_catalog_reader_class_ty *methods =
77
31.5k
    (default_catalog_reader_class_ty *) dcatr->methods;
78
79
31.5k
  if (methods->frob_new_message)
80
0
    methods->frob_new_message (dcatr, mp, msgid_pos, msgstr_pos);
81
31.5k
}
82
83
84
/* ========================================================================= */
85
/* Implementation of default_catalog_reader_ty's methods.  */
86
87
88
/* Implementation of methods declared in the superclass.  */
89
90
91
/* Prepare for first message.  */
92
void
93
default_constructor (abstract_catalog_reader_ty *catr)
94
12.0k
{
95
12.0k
  default_catalog_reader_ty *dcatr = (default_catalog_reader_ty *) catr;
96
97
12.0k
  dcatr->domain = MESSAGE_DOMAIN_DEFAULT;
98
12.0k
  dcatr->comment = NULL;
99
12.0k
  dcatr->comment_dot = NULL;
100
12.0k
  dcatr->filepos_count = 0;
101
12.0k
  dcatr->filepos = NULL;
102
12.0k
  dcatr->is_fuzzy = false;
103
456k
  for (size_t i = 0; i < NFORMATS; i++)
104
444k
    dcatr->is_format[i] = undecided;
105
12.0k
  dcatr->range.min = -1;
106
12.0k
  dcatr->range.max = -1;
107
12.0k
  dcatr->do_wrap = undecided;
108
12.0k
}
109
110
111
void
112
default_destructor (abstract_catalog_reader_ty *catr)
113
12.0k
{
114
12.0k
  default_catalog_reader_ty *dcatr = (default_catalog_reader_ty *) catr;
115
116
  /* Do not free dcatr->mdlp and dcatr->mlp.  */
117
12.0k
  if (dcatr->handle_comments)
118
12.0k
    {
119
12.0k
      if (dcatr->comment != NULL)
120
3.34k
        string_list_free (dcatr->comment);
121
12.0k
      if (dcatr->comment_dot != NULL)
122
288
        string_list_free (dcatr->comment_dot);
123
12.0k
    }
124
125
49.8k
  for (size_t j = 0; j < dcatr->filepos_count; ++j)
126
37.8k
    free ((char *) dcatr->filepos[j].file_name);
127
12.0k
  if (dcatr->filepos != NULL)
128
12.0k
    free (dcatr->filepos);
129
12.0k
}
130
131
132
void
133
default_parse_brief (abstract_catalog_reader_ty *catr)
134
12.0k
{
135
  /* We need to parse comments, because even if dcatr->handle_comments
136
     is false, we need to know which messages are fuzzy.  */
137
12.0k
  catr->pass_comments = true;
138
12.0k
}
139
140
141
void
142
default_parse_debrief (abstract_catalog_reader_ty *catr)
143
12.0k
{
144
12.0k
}
145
146
147
/* Add the accumulated comments to the message.  */
148
static void
149
default_copy_comment_state (default_catalog_reader_ty *dcatr, message_ty *mp)
150
97.2k
{
151
97.2k
  if (dcatr->handle_comments)
152
97.2k
    {
153
97.2k
      if (dcatr->comment != NULL)
154
360k
        for (size_t j = 0; j < dcatr->comment->nitems; ++j)
155
338k
          message_comment_append (mp, dcatr->comment->item[j]);
156
97.2k
      if (dcatr->comment_dot != NULL)
157
15.4k
        for (size_t j = 0; j < dcatr->comment_dot->nitems; ++j)
158
10.4k
          message_comment_dot_append (mp, dcatr->comment_dot->item[j]);
159
97.2k
    }
160
196k
  for (size_t j = 0; j < dcatr->filepos_count; ++j)
161
99.2k
    {
162
99.2k
      lex_pos_ty *pp = &dcatr->filepos[j];
163
164
99.2k
      message_comment_filepos (mp, pp->file_name, pp->line_number);
165
99.2k
    }
166
97.2k
  mp->is_fuzzy = dcatr->is_fuzzy;
167
3.69M
  for (size_t i = 0; i < NFORMATS; i++)
168
3.59M
    mp->is_format[i] = dcatr->is_format[i];
169
97.2k
  mp->range = dcatr->range;
170
97.2k
  mp->do_wrap = dcatr->do_wrap;
171
97.2k
}
172
173
174
static void
175
default_reset_comment_state (default_catalog_reader_ty *dcatr)
176
102k
{
177
102k
  if (dcatr->handle_comments)
178
102k
    {
179
102k
      if (dcatr->comment != NULL)
180
22.7k
        {
181
22.7k
          string_list_free (dcatr->comment);
182
22.7k
          dcatr->comment = NULL;
183
22.7k
        }
184
102k
      if (dcatr->comment_dot != NULL)
185
5.29k
        {
186
5.29k
          string_list_free (dcatr->comment_dot);
187
5.29k
          dcatr->comment_dot = NULL;
188
5.29k
        }
189
102k
    }
190
203k
  for (size_t j = 0; j < dcatr->filepos_count; ++j)
191
102k
    free ((char *) dcatr->filepos[j].file_name);
192
102k
  if (dcatr->filepos != NULL)
193
102k
    free (dcatr->filepos);
194
102k
  dcatr->filepos_count = 0;
195
102k
  dcatr->filepos = NULL;
196
102k
  dcatr->is_fuzzy = false;
197
3.89M
  for (size_t i = 0; i < NFORMATS; i++)
198
3.78M
    dcatr->is_format[i] = undecided;
199
102k
  dcatr->range.min = -1;
200
102k
  dcatr->range.max = -1;
201
102k
  dcatr->do_wrap = undecided;
202
102k
}
203
204
205
/* Process 'domain' directive from .po file.  */
206
void
207
default_directive_domain (abstract_catalog_reader_ty *catr,
208
                          char *name, lex_pos_ty *name_pos)
209
5.22k
{
210
5.22k
  default_catalog_reader_ty *dcatr = (default_catalog_reader_ty *) catr;
211
212
5.22k
  call_set_domain (dcatr, name, name_pos);
213
214
  /* If there are accumulated comments, throw them away, they are
215
     probably part of the file header, or about the domain directive,
216
     and will be unrelated to the next message.  */
217
5.22k
  default_reset_comment_state (dcatr);
218
5.22k
}
219
220
221
/* Process ['msgctxt'/]'msgid'/'msgstr' pair from .po file.  */
222
void
223
default_directive_message (abstract_catalog_reader_ty *catr,
224
                           char *msgctxt,
225
                           char *msgid,
226
                           lex_pos_ty *msgid_pos,
227
                           char *msgid_plural,
228
                           char *msgstr, size_t msgstr_len,
229
                           lex_pos_ty *msgstr_pos,
230
                           char *prev_msgctxt,
231
                           char *prev_msgid, char *prev_msgid_plural,
232
                           bool force_fuzzy, bool obsolete)
233
97.2k
{
234
97.2k
  default_catalog_reader_ty *dcatr = (default_catalog_reader_ty *) catr;
235
236
97.2k
  call_add_message (dcatr, msgctxt, msgid, msgid_pos, msgid_plural,
237
97.2k
                    msgstr, msgstr_len, msgstr_pos,
238
97.2k
                    prev_msgctxt, prev_msgid, prev_msgid_plural,
239
97.2k
                    force_fuzzy, obsolete);
240
241
  /* Prepare for next message.  */
242
97.2k
  default_reset_comment_state (dcatr);
243
97.2k
}
244
245
246
void
247
default_comment (abstract_catalog_reader_ty *catr, const char *s)
248
1.16M
{
249
1.16M
  default_catalog_reader_ty *dcatr = (default_catalog_reader_ty *) catr;
250
251
1.16M
  if (dcatr->handle_comments)
252
1.16M
    {
253
1.16M
      if (dcatr->comment == NULL)
254
26.0k
        dcatr->comment = string_list_alloc ();
255
1.16M
      string_list_append (dcatr->comment, s);
256
1.16M
    }
257
1.16M
}
258
259
260
void
261
default_comment_dot (abstract_catalog_reader_ty *catr, const char *s)
262
12.2k
{
263
12.2k
  default_catalog_reader_ty *dcatr = (default_catalog_reader_ty *) catr;
264
265
12.2k
  if (dcatr->handle_comments)
266
12.2k
    {
267
12.2k
      if (dcatr->comment_dot == NULL)
268
5.58k
        dcatr->comment_dot = string_list_alloc ();
269
12.2k
      string_list_append (dcatr->comment_dot, s);
270
12.2k
    }
271
12.2k
}
272
273
274
void
275
default_comment_filepos (abstract_catalog_reader_ty *catr,
276
                         const char *file_name, size_t line_number)
277
138k
{
278
138k
  default_catalog_reader_ty *dcatr = (default_catalog_reader_ty *) catr;
279
280
138k
  size_t nbytes = (dcatr->filepos_count + 1) * sizeof (dcatr->filepos[0]);
281
138k
  dcatr->filepos = xrealloc (dcatr->filepos, nbytes);
282
283
138k
  lex_pos_ty *pp = &dcatr->filepos[dcatr->filepos_count++];
284
138k
  pp->file_name = xstrdup (file_name);
285
138k
  pp->line_number = line_number;
286
138k
}
287
288
289
/* Test for '#, fuzzy' or '#= fuzzy' comments and warn.  */
290
void
291
default_comment_special (abstract_catalog_reader_ty *catr, const char *s)
292
52.9k
{
293
52.9k
  default_catalog_reader_ty *dcatr = (default_catalog_reader_ty *) catr;
294
295
52.9k
  bool tmp_fuzzy;
296
52.9k
  enum is_format tmp_format[NFORMATS];
297
52.9k
  struct argument_range tmp_range;
298
52.9k
  enum is_wrap tmp_wrap;
299
52.9k
  parse_comment_special (s, &tmp_fuzzy, tmp_format, &tmp_range, &tmp_wrap,
300
52.9k
                         NULL);
301
302
52.9k
  if (tmp_fuzzy)
303
10.5k
    dcatr->is_fuzzy = true;
304
2.01M
  for (size_t i = 0; i < NFORMATS; i++)
305
1.95M
    if (tmp_format[i] != undecided)
306
389
      dcatr->is_format[i] = tmp_format[i];
307
52.9k
  if (has_range_p (tmp_range))
308
4.23k
    {
309
4.23k
      if (has_range_p (dcatr->range))
310
2.59k
        {
311
2.59k
          if (tmp_range.min < dcatr->range.min)
312
434
            dcatr->range.min = tmp_range.min;
313
2.59k
          if (tmp_range.max > dcatr->range.max)
314
804
            dcatr->range.max = tmp_range.max;
315
2.59k
        }
316
1.63k
      else
317
1.63k
        dcatr->range = tmp_range;
318
4.23k
    }
319
52.9k
  if (tmp_wrap != undecided)
320
826
    dcatr->do_wrap = tmp_wrap;
321
52.9k
}
322
323
324
/* Default implementation of methods not inherited from the superclass.  */
325
326
327
void
328
default_set_domain (default_catalog_reader_ty *dcatr,
329
                    char *name, lex_pos_ty *name_pos)
330
5.22k
{
331
5.22k
  if (dcatr->allow_domain_directives)
332
    /* Override current domain name.  Don't free memory.  */
333
5.22k
    dcatr->domain = name;
334
0
  else
335
0
    {
336
0
      xerror (dcatr->xeh, CAT_SEVERITY_ERROR, NULL,
337
0
              name_pos->file_name, name_pos->line_number, (size_t)(-1),
338
0
              false,
339
0
              _("this file may not contain domain directives"));
340
341
      /* NAME was allocated in read-po-gram.y but is not used anywhere.  */
342
0
      free (name);
343
0
    }
344
5.22k
}
345
346
void
347
default_add_message (default_catalog_reader_ty *dcatr,
348
                     char *msgctxt,
349
                     char *msgid,
350
                     lex_pos_ty *msgid_pos,
351
                     char *msgid_plural,
352
                     char *msgstr, size_t msgstr_len,
353
                     lex_pos_ty *msgstr_pos,
354
                     char *prev_msgctxt,
355
                     char *prev_msgid,
356
                     char *prev_msgid_plural,
357
                     bool force_fuzzy, bool obsolete)
358
97.2k
{
359
97.2k
  if (dcatr->mdlp != NULL)
360
    /* Select the appropriate sublist of dcatr->mdlp.  */
361
97.2k
    dcatr->mlp = msgdomain_list_sublist (dcatr->mdlp, dcatr->domain, true);
362
363
97.2k
  message_ty *mp;
364
97.2k
  if (dcatr->allow_duplicates && msgid[0] != '\0')
365
    /* Doesn't matter if this message ID has been seen before.  */
366
0
    mp = NULL;
367
97.2k
  else
368
    /* See if this message ID has been seen before.  */
369
97.2k
    mp = message_list_search (dcatr->mlp, msgctxt, msgid);
370
371
97.2k
  if (mp)
372
65.6k
    {
373
65.6k
      if (!(dcatr->allow_duplicates_if_same_msgstr
374
0
            && msgstr_len == mp->msgstr_len
375
0
            && memeq (msgstr, mp->msgstr, msgstr_len)))
376
65.6k
        {
377
          /* We give a fatal error about this, regardless whether the
378
             translations are equal or different.  This is for consistency
379
             with msgmerge, msgcat and others.  The user can use the
380
             msguniq program to get rid of duplicates.  */
381
65.6k
          xerror2 (dcatr->xeh, CAT_SEVERITY_ERROR,
382
65.6k
                   NULL,
383
65.6k
                   msgid_pos->file_name, msgid_pos->line_number, (size_t)(-1),
384
65.6k
                   false,
385
65.6k
                   _("duplicate message definition"),
386
65.6k
                   mp,
387
65.6k
                   NULL, 0, 0,
388
65.6k
                   false,
389
65.6k
                   _("this is the location of the first definition"));
390
65.6k
        }
391
      /* We don't need the just constructed entries' parameter string
392
         (allocated in read-po-gram.y).  */
393
65.6k
      free (msgid);
394
65.6k
      if (msgid_plural != NULL)
395
65.6k
        free (msgid_plural);
396
65.6k
      free (msgstr);
397
65.6k
      if (msgctxt != NULL)
398
65.6k
        free (msgctxt);
399
65.6k
      if (prev_msgctxt != NULL)
400
65.6k
        free (prev_msgctxt);
401
65.6k
      if (prev_msgid != NULL)
402
65.6k
        free (prev_msgid);
403
65.6k
      if (prev_msgid_plural != NULL)
404
65.6k
        free (prev_msgid_plural);
405
406
      /* Add the accumulated comments to the message.  */
407
65.6k
      default_copy_comment_state (dcatr, mp);
408
65.6k
    }
409
31.5k
  else
410
31.5k
    {
411
      /* Construct message to add to the list.
412
         Obsolete message go into the list at least for duplicate checking.
413
         It's the caller's responsibility to ignore obsolete messages when
414
         appropriate.  */
415
31.5k
      mp = message_alloc (msgctxt, msgid, msgid_plural, msgstr, msgstr_len,
416
31.5k
                          msgstr_pos);
417
31.5k
      if (msgid_plural != NULL)
418
31.5k
        free (msgid_plural);
419
31.5k
      mp->prev_msgctxt = prev_msgctxt;
420
31.5k
      mp->prev_msgid = prev_msgid;
421
31.5k
      mp->prev_msgid_plural = prev_msgid_plural;
422
31.5k
      mp->obsolete = obsolete;
423
31.5k
      default_copy_comment_state (dcatr, mp);
424
31.5k
      if (force_fuzzy)
425
0
        mp->is_fuzzy = true;
426
427
31.5k
      call_frob_new_message (dcatr, mp, msgid_pos, msgstr_pos);
428
429
31.5k
      message_list_append (dcatr->mlp, mp);
430
31.5k
    }
431
97.2k
}
432
433
434
/* So that the one parser can be used for multiple programs, and also
435
   use good data hiding and encapsulation practices, an object
436
   oriented approach has been taken.  An object instance is allocated,
437
   and all actions resulting from the parse will be through
438
   invocations of method functions of that object.  */
439
440
static default_catalog_reader_class_ty default_methods =
441
{
442
  {
443
    sizeof (default_catalog_reader_ty),
444
    default_constructor,
445
    default_destructor,
446
    default_parse_brief,
447
    default_parse_debrief,
448
    default_directive_domain,
449
    default_directive_message,
450
    default_comment,
451
    default_comment_dot,
452
    default_comment_filepos,
453
    default_comment_special
454
  },
455
  default_set_domain, /* set_domain */
456
  default_add_message, /* add_message */
457
  NULL /* frob_new_message */
458
};
459
460
461
default_catalog_reader_ty *
462
default_catalog_reader_alloc (default_catalog_reader_class_ty *method_table,
463
                              xerror_handler_ty xerror_handler)
464
12.0k
{
465
12.0k
  return
466
12.0k
    (default_catalog_reader_ty *)
467
12.0k
    catalog_reader_alloc (&method_table->super, xerror_handler);
468
12.0k
}
469
470
471
/* ========================================================================= */
472
/* Exported functions.  */
473
474
475
/* If false, duplicate msgids in the same domain and file generate an error.
476
   If true, such msgids are allowed; the caller should treat them
477
   appropriately.  Defaults to false.  */
478
bool allow_duplicates = false;
479
480
481
msgdomain_list_ty *
482
read_catalog_stream (FILE *fp, const char *real_filename,
483
                     const char *logical_filename,
484
                     catalog_input_format_ty input_syntax,
485
                     xerror_handler_ty xerror_handler,
486
                     string_list_ty *arena)
487
12.0k
{
488
12.0k
  default_catalog_reader_ty *dcatr =
489
12.0k
    default_catalog_reader_alloc (&default_methods, xerror_handler);
490
12.0k
  dcatr->pass_obsolete_entries = true;
491
12.0k
  dcatr->handle_comments = true;
492
12.0k
  dcatr->allow_domain_directives = true;
493
12.0k
  dcatr->allow_duplicates = allow_duplicates;
494
12.0k
  dcatr->allow_duplicates_if_same_msgstr = false;
495
12.0k
  dcatr->file_name = real_filename;
496
12.0k
  dcatr->mdlp = msgdomain_list_alloc (!dcatr->allow_duplicates);
497
12.0k
  dcatr->mlp = msgdomain_list_sublist (dcatr->mdlp, dcatr->domain, true);
498
12.0k
  if (input_syntax->produces_utf8)
499
    /* We know a priori that input_syntax->parse convert strings to UTF-8.  */
500
0
    dcatr->mdlp->encoding = po_charset_utf8;
501
502
12.0k
  catalog_reader_parse ((abstract_catalog_reader_ty *) dcatr, fp, real_filename,
503
12.0k
                        logical_filename, false, input_syntax, arena);
504
505
12.0k
  msgdomain_list_ty *mdlp = dcatr->mdlp;
506
12.0k
  catalog_reader_free ((abstract_catalog_reader_ty *) dcatr);
507
12.0k
  return mdlp;
508
12.0k
}