source: branches/1.1.x/src/org/armedbear/lisp/util/RandomAccessCharacterFile.java

Last change on this file was 13088, checked in by Mark Evenson, 14 years ago

Fix algorithim error in writing byte sequences via RandomAccessCharacterFile?.

Found and fixed by David Kirkman.

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