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