>From cb667c106187443ff2d00bace14f0ee0686fe2fd Mon Sep 17 00:00:00 2001
From: Olof-Joachim Frahm <olof@macrolet.net>
Date: Fri, 13 Nov 2015 17:59:32 +0100
Subject: [PATCH 5/5] Support `FILE-POSITION` on string streams.

Adds a custom, seekable writer to be able to go back in the written
output for `STRING-OUTPUT-STREAM` - the input case is slightly less
complex.
---
 build.xml                                          |   1 +
 src/org/armedbear/lisp/SeekableStringWriter.java   | 140 +++++++++++++++++++++
 src/org/armedbear/lisp/StringInputStream.java      |  43 ++++++-
 src/org/armedbear/lisp/StringOutputStream.java     |  35 +++++-
 test/lisp/abcl/misc-tests.lisp                     |  11 +-
 .../armedbear/lisp/SeekableStringWriterTest.java   |  19 +++
 6 files changed, 242 insertions(+), 7 deletions(-)
 create mode 100644 src/org/armedbear/lisp/SeekableStringWriter.java
 create mode 100644 test/src/org/armedbear/lisp/SeekableStringWriterTest.java

diff --git a/build.xml b/build.xml
index 060ba99..0559941 100644
--- a/build.xml
+++ b/build.xml
@@ -946,6 +946,7 @@ ${basedir}/../cl-bench
             classname="org.junit.runner.JUnitCore">
         <arg value="org.armedbear.lisp.PathnameTest"/>
         <arg value="org.armedbear.lisp.StreamTest"/>
+        <arg value="org.armedbear.lisp.SeekableStringWriterTest"/>
         <arg value="org.armedbear.lisp.UtilitiesTest"/>
 		<!-- currently hangs(!) the running process
         <arg value="org.armedbear.lisp.util.HttpHeadTest"/>
