Tolerate the inability to convert character encodings
[pspp-builds.git] / src / libpspp / i18n.c
1 /* PSPP - a program for statistical analysis.
2    Copyright (C) 2006, 2009 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 <http://www.gnu.org/licenses/>. */
16
17 #include <config.h>
18 #include <xalloc.h>
19 #include <assert.h>
20 #include <locale.h>
21 #include <stdlib.h>
22 #include <stdio.h>
23 #include <string.h>
24 #include <libintl.h>
25 #include <iconv.h>
26 #include <errno.h>
27 #include "assertion.h"
28 #include "hmapx.h"
29 #include "hash-functions.h"
30
31 #include "i18n.h"
32
33 #include "version.h"
34
35 #include <localcharset.h>
36 #include "xstrndup.h"
37
38 #if HAVE_NL_LANGINFO
39 #include <langinfo.h>
40 #endif
41
42 struct converter
43   {
44     const char *tocode;
45     const char *fromcode;
46     iconv_t conv;
47   };
48
49 static char *default_encoding;
50 static struct hmapx map;
51
52 /* A wrapper around iconv_open */
53 static iconv_t
54 create_iconv (const char* tocode, const char* fromcode)
55 {
56   size_t hash;
57   struct hmapx_node *node;
58   struct converter *converter;
59
60   hash = hash_string (tocode, hash_string (fromcode, 0));
61   HMAPX_FOR_EACH_WITH_HASH (converter, node, hash, &map)
62     if (!strcmp (tocode, converter->tocode)
63         && !strcmp (fromcode, converter->fromcode))
64       return converter->conv;
65
66   converter = xmalloc (sizeof *converter);
67   converter->tocode = xstrdup (tocode);
68   converter->fromcode = xstrdup (fromcode);
69   converter->conv = iconv_open (tocode, fromcode);
70   hmapx_insert (&map, converter, hash);
71   
72   /* I don't think it's safe to translate this string or to use messaging
73      as the convertors have not yet been set up */
74   if ( (iconv_t) -1 == converter->conv && 0 != strcmp (tocode, fromcode))
75     {
76       const int err = errno;
77       fprintf (stderr,
78                "Warning: "
79                "cannot create a convertor for \"%s\" to \"%s\": %s\n",
80                fromcode, tocode, strerror (err));
81     }
82
83   return converter->conv;
84 }
85
86 /* Return a string based on TEXT converted according to HOW.
87    If length is not -1, then it must be the number of bytes in TEXT.
88    The returned string must be freed when no longer required.
89 */
90 char *
91 recode_string (const char *to, const char *from,
92                const char *text, int length)
93 {
94   char *outbuf = 0;
95   size_t outbufferlength;
96   size_t result;
97   char *op ;
98   size_t inbytes = 0;
99   size_t outbytes ;
100   iconv_t conv ;
101
102   /* FIXME: Need to ensure that this char is valid in the target encoding */
103   const char fallbackchar = '?';
104
105   if ( text == NULL )
106     return NULL;
107
108   if ( length == -1 )
109      length = strlen(text);
110
111
112   if (to == NULL)
113     to = default_encoding;
114
115   if (from == NULL)
116     from = default_encoding;
117
118   for ( outbufferlength = 1 ; outbufferlength != 0; outbufferlength <<= 1 )
119     if ( outbufferlength > length)
120       break;
121
122   outbuf = xmalloc(outbufferlength);
123   op = outbuf;
124
125   outbytes = outbufferlength;
126   inbytes = length;
127
128
129   conv = create_iconv (to, from);
130
131   if ( (iconv_t) -1 == conv )
132         return xstrdup (text);
133
134   do {
135     const char *ip = text;
136     result = iconv (conv, (ICONV_CONST char **) &text, &inbytes,
137                    &op, &outbytes);
138
139     if ( -1 == result )
140       {
141         int the_error = errno;
142
143         switch (the_error)
144           {
145           case EILSEQ:
146           case EINVAL:
147             if ( outbytes > 0 )
148               {
149                 *op++ = fallbackchar;
150                 outbytes--;
151                 text++;
152                 inbytes--;
153                 break;
154               }
155             /* Fall through */
156           case E2BIG:
157             free (outbuf);
158             outbufferlength <<= 1;
159             outbuf = xmalloc (outbufferlength);
160             op = outbuf;
161             outbytes = outbufferlength;
162             inbytes = length;
163             text = ip;
164             break;
165           default:
166             /* should never happen */
167             fprintf (stderr, "Character conversion error: %s\n", strerror (the_error));
168             NOT_REACHED ();
169             break;
170           }
171       }
172   } while ( -1 == result );
173
174   if (outbytes == 0 )
175     {
176       char *const oldaddr = outbuf;
177       outbuf = xrealloc (outbuf, outbufferlength + 1);
178
179       op += (outbuf - oldaddr) ;
180     }
181
182   *op = '\0';
183
184   return outbuf;
185 }
186
187
188 void
189 i18n_init (void)
190 {
191 #if ENABLE_NLS
192   setlocale (LC_CTYPE, "");
193 #if HAVE_LC_MESSAGES
194   setlocale (LC_MESSAGES, "");
195 #endif
196 #if HAVE_LC_PAPER
197   setlocale (LC_PAPER, "");
198 #endif
199   bindtextdomain (PACKAGE, locale_dir);
200   textdomain (PACKAGE);
201 #endif /* ENABLE_NLS */
202
203   assert (default_encoding == NULL);
204   default_encoding = xstrdup (locale_charset ());
205
206   hmapx_init (&map);
207 }
208
209
210 const char *
211 get_default_encoding (void)
212 {
213   return default_encoding;
214 }
215
216 void
217 set_default_encoding (const char *enc)
218 {
219   free (default_encoding);
220   default_encoding = xstrdup (enc);
221 }
222
223
224 /* Attempts to set the encoding from a locale name
225    returns true if successfull.
226    This function does not (should not!) alter the current locale.
227 */
228 bool
229 set_encoding_from_locale (const char *loc)
230 {
231   bool ok = true;
232   char *c_encoding;
233   char *loc_encoding;
234   char *tmp = xstrdup (setlocale (LC_CTYPE, NULL));
235
236   setlocale (LC_CTYPE, "C");
237   c_encoding = xstrdup (locale_charset ());
238
239   setlocale (LC_CTYPE, loc);
240   loc_encoding = xstrdup (locale_charset ());
241
242
243   if ( 0 == strcmp (loc_encoding, c_encoding))
244     {
245       ok = false;
246     }
247
248
249   setlocale (LC_CTYPE, tmp);
250
251   free (tmp);
252
253   if (ok)
254     {
255       free (default_encoding);
256       default_encoding = loc_encoding;
257     }
258   else
259     free (loc_encoding);
260
261   free (c_encoding);
262
263   return ok;
264 }
265
266 void
267 i18n_done (void)
268 {
269   struct hmapx_node *node;
270   struct converter *cvtr;
271   HMAPX_FOR_EACH (cvtr, node, &map)
272     {
273       iconv_close (cvtr->conv);
274       free (cvtr);
275     }
276
277   hmapx_destroy (&map);
278
279   free (default_encoding);
280   default_encoding = NULL;
281 }
282
283
284
285 bool
286 valid_encoding (const char *enc)
287 {
288   iconv_t conv = iconv_open ("UTF8", enc);
289
290   if ( conv == (iconv_t) -1)
291     return false;
292
293   iconv_close (conv);
294
295   return true;
296 }
297
298
299 /* Return the system local's idea of the
300    decimal seperator character */
301 char
302 get_system_decimal (void)
303 {
304   char radix_char;
305
306   char *ol = xstrdup (setlocale (LC_NUMERIC, NULL));
307   setlocale (LC_NUMERIC, "");
308
309 #if HAVE_NL_LANGINFO
310   radix_char = nl_langinfo (RADIXCHAR)[0];
311 #else
312   {
313     char buf[10];
314     snprintf (buf, sizeof buf, "%f", 2.5);
315     radix_char = buf[1];
316   }
317 #endif
318
319   /* We MUST leave LC_NUMERIC untouched, since it would
320      otherwise interfere with data_{in,out} */
321   setlocale (LC_NUMERIC, ol);
322   free (ol);
323   return radix_char;
324 }
325