first commit
This commit is contained in:
parent
352e1653f8
commit
16a197e856
44 changed files with 5942 additions and 3 deletions
914
src/main/java/net/vhati/ftldat/FTLDat.java
Normal file
914
src/main/java/net/vhati/ftldat/FTLDat.java
Normal file
|
@ -0,0 +1,914 @@
|
|||
package net.vhati.ftldat;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.MappedByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.CharsetEncoder;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Stack;
|
||||
|
||||
import net.vhati.ftldat.FileChannelRegionInputStream;
|
||||
import net.vhati.ftldat.SizeLimitInputStream;
|
||||
|
||||
|
||||
public class FTLDat {
|
||||
|
||||
/**
|
||||
* Splits a path on "/" the way FTL expects them in .dat files.
|
||||
*/
|
||||
public static String[] ftlPathSplit( String path ) {
|
||||
return path.split( "/" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates an array of Strings with "/" between them,
|
||||
* as seen in .dat files.
|
||||
* Backslashes will become forward-slashes.
|
||||
*/
|
||||
public static String ftlPathJoin( String[] chunks ) {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
boolean first = true;
|
||||
for ( String chunk : chunks ) {
|
||||
if ( chunk.length() == 0 ) continue;
|
||||
if (first) {
|
||||
buf.append( "/" );
|
||||
first = false;
|
||||
}
|
||||
buf.append( chunk );
|
||||
}
|
||||
return buf.toString().replace("\\", "/");
|
||||
}
|
||||
|
||||
public static String ftlPathJoin( String a, String b ) {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
if ( a.length() > 0 ) buf.append( a );
|
||||
if ( a.length() * b.length() > 0 ) buf.append( "/" );
|
||||
if ( b.length() > 0 ) buf.append( b );
|
||||
return buf.toString().replace("\\", "/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies all bytes from one file to another.
|
||||
*/
|
||||
public static void copyFile( File srcFile, File dstFile ) throws IOException {
|
||||
FileInputStream is = null;
|
||||
FileOutputStream os = null;
|
||||
try {
|
||||
is = new FileInputStream( srcFile );
|
||||
os = new FileOutputStream( dstFile );
|
||||
|
||||
byte[] buf = new byte[4096];
|
||||
int len;
|
||||
while ( (len = is.read(buf)) >= 0 ) {
|
||||
os.write( buf, 0, len );
|
||||
}
|
||||
}
|
||||
finally {
|
||||
try {if (is != null) is.close();}
|
||||
catch (IOException e) {}
|
||||
|
||||
try {if (os != null) os.close();}
|
||||
catch (IOException e) {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates an MD5 hash of data from an InputStream.
|
||||
*
|
||||
* The returned string will be lowercase hexadecimal.
|
||||
*/
|
||||
public static String calcStreamMD5( InputStream is ) throws NoSuchAlgorithmException, IOException {
|
||||
MessageDigest md = MessageDigest.getInstance( "MD5" );
|
||||
byte[] buf = new byte[4096];
|
||||
int len;
|
||||
while ( (len = is.read(buf)) >= 0 ) {
|
||||
md.update( buf, 0, len );
|
||||
}
|
||||
|
||||
byte[] hashBytes = md.digest();
|
||||
StringBuilder hashStringBuf = new StringBuilder();
|
||||
for ( byte b : hashBytes ) {
|
||||
hashStringBuf.append( Integer.toString( (b & 0xff) + 0x100, 16 ).substring(1) );
|
||||
}
|
||||
return hashStringBuf.toString();
|
||||
}
|
||||
|
||||
public static String calcFileMD5( File f ) throws NoSuchAlgorithmException, IOException {
|
||||
String result = null;
|
||||
FileInputStream is = null;
|
||||
try {
|
||||
is = new FileInputStream(f);
|
||||
result = FTLDat.calcStreamMD5( is );
|
||||
}
|
||||
finally {
|
||||
try {if (is != null) is.close();}
|
||||
catch (Exception e) {}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an approximate byte count for humans.
|
||||
*/
|
||||
private String humanReadableByteCount( long bytes, boolean si ) {
|
||||
int unit = si ? 1000 : 1024;
|
||||
if ( bytes < unit ) return bytes +" B";
|
||||
int exp = (int)( Math.log(bytes) / Math.log(unit) );
|
||||
String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp-1) + (si ? "" : "i");
|
||||
return String.format( "%.1f %sB", (bytes / Math.pow(unit, exp)), pre );
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static class FTLDatException extends RuntimeException {
|
||||
public FTLDatException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public FTLDatException( String message ) {
|
||||
super( message );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Information about an innerFile within a dat.
|
||||
*
|
||||
* entryOffset - Offset (written in header) to
|
||||
* the dataSize + innerPath + data.
|
||||
* innerPath - A virtual location ("dir/dir/filename").
|
||||
* dataOffset - Offset to the innerFile.
|
||||
* dataSize - Size of the innerFile.
|
||||
*/
|
||||
public static class DatEntry {
|
||||
public long entryOffset = 0;
|
||||
public String innerPath = null;
|
||||
public long dataOffset = 0;
|
||||
public long dataSize = 0;
|
||||
|
||||
public DatEntry() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A holder for (innerPath + size) results from listSizes().
|
||||
*/
|
||||
public static class PathAndSize {
|
||||
public String path = null;
|
||||
public long size = 0;
|
||||
|
||||
public PathAndSize( String path, long size ) {
|
||||
this.path = path;
|
||||
this.size = size;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A holder for results after repacking a dat.
|
||||
*/
|
||||
public static class RepackResult {
|
||||
public long oldDatLength = 0;
|
||||
public long newDatLength = 0;
|
||||
public long bytesChanged = 0;
|
||||
|
||||
public RepackResult( long oldDatLength, long newDatLength, long bytesChanged ) {
|
||||
this.oldDatLength = oldDatLength;
|
||||
this.newDatLength = newDatLength;
|
||||
this.bytesChanged = bytesChanged;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* An InputStream wrapping a ByteBuffer.
|
||||
*
|
||||
* A memory-mapped region of a file is a ByteBuffer.
|
||||
*/
|
||||
public static class ByteBufferBackedInputStream extends InputStream {
|
||||
ByteBuffer buf;
|
||||
|
||||
public ByteBufferBackedInputStream( ByteBuffer buf ) {
|
||||
this.buf = buf;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int available() throws IOException {
|
||||
if ( !buf.hasRemaining() ) return 0;
|
||||
return buf.remaining();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int read() throws IOException {
|
||||
if ( !buf.hasRemaining() ) return -1;
|
||||
return buf.get() & 0xFF;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int read( byte[] bytes, int off, int len ) throws IOException {
|
||||
if ( !buf.hasRemaining() ) return -1;
|
||||
len = Math.min( len, buf.remaining() );
|
||||
buf.get(bytes, off, len);
|
||||
return len;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public abstract static class AbstractPack {
|
||||
|
||||
/**
|
||||
* Returns a descriptive name for this dat.
|
||||
*/
|
||||
public String getName() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an list of all innerPaths.
|
||||
*/
|
||||
public List<String> list() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of pairs of (innerPath, filesize).
|
||||
*/
|
||||
public List<PathAndSize> listSizes() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds bytes read from srcFile to the pack, as innerPath.
|
||||
*/
|
||||
public void add( String innerPath, InputStream is ) throws IOException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the contents of the file with innerPath to dstFile.
|
||||
*/
|
||||
public void extractTo( String innerPath, OutputStream os ) throws FileNotFoundException, IOException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the file with innerPath from the pack.
|
||||
*/
|
||||
public void remove( String innerPath ) throws FileNotFoundException, IOException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether innerPath is in the pack.
|
||||
*/
|
||||
public boolean contains( String innerPath ) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an InputStream get bytes from an innerFile.
|
||||
*
|
||||
* Close all input streams before calling methods to
|
||||
* modify this dat. Do not pass an input stream from
|
||||
* this dat instance into another of its own methods.
|
||||
*/
|
||||
public InputStream getInputStream( String innerPath ) throws FileNotFoundException, IOException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this dat and releases any system resources associated with the stream.
|
||||
*/
|
||||
public void close() throws IOException {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A pseudo-dat backed by a real filesystem.
|
||||
*
|
||||
* Files can be independently added/removed/altered
|
||||
* directly, so long as this class is not busy
|
||||
* at the time.
|
||||
*/
|
||||
public static class FolderPack extends AbstractPack {
|
||||
private File rootDir;
|
||||
|
||||
public FolderPack( File rootDir ) {
|
||||
this.rootDir = rootDir;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return rootDir.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> list() {
|
||||
List<String> result = new ArrayList<String>();
|
||||
|
||||
Stack<String> pendingPaths = new Stack<String>();
|
||||
pendingPaths.push( "" );
|
||||
|
||||
while ( !pendingPaths.isEmpty() ) {
|
||||
String current = pendingPaths.pop();
|
||||
File tmpFile = new File( rootDir, current );
|
||||
if ( tmpFile.isFile() ) {
|
||||
result.add( current );
|
||||
}
|
||||
else if ( tmpFile.isDirectory() ) {
|
||||
for ( String childName : tmpFile.list() ) {
|
||||
pendingPaths.push( FTLDat.ftlPathJoin(current, childName) );
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PathAndSize> listSizes() {
|
||||
List<PathAndSize> result = new ArrayList<PathAndSize>();
|
||||
List<String> innerPaths = list();
|
||||
for ( String innerPath : innerPaths ) {
|
||||
File tmpFile = getFile( innerPath );
|
||||
result.add( new PathAndSize( innerPath, tmpFile.length() ) );
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add( String innerPath, InputStream is ) throws IOException {
|
||||
File dstFile = getFile( innerPath );
|
||||
if ( dstFile.exists() ) throw new FTLDatException( "File already exists" );
|
||||
|
||||
dstFile.getParentFile().mkdirs();
|
||||
|
||||
FileOutputStream os = null;
|
||||
try {
|
||||
os = new FileOutputStream( dstFile );
|
||||
|
||||
byte[] buf = new byte[4096];
|
||||
int len;
|
||||
while ( (len = is.read(buf)) >= 0 ) {
|
||||
os.write( buf, 0, len );
|
||||
}
|
||||
}
|
||||
finally {
|
||||
try {if (os != null) os.close();}
|
||||
catch (IOException e) {}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extractTo( String innerPath, OutputStream os ) throws IOException {
|
||||
File srcFile = getFile( innerPath );
|
||||
|
||||
FileInputStream is = null;
|
||||
try {
|
||||
is = new FileInputStream( srcFile );
|
||||
|
||||
byte[] buf = new byte[4096];
|
||||
int len;
|
||||
while ( (len = is.read(buf)) >= 0 ) {
|
||||
os.write( buf, 0, len );
|
||||
}
|
||||
}
|
||||
finally {
|
||||
try {if (is != null) is.close();}
|
||||
catch (IOException e) {}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove( String innerPath ) {
|
||||
File tmpFile = getFile( innerPath );
|
||||
if ( tmpFile.exists() && tmpFile.isFile() ) {
|
||||
tmpFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains( String innerPath ) {
|
||||
File tmpFile = getFile( innerPath );
|
||||
return tmpFile.exists();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream( String innerPath ) throws FileNotFoundException, IOException {
|
||||
return new FileInputStream( getFile( innerPath ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a File object for an innerPath.
|
||||
* The location it represents is not guaranteed to exist.
|
||||
*/
|
||||
public File getFile( String innerPath ) {
|
||||
return new File( rootDir, innerPath );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static class FTLPack extends AbstractPack {
|
||||
private CharsetEncoder asciiEncoder = Charset.forName("US-ASCII").newEncoder();
|
||||
|
||||
private File datFile = null;
|
||||
private RandomAccessFile raf = null;
|
||||
private ArrayList<DatEntry> entryList = null;
|
||||
private Map<String,Integer> pathToIndexMap = null;
|
||||
private ByteBuffer byteBuffer = null;
|
||||
|
||||
public FTLPack( File datFile, boolean create ) throws IOException {
|
||||
this( datFile, create, 2048 );
|
||||
}
|
||||
|
||||
public FTLPack( File datFile, boolean create, int indexSize ) throws IOException {
|
||||
this.datFile = datFile;
|
||||
raf = new RandomAccessFile( datFile, "rw" );
|
||||
|
||||
if ( create ) {
|
||||
createIndex( indexSize );
|
||||
} else {
|
||||
readIndex();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a little-endian unsigned int.
|
||||
* Java doesn't have an unsigned int primitive,
|
||||
* so a long holds the value instead.
|
||||
*/
|
||||
private long readLittleUInt() throws IOException {
|
||||
if ( byteBuffer == null || !byteBuffer.hasArray() || byteBuffer.array().length < 4 ) {
|
||||
byteBuffer = ByteBuffer.wrap( new byte[4] );
|
||||
byteBuffer.order( ByteOrder.LITTLE_ENDIAN );
|
||||
}
|
||||
raf.readFully( byteBuffer.array(), 0, 4 );
|
||||
|
||||
// Read a signed int, then discard sign
|
||||
// by casting to long and hacking off bits.
|
||||
long result = byteBuffer.getInt( 0 );
|
||||
result &= 0x00000000FFFFFFFFL;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void writeLittleUInt( long n ) throws IOException {
|
||||
if ( byteBuffer == null || !byteBuffer.hasArray() || byteBuffer.array().length < 4 ) {
|
||||
byteBuffer = ByteBuffer.wrap( new byte[4] );
|
||||
byteBuffer.order( ByteOrder.LITTLE_ENDIAN );
|
||||
}
|
||||
|
||||
// Write a signed int, after discarding sign
|
||||
// by casting from long and hacking off bits.
|
||||
byteBuffer.putInt( 0, (int)(n & 0x00000000FFFFFFFFL) );
|
||||
|
||||
raf.write( byteBuffer.array(), 0, 4 );
|
||||
}
|
||||
|
||||
private String readLittleUString() throws IOException {
|
||||
long strLen = readLittleUInt();
|
||||
byte[] strBytes = new byte[ (int)strLen ];
|
||||
raf.readFully( strBytes );
|
||||
|
||||
return new String( strBytes, asciiEncoder.charset().name() );
|
||||
}
|
||||
|
||||
private void writeLittleUString( String s ) throws IOException {
|
||||
writeLittleUInt( s.length() );
|
||||
byte[] strBytes = s.getBytes( asciiEncoder.charset().name() );
|
||||
raf.write( strBytes );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the offset to seek within the header,
|
||||
* in order to read the offset of an innerFile entry.
|
||||
*
|
||||
* @param n the nth index.
|
||||
*/
|
||||
private long getHeaderIndexPosition( int n ) {
|
||||
return ( 4 + n*4 ); // 4-byte indexSize + 4-byte indeces.
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new index.
|
||||
* WARNING: This will erase the file.
|
||||
*/
|
||||
private void createIndex( int indexSize ) throws IOException {
|
||||
entryList = new ArrayList<DatEntry>( indexSize );
|
||||
for ( int i=0; i < indexSize; i++ )
|
||||
entryList.add( null );
|
||||
|
||||
pathToIndexMap = new HashMap<String,Integer>( indexSize );
|
||||
|
||||
raf.seek( 0 );
|
||||
raf.setLength( 0 );
|
||||
writeLittleUInt( indexSize );
|
||||
for ( int i=0; i < indexSize; i++ )
|
||||
writeLittleUInt( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads (or re-reads) the index from the file.
|
||||
*/
|
||||
private void readIndex() throws IOException {
|
||||
raf.seek( 0 );
|
||||
int indexSize = (int)readLittleUInt();
|
||||
if ( indexSize * 4 > raf.length() ) {
|
||||
throw new IOException( String.format( "Corrupt dat file (%s): Its header claims to be larger than the entire file.", getName() ) );
|
||||
}
|
||||
|
||||
entryList = new ArrayList<DatEntry>( indexSize );
|
||||
for ( int i=0; i < indexSize; i++ )
|
||||
entryList.add( null );
|
||||
|
||||
pathToIndexMap = new HashMap<String,Integer>( indexSize );
|
||||
|
||||
// Store partial DatEntry objects in entryList (leaving nulls where absent).
|
||||
for ( int i=0; i < indexSize; i++ ) {
|
||||
long entryOffset = readLittleUInt();
|
||||
|
||||
if ( entryOffset != 0 ) {
|
||||
DatEntry entry = new DatEntry();
|
||||
entry.entryOffset = entryOffset;
|
||||
entryList.set( i, entry );
|
||||
}
|
||||
}
|
||||
|
||||
for ( int i=0; i < indexSize; i++ ) {
|
||||
DatEntry entry = entryList.get( i );
|
||||
if ( entry == null ) continue;
|
||||
|
||||
raf.seek( entry.entryOffset );
|
||||
entry.dataSize = readLittleUInt();
|
||||
entry.innerPath = readLittleUString();
|
||||
entry.dataOffset = raf.getChannel().position();
|
||||
|
||||
if ( pathToIndexMap.containsKey( entry.innerPath ) ) {
|
||||
throw new IOException( "InnerPath occurs more than once: "+ entry.innerPath );
|
||||
}
|
||||
pathToIndexMap.put( entry.innerPath, new Integer(i) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the nth index's entry to the end of the file.
|
||||
* It will still be nth in the header, however.
|
||||
* Used by growIndex().
|
||||
*/
|
||||
private void moveEntryToEOF( int n ) throws IOException {
|
||||
DatEntry entry = entryList.get( n );
|
||||
long oldOffset = entry.entryOffset;
|
||||
long newOffset = raf.length();
|
||||
|
||||
long totalBytes = (entry.dataOffset-entry.entryOffset) + entry.dataSize;
|
||||
long bytesRemaining = totalBytes;
|
||||
byte[] buf = new byte[4096];
|
||||
int len;
|
||||
while ( bytesRemaining > 0 ) {
|
||||
raf.seek( oldOffset + totalBytes - bytesRemaining );
|
||||
len = raf.read( buf, 0, (int)Math.min(buf.length, bytesRemaining) );
|
||||
if ( len == -1 ) {
|
||||
throw new IOException( "EOF prematurely reached reading innerPath: "+ entry.innerPath );
|
||||
}
|
||||
|
||||
raf.seek( newOffset + totalBytes - bytesRemaining );
|
||||
raf.write( buf, 0, len );
|
||||
bytesRemaining -= len;
|
||||
}
|
||||
// Update the index.
|
||||
raf.seek( getHeaderIndexPosition( n ) );
|
||||
writeLittleUInt( newOffset );
|
||||
entry.dataOffset = ( newOffset + (entry.dataOffset-entry.entryOffset) );
|
||||
entry.entryOffset = newOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the index has room for at least <amount> entries.
|
||||
* This is done by moving the first innerFile after the index
|
||||
* to the end of the file. The region it used to occupy can then
|
||||
* be filled with additional indeces.
|
||||
*/
|
||||
private void growIndex( int amount ) throws IOException {
|
||||
int freeRoom = -1;
|
||||
|
||||
while ( true ) {
|
||||
int vacancyCount = Collections.frequency( entryList, null );
|
||||
|
||||
if ( entryList.size() - vacancyCount == 0 ) {
|
||||
// There is no innerFile after the index. We can grow
|
||||
// as much as we like. Limit ourselves to amount.
|
||||
freeRoom = amount;
|
||||
break;
|
||||
}
|
||||
else {
|
||||
// Find the used index with the lowest entryOffset.
|
||||
int earliestUsedIndex = -1;
|
||||
long minEntryOffset = Long.MAX_VALUE;
|
||||
for ( int i=0; i < entryList.size(); i++ ) {
|
||||
DatEntry entry = entryList.get( i );
|
||||
if ( entry.entryOffset < minEntryOffset ) {
|
||||
earliestUsedIndex = i;
|
||||
minEntryOffset = entry.entryOffset;
|
||||
}
|
||||
}
|
||||
// (region between header and first innerFile entry) / (possible 4-byte ints).
|
||||
freeRoom = (int)( ( minEntryOffset - getHeaderIndexPosition( entryList.size() ) ) / 4 );
|
||||
|
||||
if ( freeRoom >= amount ) {
|
||||
freeRoom = amount; // We don't need hundreds of thousands more.
|
||||
break;
|
||||
}
|
||||
|
||||
// If it's not enough, move the first file and check again.
|
||||
moveEntryToEOF( earliestUsedIndex );
|
||||
}
|
||||
}
|
||||
// Expand the header to claim the vacated region.
|
||||
for ( int i=0; i < freeRoom; i++ ) {
|
||||
entryList.add( null );
|
||||
}
|
||||
raf.seek( 0 );
|
||||
writeLittleUInt( entryList.size() );
|
||||
raf.seek( getHeaderIndexPosition(entryList.size() - freeRoom) );
|
||||
for ( int i=0; i < freeRoom; i++ ) {
|
||||
writeLittleUInt( 0 );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return datFile.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> list() {
|
||||
List<String> result = new ArrayList<String>();
|
||||
result.addAll( pathToIndexMap.keySet() );
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PathAndSize> listSizes() {
|
||||
List<PathAndSize> result = new ArrayList<PathAndSize>();
|
||||
for ( DatEntry entry : entryList ) {
|
||||
if ( entry == null ) continue;
|
||||
PathAndSize pas = new PathAndSize( entry.innerPath, entry.dataSize );
|
||||
result.add( pas );
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add( String innerPath, InputStream is ) throws IOException {
|
||||
if ( pathToIndexMap.containsKey( innerPath ) ) {
|
||||
throw new FTLDatException( "InnerPath already exists: "+ innerPath );
|
||||
}
|
||||
if ( !asciiEncoder.canEncode( innerPath ) ) {
|
||||
throw new FTLDatException( "InnerPath contains non-ascii characters: "+ innerPath );
|
||||
}
|
||||
|
||||
// Find a vacancy in the header, or create one.
|
||||
int entryIndex = entryList.indexOf( null );
|
||||
if ( entryIndex == -1 ) {
|
||||
growIndex( 50 ); // Save effort for 49 future adds.
|
||||
entryIndex = entryList.indexOf( null );
|
||||
}
|
||||
|
||||
DatEntry entry = new DatEntry();
|
||||
entry.entryOffset = raf.length();
|
||||
entry.innerPath = innerPath;
|
||||
entry.dataSize = 0; // Write this later.
|
||||
|
||||
raf.seek( getHeaderIndexPosition( entryIndex ) );
|
||||
writeLittleUInt( entry.entryOffset );
|
||||
|
||||
raf.seek( entry.entryOffset );
|
||||
writeLittleUInt( entry.dataSize );
|
||||
writeLittleUString( entry.innerPath );
|
||||
entry.dataOffset = raf.getChannel().position();
|
||||
|
||||
byte[] buf = new byte[4096];
|
||||
int len;
|
||||
while ( (len = is.read(buf)) >= 0 ) {
|
||||
raf.write( buf, 0, len );
|
||||
}
|
||||
|
||||
// Go back and fill in the dataSize.
|
||||
entry.dataSize = raf.getChannel().position() - entry.dataOffset;
|
||||
raf.seek( entry.entryOffset );
|
||||
writeLittleUInt( entry.dataSize );
|
||||
|
||||
entryList.set( entryIndex, entry );
|
||||
pathToIndexMap.put( innerPath, entryIndex );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extractTo( String innerPath, OutputStream os ) throws FileNotFoundException, IOException {
|
||||
if ( !pathToIndexMap.containsKey( innerPath ) ) {
|
||||
throw new FileNotFoundException( "InnerPath does not exist: "+ innerPath );
|
||||
}
|
||||
|
||||
int entryIndex = pathToIndexMap.get( innerPath ).intValue();
|
||||
DatEntry entry = entryList.get( entryIndex );
|
||||
|
||||
raf.seek( entry.dataOffset );
|
||||
|
||||
long bytesRemaining = entry.dataSize;
|
||||
byte[] buf = new byte[4096];
|
||||
int len;
|
||||
while ( bytesRemaining > 0 ) {
|
||||
raf.seek( entry.dataOffset + entry.dataSize - bytesRemaining );
|
||||
len = raf.read( buf, 0, (int)Math.min(buf.length, bytesRemaining) );
|
||||
if ( len == -1 ) {
|
||||
throw new IOException( "EOF prematurely reached reading innerPath: "+ entry.innerPath );
|
||||
}
|
||||
|
||||
os.write( buf, 0, len );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove( String innerPath ) throws FileNotFoundException, IOException {
|
||||
if ( !pathToIndexMap.containsKey( innerPath ) ) {
|
||||
throw new FileNotFoundException( "InnerPath does not exist: "+ innerPath );
|
||||
}
|
||||
|
||||
int entryIndex = pathToIndexMap.get( innerPath ).intValue();
|
||||
pathToIndexMap.remove( innerPath );
|
||||
DatEntry removedEntry = entryList.set( entryIndex, null );
|
||||
|
||||
raf.seek( getHeaderIndexPosition( entryIndex ) );
|
||||
writeLittleUInt( 0 );
|
||||
|
||||
if ( removedEntry.dataOffset + removedEntry.dataSize == raf.length() ) {
|
||||
// Data appeared at the end. Truncate.
|
||||
raf.setLength( removedEntry.entryOffset );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains( String innerPath ) {
|
||||
return pathToIndexMap.containsKey( innerPath );
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream( String innerPath ) throws FileNotFoundException, IOException {
|
||||
if ( !pathToIndexMap.containsKey( innerPath ) ) {
|
||||
throw new FileNotFoundException( "InnerPath does not exist: "+ innerPath );
|
||||
}
|
||||
|
||||
int entryIndex = pathToIndexMap.get( innerPath ).intValue();
|
||||
DatEntry entry = entryList.get( entryIndex );
|
||||
|
||||
// Create a stream that can only see this region.
|
||||
// Multiple read-only streams can coexist (each has its own position).
|
||||
InputStream stream = new FileChannelRegionInputStream( raf.getChannel(), entry.dataOffset, entry.dataSize );
|
||||
|
||||
// Create a stream that can only see this region.
|
||||
// There must be no other reads/seeks until it is closed.
|
||||
//InputStream tmpStream = Channels.newInputStream( raf.getChannel().position( entry.dataOffset ) );
|
||||
//InputStream stream = new SizeLimitInputStream( tmpStream, entry.dataSize );
|
||||
|
||||
// Mapped regions may not garbage collect promptly.
|
||||
// That would keep the file in use: bad.
|
||||
// Closing raf doesn't affect them. :/
|
||||
// This method has best I/O performance though.
|
||||
//MappedByteBuffer buf = raf.getChannel().map( FileChannel.MapMode.READ_ONLY, entry.dataOffset, entry.dataSize );
|
||||
//buf.load();
|
||||
//InputStream stream = new ByteBufferBackedInputStream( buf );
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
raf.close();
|
||||
}
|
||||
|
||||
public List<DatEntry> listMetadata() {
|
||||
return new ArrayList<DatEntry>( entryList );
|
||||
}
|
||||
|
||||
/**
|
||||
* Repacks the dat file. This will remove gaps, which could
|
||||
* be created when adding, removing or replacing files.
|
||||
*/
|
||||
public RepackResult repack() throws IOException {
|
||||
// Build a list of non-null entries, sorted in the order their data appears.
|
||||
|
||||
ArrayList<DatEntry> tmpEntries = new ArrayList<DatEntry>( pathToIndexMap.size() );
|
||||
for ( Map.Entry<String,Integer> mapping : pathToIndexMap.entrySet() ) {
|
||||
Integer iObj = mapping.getValue();
|
||||
DatEntry entry = entryList.get( iObj.intValue() );
|
||||
if ( entry != null ) {
|
||||
tmpEntries.add( entry );
|
||||
} else { // The following should never happen!
|
||||
throw new IOException ( "Bad entryIndex for innerPath: "+ mapping.getKey() );
|
||||
}
|
||||
}
|
||||
Collections.sort( tmpEntries, new Comparator<DatEntry>() {
|
||||
public int compare( DatEntry a, DatEntry b ) {
|
||||
if ( b == null ) return -1;
|
||||
if ( a == null ) return 1;
|
||||
DatEntry dA = (DatEntry)a;
|
||||
DatEntry dB = (DatEntry)b;
|
||||
if ( dA.entryOffset < dB.entryOffset ) return -1;
|
||||
if ( dA.entryOffset > dB.entryOffset ) return 1;
|
||||
return 0;
|
||||
}
|
||||
@Override
|
||||
public boolean equals( Object o ) {
|
||||
return ( o != null ? o == this : false );
|
||||
}
|
||||
} );
|
||||
|
||||
for ( int i=0; i < tmpEntries.size()-1; i++ ) {
|
||||
DatEntry a = tmpEntries.get( i );
|
||||
DatEntry b = tmpEntries.get( i+1 );
|
||||
if ( a.dataOffset+a.dataSize > b.entryOffset ) {
|
||||
throw new IOException( String.format( "Cannot repack datfile with overlapping entries (\"%s\" and \"%s\").", a.innerPath, b.innerPath ) );
|
||||
}
|
||||
}
|
||||
|
||||
pathToIndexMap.clear(); // entryList gets replaced later.
|
||||
long bytesChanged = 0;
|
||||
|
||||
// Write the header size.
|
||||
if ( tmpEntries.size() != entryList.size() ) {
|
||||
raf.seek( 0 );
|
||||
writeLittleUInt( tmpEntries.size() );
|
||||
bytesChanged += 4;
|
||||
}
|
||||
|
||||
long pendingEntryOffset = getHeaderIndexPosition( tmpEntries.size() );
|
||||
|
||||
for ( int i=0; i < tmpEntries.size(); i++ ) {
|
||||
DatEntry entry = tmpEntries.get ( i );
|
||||
pathToIndexMap.put( entry.innerPath, new Integer(i) );
|
||||
|
||||
// Write the header index.
|
||||
raf.seek( getHeaderIndexPosition( i ) );
|
||||
writeLittleUInt( pendingEntryOffset );
|
||||
bytesChanged += 4;
|
||||
|
||||
// Shift the entry toward the start of the dat.
|
||||
if ( pendingEntryOffset != entry.entryOffset ) {
|
||||
long totalBytes = (entry.dataOffset-entry.entryOffset) + entry.dataSize;
|
||||
long bytesRemaining = totalBytes;
|
||||
byte[] buf = new byte[4096];
|
||||
int len;
|
||||
while ( bytesRemaining > 0 ) {
|
||||
raf.seek( entry.entryOffset + totalBytes - bytesRemaining );
|
||||
len = raf.read( buf, 0, (int)Math.min(buf.length, bytesRemaining) );
|
||||
if ( len == -1 ) {
|
||||
throw new IOException( "EOF prematurely reached reading innerPath: "+ entry.innerPath );
|
||||
}
|
||||
|
||||
raf.seek( pendingEntryOffset + totalBytes - bytesRemaining );
|
||||
raf.write( buf, 0, len );
|
||||
bytesRemaining -= len;
|
||||
}
|
||||
|
||||
entry.dataOffset = pendingEntryOffset + (entry.dataOffset-entry.entryOffset);
|
||||
entry.entryOffset = pendingEntryOffset;
|
||||
bytesChanged += totalBytes;
|
||||
}
|
||||
|
||||
pendingEntryOffset += (entry.dataOffset-entry.entryOffset) + entry.dataSize;
|
||||
}
|
||||
|
||||
entryList = tmpEntries;
|
||||
|
||||
long oldDatLength = raf.length();
|
||||
long newDatLength = pendingEntryOffset;
|
||||
raf.setLength( newDatLength );
|
||||
return new RepackResult( oldDatLength, newDatLength, bytesChanged );
|
||||
}
|
||||
}
|
||||
}
|
115
src/main/java/net/vhati/ftldat/FileChannelRegionInputStream.java
Normal file
115
src/main/java/net/vhati/ftldat/FileChannelRegionInputStream.java
Normal file
|
@ -0,0 +1,115 @@
|
|||
package net.vhati.ftldat;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.nio.channels.FileChannel;
|
||||
|
||||
|
||||
public class FileChannelRegionInputStream extends InputStream {
|
||||
|
||||
private FileChannel channel;
|
||||
private long regionOffset;
|
||||
private long regionLength;
|
||||
|
||||
// A buffer holds an even narrower region of the file.
|
||||
// When possible read() calls will reuse this,
|
||||
// rather than pester the channel.
|
||||
private ByteBuffer buf = null;
|
||||
private long bufOffset = 0; // Relative to regionOffset.
|
||||
private int bufLength = 0;
|
||||
|
||||
private long intraPos = 0;
|
||||
|
||||
|
||||
public FileChannelRegionInputStream( FileChannel channel, long offset, long length ) {
|
||||
this( channel, offset, length, 4096 );
|
||||
}
|
||||
|
||||
public FileChannelRegionInputStream( FileChannel channel, long offset, long length, int bufferSize ) {
|
||||
this.channel = channel;
|
||||
this.regionOffset = offset;
|
||||
this.regionLength = length;
|
||||
buf = ByteBuffer.allocate( bufferSize );
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
if ( !channel.isOpen() ) throw new ClosedChannelException();
|
||||
return bufLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if ( !channel.isOpen() ) throw new ClosedChannelException();
|
||||
if ( intraPos >= regionLength ) return -1;
|
||||
|
||||
if ( intraPos < bufOffset || intraPos >= bufOffset+bufLength ) {
|
||||
// The requested byte isn't currently buffered.
|
||||
bufOffset = intraPos;
|
||||
int len = 0; // Get *something*.
|
||||
buf.position( 0 );
|
||||
while ( len == 0 ) {
|
||||
len = channel.read( buf, regionOffset + bufOffset );
|
||||
}
|
||||
if ( len == -1 ) {
|
||||
bufLength = 0;
|
||||
return -1;
|
||||
} else {
|
||||
bufLength = len;
|
||||
}
|
||||
}
|
||||
|
||||
// Do an absolute get() from the buffer.
|
||||
int result = buf.get( (int)(intraPos - bufOffset) );
|
||||
intraPos++;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read( byte[] b, int bOff, int bLen ) throws IOException {
|
||||
if ( bLen == 0 ) return 0;
|
||||
if ( bOff < 0 ) throw new IndexOutOfBoundsException( String.format( "Index: %d, Size: %d", bOff, bLen ) );
|
||||
if ( bOff + bLen > b.length ) throw new IndexOutOfBoundsException( String.format( "Index: %d, Size: %d", (bOff+bLen), bLen ) );
|
||||
if ( !channel.isOpen() ) throw new ClosedChannelException();
|
||||
if ( intraPos >= regionLength ) return -1;
|
||||
|
||||
int bytesTotal = Math.min( bLen, (int)(regionLength - intraPos) );
|
||||
int bytesRemaining = bytesTotal;
|
||||
|
||||
if ( intraPos >= bufOffset || intraPos < bufOffset+bufLength ) {
|
||||
// Read part of the current buffer, possibly until the end.
|
||||
|
||||
buf.position( (int)(intraPos - bufOffset) );
|
||||
int bufTodo = Math.min( bytesRemaining, bufLength - (int)(intraPos - bufOffset) );
|
||||
buf.get( b, bOff, bufTodo );
|
||||
bytesRemaining -= bufTodo;
|
||||
intraPos += bufTodo;
|
||||
}
|
||||
|
||||
if ( bytesRemaining > 0 ) {
|
||||
// Refill the buffer at the current intraPos.
|
||||
|
||||
bufOffset = intraPos;
|
||||
int len = 0;
|
||||
buf.position( 0 );
|
||||
len = channel.read( buf, regionOffset + bufOffset );
|
||||
if ( len == -1 ) {
|
||||
bufLength = 0;
|
||||
throw new BufferUnderflowException();
|
||||
} else {
|
||||
bufLength = len;
|
||||
}
|
||||
|
||||
buf.position( 0 );
|
||||
int bufTodo = Math.min( bytesRemaining, bufLength );
|
||||
buf.get( b, bOff, bufTodo );
|
||||
bytesRemaining -= bufTodo;
|
||||
intraPos += bufTodo;
|
||||
}
|
||||
|
||||
return ( bytesTotal - bytesRemaining ); // Return number of bytes read.
|
||||
}
|
||||
}
|
247
src/main/java/net/vhati/ftldat/SizeLimitInputStream.java
Normal file
247
src/main/java/net/vhati/ftldat/SizeLimitInputStream.java
Normal file
|
@ -0,0 +1,247 @@
|
|||
/*
|
||||
* Input stream wrapper with a byte limit.
|
||||
* Copyright (C) 2004 Stephen Ostermiller
|
||||
* http://ostermiller.org/contact.pl?regarding=Java+Utilities
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* See COPYING.TXT for details.
|
||||
*/
|
||||
|
||||
package net.vhati.ftldat;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
|
||||
/**
|
||||
* An input stream wrapper that will read only a set number of bytes from the
|
||||
* underlying stream.
|
||||
*
|
||||
* @author Stephen Ostermiller http://ostermiller.org/contact.pl?regarding=Java+Utilities
|
||||
* @since ostermillerutils 1.04.00
|
||||
*/
|
||||
public class SizeLimitInputStream extends InputStream {
|
||||
|
||||
/**
|
||||
* The input stream that is being protected.
|
||||
* All methods should be forwarded to it,
|
||||
* after checking the size that has been read.
|
||||
*
|
||||
* @since ostermillerutils 1.04.00
|
||||
*/
|
||||
protected InputStream in;
|
||||
|
||||
/**
|
||||
* The number of bytes to read at most from this
|
||||
* Stream. Read methods should
|
||||
* check to ensure that bytesRead never
|
||||
* exceeds maxBytesToRead.
|
||||
*
|
||||
* @since ostermillerutils 1.04.00
|
||||
*/
|
||||
protected long maxBytesToRead = 0;
|
||||
|
||||
/**
|
||||
* The number of bytes that have been read
|
||||
* from this stream. Read methods should
|
||||
* check to ensure that bytesRead never
|
||||
* exceeds maxBytesToRead.
|
||||
*
|
||||
* @since ostermillerutils 1.04.00
|
||||
*/
|
||||
protected long bytesRead = 0;
|
||||
|
||||
/**
|
||||
* The number of bytes that have been read
|
||||
* from this stream since mark() was called.
|
||||
*
|
||||
* @since ostermillerutils 1.04.00
|
||||
*/
|
||||
protected long bytesReadSinceMark = 0;
|
||||
|
||||
/**
|
||||
* The number of bytes the user has request
|
||||
* to have been marked for reset.
|
||||
*
|
||||
* @since ostermillerutils 1.04.00
|
||||
*/
|
||||
protected long markReadLimitBytes = -1;
|
||||
|
||||
/**
|
||||
* Get the number of bytes actually read
|
||||
* from this stream.
|
||||
*
|
||||
* @return number of bytes that have already been taken from this stream.
|
||||
*
|
||||
* @since ostermillerutils 1.04.00
|
||||
*/
|
||||
public long getBytesRead(){
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum number of bytes left to read
|
||||
* before the limit (set in the constructor) is reached.
|
||||
*
|
||||
* @return The number of bytes that (at a maximum) are left to be taken from this stream.
|
||||
*
|
||||
* @since ostermillerutils 1.04.00
|
||||
*/
|
||||
public long getBytesLeft(){
|
||||
return maxBytesToRead - bytesRead;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell whether the number of bytes specified
|
||||
* in the constructor have been read yet.
|
||||
*
|
||||
* @return true iff the specified number of bytes have all been read.
|
||||
*
|
||||
* @since ostermillerutils 1.04.00
|
||||
*/
|
||||
public boolean allBytesRead(){
|
||||
return getBytesLeft() == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of total bytes (including bytes already read)
|
||||
* that can be read from this stream (as set in the constructor).
|
||||
* @return Maximum bytes that can be read until the size limit runs out
|
||||
*
|
||||
* @since ostermillerutils 1.04.00
|
||||
*/
|
||||
public long getMaxBytesToRead(){
|
||||
return maxBytesToRead;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new size limit input stream from
|
||||
* another stream given a size limit.
|
||||
*
|
||||
* @param in The input stream.
|
||||
* @param maxBytesToRead the max number of bytes to allow to be read from the underlying stream.
|
||||
*
|
||||
* @since ostermillerutils 1.04.00
|
||||
*/
|
||||
public SizeLimitInputStream( InputStream in, long maxBytesToRead ){
|
||||
this.in = in;
|
||||
this.maxBytesToRead = maxBytesToRead;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override public int read() throws IOException {
|
||||
if ( bytesRead >= maxBytesToRead ){
|
||||
return -1;
|
||||
}
|
||||
int b = in.read();
|
||||
if ( b != -1 ){
|
||||
bytesRead++;
|
||||
bytesReadSinceMark++;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override public int read( byte[] b ) throws IOException {
|
||||
return this.read( b, 0, b.length );
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override public int read( byte[] b, int off, int len ) throws IOException {
|
||||
if ( bytesRead >= maxBytesToRead ){
|
||||
return -1;
|
||||
}
|
||||
long bytesLeft = getBytesLeft();
|
||||
if ( len > bytesLeft ){
|
||||
len = (int)bytesLeft;
|
||||
}
|
||||
int bytesJustRead = in.read( b, off, len );
|
||||
bytesRead += bytesJustRead;
|
||||
bytesReadSinceMark += bytesJustRead;
|
||||
return bytesJustRead;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override public long skip(long n) throws IOException {
|
||||
if ( bytesRead >= maxBytesToRead ){
|
||||
return -1;
|
||||
}
|
||||
long bytesLeft = getBytesLeft();
|
||||
if ( n > bytesLeft ){
|
||||
n = bytesLeft;
|
||||
}
|
||||
return in.skip(n);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override public int available() throws IOException {
|
||||
int available = in.available();
|
||||
long bytesLeft = getBytesLeft();
|
||||
if ( available > bytesLeft ){
|
||||
available = (int)bytesLeft;
|
||||
}
|
||||
return available;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close this stream and underlying streams.
|
||||
* Calling this method may make data on the
|
||||
* underlying stream unavailable.
|
||||
* <p>
|
||||
* Consider wrapping this stream in a NoCloseStream
|
||||
* so that clients can
|
||||
* call close() with no effect.
|
||||
*
|
||||
* @since ostermillerutils 1.04.00
|
||||
*/
|
||||
@Override public void close() throws IOException {
|
||||
in.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override public void mark(int readlimit){
|
||||
if ( in.markSupported() ){
|
||||
markReadLimitBytes = readlimit;
|
||||
bytesReadSinceMark = 0;
|
||||
in.mark(readlimit);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override public void reset() throws IOException {
|
||||
if ( in.markSupported() && bytesReadSinceMark <= markReadLimitBytes ){
|
||||
bytesRead -= bytesReadSinceMark;
|
||||
in.reset();
|
||||
bytesReadSinceMark = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override public boolean markSupported(){
|
||||
return in.markSupported();
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue