#include charset.h
[flac.git] / src / share / iconvert.c
1 /*
2  * Copyright (C) 2001 Edmund Grimley Evans <edmundo@rano.org>
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 2 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, write to the Free Software
16  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17  */
18
19 #ifdef HAVE_ICONV
20
21 #include <assert.h>
22 #include <errno.h>
23 #include <iconv.h>
24 #include <stdlib.h>
25 #include <string.h>
26
27 /*
28  * Convert data from one encoding to another. Return:
29  *
30  *  -2 : memory allocation failed
31  *  -1 : unknown encoding
32  *   0 : data was converted exactly
33  *   1 : data was converted inexactly
34  *   2 : data was invalid (but still converted)
35  *
36  * We convert in two steps, via UTF-8, as this is the only
37  * reliable way of distinguishing between invalid input
38  * and valid input which iconv refuses to transliterate.
39  * We convert from UTF-8 twice, because we have no way of
40  * knowing whether the conversion was exact if iconv returns
41  * E2BIG (due to a bug in the specification of iconv).
42  * An alternative approach is to assume that the output of
43  * iconv is never more than 4 times as long as the input,
44  * but I prefer to avoid that assumption if possible.
45  */
46
47 int iconvert(const char *fromcode, const char *tocode,
48              const char *from, size_t fromlen,
49              char **to, size_t *tolen)
50 {
51   int ret = 0;
52   iconv_t cd1, cd2;
53   char *ib;
54   char *ob;
55   char *utfbuf, *outbuf, *newbuf;
56   size_t utflen, outlen, ibl, obl, k;
57   char tbuf[2048];
58
59   cd1 = iconv_open("UTF-8", fromcode);
60   if (cd1 == (iconv_t)(-1))
61     return -1;
62
63   cd2 = (iconv_t)(-1);
64   /* Don't use strcasecmp() as it's locale-dependent. */
65   if (!strchr("Uu", tocode[0]) ||
66       !strchr("Tt", tocode[1]) ||
67       !strchr("Ff", tocode[2]) ||
68       tocode[3] != '-' ||
69       tocode[4] != '8' ||
70       tocode[5] != '\0') {
71     char *tocode1;
72
73     /*
74      * Try using this non-standard feature of glibc and libiconv.
75      * This is deliberately not a config option as people often
76      * change their iconv library without rebuilding applications.
77      */
78     tocode1 = (char *)malloc(strlen(tocode) + 11);
79     if (!tocode1)
80       goto fail;
81
82     strcpy(tocode1, tocode);
83     strcat(tocode1, "//TRANSLIT");
84     cd2 = iconv_open(tocode1, "UTF-8");
85     free(tocode1);
86
87     if (cd2 == (iconv_t)(-1))
88       cd2 = iconv_open(tocode, fromcode);
89
90     if (cd2 == (iconv_t)(-1)) {
91       iconv_close(cd1);
92       return -1;
93     }
94   }
95
96   utflen = 1; /*fromlen * 2 + 1; XXX */
97   utfbuf = (char *)malloc(utflen);
98   if (!utfbuf)
99     goto fail;
100
101   /* Convert to UTF-8 */
102   ib = (char *)from;
103   ibl = fromlen;
104   ob = utfbuf;
105   obl = utflen;
106   for (;;) {
107     k = iconv(cd1, &ib, &ibl, &ob, &obl);
108     assert((!k && !ibl) ||
109            (k == (size_t)(-1) && errno == E2BIG && ibl && obl < 6) ||
110            (k == (size_t)(-1) &&
111             (errno == EILSEQ || errno == EINVAL) && ibl));
112     if (!ibl)
113       break;
114     if (obl < 6) {
115       /* Enlarge the buffer */
116       utflen *= 2;
117       newbuf = (char *)realloc(utfbuf, utflen);
118       if (!newbuf)
119         goto fail;
120       ob = (ob - utfbuf) + newbuf;
121       obl = utflen - (ob - newbuf);
122       utfbuf = newbuf;
123     }
124     else {
125       /* Invalid input */
126       ib++, ibl--;
127       *ob++ = '#', obl--;
128       ret = 2;
129       iconv(cd1, 0, 0, 0, 0);
130     }
131   }
132
133   if (cd2 == (iconv_t)(-1)) {
134     /* The target encoding was UTF-8 */
135     if (tolen)
136       *tolen = ob - utfbuf;
137     if (!to) {
138       free(utfbuf);
139       iconv_close(cd1);
140       return ret;
141     }
142     newbuf = (char *)realloc(utfbuf, (ob - utfbuf) + 1);
143     if (!newbuf)
144       goto fail;
145     ob = (ob - utfbuf) + newbuf;
146     *ob = '\0';
147     *to = newbuf;
148     iconv_close(cd1);
149     return ret;
150   }
151
152   /* Truncate the buffer to be tidy */
153   utflen = ob - utfbuf;
154   newbuf = (char *)realloc(utfbuf, utflen);
155   if (!newbuf)
156     goto fail;
157   utfbuf = newbuf;
158
159   /* Convert from UTF-8 to discover how long the output is */
160   outlen = 0;
161   ib = utfbuf;
162   ibl = utflen;
163   while (ibl) {
164     ob = tbuf;
165     obl = sizeof(tbuf);
166     k = iconv(cd2, &ib, &ibl, &ob, &obl);
167     assert((k != (size_t)(-1) && !ibl) ||
168            (k == (size_t)(-1) && errno == E2BIG && ibl) ||
169            (k == (size_t)(-1) && errno == EILSEQ && ibl));
170     if (ibl && !(k == (size_t)(-1) && errno == E2BIG)) {
171       /* Replace one character */
172       char *tb = "?";
173       size_t tbl = 1;
174
175       outlen += ob - tbuf;
176       ob = tbuf;
177       obl = sizeof(tbuf);
178       k = iconv(cd2, &tb, &tbl, &ob, &obl);
179       assert((!k && !tbl) ||
180              (k == (size_t)(-1) && errno == EILSEQ && tbl));
181       for (++ib, --ibl; ibl && (*ib & 0x80); ib++, ibl--)
182         ;
183     }
184     outlen += ob - tbuf;
185   }
186   ob = tbuf;
187   obl = sizeof(tbuf);
188   k = iconv(cd2, 0, 0, &ob, &obl);
189   assert(!k);
190   outlen += ob - tbuf;
191
192   /* Convert from UTF-8 for real */
193   outbuf = (char *)malloc(outlen + 1);
194   if (!outbuf)
195     goto fail;
196   ib = utfbuf;
197   ibl = utflen;
198   ob = outbuf;
199   obl = outlen;
200   while (ibl) {
201     k = iconv(cd2, &ib, &ibl, &ob, &obl);
202     assert((k != (size_t)(-1) && !ibl) ||
203            (k == (size_t)(-1) && errno == EILSEQ && ibl));
204     if (k && !ret)
205       ret = 1;
206     if (ibl && !(k == (size_t)(-1) && errno == E2BIG)) {
207       /* Replace one character */
208       char *tb = "?";
209       size_t tbl = 1;
210
211       k = iconv(cd2, &tb, &tbl, &ob, &obl);
212       assert((!k && !tbl) ||
213              (k == (size_t)(-1) && errno == EILSEQ && tbl));
214       for (++ib, --ibl; ibl && (*ib & 0x80); ib++, ibl--)
215         ;
216     }
217   }
218   k = iconv(cd2, 0, 0, &ob, &obl);
219   assert(!k);
220   assert(!obl);
221   *ob = '\0';
222
223   free(utfbuf);
224   iconv_close(cd1);
225   iconv_close(cd2);
226   if (tolen)
227     *tolen = outlen;
228   if (!to) {
229     free(outbuf);
230     return ret;
231   }
232   *to = outbuf;
233   return ret;
234
235  fail:
236   free(utfbuf);
237   iconv_close(cd1);
238   if (cd2 != (iconv_t)(-1))
239     iconv_close(cd2);
240   return -2;
241 }
242
243 #endif /* HAVE_ICONV */