diff --git a/src/it/unimi/dsi/fastutil/io/FastByteArrayInputStream.java b/src/it/unimi/dsi/fastutil/io/FastByteArrayInputStream.java index 4771af02..cea1a45b 100644 --- a/src/it/unimi/dsi/fastutil/io/FastByteArrayInputStream.java +++ b/src/it/unimi/dsi/fastutil/io/FastByteArrayInputStream.java @@ -13,9 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package it.unimi.dsi.fastutil.io; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectInputStream; +import java.io.UTFDataFormatException; +import java.io.UncheckedIOException; + /** Simple, fast and repositionable byte-array input stream. * *

Warning: this class implements the correct semantics @@ -25,8 +31,7 @@ * * @author Sebastiano Vigna */ - -public class FastByteArrayInputStream extends MeasurableInputStream implements RepositionableStream { +public class FastByteArrayInputStream extends MeasurableInputStream implements RepositionableStream, ObjectInput { /** The array backing the input stream. */ public byte[] array; @@ -38,7 +43,7 @@ public class FastByteArrayInputStream extends MeasurableInputStream implements R public int length; /** The current position as a distance from {@link #offset}. */ - private int position; + protected int position; /** The current mark as a position, or -1 if no mark exists. */ private int mark; @@ -52,7 +57,7 @@ public class FastByteArrayInputStream extends MeasurableInputStream implements R public FastByteArrayInputStream(final byte[] array, final int offset, final int length) { this.array = array; this.offset = offset; - this.length = length; + this.length = Math.min(length, array.length - offset); } /** Creates a new array input stream using a given array. @@ -111,7 +116,7 @@ public int read() { */ @Override - public int read(final byte b[], final int offset, final int length) { + public int read(final byte[] b, final int offset, final int length) { if (this.length == this.position) return length == 0 ? 0 : -1; final int n = Math.min(length, this.length - this.position); System.arraycopy(array, this.offset + this.position, b, offset, n); @@ -133,4 +138,147 @@ public void position(final long newPosition) { public long length() { return length; } -} + + @Override + public byte[] readAllBytes() { + return readNBytes(available()); + } + + @Override + public int readNBytes(byte[] b, int off, int len) { + int n = read(b, off, len); + return n == -1 ? 0 : n; + } + + @Override + public int read (byte[] b) { + return read(b, 0, b.length); + } + + @Override + public byte[] readNBytes (int len) { + int n = Math.min(len, available()); + byte[] result = new byte[n]; + read(result); + return result; + } + + @Override + public void skipNBytes (long n) { + skip(n); + } + + public int peek() { + if (length <= position()) return -1; + return array[(int)(offset + position())] & 0xFF; + } + + + @Override + public void readFully (byte[] b) { + read(b); + } + + @Override + public void readFully (byte[] b, int off, int len) { + read(b, off, len); + } + + @Override + public int skipBytes (int n) { + return (int) skip(n); + } + + @Override + public boolean readBoolean () { + return read() != 0; + } + + @Override + public byte readByte () { + return (byte) read(); + } + + @Override + public int readUnsignedByte () { + return read() & 0xFF; + } + + @Override + public short readShort() { + return (short)((read() << 8)|(read() & 0xFF)); + } + + @Override + public int readUnsignedShort() { + return ((read() & 0xFF) << 8)|(read() & 0xFF); + } + + @Override + public char readChar() { + return (char)(((read() & 0xFF) << 8)|(read() & 0xFF)); + } + + @Override + public int readInt() { + return read() << 24 | ((read() & 0xFF) << 16) | ((read() & 0xFF) << 8) | (read() & 0xFF); + } + + @Override + public long readLong () { + return (long) readInt() << 32 | (readInt() & 0xFFFF_FFFFL); + } + + @Override + public float readFloat () { + return Float.intBitsToFloat(readInt()); + } + + @Override + public double readDouble () { + return Double.longBitsToDouble(readLong()); + } + + @Override @Deprecated + public String readLine () { + StringBuilder sb = new StringBuilder(99); +loop: + for (int c;;){ + switch (c = read()){ + case -1: + break loop;// eof + + case '\n': + return sb.toString(); + case '\r': + if (peek() == '\n'){ + read(); + } + return sb.toString(); + + default: + sb.append((char) c); + } + } + return sb.isEmpty() ? null : sb.toString(); + } + + @Override + public String readUTF () throws UTFDataFormatException { + try { + return available() > 0 ? DataInputStream.readUTF(this) : null; + } catch (UTFDataFormatException badBinaryFormatting){ + throw badBinaryFormatting; + } catch (IOException e){ + throw new UncheckedIOException("readUTF @ "+this, e); + } + } + + /// not efficient! Only added to support custom {@link java.io.Externalizable} + @Override + public Object readObject () throws ClassNotFoundException, IOException { + try (ObjectInputStream ois = new ObjectInputStream(this)){ + return ois.readObject(); + } + } +} \ No newline at end of file diff --git a/src/it/unimi/dsi/fastutil/io/FastByteArrayOutputStream.java b/src/it/unimi/dsi/fastutil/io/FastByteArrayOutputStream.java index 04f043ef..d418d046 100644 --- a/src/it/unimi/dsi/fastutil/io/FastByteArrayOutputStream.java +++ b/src/it/unimi/dsi/fastutil/io/FastByteArrayOutputStream.java @@ -13,11 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package it.unimi.dsi.fastutil.io; import it.unimi.dsi.fastutil.bytes.ByteArrays; +import java.io.DataOutput; +import java.io.IOException; +import java.io.ObjectOutput; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; + /** Simple, fast byte-array output stream that exposes the backing array. * *

{@link java.io.ByteArrayOutputStream} is nice, but to get its content you @@ -30,7 +36,7 @@ * * @author Sebastiano Vigna */ -public class FastByteArrayOutputStream extends MeasurableOutputStream implements RepositionableStream { +public class FastByteArrayOutputStream extends MeasurableOutputStream implements RepositionableStream, ObjectOutput { /** The array backing the output stream. */ public static final int DEFAULT_INITIAL_CAPACITY = 16; @@ -42,7 +48,7 @@ public class FastByteArrayOutputStream extends MeasurableOutputStream implements public int length; /** The current writing position. */ - private int position; + protected int position; /** Creates a new array output stream with an initial capacity of {@link #DEFAULT_INITIAL_CAPACITY} bytes. */ public FastByteArrayOutputStream() { @@ -122,4 +128,122 @@ public void write(final byte[] b) { // Only to force no exception write(b, 0, b.length); } + + /** @see java.io.ByteArrayOutputStream#toString(Charset) */ + public String toString(Charset charset) { + return new String(array, 0, length, charset); + } + + /** @see java.io.ByteArrayOutputStream#writeTo(OutputStream) */ + public synchronized void writeTo(OutputStream out) throws IOException { + out.write(array, 0, length); + } + + + @Override + public void writeBoolean(boolean v) { + write(v?1:0); + } + + @Override + public void writeByte(int v) { + write(v); + } + + @Override + public void writeShort(int v) { + write(v >> 8); + write(v); + } + + @Override + public void writeChar(int v) { + write(v >> 8); + write(v); + } + + @Override + public void writeInt(int v) { + write(v >> 24); + write(v >> 16); + write(v >> 8); + write(v); + } + + @Override + public void writeLong(long v) { + writeInt((int)(v >> 32)); + writeInt((int) v); + } + + @Override + public void writeFloat(float v) { + writeInt(Float.floatToIntBits(v)); + } + + @Override + public void writeDouble(double v) { + writeLong(Double.doubleToLongBits(v)); + } + + /** + * @deprecated This method is dangerous as it discards the high byte of every character. For UTF-8, use {@link #writeUTF(String)} or {@link #write(byte[]) @code write(s.getBytes(UTF_8))}. + * @see java.io.DataOutputStream#writeBytes(String) + */ + @Override + public void writeBytes(String s) { + for (int i = 0, len = s.length(); i < len; i++){ + write((byte)s.charAt(i)); + } + } + + @Override + public void writeChars(String s) { + for (int i = 0, len = s.length(); i < len; i++){ + int v = s.charAt(i); + writeChar(v); + } + } + + @Override + public void writeUTF (String s) { + int savePos = position; + writeShort(0);// len place holder + for (int i = 0, len = s.length(); i < len; i++){ + writeUtf8Char(s.charAt(i)); + if (position - savePos > 0xFF_FF + 2){ + length = position = savePos;// rollback + throw new IllegalArgumentException("UTF encoded string too long: %d: %s".formatted(s.length(), s.substring(0, 99))); + } + } + int len = position - savePos - 2; + array[savePos] = (byte)(len >> 8); + array[savePos+1] = (byte)len; + } + + /** @see java.io.DataOutputStream#writeUTF(String,DataOutput) */ + public int writeUtf8Char(char c) { + if (c != 0 && c < 0x80){ + write(c); + return 1; + } else if (c >= 0x800){ + write(0xE0 | c >> 12 & 0x0F); + write(0x80 | c >> 6 & 0x3F); + write(0x80 | c & 0x3F); + return 3; + } else { + write(0xC0 | c >> 6 & 0x1F); + write(0x80 | c & 0x3F); + return 2; + } + } + + /// not efficient! Only added to support custom {@link java.io.Externalizable} + @Override + public void writeObject(Object obj) throws IOException { + try (ObjectOutputStream oout = new ObjectOutputStream(this)){ + oout.writeObject(obj); + oout.flush(); + } + } } \ No newline at end of file diff --git a/test/it/unimi/dsi/fastutil/io/FastByteArrayStreamsTest.java b/test/it/unimi/dsi/fastutil/io/FastByteArrayStreamsTest.java new file mode 100644 index 00000000..d23546a9 --- /dev/null +++ b/test/it/unimi/dsi/fastutil/io/FastByteArrayStreamsTest.java @@ -0,0 +1,517 @@ +package it.unimi.dsi.fastutil.io; + +import it.unimi.dsi.fastutil.bytes.ByteArrays; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.Serializable; +import java.io.UTFDataFormatException; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + @see FastByteArrayOutputStream + @see FastByteArrayInputStream + @see DataOutputStream + @see ByteArrayOutputStream + @see ByteArrayInputStream + */ +@SuppressWarnings({"resource", "IOResourceOpenedButNotSafelyClosed"}) +public class FastByteArrayStreamsTest { + /// todo put into {@link it.unimi.dsi.fastutil.bytes.ByteArrays} ? + /// @see java.util.HexFormat + private static final short[] HEX_TABLE = new short[256]; + static { + final byte[] hex = "0123456789abcdef".getBytes(StandardCharsets.ISO_8859_1); + for (int i = 0; i < 256; i ++){ + HEX_TABLE[i] = (short)((hex[i >>> 4 & 0xF] << 8) | (hex[i & 0xF] & 0xFF)); + } + } + + @SuppressWarnings("deprecation") + public static String toHexString (byte[] buffer, int fromIndex, int length) { + if (length <= 0){ return ""; } + length = Math.min(length, buffer.length - fromIndex); + + byte[] buf = new byte[length << 1];// buffer.length * 2 + + for (int dstIdx = 0, endIndex = fromIndex + length; fromIndex < endIndex; fromIndex++){ + int nextByte = buffer[fromIndex] & 0xFF; + short byteAsTwoHex = HEX_TABLE[nextByte]; + buf[dstIdx++] = (byte)(byteAsTwoHex >> 8); + buf[dstIdx++] = (byte)(byteAsTwoHex & 0xFF); + } + + return new String(buf, 0/*hi*/, 0, buf.length); + } + + public static String toHexString (byte[] buffer) { + return toHexString(buffer, 0, buffer.length); + } + + @Test + public void testHex () { + assertEquals("", toHexString(null, -1, -1)); + assertEquals("", toHexString(new byte[]{0, 1, 2, -1}, -1, -1)); + assertEquals("000102ff", toHexString(new byte[]{0, 1, 2, -1}, 0, 100)); + assertEquals("0102ff", toHexString(new byte[]{0, 1, 2, -1}, 1, 100)); + assertEquals("0102", toHexString(new byte[]{0, 1, 2, -1}, 1, 2)); + } + + @FunctionalInterface + interface ThrowingConsumer { + void accept (T value) throws Throwable; + } + + static final class OpenDataOutputStream extends DataOutputStream { + public OpenDataOutputStream (){ super(new ByteArrayOutputStream()); } + public ByteArrayOutputStream out (){ return (ByteArrayOutputStream) out; } + } + + static class OpenByteArrayInputStream extends ByteArrayInputStream { + public OpenByteArrayInputStream (){ super(ByteArrays.EMPTY_ARRAY); } + public void setArray (byte[] array) { + this.buf = array; + this.pos = 0; + this.count = buf.length; + } + } + + static final class OpenDataInputStream extends DataInputStream { + public OpenDataInputStream (){ super(new OpenByteArrayInputStream()); } + public OpenByteArrayInputStream in (){ return (OpenByteArrayInputStream) in; } + } + + private void reset (DataOutput w) { + if (w instanceof FastByteArrayOutputStream x){ + x.reset(); + } else { + ((OpenDataOutputStream) w).out().reset(); + } + } + + private byte[] toByteArray (DataOutput w) { + if (w instanceof FastByteArrayOutputStream x){ + return x.toByteArray(); + } else { + return ((OpenDataOutputStream) w).out().toByteArray(); + } + } + + private void setArray (DataInput r, byte[] array) { + if (r instanceof FastByteArrayInputStream x){ + x.array = array; + x.offset = 0; + x.position(0); + x.length = array.length; + + } else { + ((OpenDataInputStream) r).in().setArray(array); + } + } + + + /// us→us, us→jdk, jdk→us, jdk→jdk (to be 100% sure) + void x (String hex, ThrowingConsumer write, ThrowingConsumer readAndVerify) { + hex = hex.replaceAll("[\\s_]+", "").trim(); + try { + for (DataOutput w : new DataOutput[]{new FastByteArrayOutputStream(), new OpenDataOutputStream()}){ + for (DataInput r : new DataInput[]{new FastByteArrayInputStream(ByteArrays.EMPTY_ARRAY), new OpenDataInputStream()}){ + reset(w); + + write.accept(w);//1 + + byte[] array = toByteArray(w); + assertEquals(hex, toHexString(array)); + assertEquals(hex.length(), array.length * 2); + + setArray(r, array); + + readAndVerify.accept(r);//2 + } + } + } catch (Throwable e){ + throw new AssertionError(e); + } + } + + @Test + public void testEOF () throws UTFDataFormatException { + FastByteArrayInputStream r = new FastByteArrayInputStream(ByteArrays.EMPTY_ARRAY); + assertEquals(0, r.available()); + assertEquals(-1, r.read()); + assertEquals(-1, r.readByte()); + assertEquals(0xff, r.readUnsignedByte()); + assertTrue(r.readBoolean());//? + assertEquals(-1, r.readShort()); + assertEquals(0xFFFF, r.readUnsignedShort()); + assertEquals(0xFFFF, r.readChar()); + assertEquals(-1, r.readInt()); + assertEquals(-1, r.readLong()); + assertEquals(Float.NaN, r.readFloat(), 0.001); + assertEquals(Double.NaN, r.readDouble(), 0.001); + assertNull(r.readLine()); + assertNull(r.readUTF()); + } + + @Test + public void testWriteDef () { + x("00 7f ff_ff", + w->{ + w.write(0); + w.write(0x7f); + w.write(-1); + w.write(-1); + }, + r -> { + assertEquals(0, r.readUnsignedByte()); + assertEquals(0x7f, r.readUnsignedByte()); + assertEquals(0xff, r.readUnsignedByte()); + assertEquals(-1, r.readByte()); + } + ); + } + + @Test + public void testWriteArray1 () { + x("00 7f ff_ff", + w->{ + w.write(new byte[]{0, 0x7f, -1, -1}, 0, 4); + }, + r -> { + byte[] b = new byte[4]; + r.readFully(b, 0, 0); + r.readFully(b, 0, 4); + assertEquals(0, b[0]); + assertEquals(0x7f, b[1]); + assertEquals(-1, b[2]); + assertEquals(-1, b[3]); + } + ); + } + + @Test + public void testWriteArray2 () { + x("00 7f ff_ff", + w->{ + w.write(new byte[]{0, 0x7f, -1, -1}); + }, + r -> { + byte[] b = new byte[4]; + r.readFully(b); + assertEquals(0, b[0]); + assertEquals(0x7f, b[1]); + assertEquals(-1, b[2]); + assertEquals(-1, b[3]); + } + ); + } + + @Test + public void testWriteBoolean () { + x("01 00 01", + w->{ + w.writeBoolean(true); + w.writeBoolean(false); + w.writeBoolean(true); + }, + r -> { + assertTrue(r.readBoolean()); + assertFalse(r.readBoolean()); + assertTrue(r.readBoolean()); + } + ); + } + + @Test + public void testWriteByte () { + x("00 7f ff_ff", + w->{ + w.writeByte(0); + w.writeByte(0x7f); + w.writeByte(-1); + w.writeByte(-1); + }, + r -> { + assertEquals(0, r.readUnsignedByte()); + assertEquals(0x7f, r.readUnsignedByte()); + assertEquals(0xff, r.readUnsignedByte()); + assertEquals(-1, r.readByte()); + } + ); + } + + @Test + public void testWriteShort () { + x("0000 8000 1234 7fff 8000 7f56", + w->{ + w.writeShort(0); + w.writeShort(Short.MIN_VALUE); + w.writeShort(0x1234); + + w.writeShort(Short.MAX_VALUE); + w.writeShort(Short.MIN_VALUE); + w.writeShort(0x7f56); + }, + r -> { + assertEquals(0, r.readUnsignedShort()); + assertEquals(0x8000, r.readUnsignedShort()); + assertEquals(0x1234, r.readUnsignedShort()); + + assertEquals(Short.MAX_VALUE, r.readShort()); + assertEquals(Short.MIN_VALUE, r.readShort()); + assertEquals(0x7f56, r.readShort()); + } + ); + } + + @Test + public void testWriteChar () { + x("0000 8000 1234 7fff 8000 7f56", + w->{ + w.writeChar(0); + w.writeChar(Short.MIN_VALUE); + w.writeChar(0x1234); + + w.writeChar(Short.MAX_VALUE); + w.writeChar(Short.MIN_VALUE); + w.writeChar(0x7f56); + }, + r -> { + assertEquals(0, r.readChar()); + assertEquals(0x8000, r.readChar()); + assertEquals(0x1234, r.readChar()); + + assertEquals(Short.MAX_VALUE, r.readChar()); + assertEquals(0x8000, r.readChar()); + assertEquals(0x7f56, r.readChar()); + } + ); + } + + @Test + public void testWriteInt () { + x("0000_0000 0012_3456 ffff_8000 8000_0000 0000_7fff 7fff_ffff", + w->{ + w.writeInt(0); + w.writeInt(0x123456); + w.writeInt(Short.MIN_VALUE); + + w.writeInt(Integer.MIN_VALUE); + w.writeInt(Short.MAX_VALUE); + w.writeInt(Integer.MAX_VALUE); + }, + r -> { + assertEquals(0, r.readInt()); + assertEquals(0x123456, r.readInt()); + assertEquals(Short.MIN_VALUE, r.readInt()); + + assertEquals(Integer.MIN_VALUE, r.readInt()); + assertEquals(Short.MAX_VALUE, r.readInt()); + assertEquals(Integer.MAX_VALUE, r.readInt()); + } + ); + } + + @Test + public void testWriteLong () { + x("0000_0000_0000_0000 0000_0000_0012_3456 ffff_ffff_ffff_8000 ffff_ffff_8000_0000 0000_0000_0000_7fff 0000_0000_7fff_ffff 8000_0000_0000_0000 7fff_ffff_ffff_ffff", + w->{ + w.writeLong(0); + w.writeLong(0x123456); + w.writeLong(Short.MIN_VALUE); + + w.writeLong(Integer.MIN_VALUE); + w.writeLong(Short.MAX_VALUE); + w.writeLong(Integer.MAX_VALUE); + + w.writeLong(Long.MIN_VALUE); + w.writeLong(Long.MAX_VALUE); + }, + r -> { + assertEquals(0, r.readLong()); + assertEquals(0x123456, r.readLong()); + assertEquals(Short.MIN_VALUE, r.readLong()); + + assertEquals(Integer.MIN_VALUE, r.readLong()); + assertEquals(Short.MAX_VALUE, r.readLong()); + assertEquals(Integer.MAX_VALUE, r.readLong()); + + assertEquals(Long.MIN_VALUE, r.readLong()); + assertEquals(Long.MAX_VALUE, r.readLong()); + } + ); + } + + @Test + public void testWriteFloat () { + x("00000000 4991a2b0 c7000000 cf000000 46fffe00 4f000000 40490fdb", + w->{ + w.writeFloat(0); + w.writeFloat(0x123456); + w.writeFloat(Short.MIN_VALUE); + + w.writeFloat(Integer.MIN_VALUE); + w.writeFloat(Short.MAX_VALUE); + w.writeFloat(Integer.MAX_VALUE); + + w.writeFloat(3.14159265f); + }, + r -> { + assertEquals(0, r.readFloat(), 0.001); + assertEquals(0x123456, r.readFloat(), 0.001); + assertEquals(Short.MIN_VALUE, r.readFloat(), 0.001); + + assertEquals(Integer.MIN_VALUE, r.readFloat(), 0.001); + assertEquals(Short.MAX_VALUE, r.readFloat(), 0.001); + assertEquals(Integer.MAX_VALUE, r.readFloat(), 10); + + assertEquals(3.14159265f, r.readFloat(), 0.001); + } + ); + } + + @Test + public void testWriteDouble () { + x("00000000000000004132345600000000c0e0000000000000c1e000000000000040dfffc00000000041dfffffffc00000c3e000000000000043e0000000000000400921fb54442d18", + w->{ + w.writeDouble(0); + w.writeDouble(0x123456); + w.writeDouble(Short.MIN_VALUE); + + w.writeDouble(Integer.MIN_VALUE); + w.writeDouble(Short.MAX_VALUE); + w.writeDouble(Integer.MAX_VALUE); + + w.writeDouble(Long.MIN_VALUE); + w.writeDouble(Long.MAX_VALUE); + + w.writeDouble(Math.PI); + }, + r -> { + assertEquals(0, r.readDouble(), 0.001); + assertEquals(0x123456, r.readDouble(), 0.001); + assertEquals(Short.MIN_VALUE, r.readDouble(), 0.001); + + assertEquals(Integer.MIN_VALUE, r.readDouble(), 0.001); + assertEquals(Short.MAX_VALUE, r.readDouble(), 0.001); + assertEquals(Integer.MAX_VALUE, r.readDouble(), 0.001); + + assertEquals(Long.MIN_VALUE, r.readDouble(), 0.001); + assertEquals(Long.MAX_VALUE, r.readDouble(), 0.001); + + assertEquals(Math.PI, r.readDouble(), 0.001); + } + ); + } + + @Test + public void testWriteBytes () { + String s = "ISO_8859_1 is equal to Unicode.left(256)!"; + x("4049534f5f383835395f3120697320657175616c20746f20556e69636f64652e6c6566742832353629210d0a", + w->{ + w.writeBytes('@'+ s+ "\r\n"); + }, + r -> { + assertEquals('@', r.readByte()); + String z = r.readLine(); + assertEquals(s, z); + } + ); + } + + @Test + public void testWriteChars () { + String s = "Chars in UTF-16BE 🚀💯!"; + x("00400043006800610072007300200069006e0020005500540046002d00310036004200450020d83dde80d83ddcaf0021000000170043006800610072007300200069006e0020005500540046002d00310036004200450020d83dde80d83ddcaf0021", + w->{ + w.writeChars('@'+ s+ "\0");// c string + w.writeShort(s.length()); + w.writeChars(s); + }, + r -> { + assertEquals('@', r.readChar()); + + StringBuilder sb = new StringBuilder(); + char c; + while ((c = r.readChar())!=0) + sb.append(c); + assertEquals(s, sb.toString()); + + int len = r.readUnsignedShort(); + sb.setLength(0); + while (len-- >0) + sb.append(r.readChar()); + assertEquals(s, sb.toString()); + } + ); + } + + @Test + public void testWriteUTF8 () throws UTFDataFormatException { + String s = "\0\r\nChars in UTF-8 🚀💯!"; + assertEquals(23, s.length()); + assertEquals(27, s.getBytes(StandardCharsets.UTF_8).length); + x("0020c0800d0a436861727320696e205554462d3820eda0bdedba80eda0bdedb2af21", + w->{ + w.writeUTF(s); + }, + r -> { + assertEquals(s, r.readUTF()); + } + ); + + FastByteArrayOutputStream w = new FastByteArrayOutputStream(); + w.writeUTF(s.repeat(100)); + FastByteArrayInputStream r = new FastByteArrayInputStream(w.toByteArray()); + assertEquals(s.repeat(100), r.readUTF()); + } + + @Test + public void testSkipBytes () { + x("007fff0203", + w->{ + w.writeByte(0); + w.writeByte(0x7f); + w.writeByte(-1); + w.writeByte(2); + w.writeByte(3); + }, + r -> { + assertEquals(1, r.skipBytes(1)); + assertEquals(0x7f, r.readUnsignedByte()); + assertEquals(2, r.skipBytes(2)); + assertEquals(3, r.readByte()); + } + ); + } + + @Test + public void testObjectStreams () throws IOException, ClassNotFoundException { + FastByteArrayOutputStream w = new FastByteArrayOutputStream(); + + String s = "Simple Java Object"; + w.writeObject(s); + + w.writeObject(Math.PI); + + Map m = Map.of("field1", 42, 'c', 17, boolean.class, true); + w.writeObject(m); + + FastByteArrayInputStream r = new FastByteArrayInputStream(w.toByteArray()); + assertEquals(s, r.readObject()); + assertEquals(Math.PI, r.readObject()); + assertEquals(m, r.readObject()); + } +} \ No newline at end of file