package net.contrapunctus.rngzip.util; import java.io.OutputStream; import java.io.InputStream; import java.io.IOException; import java.io.EOFException; /** * This class encapsulates the encoding format used to represent block * headers in a multiplexed stream. It’s probably more complicated * than necessary, but saves a few bytes if your stream IDs and block * sizes are small. * * <p>If the stream ID is zero, we use one of the following * representations for the block header. The high bit of the first * byte must be zero. The <i>x</i> bits represent the size of the * block. There is <b>not</b> a unique representation for each block * size: if your block size fits within 14 bits, you may use any of * these. BitOutputStream, however, always chooses the shortest * representation. * * <pre> * 0xxxxxxx 0xxxxxxx 14 free bits [a] * 0xxxxxxx 1xxxxxxx 0xxxxxxx 21 free bits [b] * 0xxxxxxx 1xxxxxxx 1xxxxxxx xxxxxxxx 29 free bits [c] * </pre> * * <p>If the stream ID is in the range 1–4 (inclusive), we use one of * the following representations. The two highest bits of the first * byte must be “10”, and the next two <i>y</i> bits represent the * stream ID (00=stream 1, 01=stream 2, 10=stream 3, 11=stream 4). * * <pre> * 10yyxxxx 0xxxxxxx 11 free bits [d] * 10yyxxxx 1xxxxxxx 0xxxxxxx 18 free bits [e] * 10yyxxxx 1xxxxxxx 1xxxxxxx xxxxxxxx 26 free bits [f] * </pre> * * <p>Finally, as long as the stream ID is less than 64, we can use * one of the following representations. The two highest bits must be * “11” and the remaining six bits of the first byte encode the stream * ID. * * <pre> * 11yyyyyy xxxxxxxx 0xxxxxxx 15 free bits [g] * 11yyyyyy xxxxxxxx 1xxxxxxx xxxxxxxx 23 free bits [h] * </pre> * * <p>Since we may need to read/write compatible streams from other * languages, here are some sample encodings that exercise each case. * Stream IDs and lengths are in decimal, but the bytes are in * hexadecimal. * * <pre> * ID Block length Bytes * == ============ =========== * 0 12,121 5E 59 using [a], or * 00 DE 59 using [b], or * 00 80 AF 59 using [c], or * C0 5E 59 using [g], or * C0 00 AF 59 using [h]. * * 0 1,350,449 52 B6 31 using [b], or * 00 A9 9B 31 using [c], or * C0 29 9B 31 using [h]. * * 0 6,723,355 01 CD 97 1B using [c], or * C0 CD 97 1B using [h]. * * 3 2,000 AF 50 using [d], or * A0 8F 50 using [e], or * A0 80 87 D0 using [f], or * C3 0F 50 using [g], or * C3 00 87 D0 using [h]. * * 2 250,042 9F A1 3A using [e], or * 90 87 D0 BA using [f], or * C2 07 D0 BA using [h]. * * 4 8,385,630 B1 FF F4 5E using [f], or * C4 FF F4 5E using [h]. * * 42 10,222 EA 4F 6E using [g], or * EA 00 A7 EE using [h]. * * 60 4,874,941 FC 94 E2 BD using [h]. * </pre> * * <p class='license'>This is free software; you may modify and/or * redistribute it under the terms of the GNU General Public License, * but it comes with <b>absolutely no warranty.</b> * * @author Christopher League * @see MultiplexOutputStream * @see MultiplexInputStream */ public final class MultiplexBlockRep { /** * The largest possible block size, in bytes, is {@value} (8 MB). */ public static final int MAX_BLOCK_SIZE = 1 << 23; // 8M /** * Stream identifiers start from zero and must be less than this * constant, {@value}. */ public static final int MAX_STREAM_ID = 1 << 6; // 64 /** * This method validates that the stream ID ‘sid’ is within range. * It must be non-negative and less than or equal to * <code>MAX_STREAM_ID</code>. * @throws IllegalArgumentException if ‘sid’ is out of range. */ public static void checkStreamID(int sid) { if(sid < 0) { throw new IllegalArgumentException("streamID may not be negative"); } if(sid >= MAX_STREAM_ID) { throw new IllegalArgumentException ("streamID was "+sid+"; it should not exceed "+MAX_STREAM_ID); } } int streamID; private int size; private OutputStream os; private InputStream is; /** * Construct a block representation for encoding. Headers will be * output to the stream ‘os’ each time <code>encode</code> is * called. * @throws AssertionError if ‘os’ is null, or if the ‘streamID’ is * out of range (assuming assertions are enabled). */ public MultiplexBlockRep(OutputStream os, int streamID) { assert os != null; assert streamID >= 0 && streamID < MAX_STREAM_ID : streamID; this.os = os; this.streamID = streamID; } /** * Construct a block representation for decoding. A header will be * read from the stream ‘is’ each time <code>decode</code> is * called. * @throws AssertionError if ‘is’ is null (assuming assertions are * enabled). */ public MultiplexBlockRep(InputStream is) { assert is != null; this.is = is; } /** * Output a header for a block of ‘size’ bytes. * @throws AssertionError if this object was not constructed with * an <code>OutputStream</code> or if ‘size’ is out of range. * @see #MultiplexBlockRep(OutputStream, int) * @see #MAX_BLOCK_SIZE */ public void encode(int sz) throws IOException { assert os != null; size = sz; assert size >= 0 && size < MAX_BLOCK_SIZE : size; if(streamID == 0) encodeS0(); else if(streamID <= 4) encodeS4(); else encodeOther(); } /** * Input a block header, and return the size of that block. * @throws AssertionError if this objects was not constructed with * an <code>InputStream</code>. * @see #MultiplexBlockRep(InputStream) */ public int decode() throws IOException { assert is != null; int b0 = readByte(); if((b0 & 0x80) == 0) { // 0xxx xxxx return decodeS0(b0); } else if((b0 & 0x40) == 0) { // 10yy xxxx return decodeS4(b0); } else { // 11yy yyyy assert (b0 & 0xC0) == 0xC0 : b0; return decodeOther(b0); } } private void writeBits(int k, int mask) throws IOException { os.write(mask | size >> k); size &= (1 << k) - 1; } private int readByte() throws IOException { int b = is.read(); if(b == -1) throw new EOFException(); else return b; } private int readBytes(int max) throws IOException { boolean more; for(more = true; more && max > 1; max--) { int b = readByte(); if((b & 0x80) == 0) more = false; else b &= 0x7F; size = b | size << 7; } if(more) { int b = readByte(); size = b | size << 8; } return size; } /* Use 2, 3, or 4 bytes, beginning with "0" bit: * 0xxxxxxx 0xxxxxxx 14 free bits (7+7) * 0xxxxxxx 1xxxxxxx 0xxxxxxx 21 free bits (7+7+7) * 0xxxxxxx 1xxxxxxx 1xxxxxxx xxxxxxxx 29 free bits (7+7+7+8) */ private void encodeS0() throws IOException { if(size < 1 << 14) { writeBits(7, 0x00); writeBits(0, 0x00); } else if(size < 1 << 21) { writeBits(14, 0x00); writeBits( 7, 0x80); writeBits( 0, 0x00); } else { assert size < 1 << 29 : size; writeBits(22, 0x00); writeBits(15, 0x80); writeBits( 8, 0x80); writeBits( 0, 0x00); } } private int decodeS0(int b0) throws IOException { streamID = 0; size = b0; return readBytes(3); } /* Use 2, 3, or 4 bytes, encoding "10" and the stream ID in the * first 4 bits. * 10yyxxxx 0xxxxxxx 11 free bits (4+7) * 10yyxxxx 1xxxxxxx 0xxxxxxx 18 free bits (4+7+7) * 10yyxxxx 1xxxxxxx 1xxxxxxx xxxxxxxx 26 free bits (4+7+7+8) */ private void encodeS4() throws IOException { int mask = 0; switch(streamID) { case 1: mask = 0x80; break; // 1000 case 2: mask = 0x90; break; // 1001 case 3: mask = 0xA0; break; // 1010 case 4: mask = 0xB0; break; // 1011 default: assert false : streamID; } if(size < 1 << 11) { writeBits(7, mask); writeBits(0, 0x00); } else if(size < 1 << 18) { writeBits(14, mask); writeBits( 7, 0x80); writeBits( 0, 0x00); } else { assert size < 1 << 26 : size; writeBits(22, mask); writeBits(15, 0x80); writeBits( 8, 0x80); writeBits( 0, 0x00); } } private int decodeS4(int b0) throws IOException { switch(b0 & 0xF0) { case 0x80: streamID = 1; break; // 1000 case 0x90: streamID = 2; break; // 1001 case 0xA0: streamID = 3; break; // 1010 case 0xB0: streamID = 4; break; // 1011 default: assert false : b0; } size = b0 & 0x0F; return readBytes(3); } /* Use 3 or 4 bytes, encoding the stream ID in the first one. * 11yyyyyy xxxxxxxx 0xxxxxxx 15 free bits (8+7) * 11yyyyyy xxxxxxxx 1xxxxxxx xxxxxxxx 23 free bits (8+7+8) */ private void encodeOther() throws IOException { assert streamID < 1 << 6 : streamID; os.write(0xC0 | streamID); if(size < 1 << 15) { writeBits(7, 0x00); writeBits(0, 0x00); } else { assert size < 1 << 23 : size; writeBits(15, 0x00); writeBits( 8, 0x80); writeBits( 0, 0x00); } } private int decodeOther(int b0) throws IOException { streamID = b0 & 0x3F; int b1 = readByte(); size = b1; return readBytes(2); } }