diff --git a/src/org/armedbear/lisp/SeekableStringWriter.java b/src/org/armedbear/lisp/SeekableStringWriter.java
new file mode 100644
index 0000000..271c7c1
--- /dev/null
+++ b/src/org/armedbear/lisp/SeekableStringWriter.java
@@ -0,0 +1,140 @@
+/*
+ * SeekableStringWriter.java
+ *
+ * Copyright (C) 2016 Olof-Joachim Frahm
+ * $Id$
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ *
+ * As a special exception, the copyright holders of this library give you
+ * permission to link this library with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on this library.  If you modify this library, you may extend
+ * this exception to your version of the library, but you are not
+ * obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+package org.armedbear.lisp;
+
+import static org.armedbear.lisp.Lisp.*;
+
+import java.io.Writer;
+
+public final class SeekableStringWriter extends Writer {
+    private final StringBuffer stringBuffer;
+    private int offset = 0;
+
+    public SeekableStringWriter() {
+        stringBuffer = new StringBuffer();
+    }
+
+    public SeekableStringWriter(int initialSize) {
+        stringBuffer = new StringBuffer(initialSize);
+    }
+
+    public SeekableStringWriter append(char c) {
+        write(c);
+        return this;
+    }
+
+    public SeekableStringWriter append(CharSequence csq) {
+        write(csq.toString());
+        return this;
+    }
+
+    public SeekableStringWriter append(CharSequence csq, int start, int end) {
+        write(csq.subSequence(start, end).toString());
+        return this;
+    }
+
+    @Override
+    public void write(char[] cbuf) {
+        _write(cbuf, 0, cbuf.length);
+    }
+
+    @Override
+    public void write(char[] cbuf, int off, int len) {
+        int bufLen = cbuf.length;
+
+        if (off < 0 || off > bufLen || len < 0 || off + len > bufLen)
+            throw new IllegalArgumentException();
+
+        _write(cbuf, off, len);
+    }
+
+    @Override
+    public void write(int c) {
+        if (offset == stringBuffer.length())
+            stringBuffer.append((char) c);
+        else
+            stringBuffer.setCharAt(offset, (char) c);
+        ++offset;
+    }
+
+    @Override
+    public void write(String str) {
+        write(str, 0, str.length());
+    }
+
+    @Override
+    public void write(String str, int off, int len) {
+        write(str.toCharArray(), off, len);
+    }
+
+    private void _write(char[] cbuf, int off, int len) {
+        int strLen = stringBuffer.length();
+        int space = strLen - offset;
+
+        int written = Math.min(len, space);
+
+        if (written > 0)
+            stringBuffer.replace(offset, offset + written, new String(cbuf, off, written));
+
+        if (written < len)
+            stringBuffer.append(cbuf, off + written, len - written);
+
+        offset += len;
+    }
+
+    public void seek(int offset) {
+        if (offset < 0 || offset > stringBuffer.length())
+            throw new IllegalArgumentException();
+        this.offset = offset;
+    }
+
+    public StringBuffer getBuffer() {
+        return stringBuffer;
+    }
+
+    public int getOffset() {
+        return offset;
+    }
+
+    @Override
+    public String toString() {
+        return stringBuffer.toString();
+    }
+
+    @Override
+    public void close() {}
+
+    @Override
+    public void flush() {}
+}
diff --git a/src/org/armedbear/lisp/StringInputStream.java b/src/org/armedbear/lisp/StringInputStream.java
index f694813..4c1d70b 100644
--- a/src/org/armedbear/lisp/StringInputStream.java
+++ b/src/org/armedbear/lisp/StringInputStream.java
@@ -35,12 +35,14 @@ package org.armedbear.lisp;
 
 import static org.armedbear.lisp.Lisp.*;
 
+import java.io.IOException;
 import java.io.StringReader;
 
 public final class StringInputStream extends Stream
 {
     private final StringReader stringReader;
     private final int start;
+    private final String subString;
     
     public StringInputStream(String s)
     {
@@ -60,8 +62,9 @@ public final class StringInputStream extends Stream
         eolStyle = EolStyle.RAW;
 
         this.start = start;
-        
-        stringReader = new StringReader(s.substring(start, end));
+
+        subString = s.substring(start, end);
+        stringReader = new StringReader(subString);
         initAsCharacterInputStream(stringReader);
     }
 
@@ -93,7 +96,41 @@ public final class StringInputStream extends Stream
 
     @Override
     public int getOffset() {
-        return start + super.getOffset();
+        return start + offset;
+    }
+
+    @Override
+    protected long _getFilePosition() {
+        return getOffset();
+    }
+
+    @Override
+    protected boolean _setFilePosition(LispObject arg) {
+        try {
+            int offset;
+
+            if (arg == Keyword.START)
+                offset = 0;
+            else if (arg == Keyword.END)
+                offset = subString.length();
+            else {
+                long n = Fixnum.getValue(arg);
+                if (n < 0 || n > subString.length())
+                    error(new StreamError(this, "FILE-POSITION got out of bounds argument."));
+                offset = (int) n; // FIXME arg might be a bignum
+            }
+
+            stringReader.reset();
+            stringReader.skip(offset);
+            initAsCharacterInputStream(stringReader);
+
+            this.offset = offset;
+        }
+        catch (IOException e) {
+            error(new StreamError(this, e));
+        }
+
+        return true;
     }
     
     // ### make-string-input-stream
diff --git a/src/org/armedbear/lisp/StringOutputStream.java b/src/org/armedbear/lisp/StringOutputStream.java
index 5de5890..925970b 100644
--- a/src/org/armedbear/lisp/StringOutputStream.java
+++ b/src/org/armedbear/lisp/StringOutputStream.java
@@ -35,11 +35,12 @@ package org.armedbear.lisp;
 
 import static org.armedbear.lisp.Lisp.*;
 
+import java.io.IOException;
 import java.io.StringWriter;
 
 public final class StringOutputStream extends Stream
 {
-    private final StringWriter stringWriter;
+    private final SeekableStringWriter stringWriter;
 
     public StringOutputStream()
     {
@@ -51,7 +52,7 @@ public final class StringOutputStream extends Stream
         super(Symbol.STRING_OUTPUT_STREAM);
         this.elementType = elementType;
         this.eolStyle = EolStyle.RAW;
-        initAsCharacterOutputStream(stringWriter = new StringWriter());
+        initAsCharacterOutputStream(stringWriter = new SeekableStringWriter());
     }
 
     @Override
@@ -85,7 +86,35 @@ public final class StringOutputStream extends Stream
     {
         if (elementType == NIL)
             return 0;
-        return stringWriter.getBuffer().length();
+        return offset;
+    }
+
+    @Override
+    protected boolean _setFilePosition(LispObject arg) {
+        if (elementType == NIL)
+            return false;
+
+        try {
+            int offset;
+
+            if (arg == Keyword.START)
+                offset = 0;
+            else if (arg == Keyword.END)
+                offset = stringWriter.getBuffer().length();
+            else {
+                long n = Fixnum.getValue(arg);
+                offset = (int) n; // FIXME arg might be a bignum
+            }
+
+            stringWriter.seek(offset);
+
+            this.offset = offset;
+        }
+        catch (IllegalArgumentException e) {
+            error(new StreamError(this, e));
+        }
+
+        return true;
     }
 
     public LispObject getString()
diff --git a/test/lisp/abcl/misc-tests.lisp b/test/lisp/abcl/misc-tests.lisp
index 5e76563..6de0dfd 100644
--- a/test/lisp/abcl/misc-tests.lisp
+++ b/test/lisp/abcl/misc-tests.lisp
@@ -108,4 +108,13 @@ (deftest ticket.107
                            (setf (values (mystruct-slot struct)
                                          x)
                                  (values 42 2))))))
-  42 2)
\ No newline at end of file
+  42 2)
+
+(deftest string-output-stream.seekable
+    (string= "Goodbye, World! Something."
+             (let ((stream (make-string-output-stream)))
+               (write-string "Hello, World!   Something." stream)
+               (file-position stream :start)
+               (write-string "Goodbye, World!" stream)
+               (get-output-stream-string stream)))
+  T)
diff --git a/test/src/org/armedbear/lisp/SeekableStringWriterTest.java b/test/src/org/armedbear/lisp/SeekableStringWriterTest.java
new file mode 100644
index 0000000..1779b83
--- /dev/null
+++ b/test/src/org/armedbear/lisp/SeekableStringWriterTest.java
@@ -0,0 +1,19 @@
+package org.armedbear.lisp;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+public class SeekableStringWriterTest
+{
+  @Test
+  public void writeAndSeek() {
+    SeekableStringWriter writer = new SeekableStringWriter();
+    String buf = "sdf";
+    writer.append('a').append(buf).append(buf, 1, 2);
+    assertEquals("asdfd", writer.toString());
+    writer.seek(0);
+    writer.append("meow");
+    assertEquals("meowd", writer.toString());
+  }
+}
-- 
2.8.1

