/*
** 2013 June 16
**
** The author disclaims copyright to this source code. In place of
** a legal notice, here is a blessing:
** May you do good and not evil.
** May you find forgiveness for yourself and forgive others.
** May you share freely, never taking more than you give.
*/
package info.ata4.junity.bundle;
import info.ata4.io.DataReader;
import info.ata4.io.DataWriter;
import info.ata4.io.Struct;
import info.ata4.junity.UnityVersion;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
/**
* Structure for Unity asset bundles.
*
* @author Nico Bergemann <barracuda415 at yahoo.de>
* @unity UnityWebStreamHeader
*/
public class BundleHeader implements Struct {
public static final String SIGNATURE_WEB = "UnityWeb";
public static final String SIGNATURE_RAW = "UnityRaw";
public static final String SIGNATURE_FS = "UnityFS";
// UnityWeb or UnityRaw
private String signature;
// file version
// 6 in Unity 5.3+ (UnityFS files)
// 3 in Unity 3.5 and 4
// 2 in Unity 2.6 to 3.4
// 1 in Unity 1 to 2.5
private int streamVersion;
// player version string
// 2.x.x for Unity 2
// 3.x.x for Unity 3/4
// 5.x.x for Unity 5
private UnityVersion unityVersion = new UnityVersion();
// engine version string
private UnityVersion unityRevision = new UnityVersion();
// minimum number of bytes to read for streamed bundles,
// equal to completeFileSize for normal bundles
private long minimumStreamedBytes;
// offset to the bundle data or size of the bundle header
private int headerSize;
// equal to 1 if it's a streamed bundle, number of levelX + mainData assets
// otherwise
private int numberOfLevelsToDownload;
// list of compressed and uncompressed offsets
private List<Pair<Long, Long>> levelByteEnd = new ArrayList<>();
// equal to file size, sometimes equal to uncompressed data size without the header
private long completeFileSize;
// offset to the first asset file within the data area? equals compressed
// file size if completeFileSize contains the uncompressed data size
private long dataHeaderSize;
// (UnityFS) length of the possibly-compressed (LZMA, LZ4) bundle data header
private int compressedDataHeaderSize;
// (UnityFS) flags
// 0x100 = <unknown>
// 0x80 = data header at end of file
// 0x40 = entry info present
// 0x3f = low six bits are data header compression method
// 0 = none
// 1 = LZMA
// 3 = LZ4
private int flags;
@Override
public void read(DataReader in) throws IOException {
signature = in.readStringNull();
streamVersion = in.readInt();
unityVersion = new UnityVersion(in.readStringNull());
unityRevision = new UnityVersion(in.readStringNull());
if (signature.equals(SIGNATURE_FS)) {
// FS signature
// Expect streamVersion == 6
completeFileSize = in.readLong();
compressedDataHeaderSize = in.readInt();
dataHeaderSize = in.readInt();
flags = in.readInt();
headerSize = (int) in.position();
if ((flags & 0x80) == 0) {
// The data header is part of the bundle header
headerSize += compressedDataHeaderSize;
}
// else it's at the end of the file
} else {
// Web or Raw signature
minimumStreamedBytes = in.readUnsignedInt();
headerSize = in.readInt();
numberOfLevelsToDownload = in.readInt();
int numberOfLevels = in.readInt();
levelByteEnd.clear();
for (int i = 0; i < numberOfLevels; i++) {
levelByteEnd.add(new ImmutablePair(in.readUnsignedInt(), in.readUnsignedInt()));
}
if (streamVersion >= 2) {
completeFileSize = in.readUnsignedInt();
}
if (streamVersion >= 3) {
dataHeaderSize = in.readUnsignedInt();
}
in.readByte();
}
}
@Override
public void write(DataWriter out) throws IOException {
out.writeStringNull(signature);
out.writeInt(streamVersion);
out.writeStringNull(unityVersion.toString());
out.writeStringNull(unityRevision.toString());
out.writeUnsignedInt(minimumStreamedBytes);
out.writeInt(headerSize);
out.writeInt(numberOfLevelsToDownload);
out.writeInt(levelByteEnd.size());
for (Pair<Long, Long> offset : levelByteEnd) {
out.writeUnsignedInt(offset.getLeft());
out.writeUnsignedInt(offset.getRight());
}
if (streamVersion >= 2) {
out.writeUnsignedInt(completeFileSize);
}
if (streamVersion >= 3) {
out.writeUnsignedInt(dataHeaderSize);
}
out.writeUnsignedByte(0);
}
public boolean hasValidSignature() {
return signature.equals(SIGNATURE_WEB) || signature.equals(SIGNATURE_RAW)
|| signature.equals(SIGNATURE_FS);
}
public void compressed(boolean compressed) {
signature = compressed ? SIGNATURE_WEB : SIGNATURE_RAW;
}
public boolean compressed() {
return signature.equals(SIGNATURE_WEB);
}
public String signature() {
return signature;
}
public void signature(String signature) {
this.signature = signature;
}
public int streamVersion() {
return streamVersion;
}
public void streamVersion(int format) {
this.streamVersion = format;
}
public UnityVersion unityVersion() {
return unityVersion;
}
public void unityVersion(UnityVersion version) {
this.unityVersion = Objects.requireNonNull(version);
}
public UnityVersion unityRevision() {
return unityRevision;
}
public void unityRevision(UnityVersion revision) {
this.unityRevision = Objects.requireNonNull(revision);
}
public int headerSize() {
return headerSize;
}
public void headerSize(int dataOffset) {
this.headerSize = dataOffset;
}
public List<Pair<Long, Long>> levelByteEnd() {
return levelByteEnd;
}
public int numberOfLevels() {
return levelByteEnd.size();
}
public int numberOfLevelsToDownload() {
return numberOfLevelsToDownload;
}
public void numberOfLevelsToDownload(int numberOfLevelsToDownload) {
this.numberOfLevelsToDownload = numberOfLevelsToDownload;
}
public long completeFileSize() {
return completeFileSize;
}
public void completeFileSize(long completeFileSize) {
this.completeFileSize = completeFileSize;
}
public long minimumStreamedBytes() {
return minimumStreamedBytes;
}
public void minimumStreamedBytes(long minimumStreamedBytes) {
this.minimumStreamedBytes = minimumStreamedBytes;
}
public long dataHeaderSize() {
return dataHeaderSize;
}
public void dataHeaderSize(long dataHeaderSize) {
this.dataHeaderSize = dataHeaderSize;
}
public int compressedDataHeaderSize() { return compressedDataHeaderSize; }
public int dataHeaderCompressionScheme() { return (flags & 0x3f); }
public boolean dataHeaderAtEndOfFile() { return (flags & 0x80) != 0; }
public boolean entryInfoPresent() { return (signature.equals(SIGNATURE_FS)) ? ((flags & 0x40) != 0) : true; }
}