u8-istream: Add cast to iconv() to suppress warnings on some systems.
[pspp-builds.git] / src / libpspp / u8-istream.c
1 /* PSPP - a program for statistical analysis.
2    Copyright (C) 2010, 2011 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
19 #include "u8-istream.h"
20
21 #include <assert.h>
22 #include <errno.h>
23 #include <fcntl.h>
24 #include <iconv.h>
25 #include <limits.h>
26 #include <stdint.h>
27 #include <stdio.h>
28 #include <stdlib.h>
29 #include <string.h>
30 #include <unistd.h>
31 #include <unistr.h>
32
33 #include "libpspp/assertion.h"
34 #include "libpspp/cast.h"
35 #include "libpspp/compiler.h"
36 #include "libpspp/encoding-guesser.h"
37
38 #include "gl/c-strcase.h"
39 #include "gl/localcharset.h"
40 #include "gl/minmax.h"
41
42 enum u8_istream_state
43   {
44     S_AUTO,                     /* Stream encoding not yet known. */
45     S_UTF8,                     /* Stream encoding is known to be UTF-8. */
46     S_CONVERT                   /* Stream encoding is known but not UTF-8. */
47   };
48
49 struct u8_istream
50   {
51     int fd;
52     iconv_t converter;
53     enum u8_istream_state state;
54
55     char *buffer;
56     char *head;
57     size_t length;
58
59     char outbuf[4];
60     size_t outlen;
61   };
62
63 static ssize_t fill_buffer (struct u8_istream *);
64
65 /* Opens FILENAME, which is encoded in FROMCODE, for reading as an UTF-8
66    stream, passing FLAGS to the open() function.  Returns a new u8_istream if
67    successful, otherwise returns NULL and sets errno to an appropriate value.
68
69    The accepted forms for FROMCODE are listed at the top of
70    encoding-guesser.h. */
71 struct u8_istream *
72 u8_istream_for_file (const char *fromcode, const char *filename, int flags)
73 {
74   struct u8_istream *is;
75   int fd;
76
77   assert (!(flags & O_CREAT));
78
79   fd = open (filename, flags);
80   if (fd < 0)
81     return NULL;
82
83   is = u8_istream_for_fd (fromcode, fd);
84   if (is == NULL)
85     {
86       int save_errno = errno;
87       close (fd);
88       errno = save_errno;
89     }
90
91   return is;
92 }
93
94 /* Creates and returns a new u8_istream that reads its input from FD.  Returns
95    a new u8_istream if successful, otherwise returns NULL and sets errno to an
96    appropriate value.
97
98    The accepted forms for FROMCODE are listed at the top of
99    encoding-guesser.h. */
100 struct u8_istream *
101 u8_istream_for_fd (const char *fromcode, int fd)
102 {
103   struct u8_istream *is;
104   const char *encoding;
105
106   is = malloc (sizeof *is);
107   if (is == NULL)
108     return NULL;
109
110   is->fd = fd;
111   is->converter = (iconv_t) -1;
112   is->buffer = malloc (U8_ISTREAM_BUFFER_SIZE);
113   if (is->buffer == NULL)
114     goto error;
115   is->head = is->buffer;
116   is->length = 0;
117   is->outlen = 0;
118
119   if (fill_buffer (is) < 0)
120     goto error;
121
122   encoding = encoding_guess_head_encoding (fromcode, is->buffer, is->length);
123   if (!strcmp (encoding, "UTF-8"))
124     is->state = S_UTF8;
125   else
126     {
127       if (encoding_guess_encoding_is_auto (fromcode)
128           && !strcmp (encoding, "ASCII"))
129         is->state = S_AUTO;
130       else
131         is->state = S_CONVERT;
132
133       is->converter = iconv_open ("UTF-8",
134                                   encoding_guess_parse_encoding (fromcode));
135       if (is->converter == (iconv_t) -1)
136         goto error;
137     }
138
139   return is;
140
141 error:
142   u8_istream_free (is);
143   return NULL;
144 }
145
146 /* Closes IS and its underlying file descriptor and frees all associated
147    resources.  Returns the return value from close(). */
148 int
149 u8_istream_close (struct u8_istream *is)
150 {
151   if (is != NULL)
152     {
153       int fd = is->fd;
154       u8_istream_free (is);
155       return close (fd);
156     }
157   return 0;
158 }
159
160 /* Frees IS and associated resources, but does not close the underlying file
161    descriptor.  (Thus, the client must close the file descriptor when it is no
162    longer needed.) */
163 void
164 u8_istream_free (struct u8_istream *is)
165 {
166   if (is != NULL)
167     {
168       if (is->converter != (iconv_t) -1)
169         iconv_close (is->converter);
170       free (is->buffer);
171       free (is);
172     }
173 }
174
175 static void
176 substitute_invalid_input_byte (struct u8_istream *is)
177 {
178   assert (is->outlen == 0);
179   is->head++;
180   is->length--;
181   is->outlen = u8_uctomb (CHAR_CAST (uint8_t *, is->outbuf),
182                           0xfffd, sizeof is->outbuf);
183 }
184
185 static ssize_t
186 fill_buffer (struct u8_istream *is)
187 {
188   ssize_t n;
189
190   /* Move any unused bytes to the beginning of the input buffer. */
191   if (is->length > 0 && is->buffer != is->head)
192     memmove (is->buffer, is->head, is->length);
193   is->head = is->buffer;
194
195   /* Read more input. */
196   n = read (is->fd, is->buffer + is->length,
197             U8_ISTREAM_BUFFER_SIZE - is->length);
198   if (n > 0)
199     is->length += n;
200   return n;
201 }
202
203 static ssize_t
204 read_auto (struct u8_istream *is, char *buffer, size_t size)
205 {
206   size_t original_size = size;
207   int retval = 0;
208
209   while (size > 0)
210     {
211       if (is->length > 0)
212         {
213           size_t n_ascii;
214
215           n_ascii = encoding_guess_count_ascii (is->head,
216                                                 MIN (is->length, size));
217
218           memcpy (buffer, is->head, n_ascii);
219           buffer += n_ascii;
220           size -= n_ascii;
221
222           is->head += n_ascii;
223           is->length -= n_ascii;
224
225           if (size == 0)
226             break;
227         }
228
229       if (is->length == 0)
230         {
231           retval = fill_buffer (is);
232           if (retval > 0)
233             continue;
234           else
235             break;
236         }
237
238       /* is->head points to a byte that isn't a printable ASCII character.
239          Fill up the buffer and check for UTF-8. */
240       fill_buffer (is);
241       is->state = (encoding_guess_tail_is_utf8 (is->head, is->length)
242                    ? S_UTF8 : S_CONVERT);
243       if (size == original_size)
244         return u8_istream_read (is, buffer, size);
245       break;
246     }
247
248   return original_size - size;
249 }
250
251 static int
252 convert_iconv (iconv_t converter,
253                char **inbufp, size_t *inbytesleft,
254                char **outbufp, size_t *outbytesleft)
255 {
256   size_t n = iconv (converter, (ICONV_CONST char **) inbufp, inbytesleft,
257                     outbufp, outbytesleft);
258   return n == SIZE_MAX ? errno : 0;
259 }
260
261 static int
262 convert_utf8 (iconv_t converter UNUSED,
263               char **inbufp, size_t *inbytesleft,
264               char **outbufp, size_t *outbytesleft)
265 {
266   const uint8_t *in = CHAR_CAST (const uint8_t *, *inbufp);
267   size_t n = MIN (*inbytesleft, *outbytesleft);
268   size_t ofs = 0;
269   int error;
270
271   for (;;)
272     {
273       ucs4_t uc;
274       int mblen;
275
276       if (ofs >= n)
277         {
278           error = ofs < *inbytesleft ? E2BIG : 0;
279           break;
280         }
281
282       mblen = u8_mbtouc (&uc, in + ofs, n - ofs);
283       if (uc == 0xfffd)
284         {
285           int retval = u8_mbtoucr (&uc, in + ofs, *inbytesleft - ofs);
286           if (retval == mblen)
287             {
288               /* There's an actual U+FFFD in the input stream.  Carry on. */
289             }
290           else
291             {
292               error = (retval == -1 ? EILSEQ
293                        : retval == -2 ? EINVAL
294                        : E2BIG);
295               break;
296             }
297         }
298
299       ofs += mblen;
300     }
301
302   if (ofs > 0)
303     {
304       memcpy (*outbufp, *inbufp, ofs);
305       *inbufp += ofs;
306       *inbytesleft -= ofs;
307       *outbufp += ofs;
308       *outbytesleft -= ofs;
309     }
310
311   return error;
312 }
313
314 static ssize_t
315 read_convert (struct u8_istream *is,
316               int (*convert) (iconv_t converter,
317                               char **inbufp, size_t *inbytesleft,
318                               char **outbufp, size_t *outbytesleft),
319               char *buffer, size_t size)
320 {
321   size_t original_size = size;
322
323   while (size > 0)
324     {
325       ssize_t n_read;
326
327       if (is->outlen > 0)
328         {
329           size_t n = MIN (size, is->outlen);
330
331           memcpy (buffer, is->outbuf, n);
332           is->outlen -= n;
333           if (is->outlen > 0)
334             memmove (is->outbuf, is->outbuf + n, is->outlen);
335
336           buffer += n;
337           size -= n;
338
339           if (size == 0)
340             break;
341         }
342
343       if (is->length)
344         {
345           int error = convert (is->converter,
346                                &is->head, &is->length,
347                                &buffer, &size);
348           if (size == 0)
349             break;
350
351           switch (error)
352             {
353             case 0:
354               /* Converted all of the input into output, possibly with space
355                  for output left over.
356
357                  Read more input. */
358               break;
359
360             case EILSEQ:
361               substitute_invalid_input_byte (is);
362               continue;
363
364             case EINVAL:
365               /* Incomplete byte sequence at end of input.  Read more
366                  input. */
367               break;
368
369             default:
370               /* A real error of some kind (ENOMEM?). */
371               return -1;
372
373             case E2BIG:
374               /* Ran out of room for output.
375                  Convert into outbuf and copy from there instead. */
376               {
377                 char *outptr = is->outbuf;
378                 size_t outleft = sizeof is->outbuf;
379
380                 error = convert (is->converter,
381                                  &is->head, &is->length,
382                                  &outptr, &outleft);
383                 is->outlen = outptr - is->outbuf;
384                 if (is->outlen > 0)
385                   continue;
386
387                 switch (error)
388                   {
389                   case EILSEQ:
390                     substitute_invalid_input_byte (is);
391                     continue;
392
393                   case E2BIG:
394                   case EINVAL:
395                     continue;
396
397                   default:
398                     /* A real error of some kind (ENOMEM?). */
399                     return -1;
400                   }
401               }
402             }
403         }
404
405       assert (is->length <= MB_LEN_MAX);
406       n_read = fill_buffer (is);
407       if (n_read <= 0)
408         {
409           if (original_size != size)
410             {
411               /* We produced some output so don't report EOF or error yet. */
412               break;
413             }
414           else if (n_read == 0 && is->length != 0)
415             {
416               /* Incomplete byte sequence at end of file. */
417               substitute_invalid_input_byte (is);
418             }
419           else
420             {
421               /* Propagate end-of-file or error to caller. */
422               return n_read;
423             }
424         }
425     }
426
427   return original_size - size;
428 }
429
430 /* Reads up to SIZE bytes of UTF-8 text from IS into BUFFER.  Returns the
431    number of bytes read if successful, 0 at end of file, or -1 if an error
432    occurred before any data could be read.  Upon error, sets errno to an
433    appropriate value. */
434 ssize_t
435 u8_istream_read (struct u8_istream *is, char *buffer, size_t size)
436 {
437   switch (is->state)
438     {
439     case S_CONVERT:
440       return read_convert (is, convert_iconv, buffer, size);
441
442     case S_AUTO:
443       return read_auto (is, buffer, size);
444
445     case S_UTF8:
446       return read_convert (is, convert_utf8, buffer, size);
447     }
448
449   NOT_REACHED ();
450 }
451
452 /* Returns the file descriptor underlying IS. */
453 int
454 u8_istream_fileno (const struct u8_istream *is)
455 {
456   return is->fd;
457 }
458 \f
459 /* Test functions.
460
461    These functions are probably useful only for white-box testing. */
462
463 /* Returns true if the encoding of the file being read by IS is not yet
464    known. */
465 bool
466 u8_istream_is_auto (const struct u8_istream *is)
467 {
468   return is->state == S_AUTO;
469 }
470
471 /* Returns true if the encoding of the file being read by IS has been
472    determined to be UTF-8. */
473 bool
474 u8_istream_is_utf8 (const struct u8_istream *is)
475 {
476   return is->state == S_UTF8;
477 }