blob: 460ab8cd0fa9299557dbdee4cf4eef3a1157a728 [file] [log] [blame]
/* Writing C# satellite assemblies.
Copyright (C) 2003-2010, 2016, 2018-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
#include <alloca.h>
/* Specification. */
#include "write-csharp.h"
#include <errno.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#if !defined S_ISDIR && defined S_IFDIR
# define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR)
#endif
#if !S_IRUSR && S_IREAD
# define S_IRUSR S_IREAD
#endif
#if !S_IRUSR
# define S_IRUSR 00400
#endif
#if !S_IWUSR && S_IWRITE
# define S_IWUSR S_IWRITE
#endif
#if !S_IWUSR
# define S_IWUSR 00200
#endif
#if !S_IXUSR && S_IEXEC
# define S_IXUSR S_IEXEC
#endif
#if !S_IXUSR
# define S_IXUSR 00100
#endif
#if !S_IRGRP
# define S_IRGRP (S_IRUSR >> 3)
#endif
#if !S_IWGRP
# define S_IWGRP (S_IWUSR >> 3)
#endif
#if !S_IXGRP
# define S_IXGRP (S_IXUSR >> 3)
#endif
#if !S_IROTH
# define S_IROTH (S_IRUSR >> 6)
#endif
#if !S_IWOTH
# define S_IWOTH (S_IWUSR >> 6)
#endif
#if !S_IXOTH
# define S_IXOTH (S_IXUSR >> 6)
#endif
#include "attribute.h"
#include "c-ctype.h"
#include "relocatable.h"
#include "error.h"
#include "xerror.h"
#include "csharpcomp.h"
#include "message.h"
#include "msgfmt.h"
#include "msgl-iconv.h"
#include "msgl-header.h"
#include "plural-exp.h"
#include "po-charset.h"
#include "xalloc.h"
#include "xmalloca.h"
#include "concat-filename.h"
#include "fwriteerror.h"
#include "clean-temp.h"
#include "unistr.h"
#include "gettext.h"
#define _(str) gettext (str)
/* Convert a resource name to a class name.
Return a nonempty string consisting of alphanumerics and underscores
and starting with a letter or underscore. */
static char *
construct_class_name (const char *resource_name)
{
/* This code must be kept consistent with intl.cs, function
GettextResourceManager.ConstructClassName. */
/* We could just return an arbitrary fixed class name, like "Messages",
assuming that every assembly will only ever contain one
GettextResourceSet subclass, but this assumption would break the day
we want to support multi-domain PO files in the same format... */
bool valid;
const char *p;
/* Test for a valid ASCII identifier:
- nonempty,
- first character is A..Za..z_ - see x-csharp.c:is_identifier_start.
- next characters are A..Za..z_0..9 - see x-csharp.c:is_identifier_part.
*/
valid = (resource_name[0] != '\0');
for (p = resource_name; valid && *p != '\0'; p++)
{
char c = *p;
if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c == '_')
|| (p > resource_name && c >= '0' && c <= '9')))
valid = false;
}
if (valid)
return xstrdup (resource_name);
else
{
static const char hexdigit[] = "0123456789abcdef";
const char *str = resource_name;
const char *str_limit = str + strlen (str);
char *class_name = XNMALLOC (12 + 6 * (str_limit - str) + 1, char);
char *b;
b = class_name;
memcpy (b, "__UESCAPED__", 12); b += 12;
while (str < str_limit)
{
ucs4_t uc;
str += u8_mbtouc (&uc, (const unsigned char *) str, str_limit - str);
if (uc >= 0x10000)
{
*b++ = '_';
*b++ = 'U';
*b++ = hexdigit[(uc >> 28) & 0x0f];
*b++ = hexdigit[(uc >> 24) & 0x0f];
*b++ = hexdigit[(uc >> 20) & 0x0f];
*b++ = hexdigit[(uc >> 16) & 0x0f];
*b++ = hexdigit[(uc >> 12) & 0x0f];
*b++ = hexdigit[(uc >> 8) & 0x0f];
*b++ = hexdigit[(uc >> 4) & 0x0f];
*b++ = hexdigit[uc & 0x0f];
}
else if (!((uc >= 'A' && uc <= 'Z') || (uc >= 'a' && uc <= 'z')
|| (uc >= '0' && uc <= '9')))
{
*b++ = '_';
*b++ = 'u';
*b++ = hexdigit[(uc >> 12) & 0x0f];
*b++ = hexdigit[(uc >> 8) & 0x0f];
*b++ = hexdigit[(uc >> 4) & 0x0f];
*b++ = hexdigit[uc & 0x0f];
}
else
*b++ = uc;
}
*b++ = '\0';
return (char *) xrealloc (class_name, b - class_name);
}
}
/* Write a string in C# Unicode notation to the given stream. */
static void
write_csharp_string (FILE *stream, const char *str)
{
static const char hexdigit[] = "0123456789abcdef";
const char *str_limit = str + strlen (str);
fprintf (stream, "\"");
while (str < str_limit)
{
ucs4_t uc;
str += u8_mbtouc (&uc, (const unsigned char *) str, str_limit - str);
if (uc == 0x0000)
fprintf (stream, "\\0");
else if (uc == 0x0007)
fprintf (stream, "\\a");
else if (uc == 0x0008)
fprintf (stream, "\\b");
else if (uc == 0x0009)
fprintf (stream, "\\t");
else if (uc == 0x000a)
fprintf (stream, "\\n");
else if (uc == 0x000b)
fprintf (stream, "\\v");
else if (uc == 0x000c)
fprintf (stream, "\\f");
else if (uc == 0x000d)
fprintf (stream, "\\r");
else if (uc == 0x0022)
fprintf (stream, "\\\"");
else if (uc == 0x005c)
fprintf (stream, "\\\\");
else if (uc >= 0x0020 && uc < 0x007f)
fprintf (stream, "%c", (int) uc);
else if (uc < 0x10000)
fprintf (stream, "\\u%c%c%c%c",
hexdigit[(uc >> 12) & 0x0f], hexdigit[(uc >> 8) & 0x0f],
hexdigit[(uc >> 4) & 0x0f], hexdigit[uc & 0x0f]);
else
fprintf (stream, "\\U%c%c%c%c%c%c%c%c",
hexdigit[(uc >> 28) & 0x0f], hexdigit[(uc >> 24) & 0x0f],
hexdigit[(uc >> 20) & 0x0f], hexdigit[(uc >> 16) & 0x0f],
hexdigit[(uc >> 12) & 0x0f], hexdigit[(uc >> 8) & 0x0f],
hexdigit[(uc >> 4) & 0x0f], hexdigit[uc & 0x0f]);
}
fprintf (stream, "\"");
}
/* Write a (msgctxt, msgid) pair as a string in C# Unicode notation to the
given stream. */
static void
write_csharp_msgid (FILE *stream, message_ty *mp)
{
const char *msgctxt = mp->msgctxt;
const char *msgid = mp->msgid;
if (msgctxt == NULL)
write_csharp_string (stream, msgid);
else
{
size_t msgctxt_len = strlen (msgctxt);
size_t msgid_len = strlen (msgid);
size_t combined_len = msgctxt_len + 1 + msgid_len;
char *combined;
combined = (char *) xmalloca (combined_len + 1);
memcpy (combined, msgctxt, msgctxt_len);
combined[msgctxt_len] = MSGCTXT_SEPARATOR;
memcpy (combined + msgctxt_len + 1, msgid, msgid_len + 1);
write_csharp_string (stream, combined);
freea (combined);
}
}
/* Write C# code that returns the value for a message. If the message
has plural forms, it is an expression of type System.String[], otherwise it
is an expression of type System.String. */
static void
write_csharp_msgstr (FILE *stream, message_ty *mp)
{
if (mp->msgid_plural != NULL)
{
bool first;
const char *p;
fprintf (stream, "new System.String[] { ");
for (p = mp->msgstr, first = true;
p < mp->msgstr + mp->msgstr_len;
p += strlen (p) + 1, first = false)
{
if (!first)
fprintf (stream, ", ");
write_csharp_string (stream, p);
}
fprintf (stream, " }");
}
else
{
if (mp->msgstr_len != strlen (mp->msgstr) + 1)
abort ();
write_csharp_string (stream, mp->msgstr);
}
}
/* Tests whether a plural expression, evaluated according to the C rules,
can only produce the values 0 and 1. */
static bool
is_expression_boolean (struct expression *exp)
{
switch (exp->operation)
{
case var:
case mult:
case divide:
case module:
case plus:
case minus:
return false;
case lnot:
case less_than:
case greater_than:
case less_or_equal:
case greater_or_equal:
case equal:
case not_equal:
case land:
case lor:
return true;
case num:
return (exp->val.num == 0 || exp->val.num == 1);
case qmop:
return is_expression_boolean (exp->val.args[1])
&& is_expression_boolean (exp->val.args[2]);
default:
abort ();
}
}
/* Write C# code that evaluates a plural expression according to the C rules.
The variable is called 'n'. */
static void
write_csharp_expression (FILE *stream, const struct expression *exp, bool as_boolean)
{
/* We use parentheses everywhere. This frees us from tracking the priority
of arithmetic operators. */
if (as_boolean)
{
/* Emit a C# expression of type 'bool'. */
switch (exp->operation)
{
case num:
fprintf (stream, "%s", exp->val.num ? "true" : "false");
return;
case lnot:
fprintf (stream, "(!");
write_csharp_expression (stream, exp->val.args[0], true);
fprintf (stream, ")");
return;
case less_than:
fprintf (stream, "(");
write_csharp_expression (stream, exp->val.args[0], false);
fprintf (stream, " < ");
write_csharp_expression (stream, exp->val.args[1], false);
fprintf (stream, ")");
return;
case greater_than:
fprintf (stream, "(");
write_csharp_expression (stream, exp->val.args[0], false);
fprintf (stream, " > ");
write_csharp_expression (stream, exp->val.args[1], false);
fprintf (stream, ")");
return;
case less_or_equal:
fprintf (stream, "(");
write_csharp_expression (stream, exp->val.args[0], false);
fprintf (stream, " <= ");
write_csharp_expression (stream, exp->val.args[1], false);
fprintf (stream, ")");
return;
case greater_or_equal:
fprintf (stream, "(");
write_csharp_expression (stream, exp->val.args[0], false);
fprintf (stream, " >= ");
write_csharp_expression (stream, exp->val.args[1], false);
fprintf (stream, ")");
return;
case equal:
fprintf (stream, "(");
write_csharp_expression (stream, exp->val.args[0], false);
fprintf (stream, " == ");
write_csharp_expression (stream, exp->val.args[1], false);
fprintf (stream, ")");
return;
case not_equal:
fprintf (stream, "(");
write_csharp_expression (stream, exp->val.args[0], false);
fprintf (stream, " != ");
write_csharp_expression (stream, exp->val.args[1], false);
fprintf (stream, ")");
return;
case land:
fprintf (stream, "(");
write_csharp_expression (stream, exp->val.args[0], true);
fprintf (stream, " && ");
write_csharp_expression (stream, exp->val.args[1], true);
fprintf (stream, ")");
return;
case lor:
fprintf (stream, "(");
write_csharp_expression (stream, exp->val.args[0], true);
fprintf (stream, " || ");
write_csharp_expression (stream, exp->val.args[1], true);
fprintf (stream, ")");
return;
case qmop:
if (is_expression_boolean (exp->val.args[1])
&& is_expression_boolean (exp->val.args[2]))
{
fprintf (stream, "(");
write_csharp_expression (stream, exp->val.args[0], true);
fprintf (stream, " ? ");
write_csharp_expression (stream, exp->val.args[1], true);
fprintf (stream, " : ");
write_csharp_expression (stream, exp->val.args[2], true);
fprintf (stream, ")");
return;
}
FALLTHROUGH;
case var:
case mult:
case divide:
case module:
case plus:
case minus:
fprintf (stream, "(");
write_csharp_expression (stream, exp, false);
fprintf (stream, " != 0)");
return;
default:
abort ();
}
}
else
{
/* Emit a C# expression of type 'long'. */
switch (exp->operation)
{
case var:
fprintf (stream, "n");
return;
case num:
fprintf (stream, "%lu", exp->val.num);
return;
case mult:
fprintf (stream, "(");
write_csharp_expression (stream, exp->val.args[0], false);
fprintf (stream, " * ");
write_csharp_expression (stream, exp->val.args[1], false);
fprintf (stream, ")");
return;
case divide:
fprintf (stream, "(");
write_csharp_expression (stream, exp->val.args[0], false);
fprintf (stream, " / ");
write_csharp_expression (stream, exp->val.args[1], false);
fprintf (stream, ")");
return;
case module:
fprintf (stream, "(");
write_csharp_expression (stream, exp->val.args[0], false);
fprintf (stream, " %% ");
write_csharp_expression (stream, exp->val.args[1], false);
fprintf (stream, ")");
return;
case plus:
fprintf (stream, "(");
write_csharp_expression (stream, exp->val.args[0], false);
fprintf (stream, " + ");
write_csharp_expression (stream, exp->val.args[1], false);
fprintf (stream, ")");
return;
case minus:
fprintf (stream, "(");
write_csharp_expression (stream, exp->val.args[0], false);
fprintf (stream, " - ");
write_csharp_expression (stream, exp->val.args[1], false);
fprintf (stream, ")");
return;
case qmop:
fprintf (stream, "(");
write_csharp_expression (stream, exp->val.args[0], true);
fprintf (stream, " ? ");
write_csharp_expression (stream, exp->val.args[1], false);
fprintf (stream, " : ");
write_csharp_expression (stream, exp->val.args[2], false);
fprintf (stream, ")");
return;
case lnot:
case less_than:
case greater_than:
case less_or_equal:
case greater_or_equal:
case equal:
case not_equal:
case land:
case lor:
fprintf (stream, "(");
write_csharp_expression (stream, exp, true);
fprintf (stream, " ? 1 : 0)");
return;
default:
abort ();
}
}
}
/* Write the C# code for the GettextResourceSet subclass to the given stream.
Note that we use fully qualified class names and no "using" statements,
because applications can have their own classes called X.Y.Hashtable or
X.Y.String. */
static void
write_csharp_code (FILE *stream, const char *culture_name, const char *class_name, message_list_ty *mlp)
{
const char *last_dot;
const char *class_name_last_part;
unsigned int plurals;
size_t j;
fprintf (stream,
"/* Automatically generated by GNU msgfmt. Do not modify! */\n");
/* We chose to use a "using" statement here, to avoid a bug in the pnet-0.6.0
compiler. */
fprintf (stream, "using GNU.Gettext;\n");
/* Assign a strong name to the assembly, so that two different localizations
of the same domain can be loaded one after the other. This strong name
tells the Global Assembly Cache that they are meant to be different. */
fprintf (stream, "[assembly: System.Reflection.AssemblyCulture(");
write_csharp_string (stream, culture_name);
fprintf (stream, ")]\n");
last_dot = strrchr (class_name, '.');
if (last_dot != NULL)
{
fprintf (stream, "namespace ");
fwrite (class_name, 1, last_dot - class_name, stream);
fprintf (stream, " {\n");
class_name_last_part = last_dot + 1;
}
else
class_name_last_part = class_name;
fprintf (stream, "public class %s : GettextResourceSet {\n",
class_name_last_part);
/* Determine whether there are plural messages. */
plurals = 0;
for (j = 0; j < mlp->nitems; j++)
if (mlp->item[j]->msgid_plural != NULL)
plurals++;
/* Emit the constructor. */
fprintf (stream, " public %s ()\n", class_name_last_part);
fprintf (stream, " : base () {\n");
fprintf (stream, " }\n");
/* Emit the TableInitialized field. */
fprintf (stream, " private bool TableInitialized;\n");
/* Emit the ReadResources method. */
fprintf (stream, " protected override void ReadResources () {\n");
/* In some implementations, such as mono < 2009-02-27, the ReadResources
method is called just once, when Table == null. In other implementations,
such as mono >= 2009-02-27, it is called at every GetObject call, and it
is responsible for doing the initialization only once, even when called
simultaneously from multiple threads. */
fprintf (stream, " if (!TableInitialized) {\n");
fprintf (stream, " lock (this) {\n");
fprintf (stream, " if (!TableInitialized) {\n");
/* In some implementations, the ResourceSet constructor initializes Table
before calling ReadResources(). In other implementations, the
ReadResources() method is expected to initialize the Table. */
fprintf (stream, " if (Table == null)\n");
fprintf (stream, " Table = new System.Collections.Hashtable();\n");
fprintf (stream, " System.Collections.Hashtable t = Table;\n");
for (j = 0; j < mlp->nitems; j++)
{
fprintf (stream, " t.Add(");
write_csharp_msgid (stream, mlp->item[j]);
fprintf (stream, ",");
write_csharp_msgstr (stream, mlp->item[j]);
fprintf (stream, ");\n");
}
fprintf (stream, " TableInitialized = true;\n");
fprintf (stream, " }\n");
fprintf (stream, " }\n");
fprintf (stream, " }\n");
fprintf (stream, " }\n");
/* Emit the msgid_plural strings. Only used by msgunfmt. */
if (plurals)
{
fprintf (stream, " public static System.Collections.Hashtable GetMsgidPluralTable () {\n");
fprintf (stream, " System.Collections.Hashtable t = new System.Collections.Hashtable();\n");
for (j = 0; j < mlp->nitems; j++)
if (mlp->item[j]->msgid_plural != NULL)
{
fprintf (stream, " t.Add(");
write_csharp_msgid (stream, mlp->item[j]);
fprintf (stream, ",");
write_csharp_string (stream, mlp->item[j]->msgid_plural);
fprintf (stream, ");\n");
}
fprintf (stream, " return t;\n");
fprintf (stream, " }\n");
}
/* Emit the PluralEval function. It is a subroutine for GetPluralString. */
if (plurals)
{
message_ty *header_entry;
const struct expression *plural;
unsigned long int nplurals;
header_entry = message_list_search (mlp, NULL, "");
extract_plural_expression (header_entry ? header_entry->msgstr : NULL,
&plural, &nplurals);
fprintf (stream, " protected override long PluralEval (long n) {\n");
fprintf (stream, " return ");
write_csharp_expression (stream, plural, false);
fprintf (stream, ";\n");
fprintf (stream, " }\n");
}
/* Terminate the class. */
fprintf (stream, "}\n");
if (last_dot != NULL)
/* Terminate the namespace. */
fprintf (stream, "}\n");
}
int
msgdomain_write_csharp (message_list_ty *mlp, const char *canon_encoding,
const char *resource_name, const char *locale_name,
const char *directory)
{
int retval;
struct temp_dir *tmpdir;
char *culture_name;
char *output_file;
char *class_name;
char *csharp_file_name;
FILE *csharp_file;
const char *gettextlibdir;
const char *csharp_sources[1];
const char *libdirs[1];
const char *libraries[1];
/* If no entry for this resource/domain, don't even create the file. */
if (mlp->nitems == 0)
return 0;
retval = 1;
/* Convert the messages to Unicode. */
iconv_message_list (mlp, canon_encoding, po_charset_utf8, NULL);
/* Support for "reproducible builds": Delete information that may vary
between builds in the same conditions. */
message_list_delete_header_field (mlp, "POT-Creation-Date:");
/* Create a temporary directory where we can put the C# file.
A simple temporary file would also be possible but would require us to
define our own variant of mkstemp(): On one hand the functions mktemp(),
tmpnam(), tempnam() present a security risk, and on the other hand the
function mkstemp() doesn't allow to specify a fixed suffix of the file.
It is simpler to create a temporary directory. */
tmpdir = create_temp_dir ("msg", NULL, false);
if (tmpdir == NULL)
goto quit1;
/* Assign a default value to the resource name. */
if (resource_name == NULL)
resource_name = "Messages";
/* Convert the locale name to a .NET specific culture name. */
culture_name = xstrdup (locale_name);
{
char *p;
for (p = culture_name; *p != '\0'; p++)
if (*p == '_')
*p = '-';
if (strncmp (culture_name, "sr-CS", 5) == 0)
memcpy (culture_name, "sr-SP", 5);
p = strchr (culture_name, '@');
if (p != NULL)
{
if (strcmp (p, "@latin") == 0)
strcpy (p, "-Latn");
else if (strcmp (p, "@cyrillic") == 0)
strcpy (p, "-Cyrl");
}
if (strcmp (culture_name, "sr-SP") == 0)
{
free (culture_name);
culture_name = xstrdup ("sr-SP-Latn");
}
else if (strcmp (culture_name, "uz-UZ") == 0)
{
free (culture_name);
culture_name = xstrdup ("uz-UZ-Latn");
}
}
/* Compute the output file name. This code must be kept consistent with
intl.cs, function GetSatelliteAssembly(). */
{
char *output_dir = xconcatenated_filename (directory, culture_name, NULL);
struct stat statbuf;
/* Try to create the output directory if it does not yet exist. */
if (stat (output_dir, &statbuf) < 0 && errno == ENOENT)
if (mkdir (output_dir, S_IRUSR | S_IWUSR | S_IXUSR
| S_IRGRP | S_IWGRP | S_IXGRP
| S_IROTH | S_IWOTH | S_IXOTH) < 0)
{
error (0, errno, _("failed to create directory \"%s\""), output_dir);
free (output_dir);
goto quit2;
}
output_file =
xconcatenated_filename (output_dir, resource_name, ".resources.dll");
free (output_dir);
}
/* Compute the class name. This code must be kept consistent with intl.cs,
function InstantiateResourceSet(). */
{
char *class_name_part1 = construct_class_name (resource_name);
char *p;
class_name =
XNMALLOC (strlen (class_name_part1) + 1 + strlen (culture_name) + 1, char);
sprintf (class_name, "%s_%s", class_name_part1, culture_name);
for (p = class_name + strlen (class_name_part1) + 1; *p != '\0'; p++)
if (*p == '-')
*p = '_';
free (class_name_part1);
}
/* Compute the temporary C# file name. It must end in ".cs", so that
the C# compiler recognizes that it is C# source code. */
csharp_file_name =
xconcatenated_filename (tmpdir->dir_name, "resset.cs", NULL);
/* Create the C# file. */
register_temp_file (tmpdir, csharp_file_name);
csharp_file = fopen_temp (csharp_file_name, "w", false);
if (csharp_file == NULL)
{
error (0, errno, _("failed to create \"%s\""), csharp_file_name);
unregister_temp_file (tmpdir, csharp_file_name);
goto quit3;
}
write_csharp_code (csharp_file, culture_name, class_name, mlp);
if (fwriteerror_temp (csharp_file))
{
error (0, errno, _("error while writing \"%s\" file"), csharp_file_name);
goto quit3;
}
/* Make it possible to override the .dll location. This is
necessary for running the testsuite before "make install". */
gettextlibdir = getenv ("GETTEXTCSHARPLIBDIR");
if (gettextlibdir == NULL || gettextlibdir[0] == '\0')
gettextlibdir = relocate (LIBDIR);
/* Compile the C# file to a .dll file. */
csharp_sources[0] = csharp_file_name;
libdirs[0] = gettextlibdir;
libraries[0] = "GNU.Gettext";
if (compile_csharp_class (csharp_sources, 1, libdirs, 1, libraries, 1,
output_file, true, false, verbose > 0))
{
if (!verbose)
error (0, 0, _("compilation of C# class failed, please try --verbose"));
else
error (0, 0, _("compilation of C# class failed"));
goto quit3;
}
retval = 0;
quit3:
free (csharp_file_name);
free (class_name);
free (output_file);
quit2:
free (culture_name);
cleanup_temp_dir (tmpdir);
quit1:
return retval;
}