blob: afa060a6e0339b7dd7f4d1f03ab2b20c1f4765ed [file] [log] [blame]
/* Writing binary .mo files.
Copyright (C) 1995-1998, 2000-2007, 2016, 2020 Free Software Foundation, Inc.
Written by Ulrich Drepper <drepper@gnu.ai.mit.edu>, April 1995.
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
#include <alloca.h>
/* Specification. */
#include "write-mo.h"
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if HAVE_SYS_PARAM_H
# include <sys/param.h>
#endif
/* These two include files describe the binary .mo format. */
#include "gmo.h"
#include "hash-string.h"
#include "byteswap.h"
#include "error.h"
#include "mem-hash-map.h"
#include "message.h"
#include "format.h"
#include "xsize.h"
#include "xalloc.h"
#include "xmalloca.h"
#include "msgl-header.h"
#include "binary-io.h"
#include "supersede.h"
#include "fwriteerror.h"
#include "gettext.h"
#define _(str) gettext (str)
#define freea(p) /* nothing */
/* Usually defined in <sys/param.h>. */
#ifndef roundup
# if defined __GNUC__ && __GNUC__ >= 2
# define roundup(x, y) ({typeof(x) _x = (x); typeof(y) _y = (y); \
((_x + _y - 1) / _y) * _y; })
# else
# define roundup(x, y) ((((x)+((y)-1))/(y))*(y))
# endif /* GNU CC2 */
#endif /* roundup */
/* Alignment of strings in resulting .mo file. */
size_t alignment;
/* True if writing a .mo file in opposite endianness than the host. */
bool byteswap;
/* True if no hash table in .mo is wanted. */
bool no_hash_table;
/* Destructively changes the byte order of a 32-bit value in memory. */
#define BSWAP32(x) (x) = bswap_32 (x)
/* Indices into the strings contained in 'struct pre_message' and
'struct pre_sysdep_message'. */
enum
{
M_ID = 0, /* msgid - the original string */
M_STR = 1 /* msgstr - the translated string */
};
/* An intermediate data structure representing a 'struct string_desc'. */
struct pre_string
{
size_t length;
const char *pointer;
};
/* An intermediate data structure representing a message. */
struct pre_message
{
struct pre_string str[2];
const char *id_plural;
size_t id_plural_len;
};
static int
compare_id (const void *pval1, const void *pval2)
{
return strcmp (((const struct pre_message *) pval1)->str[M_ID].pointer,
((const struct pre_message *) pval2)->str[M_ID].pointer);
}
/* An intermediate data structure representing a 'struct sysdep_segment'. */
struct pre_sysdep_segment
{
size_t length;
const char *pointer;
};
/* An intermediate data structure representing a 'struct segment_pair'. */
struct pre_segment_pair
{
size_t segsize;
const char *segptr;
size_t sysdepref;
};
/* An intermediate data structure representing a 'struct sysdep_string'. */
struct pre_sysdep_string
{
unsigned int segmentcount;
struct pre_segment_pair segments[1];
};
/* An intermediate data structure representing a message with system dependent
strings. */
struct pre_sysdep_message
{
struct pre_sysdep_string *str[2];
const char *id_plural;
size_t id_plural_len;
};
/* Write the message list to the given open file. */
static void
write_table (FILE *output_file, message_list_ty *mlp)
{
char **msgctid_arr;
size_t nstrings;
struct pre_message *msg_arr;
size_t n_sysdep_strings;
struct pre_sysdep_message *sysdep_msg_arr;
size_t n_sysdep_segments;
struct pre_sysdep_segment *sysdep_segments;
bool have_outdigits;
int major_revision;
int minor_revision;
bool omit_hash_table;
nls_uint32 hash_tab_size;
struct mo_file_header header; /* Header of the .mo file to be written. */
size_t header_size;
size_t offset;
struct string_desc *orig_tab;
struct string_desc *trans_tab;
size_t sysdep_tab_offset = 0;
size_t end_offset;
char *null;
size_t j, m;
/* First pass: Move the static string pairs into an array, for sorting,
and at the same time, compute the segments of the system dependent
strings. */
msgctid_arr = XNMALLOC (mlp->nitems, char *);
nstrings = 0;
msg_arr = XNMALLOC (mlp->nitems, struct pre_message);
n_sysdep_strings = 0;
sysdep_msg_arr = XNMALLOC (mlp->nitems, struct pre_sysdep_message);
n_sysdep_segments = 0;
sysdep_segments = NULL;
have_outdigits = false;
for (j = 0; j < mlp->nitems; j++)
{
message_ty *mp = mlp->item[j];
size_t msgctlen;
char *msgctid;
struct interval *intervals[2];
size_t nintervals[2];
/* Concatenate mp->msgctxt and mp->msgid into msgctid. */
msgctlen = (mp->msgctxt != NULL ? strlen (mp->msgctxt) + 1 : 0);
msgctid = XNMALLOC (msgctlen + strlen (mp->msgid) + 1, char);
if (mp->msgctxt != NULL)
{
memcpy (msgctid, mp->msgctxt, msgctlen - 1);
msgctid[msgctlen - 1] = MSGCTXT_SEPARATOR;
}
strcpy (msgctid + msgctlen, mp->msgid);
msgctid_arr[j] = msgctid;
intervals[M_ID] = NULL;
nintervals[M_ID] = 0;
intervals[M_STR] = NULL;
nintervals[M_STR] = 0;
/* Test if mp contains system dependent strings and thus
requires the use of the .mo file minor revision 1. */
if (possible_format_p (mp->is_format[format_c])
|| possible_format_p (mp->is_format[format_objc]))
{
/* Check whether msgid or msgstr contain ISO C 99 <inttypes.h>
format string directives. No need to check msgid_plural, because
it is not accessed by the [n]gettext() function family. */
const char *p_end;
const char *p;
get_sysdep_c_format_directives (mp->msgid, false,
&intervals[M_ID], &nintervals[M_ID]);
if (msgctlen > 0)
{
struct interval *id_intervals = intervals[M_ID];
size_t id_nintervals = nintervals[M_ID];
if (id_nintervals > 0)
{
unsigned int i;
for (i = 0; i < id_nintervals; i++)
{
id_intervals[i].startpos += msgctlen;
id_intervals[i].endpos += msgctlen;
}
}
}
p_end = mp->msgstr + mp->msgstr_len;
for (p = mp->msgstr; p < p_end; p += strlen (p) + 1)
{
struct interval *part_intervals;
size_t part_nintervals;
get_sysdep_c_format_directives (p, true,
&part_intervals,
&part_nintervals);
if (part_nintervals > 0)
{
size_t d = p - mp->msgstr;
unsigned int i;
intervals[M_STR] =
(struct interval *)
xrealloc (intervals[M_STR],
(nintervals[M_STR] + part_nintervals)
* sizeof (struct interval));
for (i = 0; i < part_nintervals; i++)
{
intervals[M_STR][nintervals[M_STR] + i].startpos =
d + part_intervals[i].startpos;
intervals[M_STR][nintervals[M_STR] + i].endpos =
d + part_intervals[i].endpos;
}
nintervals[M_STR] += part_nintervals;
}
}
}
if (nintervals[M_ID] > 0 || nintervals[M_STR] > 0)
{
/* System dependent string pair. */
for (m = 0; m < 2; m++)
{
struct pre_sysdep_string *pre =
(struct pre_sysdep_string *)
xmalloc (xsum (sizeof (struct pre_sysdep_string),
xtimes (nintervals[m],
sizeof (struct pre_segment_pair))));
const char *str;
size_t str_len;
size_t lastpos;
unsigned int i;
if (m == M_ID)
{
str = msgctid; /* concatenation of mp->msgctxt + mp->msgid */
str_len = strlen (msgctid) + 1;
}
else
{
str = mp->msgstr;
str_len = mp->msgstr_len;
}
lastpos = 0;
pre->segmentcount = nintervals[m];
for (i = 0; i < nintervals[m]; i++)
{
size_t length;
const char *pointer;
size_t r;
pre->segments[i].segptr = str + lastpos;
pre->segments[i].segsize = intervals[m][i].startpos - lastpos;
length = intervals[m][i].endpos - intervals[m][i].startpos;
pointer = str + intervals[m][i].startpos;
if (length >= 2
&& pointer[0] == '<' && pointer[length - 1] == '>')
{
/* Skip the '<' and '>' markers. */
length -= 2;
pointer += 1;
}
for (r = 0; r < n_sysdep_segments; r++)
if (sysdep_segments[r].length == length
&& memcmp (sysdep_segments[r].pointer, pointer, length)
== 0)
break;
if (r == n_sysdep_segments)
{
n_sysdep_segments++;
sysdep_segments =
(struct pre_sysdep_segment *)
xrealloc (sysdep_segments,
n_sysdep_segments
* sizeof (struct pre_sysdep_segment));
sysdep_segments[r].length = length;
sysdep_segments[r].pointer = pointer;
}
pre->segments[i].sysdepref = r;
if (length == 1 && *pointer == 'I')
have_outdigits = true;
lastpos = intervals[m][i].endpos;
}
pre->segments[i].segptr = str + lastpos;
pre->segments[i].segsize = str_len - lastpos;
pre->segments[i].sysdepref = SEGMENTS_END;
sysdep_msg_arr[n_sysdep_strings].str[m] = pre;
}
sysdep_msg_arr[n_sysdep_strings].id_plural = mp->msgid_plural;
sysdep_msg_arr[n_sysdep_strings].id_plural_len =
(mp->msgid_plural != NULL ? strlen (mp->msgid_plural) + 1 : 0);
n_sysdep_strings++;
}
else
{
/* Static string pair. */
msg_arr[nstrings].str[M_ID].pointer = msgctid;
msg_arr[nstrings].str[M_ID].length = strlen (msgctid) + 1;
msg_arr[nstrings].str[M_STR].pointer = mp->msgstr;
msg_arr[nstrings].str[M_STR].length = mp->msgstr_len;
msg_arr[nstrings].id_plural = mp->msgid_plural;
msg_arr[nstrings].id_plural_len =
(mp->msgid_plural != NULL ? strlen (mp->msgid_plural) + 1 : 0);
nstrings++;
}
for (m = 0; m < 2; m++)
if (intervals[m] != NULL)
free (intervals[m]);
}
/* Sort the table according to original string. */
if (nstrings > 0)
qsort (msg_arr, nstrings, sizeof (struct pre_message), compare_id);
/* We need major revision 1 if there are system dependent strings that use
"I" because older versions of gettext() crash when this occurs in a .mo
file. Otherwise use major revision 0. */
major_revision =
(have_outdigits ? MO_REVISION_NUMBER_WITH_SYSDEP_I : MO_REVISION_NUMBER);
/* We need minor revision 1 if there are system dependent strings.
Otherwise we choose minor revision 0 because it's supported by older
versions of libintl and revision 1 isn't. */
minor_revision = (n_sysdep_strings > 0 ? 1 : 0);
/* In minor revision >= 1, the hash table is obligatory. */
omit_hash_table = (no_hash_table && minor_revision == 0);
/* This should be explained:
Each string has an associate hashing value V, computed by a fixed
function. To locate the string we use open addressing with double
hashing. The first index will be V % M, where M is the size of the
hashing table. If no entry is found, iterating with a second,
independent hashing function takes place. This second value will
be 1 + V % (M - 2).
The approximate number of probes will be
for unsuccessful search: (1 - N / M) ^ -1
for successful search: - (N / M) ^ -1 * ln (1 - N / M)
where N is the number of keys.
If we now choose M to be the next prime bigger than 4 / 3 * N,
we get the values
4 and 1.85 resp.
Because unsuccessful searches are unlikely this is a good value.
Formulas: [Knuth, The Art of Computer Programming, Volume 3,
Sorting and Searching, 1973, Addison Wesley] */
if (!omit_hash_table)
{
hash_tab_size = next_prime ((mlp->nitems * 4) / 3);
/* Ensure M > 2. */
if (hash_tab_size <= 2)
hash_tab_size = 3;
}
else
hash_tab_size = 0;
/* Second pass: Fill the structure describing the header. At the same time,
compute the sizes and offsets of the non-string parts of the file. */
/* Magic number. */
header.magic = _MAGIC;
/* Revision number of file format. */
header.revision = (major_revision << 16) + minor_revision;
header_size =
(minor_revision == 0
? offsetof (struct mo_file_header, n_sysdep_segments)
: sizeof (struct mo_file_header));
offset = header_size;
/* Number of static string pairs. */
header.nstrings = nstrings;
/* Offset of table for original string offsets. */
header.orig_tab_offset = offset;
offset += nstrings * sizeof (struct string_desc);
orig_tab = XNMALLOC (nstrings, struct string_desc);
/* Offset of table for translated string offsets. */
header.trans_tab_offset = offset;
offset += nstrings * sizeof (struct string_desc);
trans_tab = XNMALLOC (nstrings, struct string_desc);
/* Size of hash table. */
header.hash_tab_size = hash_tab_size;
/* Offset of hash table. */
header.hash_tab_offset = offset;
offset += hash_tab_size * sizeof (nls_uint32);
if (minor_revision >= 1)
{
/* Size of table describing system dependent segments. */
header.n_sysdep_segments = n_sysdep_segments;
/* Offset of table describing system dependent segments. */
header.sysdep_segments_offset = offset;
offset += n_sysdep_segments * sizeof (struct sysdep_segment);
/* Number of system dependent string pairs. */
header.n_sysdep_strings = n_sysdep_strings;
/* Offset of table for original sysdep string offsets. */
header.orig_sysdep_tab_offset = offset;
offset += n_sysdep_strings * sizeof (nls_uint32);
/* Offset of table for translated sysdep string offsets. */
header.trans_sysdep_tab_offset = offset;
offset += n_sysdep_strings * sizeof (nls_uint32);
/* System dependent string descriptors. */
sysdep_tab_offset = offset;
for (m = 0; m < 2; m++)
for (j = 0; j < n_sysdep_strings; j++)
offset += sizeof (struct sysdep_string)
+ sysdep_msg_arr[j].str[m]->segmentcount
* sizeof (struct segment_pair);
}
end_offset = offset;
/* Third pass: Write the non-string parts of the file. At the same time,
compute the offsets of each string, including the proper alignment. */
/* Write the header out. */
if (byteswap)
{
BSWAP32 (header.magic);
BSWAP32 (header.revision);
BSWAP32 (header.nstrings);
BSWAP32 (header.orig_tab_offset);
BSWAP32 (header.trans_tab_offset);
BSWAP32 (header.hash_tab_size);
BSWAP32 (header.hash_tab_offset);
if (minor_revision >= 1)
{
BSWAP32 (header.n_sysdep_segments);
BSWAP32 (header.sysdep_segments_offset);
BSWAP32 (header.n_sysdep_strings);
BSWAP32 (header.orig_sysdep_tab_offset);
BSWAP32 (header.trans_sysdep_tab_offset);
}
}
fwrite (&header, header_size, 1, output_file);
/* Table for original string offsets. */
/* Here output_file is at position header.orig_tab_offset. */
for (j = 0; j < nstrings; j++)
{
offset = roundup (offset, alignment);
orig_tab[j].length =
msg_arr[j].str[M_ID].length + msg_arr[j].id_plural_len;
orig_tab[j].offset = offset;
offset += orig_tab[j].length;
/* Subtract 1 because of the terminating NUL. */
orig_tab[j].length--;
}
if (byteswap)
for (j = 0; j < nstrings; j++)
{
BSWAP32 (orig_tab[j].length);
BSWAP32 (orig_tab[j].offset);
}
fwrite (orig_tab, nstrings * sizeof (struct string_desc), 1, output_file);
/* Table for translated string offsets. */
/* Here output_file is at position header.trans_tab_offset. */
for (j = 0; j < nstrings; j++)
{
offset = roundup (offset, alignment);
trans_tab[j].length = msg_arr[j].str[M_STR].length;
trans_tab[j].offset = offset;
offset += trans_tab[j].length;
/* Subtract 1 because of the terminating NUL. */
trans_tab[j].length--;
}
if (byteswap)
for (j = 0; j < nstrings; j++)
{
BSWAP32 (trans_tab[j].length);
BSWAP32 (trans_tab[j].offset);
}
fwrite (trans_tab, nstrings * sizeof (struct string_desc), 1, output_file);
/* Skip this part when no hash table is needed. */
if (!omit_hash_table)
{
nls_uint32 *hash_tab;
unsigned int j;
/* Here output_file is at position header.hash_tab_offset. */
/* Allocate room for the hashing table to be written out. */
hash_tab = XNMALLOC (hash_tab_size, nls_uint32);
memset (hash_tab, '\0', hash_tab_size * sizeof (nls_uint32));
/* Insert all values in the hash table, following the algorithm described
above. */
for (j = 0; j < nstrings; j++)
{
nls_uint32 hash_val = hash_string (msg_arr[j].str[M_ID].pointer);
nls_uint32 idx = hash_val % hash_tab_size;
if (hash_tab[idx] != 0)
{
/* We need the second hashing function. */
nls_uint32 incr = 1 + (hash_val % (hash_tab_size - 2));
do
if (idx >= hash_tab_size - incr)
idx -= hash_tab_size - incr;
else
idx += incr;
while (hash_tab[idx] != 0);
}
hash_tab[idx] = j + 1;
}
/* Write the hash table out. */
if (byteswap)
for (j = 0; j < hash_tab_size; j++)
BSWAP32 (hash_tab[j]);
fwrite (hash_tab, hash_tab_size * sizeof (nls_uint32), 1, output_file);
free (hash_tab);
}
if (minor_revision >= 1)
{
struct sysdep_segment *sysdep_segments_tab;
nls_uint32 *sysdep_tab;
size_t stoffset;
unsigned int i;
/* Here output_file is at position header.sysdep_segments_offset. */
sysdep_segments_tab =
XNMALLOC (n_sysdep_segments, struct sysdep_segment);
for (i = 0; i < n_sysdep_segments; i++)
{
offset = roundup (offset, alignment);
/* The "+ 1" accounts for the trailing NUL byte. */
sysdep_segments_tab[i].length = sysdep_segments[i].length + 1;
sysdep_segments_tab[i].offset = offset;
offset += sysdep_segments_tab[i].length;
}
if (byteswap)
for (i = 0; i < n_sysdep_segments; i++)
{
BSWAP32 (sysdep_segments_tab[i].length);
BSWAP32 (sysdep_segments_tab[i].offset);
}
fwrite (sysdep_segments_tab,
n_sysdep_segments * sizeof (struct sysdep_segment), 1,
output_file);
free (sysdep_segments_tab);
sysdep_tab = XNMALLOC (n_sysdep_strings, nls_uint32);
stoffset = sysdep_tab_offset;
for (m = 0; m < 2; m++)
{
/* Here output_file is at position
m == M_ID -> header.orig_sysdep_tab_offset,
m == M_STR -> header.trans_sysdep_tab_offset. */
for (j = 0; j < n_sysdep_strings; j++)
{
sysdep_tab[j] = stoffset;
stoffset += sizeof (struct sysdep_string)
+ sysdep_msg_arr[j].str[m]->segmentcount
* sizeof (struct segment_pair);
}
/* Write the table for original/translated sysdep string offsets. */
if (byteswap)
for (j = 0; j < n_sysdep_strings; j++)
BSWAP32 (sysdep_tab[j]);
fwrite (sysdep_tab, n_sysdep_strings * sizeof (nls_uint32), 1,
output_file);
}
free (sysdep_tab);
/* Here output_file is at position sysdep_tab_offset. */
for (m = 0; m < 2; m++)
for (j = 0; j < n_sysdep_strings; j++)
{
struct pre_sysdep_message *msg = &sysdep_msg_arr[j];
struct pre_sysdep_string *pre = msg->str[m];
struct sysdep_string *str =
(struct sysdep_string *)
xmalloca (sizeof (struct sysdep_string)
+ pre->segmentcount * sizeof (struct segment_pair));
unsigned int i;
offset = roundup (offset, alignment);
str->offset = offset;
for (i = 0; i <= pre->segmentcount; i++)
{
str->segments[i].segsize = pre->segments[i].segsize;
str->segments[i].sysdepref = pre->segments[i].sysdepref;
offset += str->segments[i].segsize;
}
if (m == M_ID && msg->id_plural_len > 0)
{
str->segments[pre->segmentcount].segsize += msg->id_plural_len;
offset += msg->id_plural_len;
}
if (byteswap)
{
BSWAP32 (str->offset);
for (i = 0; i <= pre->segmentcount; i++)
{
BSWAP32 (str->segments[i].segsize);
BSWAP32 (str->segments[i].sysdepref);
}
}
fwrite (str,
sizeof (struct sysdep_string)
+ pre->segmentcount * sizeof (struct segment_pair),
1, output_file);
freea (str);
}
}
/* Here output_file is at position end_offset. */
free (trans_tab);
free (orig_tab);
/* Fourth pass: Write the strings. */
offset = end_offset;
/* A few zero bytes for padding. */
null = (char *) alloca (alignment);
memset (null, '\0', alignment);
/* Now write the original strings. */
for (j = 0; j < nstrings; j++)
{
fwrite (null, roundup (offset, alignment) - offset, 1, output_file);
offset = roundup (offset, alignment);
fwrite (msg_arr[j].str[M_ID].pointer, msg_arr[j].str[M_ID].length, 1,
output_file);
if (msg_arr[j].id_plural_len > 0)
fwrite (msg_arr[j].id_plural, msg_arr[j].id_plural_len, 1,
output_file);
offset += msg_arr[j].str[M_ID].length + msg_arr[j].id_plural_len;
}
/* Now write the translated strings. */
for (j = 0; j < nstrings; j++)
{
fwrite (null, roundup (offset, alignment) - offset, 1, output_file);
offset = roundup (offset, alignment);
fwrite (msg_arr[j].str[M_STR].pointer, msg_arr[j].str[M_STR].length, 1,
output_file);
offset += msg_arr[j].str[M_STR].length;
}
if (minor_revision >= 1)
{
unsigned int i;
for (i = 0; i < n_sysdep_segments; i++)
{
fwrite (null, roundup (offset, alignment) - offset, 1, output_file);
offset = roundup (offset, alignment);
fwrite (sysdep_segments[i].pointer, sysdep_segments[i].length, 1,
output_file);
fwrite (null, 1, 1, output_file);
offset += sysdep_segments[i].length + 1;
}
for (m = 0; m < 2; m++)
for (j = 0; j < n_sysdep_strings; j++)
{
struct pre_sysdep_message *msg = &sysdep_msg_arr[j];
struct pre_sysdep_string *pre = msg->str[m];
fwrite (null, roundup (offset, alignment) - offset, 1,
output_file);
offset = roundup (offset, alignment);
for (i = 0; i <= pre->segmentcount; i++)
{
fwrite (pre->segments[i].segptr, pre->segments[i].segsize, 1,
output_file);
offset += pre->segments[i].segsize;
}
if (m == M_ID && msg->id_plural_len > 0)
{
fwrite (msg->id_plural, msg->id_plural_len, 1, output_file);
offset += msg->id_plural_len;
}
free (pre);
}
}
freea (null);
for (j = 0; j < mlp->nitems; j++)
free (msgctid_arr[j]);
free (sysdep_msg_arr);
free (msg_arr);
free (msgctid_arr);
}
int
msgdomain_write_mo (message_list_ty *mlp,
const char *domain_name,
const char *file_name)
{
/* If no entry for this domain don't even create the file. */
if (mlp->nitems != 0)
{
/* 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)
{
FILE *output_file = stdout;
SET_BINARY (fileno (output_file));
write_table (output_file, mlp);
/* Make sure nothing went wrong. */
if (fwriteerror (output_file))
error (EXIT_FAILURE, errno, _("error while writing \"%s\" file"),
file_name);
}
else
{
/* Supersede, don't overwrite, the output file. Otherwise, processes
that are currently using (via mmap!) the output file could crash
(through SIGSEGV or SIGBUS). */
struct supersede_final_action action;
FILE *output_file =
fopen_supersede (file_name, "wb", true, true, &action);
if (output_file == NULL)
{
error (0, errno, _("error while opening \"%s\" for writing"),
file_name);
return 1;
}
write_table (output_file, mlp);
/* Make sure nothing went wrong. */
if (fwriteerror_supersede (output_file, &action))
error (EXIT_FAILURE, errno, _("error while writing \"%s\" file"),
file_name);
}
}
return 0;
}