blob: e7e054c6822628b4b31f7b6c46c059fd178a2a63 [file] [log] [blame]
/* Writing Qt .qm files.
Copyright (C) 2003, 2005-2007, 2009, 2016, 2020 Free Software Foundation, Inc.
Written by Bruno Haible <bruno@clisp.org>, 2003.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
/* Specification. */
#include "write-qt.h"
#include <assert.h>
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "error.h"
#include "xerror.h"
#include "message.h"
#include "po-charset.h"
#include "msgl-iconv.h"
#include "msgl-header.h"
#include "hash-string.h"
#include "unistr.h"
#include "xalloc.h"
#include "obstack.h"
#include "mem-hash-map.h"
#include "binary-io.h"
#include "fwriteerror.h"
#include "gettext.h"
#define _(str) gettext (str)
/* Qt .qm files are read by the QTranslator::load() function and written
by the Qt QTranslator::save() function.
The Qt tool 'msg2qm' uses the latter function and can convert PO files
to .qm files. But since 'msg2qm' is marked as an "old" tool in Qt 3.0.5's
i18n.html documentation and therefore likely to disappear, we provide the
same functionality here.
The format of .qm files, as reverse engineered from the functions
QTranslator::save(const QString& filename, SaveMode mode)
QTranslator::squeeze(SaveMode mode)
QTranslatorMessage::write(QDataStream& stream, bool strip, Prefix prefix)
elfHash(const char* name)
in qt-3.0.5, is as follows:
It's a binary data format. Elements are u8 (byte), u16, u32. They are
written in big-endian order.
The file starts with a magic string of 16 bytes:
3C B8 64 18 CA EF 9C 95 CD 21 1C BF 60 A1 BD DD
Then come three sections. Each of the three sections is optional. Each
has this structure:
struct {
u8 section_type; // 0x42 = hashes, 0x69 = messages, 0x2f = contexts
u32 length; // number of bytes of the data
u8 data[length];
};
In the first section, the hashes section, the data has the following
structure:
It's a sorted array of
struct {
u32 hashcode; // elfHash of the concatenation of msgid and
// disambiguating-comment
u32 offset; // offset within the data[] of the messages section
};
It's sorted in ascending order by hashcode as primary sorting criteria
and - when the hashcodes are the same - by offset as secondary criteria.
In the second section, the messages section, the data has the following
structure:
It's a sequence of records, each representing a message, in no
particular order. Each record is a sequence of subsections, each
introduced by a particular subsection tag. The possible subsection tags
are (and they usually occur in this order):
- 03: Translation. Followed by the msgstr in UCS-2 or UTF-16 format:
struct {
u32 length;
u16 chars[length/2];
};
- 08: Disambiguating-comment. Followed by the NUL-terminated,
ISO-8859-1 encoded, disambiguating-comment string:
struct {
u32 length; // number of bytes including the NUL at the end
u8 chars[length];
};
- 06: SourceText, i.e. msgid. Followed by the NUL-terminated,
ISO-8859-1 encoded, msgid:
struct {
u32 length; // number of bytes including the NUL at the end
u8 chars[length];
};
- 02: SourceText16, i.e. msgid. Encoded as UCS-2, but must actually
be ISO-8859-1.
struct {
u32 length;
u16 chars[length/2];
};
This subsection tag is obsoleted by SourceText.
- 07: Context. Followed by the NUL-terminated, ISO-8859-1 encoded,
context string (usually a C++ class name or empty):
struct {
u32 length; // number of bytes including the NUL at the end
u8 chars[length];
};
- 04: Context16. Encoded as UCS-2, but must actually be ISO-8859-1.
struct {
u32 length;
u16 chars[length/2];
};
This subsection tag is obsoleted by Context.
- 05: Hash. Followed by
struct {
u32 hashcode; // elfHash of the concatenation of msgid and
// disambiguating-comment
};
- 01: End. Designates the end of the record. No further data.
Usually the following subsections are written, but some of them are
optional:
- 03: Translation.
- 08: Disambiguating-comment (optional).
- 06: SourceText (optional).
- 07: Context (optional).
- 05: Hash.
- 01: End.
A subsection can be omitted if the value to be output is the same as
for the previous record.
The third section, the contexts section, contains the set of all occurring
context strings. This section is optional; it is used to speed up the
search. The data is a hash table with the following structure:
struct {
u16 table_size;
u16 buckets[table_size];
u8 pool[...];
};
pool[...] contains:
u16 zero;
for i = 0, ..., table_size:
if there are context strings with elfHash(context)%table_size == i:
for all context strings with elfHash(context)%table_size == i:
len := min(length(context),255); // truncated to length 255
struct {
u8 len;
u8 chars[len];
};
struct {
u8 zero[1]; // signals the end of this bucket
u8 padding[0 or 1]; // padding for even number of bytes
};
buckets[i] is 0 for an empty bucket, or the offset in pool[] where
the context strings for this bucket start, divided by 2.
This context section must not be used
- if the empty context is used, or
- if a context of length > 255 is used, or
- if the context pool's size would be > 2^17.
The elfHash function is the same as our hash_string function, except that
at the end it maps a hash code of 0x00000000 to 0x00000001.
When we convert from PO file format, all disambiguating-comments and
contexts are empty, and therefore the contexts section can be omitted. */
/* Write a u8 (a single byte) to the output stream. */
static inline void
write_u8 (FILE *output_file, unsigned char value)
{
putc (value, output_file);
}
/* Write a u16 (two bytes) to the output stream. */
static inline void
write_u16 (FILE *output_file, unsigned short value)
{
unsigned char data[2];
data[0] = (value >> 8) & 0xff;
data[1] = value & 0xff;
fwrite (data, 2, 1, output_file);
}
/* Write a u32 (four bytes) to the output stream. */
static inline void
write_u32 (FILE *output_file, unsigned int value)
{
unsigned char data[4];
data[0] = (value >> 24) & 0xff;
data[1] = (value >> 16) & 0xff;
data[2] = (value >> 8) & 0xff;
data[3] = value & 0xff;
fwrite (data, 4, 1, output_file);
}
#define obstack_chunk_alloc xmalloc
#define obstack_chunk_free free
/* Add a u8 (a single byte) to an obstack. */
static void
append_u8 (struct obstack *mempool, unsigned char value)
{
unsigned char data[1];
data[0] = value;
obstack_grow (mempool, data, 1);
}
/* Add a u16 (two bytes) to an obstack. */
static void
append_u16 (struct obstack *mempool, unsigned short value)
{
unsigned char data[2];
data[0] = (value >> 8) & 0xff;
data[1] = value & 0xff;
obstack_grow (mempool, data, 2);
}
/* Add a u32 (four bytes) to an obstack. */
static void
append_u32 (struct obstack *mempool, unsigned int value)
{
unsigned char data[4];
data[0] = (value >> 24) & 0xff;
data[1] = (value >> 16) & 0xff;
data[2] = (value >> 8) & 0xff;
data[3] = value & 0xff;
obstack_grow (mempool, data, 4);
}
/* Add an ISO-8859-1 encoded string to an obstack. */
static void
append_base_string (struct obstack *mempool, const char *string)
{
size_t length = strlen (string) + 1;
append_u32 (mempool, length);
obstack_grow (mempool, string, length);
}
/* Add an UTF-16 encoded string to an obstack. */
static void
append_unicode_string (struct obstack *mempool, const unsigned short *string,
size_t length)
{
append_u32 (mempool, length * 2);
for (; length > 0; string++, length--)
append_u16 (mempool, *string);
}
/* Retrieve a 4-byte integer from memory. */
static inline unsigned int
peek_u32 (const unsigned char *p)
{
return (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3];
}
/* Convert an UTF-8 string to ISO-8859-1, without error checking. */
static char *
conv_to_iso_8859_1 (const char *string)
{
size_t length = strlen (string);
const char *str = string;
const char *str_limit = string + length;
/* Conversion to ISO-8859-1 can only reduce the number of bytes. */
char *result = XNMALLOC (length + 1, char);
char *q = result;
while (str < str_limit)
{
ucs4_t uc;
str += u8_mbtouc (&uc, (const unsigned char *) str, str_limit - str);
/* It has already been verified that the string fits in ISO-8859-1. */
if (!(uc < 0x100))
abort ();
/* Store as ISO-8859-1. */
*q++ = (unsigned char) uc;
}
*q = '\0';
assert (q - result <= length);
return result;
}
/* Convert an UTF-8 string to UTF-16, returning its size (number of UTF-16
codepoints) in *SIZEP. */
static unsigned short *
conv_to_utf16 (const char *string, size_t *sizep)
{
size_t length = strlen (string);
const char *str = string;
const char *str_limit = string + length;
/* Conversion to UTF-16 can at most double the number of bytes. */
unsigned short *result = XNMALLOC (length, unsigned short);
unsigned short *q = result;
while (str < str_limit)
{
ucs4_t uc;
str += u8_mbtouc (&uc, (const unsigned char *) str, str_limit - str);
if (uc < 0x10000)
/* UCS-2 character. */
*q++ = (unsigned short) uc;
else
{
/* UTF-16 surrogate. */
*q++ = 0xd800 + ((uc - 0x10000) >> 10);
*q++ = 0xdc00 + ((uc - 0x10000) & 0x3ff);
}
}
assert (q - result <= 2 * length);
*sizep = q - result;
return result;
}
/* Return the Qt hash code of a string. */
static unsigned int
string_hashcode (const char *str)
{
unsigned int h;
h = hash_string (str);
if (h == 0)
h = 1;
return h;
}
/* Compare two entries of the hashes section. */
static int
cmp_hashes (const void *va, const void *vb)
{
const unsigned char *a = (const unsigned char *) va;
const unsigned char *b = (const unsigned char *) vb;
unsigned int a_hashcode = peek_u32 (a);
unsigned int b_hashcode = peek_u32 (b);
if (a_hashcode != b_hashcode)
return (a_hashcode >= b_hashcode ? 1 : -1);
else
{
unsigned int a_offset = peek_u32 (a + 4);
unsigned int b_offset = peek_u32 (b + 4);
if (a_offset != b_offset)
return (a_offset >= b_offset ? 1 : -1);
else
return 0;
}
}
/* Write a section to the output stream. */
static void
write_section (FILE *output_file, unsigned char tag, void *data, size_t size)
{
/* A section can be omitted if it is empty. */
if (size > 0)
{
write_u8 (output_file, tag);
write_u32 (output_file, size);
fwrite (data, size, 1, output_file);
}
}
/* Write an entire .qm file. */
static void
write_qm (FILE *output_file, message_list_ty *mlp)
{
static unsigned char magic[16] =
{
0x3C, 0xB8, 0x64, 0x18, 0xCA, 0xEF, 0x9C, 0x95,
0xCD, 0x21, 0x1C, 0xBF, 0x60, 0xA1, 0xBD, 0xDD
};
struct obstack hashes_pool;
struct obstack messages_pool;
size_t j;
obstack_init (&hashes_pool);
obstack_init (&messages_pool);
/* Prepare the hashes section and the messages section. */
for (j = 0; j < mlp->nitems; j++)
{
message_ty *mp = mlp->item[j];
/* No need to emit the header entry, it's not needed at runtime. */
if (!is_header (mp))
{
char *msgctxt_as_iso_8859_1 =
conv_to_iso_8859_1 (mp->msgctxt != NULL ? mp->msgctxt : "");
char *msgid_as_iso_8859_1 = conv_to_iso_8859_1 (mp->msgid);
size_t msgstr_len;
unsigned short *msgstr_as_utf16 =
conv_to_utf16 (mp->msgstr, &msgstr_len);
unsigned int hashcode = string_hashcode (msgid_as_iso_8859_1);
unsigned int offset = obstack_object_size (&messages_pool);
/* Add a record to the hashes section. */
append_u32 (&hashes_pool, hashcode);
append_u32 (&hashes_pool, offset);
/* Add a record to the messages section. */
append_u8 (&messages_pool, 0x03);
append_unicode_string (&messages_pool, msgstr_as_utf16, msgstr_len);
append_u8 (&messages_pool, 0x08);
append_base_string (&messages_pool, "");
append_u8 (&messages_pool, 0x06);
append_base_string (&messages_pool, msgid_as_iso_8859_1);
append_u8 (&messages_pool, 0x07);
append_base_string (&messages_pool, msgctxt_as_iso_8859_1);
append_u8 (&messages_pool, 0x05);
append_u32 (&messages_pool, hashcode);
append_u8 (&messages_pool, 0x01);
free (msgstr_as_utf16);
free (msgid_as_iso_8859_1);
free (msgctxt_as_iso_8859_1);
}
}
/* Sort the hashes section. */
{
size_t nstrings = obstack_object_size (&hashes_pool) / 8;
if (nstrings > 0)
qsort (obstack_base (&hashes_pool), nstrings, 8, cmp_hashes);
}
/* Write the magic number. */
fwrite (magic, sizeof (magic), 1, output_file);
/* Write the hashes section. */
write_section (output_file, 0x42, obstack_base (&hashes_pool),
obstack_object_size (&hashes_pool));
/* Write the messages section. */
write_section (output_file, 0x69, obstack_base (&messages_pool),
obstack_object_size (&messages_pool));
/* Decide whether to write a contexts section. */
{
bool can_write_contexts = true;
for (j = 0; j < mlp->nitems; j++)
{
message_ty *mp = mlp->item[j];
if (!is_header (mp))
if (mp->msgctxt == NULL || mp->msgctxt[0] == '\0'
|| strlen (mp->msgctxt) > 255)
{
can_write_contexts = false;
break;
}
}
if (can_write_contexts)
{
hash_table all_contexts;
size_t num_contexts;
unsigned long table_size;
/* Collect the contexts, removing duplicates. */
hash_init (&all_contexts, 10);
for (j = 0; j < mlp->nitems; j++)
{
message_ty *mp = mlp->item[j];
if (!is_header (mp))
hash_insert_entry (&all_contexts,
mp->msgctxt, strlen (mp->msgctxt) + 1,
NULL);
}
/* Compute the number of different contexts. */
num_contexts = all_contexts.size;
/* Compute a suitable hash table size. */
table_size = next_prime (num_contexts * 1.7);
if (table_size >= 0x10000)
table_size = 65521;
/* Put the contexts into a hash table of size table_size. */
{
struct list_cell { const char *context; struct list_cell *next; };
struct list_cell *list_memory =
XNMALLOC (table_size, struct list_cell);
struct list_cell *freelist;
struct bucket { struct list_cell *head; struct list_cell **tail; };
struct bucket *buckets = XNMALLOC (table_size, struct bucket);
size_t i;
freelist = list_memory;
for (i = 0; i < table_size; i++)
{
buckets[i].head = NULL;
buckets[i].tail = &buckets[i].head;
}
{
void *iter;
const void *key;
size_t keylen;
void *null;
iter = NULL;
while (hash_iterate (&all_contexts, &iter, &key, &keylen, &null)
== 0)
{
const char *context = (const char *)key;
i = string_hashcode (context) % table_size;
freelist->context = context;
freelist->next = NULL;
*buckets[i].tail = freelist;
buckets[i].tail = &freelist->next;
freelist++;
}
}
/* Determine the total context pool size. */
{
size_t pool_size;
pool_size = 2;
for (i = 0; i < table_size; i++)
if (buckets[i].head != NULL)
{
const struct list_cell *p;
for (p = buckets[i].head; p != NULL; p = p->next)
pool_size += 1 + strlen (p->context);
pool_size++;
if ((pool_size % 2) != 0)
pool_size++;
}
if (pool_size <= 0x20000)
{
/* Prepare the contexts section. */
struct obstack contexts_pool;
size_t pool_offset;
obstack_init (&contexts_pool);
append_u16 (&contexts_pool, table_size);
pool_offset = 2;
for (i = 0; i < table_size; i++)
if (buckets[i].head != NULL)
{
const struct list_cell *p;
append_u16 (&contexts_pool, pool_offset / 2);
for (p = buckets[i].head; p != NULL; p = p->next)
pool_offset += 1 + strlen (p->context);
pool_offset++;
if ((pool_offset % 2) != 0)
pool_offset++;
}
else
append_u16 (&contexts_pool, 0);
if (!(pool_offset == pool_size))
abort ();
append_u16 (&contexts_pool, 0);
pool_offset = 2;
for (i = 0; i < table_size; i++)
if (buckets[i].head != NULL)
{
const struct list_cell *p;
for (p = buckets[i].head; p != NULL; p = p->next)
{
append_u8 (&contexts_pool, strlen (p->context));
obstack_grow (&contexts_pool,
p->context, strlen (p->context));
pool_offset += 1 + strlen (p->context);
}
append_u8 (&contexts_pool, 0);
pool_offset++;
if ((pool_offset % 2) != 0)
{
append_u8 (&contexts_pool, 0);
pool_offset++;
}
}
if (!(pool_offset == pool_size))
abort ();
if (!(obstack_object_size (&contexts_pool)
== 2 + 2 * table_size + pool_size))
abort ();
/* Write the contexts section. */
write_section (output_file, 0x2f, obstack_base (&contexts_pool),
obstack_object_size (&contexts_pool));
obstack_free (&contexts_pool, NULL);
}
}
free (buckets);
free (list_memory);
}
hash_destroy (&all_contexts);
}
}
obstack_free (&messages_pool, NULL);
obstack_free (&hashes_pool, NULL);
}
int
msgdomain_write_qt (message_list_ty *mlp, const char *canon_encoding,
const char *domain_name, const char *file_name)
{
/* If no entry for this domain don't even create the file. */
if (mlp->nitems != 0)
{
FILE *output_file;
/* Determine whether mlp has plural entries. */
{
bool has_plural;
size_t j;
has_plural = false;
for (j = 0; j < mlp->nitems; j++)
if (mlp->item[j]->msgid_plural != NULL)
has_plural = true;
if (has_plural)
{
multiline_error (xstrdup (""),
xstrdup (_("\
message catalog has plural form translations\n\
but the Qt message catalog format doesn't support plural handling\n")));
return 1;
}
}
/* Convert the messages to Unicode. */
iconv_message_list (mlp, canon_encoding, po_charset_utf8, NULL);
/* Determine whether mlp has non-ISO-8859-1 msgctxt entries. */
{
size_t j;
for (j = 0; j < mlp->nitems; j++)
{
const char *string = mlp->item[j]->msgctxt;
if (string != NULL)
{
/* An UTF-8 encoded string fits in ISO-8859-1 if and only if
all its bytes are < 0xc4. */
for (; *string; string++)
if ((unsigned char) *string >= 0xc4)
{
multiline_error (xstrdup (""),
xstrdup (_("\
message catalog has msgctxt strings containing characters outside ISO-8859-1\n\
but the Qt message catalog format supports Unicode only in the translated\n\
strings, not in the context strings\n")));
return 1;
}
}
}
}
/* Determine whether mlp has non-ISO-8859-1 msgid entries. */
{
size_t j;
for (j = 0; j < mlp->nitems; j++)
{
const char *string = mlp->item[j]->msgid;
/* An UTF-8 encoded string fits in ISO-8859-1 if and only if all
its bytes are < 0xc4. */
for (; *string; string++)
if ((unsigned char) *string >= 0xc4)
{
multiline_error (xstrdup (""),
xstrdup (_("\
message catalog has msgid strings containing characters outside ISO-8859-1\n\
but the Qt message catalog format supports Unicode only in the translated\n\
strings, not in the untranslated strings\n")));
return 1;
}
}
}
/* Support for "reproducible builds": Delete information that may vary
between builds in the same conditions. */
message_list_delete_header_field (mlp, "POT-Creation-Date:");
if (strcmp (domain_name, "-") == 0)
{
output_file = stdout;
SET_BINARY (fileno (output_file));
}
else
{
output_file = fopen (file_name, "wb");
if (output_file == NULL)
{
error (0, errno, _("error while opening \"%s\" for writing"),
file_name);
return 1;
}
}
write_qm (output_file, mlp);
/* Make sure nothing went wrong. */
if (fwriteerror (output_file))
error (EXIT_FAILURE, errno, _("error while writing \"%s\" file"),
file_name);
}
return 0;
}