source: trunk/abcl/src/org/armedbear/lisp/util/RandomAccessCharacterFile.java

Last change on this file was 15387, checked in by Mark Evenson, 4 years ago

Reset file charset decoder before conversion

Using FILE-POSITION to rewind streams that had advanced past the EOF
was throwing internal Java exceptions.

Fixes <https://abcl.org/trac/ticket/473>.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
File size: 20.7 KB
Line 
1/*
2 * RandomAccessCharacterFile.java
3 *
4 * Copyright (C) 2008 Hideo at Yokohama
5 * Copyright (C) 2008-2009 Erik Huelsmann
6 * $Id: RandomAccessCharacterFile.java 15387 2020-10-01 22:29:11Z mevenson $
7 *
8 * This program is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU General Public License
10 * as published by the Free Software Foundation; either version 2
11 * of the License, or (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, .
21 *
22 * As a special exception, the copyright holders of this library give you
23 * permission to link this library with independent modules to produce an
24 * executable, regardless of the license terms of these independent
25 * modules, and to copy and distribute the resulting executable under
26 * terms of your choice, provided that you also meet, for each linked
27 * independent module, the terms and conditions of the license of that
28 * module.  An independent module is a module which is not derived from
29 * or based on this library.  If you modify this library, you may extend
30 * this exception to your version of the library, but you are not
31 * obligated to do so.  If you do not wish to do so, delete this
32 * exception statement from your version.
33 */
34
35package org.armedbear.lisp.util;
36
37import java.io.IOException;
38import java.io.PushbackInputStream;
39import java.io.OutputStream;
40import java.io.RandomAccessFile;
41import java.io.PushbackReader;
42import java.io.Reader;
43import java.io.StringReader;
44import java.io.Writer;
45import java.nio.ByteBuffer;
46import java.nio.CharBuffer;
47import java.nio.channels.FileChannel;
48import java.nio.charset.Charset;
49import java.nio.charset.CharsetDecoder;
50import java.nio.charset.CharsetEncoder;
51import java.nio.charset.CoderMalfunctionError;
52import java.nio.charset.CoderResult;
53import java.nio.charset.CodingErrorAction;
54import java.nio.charset.UnsupportedCharsetException;
55
56import org.armedbear.lisp.Debug;
57
58import static org.armedbear.lisp.Lisp.error;
59
60import org.armedbear.lisp.SimpleError;
61import org.armedbear.lisp.SimpleString;
62
63public class RandomAccessCharacterFile {
64
65    private class RandomAccessInputStream extends PushbackInputStream {
66
67        public RandomAccessInputStream() {
68            super(null);
69        }
70
71        private byte[] read_buf = new byte[1];
72
73        @Override
74        public final int read() throws IOException {
75            int len = read(read_buf);
76            if (len == 1) {
77                // byte is signed, char is unsigned, int is signed.
78                // buf can hold 0xff, we want it as 0xff in int, not -1.
79                return 0xff & (int) read_buf[0];
80            } else {
81                return -1;
82            }
83            // ### BUG: 'int read()' is to return a *codepoint*,
84            // not the half of a surrogate pair!
85        }
86
87        @Override
88        public final int read(byte[] b, int off, int len) throws IOException {
89            return RandomAccessCharacterFile.this.read(b, off, len);
90        }
91
92        @Override
93        public final void unread(int b) throws IOException {
94            RandomAccessCharacterFile.this.unreadByte((byte)b);
95        }
96
97        @Override
98        public final void unread(byte[] b, int off, int len) throws IOException {
99            for (int i = 0; i < len; i++)
100                this.unread(b[off+i]);
101        }
102
103        @Override
104        public final void unread(byte[] b) throws IOException {
105            this.unread(b, 0, b.length);
106        }
107
108        @Override
109        public final int available() throws IOException {
110            return (int)(RandomAccessCharacterFile.this.length()
111                            - RandomAccessCharacterFile.this.position());
112        }
113
114        @Override
115        public final synchronized void mark(int readlimit) {
116        }
117
118        @Override
119        public final boolean markSupported() {
120            return false;
121        }
122
123        @Override
124        public final synchronized void reset() throws IOException {
125            throw new IOException("Operation not supported");
126        }
127
128        @Override
129        public final long skip(long n) throws IOException {
130            RandomAccessCharacterFile.this.position(RandomAccessCharacterFile.this.position()+n);
131            return n;
132        }
133
134        @Override
135        public final int read(byte[] b) throws IOException {
136            return this.read(b, 0, b.length);
137        }
138
139        @Override
140        public final void close() throws IOException {
141            RandomAccessCharacterFile.this.close();
142        }
143    }
144
145    private class RandomAccessOutputStream extends OutputStream {
146
147        RandomAccessOutputStream() {
148        }
149
150        private byte[] buf = new byte[1];
151        public final void write(int b) throws IOException {
152            buf[0] = (byte)b;
153            RandomAccessCharacterFile.this.write(buf, 0, 1);
154        }
155
156        @Override
157        public final void write(byte[] b) throws IOException {
158            RandomAccessCharacterFile.this.write(b, 0, b.length);
159        }
160
161        @Override
162        public final void write(byte[] b, int off, int len) throws IOException {
163            RandomAccessCharacterFile.this.write(b, off, len);
164        }
165
166        @Override
167        public final void flush() throws IOException {
168            RandomAccessCharacterFile.this.flush();
169        }
170
171        @Override
172        public final void close() throws IOException {
173            RandomAccessCharacterFile.this.close();
174        }
175    }
176
177    // dummy reader which we need to call the Pushback constructor
178    // because a null value won't work
179    static Reader staticReader = new StringReader("");
180
181    private class RandomAccessReader extends PushbackReader {
182
183        RandomAccessReader() {
184                // because we override all methods of Pushbackreader,
185                // staticReader will never be referenced
186                super(staticReader);
187        }
188
189        @Override
190        public final void close() throws IOException {
191            RandomAccessCharacterFile.this.close();
192        }
193
194        private char[] read_buf = new char[1];
195
196        @Override
197        public final int read() throws IOException {
198            int n = this.read(read_buf);
199
200            if (n == 1)
201                return read_buf[0];
202            else
203                return -1;
204            // ### BUG: 'int read()' is to return a codepoint!
205            // not the half of a surrogate pair!
206        }
207
208        @Override
209        public final void unread(int c) throws IOException {
210            RandomAccessCharacterFile.this.unreadChar((char)c);
211        }
212
213        @Override
214        public final void unread(char[] cbuf, int off, int len) throws IOException {
215            for (int i = 0; i < len; i++)
216                this.unread(cbuf[off+i]);
217        }
218
219        @Override
220        public final void unread(char[] cbuf) throws IOException {
221            this.unread(cbuf, 0, cbuf.length);
222        }
223
224        @Override
225        public final int read(CharBuffer target) throws IOException {
226            //FIXME: to be implemented
227            throw new IOException("Not implemented");
228        }
229
230        @Override
231        public final int read(char[] cbuf) throws IOException {
232            return RandomAccessCharacterFile.this.read(cbuf, 0, cbuf.length);
233        }
234
235        @Override
236        public final int read(char[] cb, int off, int len) throws IOException {
237            return RandomAccessCharacterFile.this.read(cb, off, len);
238        }
239
240        @Override
241        public final boolean ready() throws IOException {
242            return true;
243        }
244    }
245
246    private class RandomAccessWriter extends Writer {
247
248        RandomAccessWriter() {
249        }
250
251        public final void close() throws IOException {
252            RandomAccessCharacterFile.this.close();
253        }
254
255        public final void flush() throws IOException {
256            RandomAccessCharacterFile.this.flush();
257        }
258
259        @Override
260        public final void write(char[] cb, int off, int len) throws IOException {
261            RandomAccessCharacterFile.this.write(cb, off, len);
262        }
263
264    }
265
266
267    final static int BUFSIZ = 4*1024; // setting this to a small value like 8 is helpful for testing.
268
269    private RandomAccessWriter writer;
270    private RandomAccessReader reader;
271    private RandomAccessInputStream inputStream;
272    private RandomAccessOutputStream outputStream;
273    private FileChannel fcn;
274
275    private Charset cset;
276    private CharsetEncoder cenc;
277    private CharsetDecoder cdec;
278
279    /**
280     * bbuf is treated as a cache of the file content.
281     * If it points to somewhere in the middle of the file, it holds the copy of the file content,
282     * even when you are writing a large chunk of data.  If you write in the middle of a file,
283     * bbuf first gets filled with contents of the data, and only after that any new data is
284     * written on bbuf.
285     * The exception is when you are appending data at the end of the file.
286     */
287    private ByteBuffer bbuf;
288    private boolean bbufIsDirty; /* whether bbuf holds data that must be written. */
289    private boolean bbufIsReadable; /* whether bbuf.remaining() contains readable content. */
290    private long bbufpos; /* where the beginning of bbuf is pointing in the file now. */
291
292    public RandomAccessCharacterFile(RandomAccessFile raf, String encoding) throws IOException {
293
294        fcn = raf.getChannel();
295
296        setEncoding(encoding);
297        bbuf = ByteBuffer.allocate(BUFSIZ);
298
299        // there is no readable data available in the buffers.
300        ((java.nio.Buffer)bbuf).flip();
301
302        // there is no write pending data in the buffers.
303        bbufIsDirty = false;
304
305        bbufIsReadable = true;
306
307        bbufpos = fcn.position();
308
309        reader = new RandomAccessReader();
310        writer = new RandomAccessWriter();
311        inputStream = new RandomAccessInputStream();
312        outputStream = new RandomAccessOutputStream();
313    }
314
315    public void setEncoding(String encoding) {
316      if (encoding == null) {
317        cset = Charset.defaultCharset();
318      } else {
319        try {
320          cset = Charset.forName(encoding);
321        } catch (UnsupportedCharsetException e) {
322          error(new SimpleError("Undefined encoding: " + encoding));
323        }
324      }
325      cdec = cset.newDecoder();
326      cdec.onMalformedInput(CodingErrorAction.REPLACE);
327      cdec.onUnmappableCharacter(CodingErrorAction.REPLACE);
328      cenc = cset.newEncoder();
329    }
330
331    public Writer getWriter() {
332        return writer;
333    }
334
335    public PushbackReader getReader() {
336        return reader;
337    }
338
339    public PushbackInputStream getInputStream() {
340        return inputStream;
341    }
342
343    public OutputStream getOutputStream() {
344        return outputStream;
345    }
346
347    public final void close() throws IOException {
348        internalFlush(true);
349        fcn.close();
350    }
351
352    public final void flush() throws IOException {
353        internalFlush(false);
354    }
355
356    private final boolean ensureReadBbuf(boolean force) throws IOException {
357        boolean bufReady = true;
358
359        if ((bbuf.remaining() == 0) || force || ! bbufIsReadable) {
360            // need to read from the file.
361
362            if (bbufIsDirty) {
363                flushBbuf(false);
364                ((java.nio.Buffer)bbuf).clear();
365                bbufIsReadable = false;
366            } else {
367                int bbufEnd = bbufIsReadable
368                  ? ((java.nio.Buffer)bbuf).limit()
369                  : ((java.nio.Buffer)bbuf).position();
370                fcn.position(bbufpos + bbufEnd);
371                bbufpos += ((java.nio.Buffer)bbuf).position();
372                if (bbufIsReadable) {
373                  bbuf.compact();
374                  bbufIsReadable = false;
375                } else //must discard the junk bytes after ((java.nio.Buffer)bbuf).position()
376                  ((java.nio.Buffer)bbuf).clear();
377            }
378
379            bufReady = (fcn.read(bbuf) != -1);
380            ((java.nio.Buffer)bbuf).flip();
381            bbufIsReadable = true;
382        }
383
384        return bufReady;
385    }
386
387
388    final int read(char[] cb, int off, int len) throws IOException {
389        CharBuffer cbuf = CharBuffer.wrap(cb, off, len);
390        cdec.reset();
391        boolean decodeWasUnderflow = false;
392        boolean atEof = false;
393        while ((cbuf.remaining() > 0) && ! atEof) {
394            int oldRemaining = cbuf.remaining();
395            atEof = ! ensureReadBbuf(decodeWasUnderflow);
396            CoderResult r;
397            try {
398              r = cdec.decode(bbuf, cbuf, atEof );
399            } catch (IllegalStateException e) {
400              throw new IOException("CharsetDecoder failed", e);
401            } catch (CoderMalfunctionError e) {
402              throw new IOException("CharsetDecoder malfunction", e);
403            }
404             
405            if (oldRemaining == cbuf.remaining()
406                && CoderResult.OVERFLOW == r) {
407                // if this happens, the decoding failed
408                // but the bufs didn't advance. Advance
409                // them manually and do manual replacing,
410                // otherwise we loop endlessly. This occurs
411                // at least when parsing latin1 files with
412                // lowercase o-umlauts in them
413                // Note that this is at the moment copy-paste
414                // with DecodingReader.read()
415                cbuf.put('?');
416                bbuf.get();
417            }
418            decodeWasUnderflow = (CoderResult.UNDERFLOW == r);
419        }
420        if (cbuf.remaining() == len) {
421            return -1;
422        } else {
423            return len - cbuf.remaining();
424        }
425    }
426
427    final void write(char[] cb, int off, int len) throws IOException {
428        CharBuffer cbuf = CharBuffer.wrap(cb, off, len);
429        encodeAndWrite(cbuf, false, false);
430    }
431
432    private final void internalFlush(boolean endOfFile) throws IOException {
433        if (endOfFile) {
434            CharBuffer cbuf = CharBuffer.allocate(0);
435            encodeAndWrite(cbuf, true, endOfFile);
436        } else {
437            flushBbuf(false);
438        }
439    }
440
441    private final void encodeAndWrite(CharBuffer cbuf, boolean flush,
442                                      boolean endOfFile) throws IOException {
443        while (cbuf.remaining() > 0) {
444            CoderResult r = cenc.encode(cbuf, bbuf, endOfFile);
445            bbufIsDirty = true;
446            if (CoderResult.OVERFLOW == r || bbuf.remaining() == 0) {
447                flushBbuf(false);
448                ((java.nio.Buffer)bbuf).clear();
449                bbufIsReadable = false;
450            }
451            if (r.isUnmappable()) {
452                throw new RACFUnmappableCharacterException(((java.nio.Buffer)cbuf).position(),
453                                                           cbuf.charAt(((java.nio.Buffer)cbuf).position()),
454                                                           cset.name());
455            }
456            if (r.isMalformed()) {
457                // We don't really expect Malformed, but not handling it
458                // will cause an infinite loop if we don't...
459                throw new RACFMalformedInputException(((java.nio.Buffer)cbuf).position(),
460                                                      cbuf.charAt(((java.nio.Buffer)cbuf).position()),
461                                                      cset.name());
462            }
463            // UNDERFLOW is the normal condition where cbuf runs out
464            // before bbuf is filled.
465        }
466        if (((java.nio.Buffer)bbuf).position() > 0 && bbufIsDirty && flush) {
467            flushBbuf(false);
468        }
469    }
470
471    public final void position(long newPosition) throws IOException {
472        flushBbuf(true);
473        long bbufend = bbufpos // in case bbuf is readable, its contents is valid
474            + (bbufIsReadable
475         ? ((java.nio.Buffer)bbuf).limit()
476         : ((java.nio.Buffer)bbuf).position()); // beyond position()
477        if (newPosition >= bbufpos && newPosition < bbufend) {
478            // near seek. within existing data of bbuf.
479            if (!bbufIsReadable) { //rewinding. keep tail buffered.
480              ((java.nio.Buffer)bbuf).limit(((java.nio.Buffer)bbuf).position());
481              bbufIsReadable = true;
482            }
483            ((java.nio.Buffer)bbuf).position((int)(newPosition - bbufpos));
484        } else {
485            fcn.position(newPosition);
486            // far seek; discard the buffer (it's already cleared)
487            ((java.nio.Buffer)bbuf).clear();
488            ((java.nio.Buffer)bbuf).flip(); // "there is no useful data on this buffer yet."
489            bbufIsReadable = true;
490            bbufpos = newPosition;
491        }
492    }
493
494    public final long position() throws IOException {
495        return bbufpos + ((java.nio.Buffer)bbuf).position(); // the logical position within the file.
496    }
497
498    public final long length() throws IOException {
499        flushBbuf(true);
500        return fcn.size();
501    }
502
503    final void flushBbuf(boolean commitOnly) throws IOException {
504        if (commitOnly && !bbufIsDirty)
505            return;
506        //otherwise, we do at least need to increase bbufpos
507
508        fcn.position(bbufpos);
509
510        // if the buffer is dirty, the modifications have to be
511        // before position(): before re-positioning, this.position()
512        // calls this function.
513        if (commitOnly) {
514            ByteBuffer dup = bbuf.duplicate();
515            ((java.nio.Buffer)dup).flip();
516            fcn.write(dup);
517            //ideally, should restore fcn.position(). but don't for performance.
518//            fcn.position(fcn.position()-dup.position());
519            bbufIsDirty = false; //this fixed stas's bug, but not mine.
520            return;
521        }
522       
523        if (bbufIsDirty) {
524          ((java.nio.Buffer)bbuf).flip();
525          fcn.write(bbuf);
526        }
527
528        bbufpos += ((java.nio.Buffer)bbuf).position();
529        ((java.nio.Buffer)bbuf).clear();
530        ((java.nio.Buffer)bbuf).flip(); // there's no useable data in this buffer
531        bbufIsDirty = false;
532        bbufIsReadable = true;
533    }
534
535    public final int read(byte[] b, int off, int len) throws IOException {
536        int pos = off;
537        boolean atEof = false;
538        while (pos - off < len && ! atEof) {
539
540            atEof = ! ensureReadBbuf(false);
541            int want = Math.min(off + len - pos, bbuf.remaining());
542            bbuf.get(b, pos, want);
543            pos += want;
544        }
545        return pos - off;
546    }
547
548    // a method corresponding to the good ol' ungetc in C.
549    // This function may fail when using (combined) character codes that use
550    // escape sequences to switch between sub-codes.
551    // ASCII, ISO-8859 series, any 8bit code are OK, all unicode variations are OK,
552    // but applications of the ISO-2022 encoding framework can have trouble.
553    // Example of such code is ISO-2022-JP which is used in Japanese e-mail.
554    private CharBuffer singleCharBuf;
555    private ByteBuffer shortByteBuf;
556    public final void unreadChar(char c) throws IOException {
557        // algorithm :
558        //  1. encode c into bytes, to find out how many bytes it corresponds to
559        //  2. move the position backwards that many bytes.
560        //  ** we stop here.  Don't bother to write the bytes to the buffer,
561        //     assuming that it is the same as the original data.
562        //     If we allow to write back different characters, the buffer must get 'dirty'
563        //     but that would require read/write permissions on files you use unreadChar,
564        //     even if you are just reading for some tokenizer.
565        //
566        //  So we don't do the following.
567        //  3. write the bytes.
568        //  4. move the position back again.
569        if (singleCharBuf == null) {
570            singleCharBuf = CharBuffer.allocate(1);
571            shortByteBuf = ByteBuffer.allocate((int)cenc.maxBytesPerChar());
572        }
573        ((java.nio.Buffer)singleCharBuf).clear();
574        singleCharBuf.append(c);
575        ((java.nio.Buffer)singleCharBuf).flip();
576        ((java.nio.Buffer)shortByteBuf).clear();
577        cenc.encode(singleCharBuf, shortByteBuf, false);
578        int n = ((java.nio.Buffer)shortByteBuf).position();
579        long pos = position() - n;
580        position(pos);
581    }
582
583    public final void unreadByte(byte b) throws IOException {
584        long pos = position() - 1;
585        position(pos);
586    }
587
588    final void write(byte[] b, int off, int len) throws IOException {
589        int pos = off;
590        while (pos < off + len) {
591            if (bbuf.remaining() == 0) {
592                flushBbuf(false);
593                ((java.nio.Buffer)bbuf).clear();
594                bbufIsReadable = false;
595            }
596            int thisBatchLen = Math.min(off + len - pos, bbuf.remaining());
597            bbuf.put(b, pos, thisBatchLen);
598            pos += thisBatchLen;
599            bbufIsDirty = true;
600        }
601    }
602}
Note: See TracBrowser for help on using the repository browser.