hudson.remoting.BinarySafeStream Maven / Gradle / Ivy
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package hudson.remoting;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.FilterOutputStream;
import java.io.UnsupportedEncodingException;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
/**
* Tunnels byte stream into another byte stream so that binary data
* can be sent across binary-unsafe stream.
*
*
* This implementation uses a variation of base64. A care has been
* taken to ensure that the following scenario is handled correctly.
*
*
* -
* If the writing side flush, the reading side should see everything
* written by then, without blocking (even if this happens outside the 3-byte boundary)
*
-
* Reading side won't block unnecessarily.
*
*
* @author Kohsuke Kawaguchi
*/
public final class BinarySafeStream {
// no instantiation
private BinarySafeStream() {}
/**
* Decode binary safe stream.
*/
public static InputStream wrap(InputStream in) {
return new FilterInputStream(in) {
/**
* Place a part of the decoded triplet that hasn's read by the caller yet.
* We allocate four bytes because of the way we implement {@link #read(byte[], int, int)},
* which puts encoded base64 in the given array during the computation.
*/
final byte[] triplet = new byte[4];
/**
* Remaining number of valid data in {@link #triplet}.
* -1 to indicate EOF. Otherwise always 0-2.
* Valid data starts at triplet[3-remaining]
*/
int remaining=0;
final byte[] qualtet = new byte[4];
int input = 0;
public int read() throws IOException {
if(remaining==0) {
remaining = _read(triplet,0,3);
if(00;
return ((int) triplet[3 - remaining--]) & 0xFF;
}
public int read(byte b[], int off, int len) throws IOException {
if(remaining==-1) return -1; // EOF
if(len<4) {
// not enough space to process encoded data in the given buffer, so revert to the read-by-char
int read = 0;
int ch;
while(len>0 && (ch=read())!=-1) {
b[off++] = (byte)ch;
read++;
len--;
}
return read;
}
// first copy any remaining bytes in triplet to output
int l = Math.min(len,remaining);
if(l>0) {
System.arraycopy(triplet,3-remaining,b,off,l);
off+=l;
len-=l;
remaining=0;
if(super.available()==0)
// the next read() may block, so let's return now
return l;
if(len<4)
// not enough space to call _read(). abort.
return l;
// otherwise try to read more
int r = _read(b,off,len);
if(r==-1) return l;
else return l+r;
}
return _read(b,off,len);
}
/**
* The same as {@link #read(byte[], int, int)} but the buffer must be
* longer than off+4,
*/
private int _read(byte b[], int off, int len) throws IOException {
assert remaining==0;
assert b.length>=off+4;
int totalRead = 0;
// read in the rest
if(len>0) {
// put the remaining data from previous run at the top.
if(input>0)
System.arraycopy(qualtet,0, b, off,input);
// for us to return any byte we need to at least read 4 bytes,
// so insist on getting four bytes at least. When stream is flushed
// we get extra '=' in the middle.
int l=input; // l = # of total encoded bytes to be processed in this round
while(l<4) {
int r = super.read(b, off + l, Math.max(len,4) - l);
if(r==-1) {
if(l%4!=0)
throw new IOException("Unexpected stream termination");
if(l==0)
return -1; // EOF, and no data to process
}
l += r;
}
// we can only decode multiple of 4, so write back any remaining data to qualtet.
// this also updates 'input' correctly.
input = l%4;
if(input>0) {
System.arraycopy(b, off +l-input,qualtet,0,input);
l-=input;
}
// now we just need to convert four at a time
assert l%4==0;
for( int base= off; l>0; l-=4 ) {
// convert b[base...base+3] to b[off...off+2]
// note that the buffer can be overlapping
int c0 = DECODING_TABLE[b[base++]];
int c1 = DECODING_TABLE[b[base++]];
int c2 = DECODING_TABLE[b[base++]];
int c3 = DECODING_TABLE[b[base++]];
if(c0<0 || c1<0 || c2<-1 || c3<-1) {
// illegal input. note that '=' never shows up as 1st or 2nd char
// hence the check for the 1st half and 2nd half are different.
// now try to report what we saw.
// the remaining buffer is b[base-4 ... base-4+l]
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write(b,base-4,l);
// plus we might be able to read more bytes from the underlying stream
int avail = super.available();
if(avail >0) {
byte[] buf = new byte[avail];
baos.write(buf,0,super.read(buf));
}
StringBuilder buf = new StringBuilder("Invalid encoded sequence encountered:");
for (byte ch : baos.toByteArray())
buf.append(String.format(" %02X",ch));
throw new IOException(buf.toString());
}
b[off++] = (byte) ((c0<<2) | (c1>>4));
totalRead++;
if(c2!=-1) {
b[off++] = (byte) ((c1<<4) | (c2>>2));
totalRead++;
if(c3!=-1) {
b[off++] = (byte) ((c2<<6) | c3);
totalRead++;
}
}
}
}
return totalRead;
}
public int available() throws IOException {
// roughly speaking we got 3/4 of the underlying available bytes
return super.available()*3/4;
}
};
}
/**
* Wraps an {@link OutputStream} to encoding {@link OutputStream}.
*
* @param out
* This output stream should be buffered for better performance.
*/
public static OutputStream wrap(OutputStream out) {
return new FilterOutputStream(out) {
private final byte[] triplet = new byte[3];
private int remaining=0;
private final byte[] out = new byte[4];
public void write(int b) throws IOException {
if(remaining==2) {
_write(triplet[0],triplet[1],(byte)b);
remaining=0;
} else {
triplet[remaining++]=(byte)b;
}
}
public void write(byte b[], int off, int len) throws IOException {
// if there's anything left in triplet from the last write, try to write them first
if(remaining>0) {
while(len>0 && remaining<3) {
triplet[remaining++] = b[off++];
len--;
}
if(remaining==3) {
_write(triplet[0],triplet[1],triplet[2]);
remaining = 0;
}
}
// then convert chunks as much as possible
while(len>=3) {
_write(b[off++],b[off++],b[off++]);
len-=3;
}
// store remaining stuff back to triplet
assert 0<=len && len<3;
while(len>0) {
triplet[remaining++] = b[off++];
len--;
}
}
private void _write(byte a, byte b, byte c) throws IOException {
out[0] = ENCODING_TABLE[(a>>2)&0x3F];
out[1] = ENCODING_TABLE[((a<<4)&0x3F|(b>>4)&0x0F)];
out[2] = ENCODING_TABLE[((b<<2)&0x3F|(c>>6)&0x03)];
out[3] = ENCODING_TABLE[c&0x3F];
super.out.write(out,0,4);
}
public void flush() throws IOException {
int a = triplet[0];
int b = triplet[1];
a&=0xFF;
b&=0xFF;
switch(remaining) {
case 0:
// noop
break;
case 1:
out[0] = ENCODING_TABLE[(a>>2)&0x3F];
out[1] = ENCODING_TABLE[(a<<4)&0x3F];
out[2] = '=';
out[3] = '=';
super.out.write(out,0,4);
remaining = 0;
break;
case 2:
out[0] = ENCODING_TABLE[(a>>2)&0x3F];
out[1] = ENCODING_TABLE[((a<<4)|(b>>4))&0x3F];
out[2] = ENCODING_TABLE[(b<<2)&0x3F];
out[3] = '=';
super.out.write(out,0,4);
remaining = 0;
break;
default:
throw new AssertionError();
}
super.flush();
}
};
}
private static final byte[] ENCODING_TABLE;
/**
* 0-63 are the index value, -1 is '='.
* -2 indicates all the other illegal characters.
*/
private static final int[] DECODING_TABLE = new int[128];
static {
try {
ENCODING_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".getBytes("US-ASCII");
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
Arrays.fill(DECODING_TABLE,-2);
for (int i = 0; i < ENCODING_TABLE.length; i++)
DECODING_TABLE[ENCODING_TABLE[i]] = i;
DECODING_TABLE['='] = -1;
}
}