// Copied from a snapshot of Slipstream Mod Manager after 1.4.
// https://github.com/Vhati/Slipstream-Mod-Manager/blob/b94440e0a0e0f295a181706129dddd745cb0d85c/src/main/java/net/vhati/ftldat/FTLDat.java
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; // For Memory-mapped streams
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;
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.
*/
public static 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 );
}
/**
* 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.
*
* The contains() method returns true for directories,
* but remove() will not delete them. The list()
* method will not include directories themselves,
* only files within.
*/
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 IOException( "InnerPath already exists: "+ innerPath );
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 ) {
if ( innerPath.indexOf("\\") != -1 ) throw new IllegalArgumentException( "InnerPath contains backslashes: "+ innerPath );
File tmpFile = new File( rootDir, innerPath );
// Check if the file is inside rootDir.
File parentDir = tmpFile.getParentFile();
while( parentDir != null ) {
if ( parentDir.equals( rootDir ) ) return tmpFile;
parentDir = parentDir.getParentFile();
}
throw new IllegalArgumentException( String.format( "InnerPath \"%s\" is outside the FolderPack at \"%s\".", innerPath, rootDir ) );
}
}
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;
/**
* Opens or creates a dat in various modes.
* When creating, the initial index size will be 2048.
*
* @see FTLPack(File datFile, String mode, int indexSize)
*/
public FTLPack( File datFile, String mode ) throws IOException {
this( datFile, mode, 2048 );
}
/**
* Opens or creates a dat in various modes.
*
* The mode must be one of the following:
* r - opens an existing dat, read-only.
* r+ - opens an existing dat, read/write.
* w+ - creates a new empty dat, read/write.
*
* @param datFile a file to open/create
* @param mode see above
* @param indexSize size of the initial index if creating
*/
public FTLPack( File datFile, String mode, int indexSize ) throws IOException {
if ( mode.equals( "r" ) ) {
if ( !datFile.exists() )
throw new FileNotFoundException( String.format( "The datFile was not found: %s", datFile.getPath() ) );
this.datFile = datFile;
raf = new RandomAccessFile( datFile, "r" );
readIndex();
}
else if ( mode.equals( "r+" ) ) {
if ( !datFile.exists() )
throw new FileNotFoundException( String.format( "The datFile was not found: %s", datFile.getPath() ) );
this.datFile = datFile;
raf = new RandomAccessFile( datFile, "rw" );
readIndex();
}
else if ( mode.equals( "w+" ) ) {
this.datFile = datFile;
raf = new RandomAccessFile( datFile, "rw" );
createIndex( indexSize );
}
else {
throw new IllegalArgumentException( String.format( "FTLPack constructor's mode arg was not 'r', 'r+', or 'w+' (%s).", mode ) );
}
}
/**
* 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 ( innerPath.indexOf("\\") != -1 ) throw new IllegalArgumentException( "InnerPath contains backslashes: "+ innerPath );
if ( pathToIndexMap.containsKey( innerPath ) ) {
throw new IOException( "InnerPath already exists: "+ innerPath );
}
if ( !asciiEncoder.canEncode( innerPath ) ) {
throw new IllegalArgumentException( "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 ( innerPath.indexOf("\\") != -1 ) throw new IllegalArgumentException( "InnerPath contains backslashes: "+ innerPath );
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 ( innerPath.indexOf("\\") != -1 ) throw new IllegalArgumentException( "InnerPath contains backslashes: "+ innerPath );
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 ) {
if ( innerPath.indexOf("\\") != -1 ) throw new IllegalArgumentException( "InnerPath contains backslashes: "+ innerPath );
return pathToIndexMap.containsKey( innerPath );
}
@Override
public InputStream getInputStream( String innerPath ) throws FileNotFoundException, IOException {
if ( innerPath.indexOf("\\") != -1 ) throw new IllegalArgumentException( "InnerPath contains backslashes: "+ innerPath );
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 );
// 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 );
}
}
}