370b2eb65df7e3d2c54000cdafc4fbeef6ee1c32
[lms] / lightmediascanner / src / lib / lightmediascanner_charset_conv.c
1 /**
2  * Copyright (C) 2007 by INdT
3  *
4  * This program is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public License
6  * as published by the Free Software Foundation; either version 2
7  * of the License, or (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 Lesser 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  * @author Gustavo Sverzut Barbieri <gustavo.barbieri@openbossa.org>
19  */
20
21 #include "lightmediascanner_charset_conv.h"
22 #include <iconv.h>
23 #include <stdio.h>
24 #include <stdlib.h>
25 #include <string.h>
26 #include <errno.h>
27 #include <ctype.h>
28
29 struct lms_charset_conv {
30     iconv_t check;
31     iconv_t fallback;
32     unsigned int size;
33     iconv_t *convs;
34     char **names;
35 };
36
37 /**
38  * Create a new charset conversion tool controlling its behavior.
39  *
40  * Conversion tool will try to convert provided strings to UTF-8, just need
41  * to register known charsets with lms_charset_conv_add() and then call
42  * lms_charset_conv().
43  *
44  * @return newly allocated conversion tool or NULL on error.
45  */
46 lms_charset_conv_t *
47 lms_charset_conv_new_full(int use_check, int use_fallback)
48 {
49     lms_charset_conv_t *lcc;
50
51     lcc = malloc(sizeof(*lcc));
52     if (!lcc) {
53         perror("malloc");
54         return NULL;
55     }
56
57     if (!use_check)
58         lcc->check = (iconv_t)-1;
59     else {
60         lcc->check = iconv_open("UTF-8", "UTF-8");
61         if (lcc->check == (iconv_t)-1) {
62             perror("ERROR: could not create conversion checker");
63             goto error_check;
64         }
65     }
66
67     if (!use_fallback)
68         lcc->fallback = (iconv_t)-1;
69     else {
70         lcc->fallback = iconv_open("UTF-8//IGNORE", "UTF-8");
71         if (lcc->fallback == (iconv_t)-1) {
72             perror("ERROR: could not create conversion fallback");
73             goto error_fallback;
74         }
75     }
76
77     lcc->size = 0;
78     lcc->convs = NULL;
79     lcc->names = NULL;
80     return lcc;
81
82   error_fallback:
83     if (lcc->check != (iconv_t)-1)
84         iconv_close(lcc->check);
85   error_check:
86     free(lcc);
87
88     return NULL;
89 }
90
91 /**
92  * Create a new charset conversion tool.
93  *
94  * Conversion tool will try to convert provided strings to UTF-8, just need
95  * to register known charsets with lms_charset_conv_add() and then call
96  * lms_charset_conv().
97  *
98  * @return newly allocated conversion tool or NULL on error.
99  */
100 lms_charset_conv_t *
101 lms_charset_conv_new(void)
102 {
103     return lms_charset_conv_new_full(1, 1);
104 }
105
106 /**
107  * Free existing charset conversion tool.
108  *
109  * @param lcc existing Light Media Scanner charset conversion.
110  */
111 void
112 lms_charset_conv_free(lms_charset_conv_t *lcc)
113 {
114     int i;
115
116     if (!lcc)
117         return;
118
119     if (lcc->check != (iconv_t)-1)
120         iconv_close(lcc->check);
121     if (lcc->fallback != (iconv_t)-1)
122         iconv_close(lcc->fallback);
123
124     for (i = 0; i < lcc->size; i++) {
125         iconv_close(lcc->convs[i]);
126         free(lcc->names[i]);
127     }
128
129     if (lcc->convs)
130         free(lcc->convs);
131     if (lcc->names)
132         free(lcc->names);
133     free(lcc);
134 }
135
136 /**
137  * Register new charset to conversion tool.
138  *
139  * @param lcc existing Light Media Scanner charset conversion.
140  * @param charset charset name as understood by iconv_open(3).
141  *
142  * @return On success 0 is returned.
143  */
144 int
145 lms_charset_conv_add(lms_charset_conv_t *lcc, const char *charset)
146 {
147     iconv_t cd, *convs;
148     char **names;
149     int idx, ns;
150
151     if (!lcc)
152         return -1;
153
154     if (!charset)
155         return -2;
156
157     cd = iconv_open("UTF-8", charset);
158     if (cd == (iconv_t)-1) {
159         fprintf(stderr, "ERROR: could not add conversion charset '%s': %s\n",
160                 charset, strerror(errno));
161         return -3;
162     }
163
164     idx = lcc->size;
165     ns = lcc->size + 1;
166
167     convs = realloc(lcc->convs, ns * sizeof(*convs));
168     if (!convs)
169         goto realloc_error;
170     lcc->convs = convs;
171     lcc->convs[idx] = cd;
172
173     names = realloc(lcc->names, ns * sizeof(*names));
174     if (!names)
175         goto realloc_error;
176     lcc->names = names;
177     lcc->names[idx] = strdup(charset);
178     if (!lcc->names[idx])
179         goto realloc_error;
180
181     lcc->size = ns;
182     return 0;
183
184   realloc_error:
185     perror("realloc");
186     iconv_close(cd);
187     return -4;
188 }
189
190 static int
191 _find(const lms_charset_conv_t *lcc, const char *charset)
192 {
193     int i;
194
195     for (i = 0; i < lcc->size; i++)
196         if (strcmp(lcc->names[i], charset) == 0)
197             return i;
198
199     return -1;
200 }
201
202 /**
203  * Forget about previously registered charset in conversion tool.
204  *
205  * @param lcc existing Light Media Scanner charset conversion.
206  * @param charset charset name.
207  *
208  * @return On success 0 is returned.
209  */
210 int
211 lms_charset_conv_del(lms_charset_conv_t *lcc, const char *charset)
212 {
213     iconv_t *convs;
214     char **names;
215     int idx;
216
217     if (!lcc)
218         return -1;
219
220     if (!charset)
221         return -2;
222
223     idx = _find(lcc, charset);
224     if (idx < 0) {
225         fprintf(stderr, "ERROR: could not find charset '%s'\n", charset);
226         return -3;
227     }
228
229     iconv_close(lcc->convs[idx]);
230     free(lcc->names[idx]);
231
232     lcc->size--;
233     for (; idx < lcc->size; idx++) {
234         lcc->convs[idx] = lcc->convs[idx + 1];
235         lcc->names[idx] = lcc->names[idx + 1];
236     }
237
238     convs = realloc(lcc->convs, lcc->size * sizeof(*convs));
239     if (convs)
240         lcc->convs = convs;
241     else
242         perror("could not realloc 'convs'");
243
244     names = realloc(lcc->names, lcc->size * sizeof(*names));
245     if (names)
246         lcc->names = names;
247     else
248         perror("could not realloc 'names'");
249
250     return 0;
251 }
252
253 static int
254 _check(lms_charset_conv_t *lcc, const char *istr, unsigned int ilen, char *ostr, unsigned int olen)
255 {
256     char *inbuf, *outbuf;
257     size_t r, inlen, outlen;
258
259     if (lcc->check == (iconv_t)-1)
260         return -1;
261
262     inbuf = (char *)istr;
263     inlen = ilen;
264     outbuf = ostr;
265     outlen = olen;
266
267     iconv(lcc->check, NULL, NULL, NULL, NULL);
268     r = iconv(lcc->check, &inbuf, &inlen, &outbuf, &outlen);
269     if (r == (size_t)-1)
270         return -1;
271     else
272         return 0;
273 }
274
275 static int
276 _conv(iconv_t cd, char **p_str, unsigned int *p_len, char *ostr, unsigned int olen)
277 {
278     char *inbuf, *outbuf;
279     size_t r, inlen, outlen;
280
281     inbuf = *p_str;
282     inlen = *p_len;
283     outbuf = ostr;
284     outlen = olen;
285
286     iconv(cd, NULL, NULL, NULL, NULL);
287     r = iconv(cd, &inbuf, &inlen, &outbuf, &outlen);
288     if (r == (size_t)-1)
289         return -1;
290
291     *p_len = olen - outlen;
292     free(*p_str);
293     *p_str = ostr;
294
295     outbuf = realloc(*p_str, *p_len + 1);
296     if (!outbuf)
297         perror("realloc");
298     else
299         *p_str = outbuf;
300
301     (*p_str)[*p_len] = '\0';
302
303     return 0;
304 }
305
306 static void
307 _fix_non_ascii(char *s, int len)
308 {
309     for (; len > 0; len--, s++)
310         if (!isprint(*s))
311             *s = '?';
312 }
313
314 /**
315  * If required, do charset conversion to UTF-8.
316  *
317  * @param lcc existing Light Media Scanner charset conversion.
318  * @param p_str string to be converted.
319  * @param p_len string size.
320  *
321  * @note the check for string being already UTF-8 is not reliable,
322  *       some cases might show false positives (UTF-16 is considered UTF-8).
323  * @see lms_charset_conv_check()
324  *
325  * @return On success 0 is returned.
326  */
327 int
328 lms_charset_conv(lms_charset_conv_t *lcc, char **p_str, unsigned int *p_len)
329 {
330     char *outstr;
331     int i, outlen;
332
333     if (!lcc)
334         return -1;
335     if (!p_str)
336         return -2;
337     if (!p_len)
338         return -3;
339     if (!*p_str || !*p_len)
340         return 0;
341
342     outlen = 2 * *p_len;
343     outstr = malloc(outlen + 1);
344     if (!outstr) {
345         perror("malloc");
346         return -4;
347     }
348
349     if (_check(lcc, *p_str, *p_len, outstr, outlen) == 0) {
350         free(outstr);
351         return 0;
352     }
353
354     for (i = 0; i < lcc->size; i++)
355         if (_conv(lcc->convs[i], p_str, p_len, outstr, outlen) == 0)
356             return 0;
357
358     if (lcc->fallback == (iconv_t)-1)
359         return -5;
360
361     fprintf(stderr,
362             "WARNING: could not convert '%*s' to any charset, use fallback\n",
363             *p_len, *p_str);
364     i = _conv(lcc->fallback, p_str, p_len, outstr, outlen);
365     if (i < 0) {
366         _fix_non_ascii(*p_str, *p_len);
367         free(outstr);
368     }
369     return i;
370 }
371
372 /**
373  * Forcefully do charset conversion to UTF-8.
374  *
375  * @param lcc existing Light Media Scanner charset conversion.
376  * @param p_str string to be converted.
377  * @param p_len string size.
378  *
379  * @note This function does not check for the string being in UTF-8 before
380  *       doing the conversion, use it if you are sure about the charset.
381  *       In this case you'll usually have just one charset added.
382  *
383  * @return On success 0 is returned.
384  */
385 int
386 lms_charset_conv_force(lms_charset_conv_t *lcc, char **p_str, unsigned int *p_len)
387 {
388     char *outstr;
389     int i, outlen;
390
391     if (!lcc)
392         return -1;
393     if (!p_str)
394         return -2;
395     if (!p_len)
396         return -3;
397     if (!*p_str || !*p_len)
398         return 0;
399
400     outlen = 2 * *p_len;
401     outstr = malloc(outlen + 1);
402     if (!outstr) {
403         perror("malloc");
404         return -4;
405     }
406
407     for (i = 0; i < lcc->size; i++)
408         if (_conv(lcc->convs[i], p_str, p_len, outstr, outlen) == 0)
409             return 0;
410
411     if (lcc->fallback == (iconv_t)-1)
412         return -5;
413
414     fprintf(stderr,
415             "WARNING: could not convert '%*s' to any charset, use fallback\n",
416             *p_len, *p_str);
417     i = _conv(lcc->fallback, p_str, p_len, outstr, outlen);
418     if (i < 0) {
419         _fix_non_ascii(*p_str, *p_len);
420         free(outstr);
421     }
422     return i;
423 }
424
425 /**
426  * Check if strings is not UTF-8 and conversion is required.
427  *
428  * @param lcc existing Light Media Scanner charset conversion.
429  * @param str string to be analysed.
430  * @param len string size.
431  *
432  * @note current implementation is not reliable, it tries to convert from
433  *       UTF-8 to UTF-8. Some cases, like ISO-8859-1 will work, but some like
434  *       UTF-16 to UTF-8 will say it's already in the correct charset,
435  *       even if it's not.
436  *
437  * @return 0 if string is already UTF-8.
438  */
439 int
440 lms_charset_conv_check(lms_charset_conv_t *lcc, const char *str, unsigned int len)
441 {
442     char *outstr;
443     int r, outlen;
444
445     if (!lcc)
446         return -1;
447     if (!str || !len)
448         return 0;
449
450     outlen = 2 * len;
451     outstr = malloc(outlen);
452     if (!outstr) {
453         perror("malloc");
454         return -2;
455     }
456
457     r = _check(lcc, str, len, outstr, outlen);
458     free(outstr);
459     return r;
460 }