using K4os.Compression.LZ4; using System; using System.IO; using System.Linq; namespace AssetStudio { [Flags] public enum ArchiveFlags { CompressionTypeMask = 0x3f, BlocksAndDirectoryInfoCombined = 0x40, BlocksInfoAtTheEnd = 0x80, OldWebPluginCompatibility = 0x100, BlockInfoNeedPaddingAtStart = 0x200 } [Flags] public enum StorageBlockFlags { CompressionTypeMask = 0x3f, Streamed = 0x40 } public enum CompressionType { None, Lzma, Lz4, Lz4HC, Lzham, Lz4Mr0k } public class BundleFile { public class Header { public string signature; public uint version; public string unityVersion; public string unityRevision; public long size; public uint compressedBlocksInfoSize; public uint uncompressedBlocksInfoSize; public ArchiveFlags flags; } public class StorageBlock { public uint compressedSize; public uint uncompressedSize; public StorageBlockFlags flags; } public class Node { public long offset; public long size; public uint flags; public string path; } public Header m_Header; private StorageBlock[] m_BlocksInfo; private Node[] m_DirectoryInfo; public StreamFile[] FileList; public BundleFile(FileReader reader) { m_Header = new Header(); m_Header.signature = reader.ReadStringToNull(); if (reader.Game.Name == "BH3") { m_Header.version = 6; m_Header.unityVersion = "5.x.x"; m_Header.unityRevision = "2017.4.18f1"; } else if (reader.Game.Name == "SR_CB2" || reader.Game.Name == "SR_CB3") { var readHeader = m_Header.signature != "ENCR"; if (!readHeader) { m_Header.version = 7; m_Header.unityVersion = "5.x.x"; m_Header.unityRevision = "2019.4.32f1"; } else { m_Header.version = reader.ReadUInt32(); m_Header.unityVersion = reader.ReadStringToNull(); m_Header.unityRevision = reader.ReadStringToNull(); } } else { m_Header.version = reader.ReadUInt32(); m_Header.unityVersion = reader.ReadStringToNull(); m_Header.unityRevision = reader.ReadStringToNull(); } switch (m_Header.signature) { case "UnityArchive": break; //TODO case "UnityWeb": case "UnityRaw": if (m_Header.version == 6) { goto case "UnityFS"; } ReadHeaderAndBlocksInfo(reader); using (var blocksStream = CreateBlocksStream(reader.FullPath)) { ReadBlocksAndDirectory(reader, blocksStream); ReadFiles(blocksStream, reader.FullPath); } break; case "UnityFS": case "ENCR": ReadHeader(reader); if (reader.Game.Name == "ZZZ_CB1") { reader.AlignStream(0x10); } ReadBlocksInfoAndDirectory(reader); using (var blocksStream = CreateBlocksStream(reader.FullPath)) { ReadBlocks(reader, blocksStream); ReadFiles(blocksStream, reader.FullPath); } break; } } private void ReadHeaderAndBlocksInfo(EndianBinaryReader reader) { if (m_Header.version >= 4) { var hash = reader.ReadBytes(16); var crc = reader.ReadUInt32(); } var minimumStreamedBytes = reader.ReadUInt32(); m_Header.size = reader.ReadUInt32(); var numberOfLevelsToDownloadBeforeStreaming = reader.ReadUInt32(); var levelCount = reader.ReadInt32(); m_BlocksInfo = new StorageBlock[1]; for (int i = 0; i < levelCount; i++) { var storageBlock = new StorageBlock() { compressedSize = reader.ReadUInt32(), uncompressedSize = reader.ReadUInt32(), }; if (i == levelCount - 1) { m_BlocksInfo[0] = storageBlock; } } if (m_Header.version >= 2) { var completeFileSize = reader.ReadUInt32(); } if (m_Header.version >= 3) { var fileInfoHeaderSize = reader.ReadUInt32(); } reader.Position = m_Header.size; } private Stream CreateBlocksStream(string path) { Stream blocksStream; var uncompressedSizeSum = m_BlocksInfo.Sum(x => x.uncompressedSize); if (uncompressedSizeSum >= int.MaxValue) { /*var memoryMappedFile = MemoryMappedFile.CreateNew(null, uncompressedSizeSum); assetsDataStream = memoryMappedFile.CreateViewStream();*/ blocksStream = new FileStream(path + ".temp", FileMode.Create, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.DeleteOnClose); } else { blocksStream = new MemoryStream((int)uncompressedSizeSum); } return blocksStream; } private void ReadBlocksAndDirectory(EndianBinaryReader reader, Stream blocksStream) { var isCompressed = m_Header.signature == "UnityWeb"; foreach (var blockInfo in m_BlocksInfo) { var uncompressedBytes = reader.ReadBytes((int)blockInfo.compressedSize); if (isCompressed) { using (var memoryStream = new MemoryStream(uncompressedBytes)) { using (var decompressStream = SevenZipHelper.StreamDecompress(memoryStream)) { uncompressedBytes = decompressStream.ToArray(); } } } blocksStream.Write(uncompressedBytes, 0, uncompressedBytes.Length); } blocksStream.Position = 0; var blocksReader = new EndianBinaryReader(blocksStream); var nodesCount = blocksReader.ReadInt32(); m_DirectoryInfo = new Node[nodesCount]; for (int i = 0; i < nodesCount; i++) { m_DirectoryInfo[i] = new Node { path = blocksReader.ReadStringToNull(), offset = blocksReader.ReadUInt32(), size = blocksReader.ReadUInt32() }; } } public void ReadFiles(Stream blocksStream, string path) { FileList = new StreamFile[m_DirectoryInfo.Length]; for (int i = 0; i < m_DirectoryInfo.Length; i++) { var node = m_DirectoryInfo[i]; var file = new StreamFile(); FileList[i] = file; file.path = node.path; file.fileName = Path.GetFileName(node.path); if (node.size >= int.MaxValue) { /*var memoryMappedFile = MemoryMappedFile.CreateNew(null, entryinfo_size); file.stream = memoryMappedFile.CreateViewStream();*/ var extractPath = path + "_unpacked" + Path.DirectorySeparatorChar; Directory.CreateDirectory(extractPath); file.stream = new FileStream(extractPath + file.fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite); } else { file.stream = new MemoryStream((int)node.size); } blocksStream.Position = node.offset; blocksStream.CopyTo(file.stream, node.size); file.stream.Position = 0; } } private void DecryptHeader(int key) { var rand = new XORShift128(); rand.InitSeed(key); m_Header.flags ^= (ArchiveFlags)rand.NextDecryptInt(); m_Header.size ^= rand.NextDecryptLong(); m_Header.uncompressedBlocksInfoSize ^= rand.NextDecryptUInt(); m_Header.compressedBlocksInfoSize ^= rand.NextDecryptUInt(); } private void ReadHeader(EndianBinaryReader reader) { if (reader.Game.Name == "BH3") { var key = reader.ReadInt32(); m_Header.flags = (ArchiveFlags)reader.ReadUInt32(); m_Header.size = reader.ReadInt64(); m_Header.uncompressedBlocksInfoSize = reader.ReadUInt32(); m_Header.compressedBlocksInfoSize = reader.ReadUInt32(); DecryptHeader(key); var encUnityVersion = reader.ReadStringToNull(); var encUnityRevision = reader.ReadStringToNull(); } else { m_Header.size = reader.ReadInt64(); m_Header.compressedBlocksInfoSize = reader.ReadUInt32(); m_Header.uncompressedBlocksInfoSize = reader.ReadUInt32(); m_Header.flags = (ArchiveFlags)reader.ReadUInt32(); } } private void ReadBlocksInfoAndDirectory(EndianBinaryReader reader) { byte[] blocksInfoBytes; if (m_Header.version >= 7 && reader.Game.Name != "SR_CB2" && reader.Game.Name != "SR_CB3") { reader.AlignStream(16); } if ((m_Header.flags & ArchiveFlags.BlocksInfoAtTheEnd) != 0) //kArchiveBlocksInfoAtTheEnd { var position = reader.Position; reader.Position = reader.BaseStream.Length - m_Header.compressedBlocksInfoSize; blocksInfoBytes = reader.ReadBytes((int)m_Header.compressedBlocksInfoSize); reader.Position = position; } else //0x40 BlocksAndDirectoryInfoCombined { blocksInfoBytes = reader.ReadBytes((int)m_Header.compressedBlocksInfoSize); } MemoryStream blocksInfoUncompresseddStream; var uncompressedSize = m_Header.uncompressedBlocksInfoSize; var compressionType = (CompressionType)(m_Header.flags & ArchiveFlags.CompressionTypeMask); switch (compressionType) //kArchiveCompressionTypeMask { case CompressionType.None: //None { blocksInfoUncompresseddStream = new MemoryStream(blocksInfoBytes); break; } case CompressionType.Lzma: //LZMA { blocksInfoUncompresseddStream = new MemoryStream((int)(uncompressedSize)); using (var blocksInfoCompressedStream = new MemoryStream(blocksInfoBytes)) { SevenZipHelper.StreamDecompress(blocksInfoCompressedStream, blocksInfoUncompresseddStream, m_Header.compressedBlocksInfoSize, m_Header.uncompressedBlocksInfoSize); } blocksInfoUncompresseddStream.Position = 0; break; } case CompressionType.Lz4: //LZ4 case CompressionType.Lz4HC: //LZ4HC { var uncompressedBytes = new byte[uncompressedSize]; var numWrite = LZ4Codec.Decode(blocksInfoBytes, uncompressedBytes); if (numWrite != uncompressedSize) { throw new IOException($"Lz4 decompression error, write {numWrite} bytes but expected {uncompressedSize} bytes"); } blocksInfoUncompresseddStream = new MemoryStream(uncompressedBytes); break; } case CompressionType.Lz4Mr0k: //Lz4Mr0k var blocksInfoSize = blocksInfoBytes.Length; if (Mr0k.IsMr0k(blocksInfoBytes)) { Mr0k.Decrypt(ref blocksInfoBytes, ref blocksInfoSize); } goto case CompressionType.Lz4HC; default: throw new IOException($"Unsupported compression type {compressionType}"); } using (var blocksInfoReader = new EndianBinaryReader(blocksInfoUncompresseddStream)) { if ((reader.Game.Name != "SR_CB2" && reader.Game.Name != "SR_CB3") || m_Header.signature != "ENCR") { var uncompressedDataHash = blocksInfoReader.ReadBytes(16); } var blocksInfoCount = blocksInfoReader.ReadInt32(); m_BlocksInfo = new StorageBlock[blocksInfoCount]; for (int i = 0; i < blocksInfoCount; i++) { m_BlocksInfo[i] = new StorageBlock { uncompressedSize = blocksInfoReader.ReadUInt32(), compressedSize = blocksInfoReader.ReadUInt32(), flags = (StorageBlockFlags)blocksInfoReader.ReadUInt16() }; } var nodesCount = blocksInfoReader.ReadInt32(); m_DirectoryInfo = new Node[nodesCount]; for (int i = 0; i < nodesCount; i++) { m_DirectoryInfo[i] = new Node { offset = blocksInfoReader.ReadInt64(), size = blocksInfoReader.ReadInt64(), flags = blocksInfoReader.ReadUInt32(), path = blocksInfoReader.ReadStringToNull(), }; } } } private void ReadBlocks(EndianBinaryReader reader, Stream blocksStream) { foreach (var blockInfo in m_BlocksInfo) { var compressionType = (CompressionType)(blockInfo.flags & StorageBlockFlags.CompressionTypeMask); switch (compressionType) //kStorageBlockCompressionTypeMask { case CompressionType.None: //None { reader.BaseStream.CopyTo(blocksStream, blockInfo.compressedSize); break; } case CompressionType.Lzma: //LZMA { SevenZipHelper.StreamDecompress(reader.BaseStream, blocksStream, blockInfo.compressedSize, blockInfo.uncompressedSize); break; } case CompressionType.Lz4: //LZ4 case CompressionType.Lz4HC: //LZ4HC case CompressionType.Lz4Mr0k: //Lz4Mr0k { var compressedSize = (int)blockInfo.compressedSize; var compressedBytes = new byte[compressedSize]; reader.Read(compressedBytes, 0, compressedSize); if (compressionType == CompressionType.Lz4Mr0k && Mr0k.IsMr0k(compressedBytes)) { Mr0k.Decrypt(ref compressedBytes, ref compressedSize); } var uncompressedSize = (int)blockInfo.uncompressedSize; var uncompressedBytes = new byte[uncompressedSize]; var numWrite = LZ4Codec.Decode(compressedBytes, 0, compressedSize, uncompressedBytes, 0, uncompressedSize); if (numWrite != uncompressedSize) { throw new IOException($"Lz4 decompression error, write {numWrite} bytes but expected {uncompressedSize} bytes"); } blocksStream.Write(uncompressedBytes, 0, uncompressedSize); break; } default: throw new IOException($"Unsupported compression type {compressionType}"); } } blocksStream.Position = 0; } } }