Add multiBundle support

including fake headers
This commit is contained in:
VaDiM 2025-03-08 20:45:03 +03:00
parent db4eb30a27
commit bc0e32efec
8 changed files with 317 additions and 69 deletions

View File

@ -201,7 +201,7 @@ namespace AssetStudio
{
if (!assetsFileListHash.Contains(reader.FileName))
{
Logger.Info($"Loading {reader.FullPath}");
Logger.Info($"Loading \"{reader.FullPath}\"");
try
{
var assetsFile = new SerializedFile(reader, this);
@ -248,13 +248,13 @@ namespace AssetStudio
}
catch (Exception e)
{
Logger.Warning($"Failed to read assets file {reader.FullPath}\r\n{e}");
Logger.Warning($"Failed to read assets file \"{reader.FullPath}\"\n{e}");
reader.Dispose();
}
}
else
{
Logger.Info($"Skipping {reader.FullPath}");
Logger.Info($"Skipping \"{reader.FullPath}\"");
reader.Dispose();
}
return true;
@ -284,38 +284,46 @@ namespace AssetStudio
}
catch (Exception e)
{
Logger.Warning($"Failed to read assets file {reader.FullPath} from {Path.GetFileName(originalPath)}\r\n{e}");
Logger.Warning($"Failed to read assets file \"{reader.FullPath}\" from {Path.GetFileName(originalPath)}\n{e}");
resourceFileReaders.TryAdd(reader.FileName, reader);
}
}
else
{
Logger.Info($"Skipping {originalPath} ({reader.FileName})");
Logger.Info($"Skipping \"{originalPath}\" ({reader.FileName})");
}
return true;
}
private bool LoadBundleFile(FileReader reader, string originalPath = null)
{
Logger.Info("Loading " + reader.FullPath);
Logger.Info($"Loading \"{reader.FullPath}\"");
Logger.Debug($"Bundle offset: {reader.Position}");
var bundleStream = new OffsetStream(reader);
var bundleReader = new FileReader(reader.FullPath, bundleStream);
try
{
var bundleFile = new BundleFile(reader, ZstdEnabled, specifiedUnityVersion);
foreach (var file in bundleFile.fileList)
var bundleFile = new BundleFile(bundleReader, ZstdEnabled, specifiedUnityVersion);
var isLoaded = LoadBundleFiles(bundleReader, bundleFile, originalPath);
if (!isLoaded)
return false;
while (bundleFile.IsMultiBundle && isLoaded)
{
var dummyPath = Path.Combine(Path.GetDirectoryName(reader.FullPath), file.fileName);
var subReader = new FileReader(dummyPath, file.stream);
if (subReader.FileType == FileType.AssetsFile)
bundleStream.Offset = reader.Position;
bundleReader = new FileReader($"{reader.FullPath}_0x{bundleStream.Offset:X}", bundleStream);
if (bundleReader.Position > 0)
{
if (!LoadAssetsFromMemory(subReader, originalPath ?? reader.FullPath, bundleFile.m_Header.unityRevision))
return false;
}
else
{
resourceFileReaders.TryAdd(file.fileName, subReader);
bundleStream.Offset += bundleReader.Position;
bundleReader.FullPath = $"{reader.FullPath}_0x{bundleStream.Offset:X}";
bundleReader.FileName = $"{reader.FileName}_0x{bundleStream.Offset:X}";
}
Logger.Info($"[MultiBundle] Loading \"{reader.FileName}\" from offset: 0x{bundleStream.Offset:X}");
bundleFile = new BundleFile(bundleReader, ZstdEnabled, specifiedUnityVersion);
isLoaded = LoadBundleFiles(bundleReader, bundleFile, originalPath ?? reader.FullPath);
}
return true;
return isLoaded;
}
catch (NotSupportedException e)
{
@ -324,23 +332,42 @@ namespace AssetStudio
}
catch (Exception e)
{
var str = $"Error while reading bundle file {reader.FullPath}";
var str = $"Error while reading bundle file \"{bundleReader.FullPath}\"";
if (originalPath != null)
{
str += $" from {Path.GetFileName(originalPath)}";
}
Logger.Warning($"{str}\r\n{e}");
Logger.Warning($"{str}\n{e}");
return true;
}
finally
{
reader.Dispose();
bundleReader.Dispose();
}
}
private bool LoadBundleFiles(FileReader reader, BundleFile bundleFile, string originalPath = null)
{
foreach (var file in bundleFile.fileList)
{
var dummyPath = Path.Combine(Path.GetDirectoryName(reader.FullPath), file.fileName);
var subReader = new FileReader(dummyPath, file.stream);
if (subReader.FileType == FileType.AssetsFile)
{
if (!LoadAssetsFromMemory(subReader, originalPath ?? reader.FullPath, bundleFile.m_Header.unityRevision))
return false;
}
else
{
resourceFileReaders.TryAdd(file.fileName, subReader);
}
}
return true;
}
private void LoadWebFile(FileReader reader)
{
Logger.Info("Loading " + reader.FullPath);
Logger.Info($"Loading \"{reader.FullPath}\"");
try
{
var webFile = new WebFile(reader);
@ -367,7 +394,7 @@ namespace AssetStudio
}
catch (Exception e)
{
Logger.Error($"Error while reading web file {reader.FullPath}", e);
Logger.Error($"Error while reading web file \"{reader.FullPath}\"", e);
}
finally
{
@ -427,7 +454,7 @@ namespace AssetStudio
}
catch (Exception e)
{
Logger.Warning($"Error while reading zip split file {basePath}\r\n{e}");
Logger.Warning($"Error while reading zip split file \"{basePath}\"\n{e}");
}
}
@ -461,7 +488,7 @@ namespace AssetStudio
}
catch (Exception e)
{
Logger.Warning($"Error while reading zip entry {entry.FullName}\r\n{e}");
Logger.Warning($"Error while reading zip entry \"{entry.FullName}\"\n{e}");
}
}
}

View File

@ -42,6 +42,8 @@ namespace AssetStudio
public class BundleFile
{
public readonly bool IsMultiBundle;
public class Header
{
public string signature;
@ -102,6 +104,17 @@ namespace AssetStudio
case "UnityFS":
ReadHeader(reader);
var bundleSize = m_Header.size;
var streamSize = reader.BaseStream.Length;
if (bundleSize > streamSize)
{
Logger.Warning("Bundle size is incorrect.");
}
else if (streamSize - bundleSize > 200)
{
IsMultiBundle = true;
}
var isUnityCnEnc = false;
var unityVer = m_Header.unityRevision;
if (specUnityVer != null)

View File

@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using static AssetStudio.EndianSpanReader;
namespace AssetStudio
{
@ -59,24 +57,25 @@ namespace AssetStudio
}
isBigEndian = BitConverter.ToBoolean(modelData, 5);
var modelDataSpan = modelData.AsSpan();
//offsets
var countInfoTableOffset = (int)SpanToUint32(modelData, 64, isBigEndian);
var canvasInfoOffset = (int)SpanToUint32(modelData, 68, isBigEndian);
var partIdsOffset = SpanToUint32(modelData, 76, isBigEndian);
var parameterIdsOffset = SpanToUint32(modelData, 264, isBigEndian);
var countInfoTableOffset = (int)modelDataSpan.ReadUInt32(64, isBigEndian);
var canvasInfoOffset = (int)modelDataSpan.ReadUInt32(68, isBigEndian);
var partIdsOffset = modelDataSpan.ReadUInt32(76, isBigEndian);
var parameterIdsOffset = modelDataSpan.ReadUInt32(264, isBigEndian);
//canvas
PixelPerUnit = ToSingle(modelData, canvasInfoOffset, isBigEndian);
CentralPosX = ToSingle(modelData, canvasInfoOffset + 4, isBigEndian);
CentralPosY = ToSingle(modelData, canvasInfoOffset + 8, isBigEndian);
CanvasWidth = ToSingle(modelData, canvasInfoOffset + 12, isBigEndian);
CanvasHeight = ToSingle(modelData, canvasInfoOffset + 16, isBigEndian);
PixelPerUnit = modelDataSpan.ReadSingle(canvasInfoOffset, isBigEndian);
CentralPosX = modelDataSpan.ReadSingle(canvasInfoOffset + 4, isBigEndian);
CentralPosY = modelDataSpan.ReadSingle(canvasInfoOffset + 8, isBigEndian);
CanvasWidth = modelDataSpan.ReadSingle(canvasInfoOffset + 12, isBigEndian);
CanvasHeight = modelDataSpan.ReadSingle(canvasInfoOffset + 16, isBigEndian);
//model
PartCount = SpanToUint32(modelData, countInfoTableOffset, isBigEndian);
ParamCount = SpanToUint32(modelData, countInfoTableOffset + 20, isBigEndian);
PartNames = ReadMocStringHashSet(modelData, (int)partIdsOffset, (int)PartCount);
ParamNames = ReadMocStringHashSet(modelData, (int)parameterIdsOffset, (int)ParamCount);
PartCount = modelDataSpan.ReadUInt32(countInfoTableOffset, isBigEndian);
ParamCount = modelDataSpan.ReadUInt32(countInfoTableOffset + 20, isBigEndian);
PartNames = ReadMocStrings(modelData, (int)partIdsOffset, (int)PartCount);
ParamNames = ReadMocStrings(modelData, (int)parameterIdsOffset, (int)ParamCount);
}
public void SaveMoc3(string savePath)
@ -103,16 +102,7 @@ namespace AssetStudio
}
}
private static float ToSingle(ReadOnlySpan<byte> data, int index, bool isBigEndian) //net framework ver
{
var bytes = data.Slice(index, index + 4).ToArray();
if ((isBigEndian && BitConverter.IsLittleEndian) || (!isBigEndian && !BitConverter.IsLittleEndian))
(bytes[0], bytes[1], bytes[2], bytes[3]) = (bytes[3], bytes[2], bytes[1], bytes[0]);
return BitConverter.ToSingle(bytes, 0);
}
private static HashSet<string> ReadMocStringHashSet(ReadOnlySpan<byte> data, int index, int count)
private static HashSet<string> ReadMocStrings(Span<byte> data, int index, int count)
{
const int strLen = 64;
var strHashSet = new HashSet<string>();
@ -120,8 +110,8 @@ namespace AssetStudio
{
if (index + i * strLen <= data.Length)
{
var buff = data.Slice(index + i * strLen, strLen);
strHashSet.Add(Encoding.UTF8.GetString(buff.ToArray()).TrimEnd('\0'));
var str = data.Slice(index + i * strLen, strLen).ReadStringToNull();
strHashSet.Add(str);
}
}
return strHashSet;

View File

@ -82,6 +82,7 @@ namespace AssetStudio
return base.ReadUInt64();
}
#if NETFRAMEWORK
public override float ReadSingle()
{
if (Endian == EndianType.BigEndian)
@ -103,5 +104,26 @@ namespace AssetStudio
}
return base.ReadDouble();
}
#else
public override float ReadSingle()
{
if (Endian == EndianType.BigEndian)
{
Read(buffer, 0, 4);
return BinaryPrimitives.ReadSingleBigEndian(buffer);
}
return base.ReadSingle();
}
public override double ReadDouble()
{
if (Endian == EndianType.BigEndian)
{
Read(buffer, 0, 8);
return BinaryPrimitives.ReadDoubleBigEndian(buffer);
}
return base.ReadDouble();
}
#endif
}
}

View File

@ -1,29 +1,90 @@
using System;
using System.Buffers.Binary;
using System.Text;
namespace AssetStudio
{
public static class EndianSpanReader
{
public static uint SpanToUint32(Span<byte> data, int start, bool isBigEndian)
public static uint ReadUInt32(this Span<byte> data, int start, bool isBigEndian)
{
return SpanToUInt32(data, start, isBigEndian);
}
public static uint SpanToUInt32(Span<byte> data, int start, bool isBigEndian)
{
return isBigEndian
? BinaryPrimitives.ReadUInt32BigEndian(data.Slice(start))
: BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(start));
}
public static uint SpanToUint16(Span<byte> data, int start, bool isBigEndian)
public static long ReadUInt16(this Span<byte> data, int start, bool isBigEndian)
{
return SpanToUInt16(data, start, isBigEndian);
}
public static uint SpanToUInt16(Span<byte> data, int start, bool isBigEndian)
{
return isBigEndian
? BinaryPrimitives.ReadUInt16BigEndian(data.Slice(start))
: BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(start));
}
public static long ReadInt64(this Span<byte> data, int start, bool isBigEndian)
{
return SpanToInt64(data, start, isBigEndian);
}
public static long SpanToInt64(Span<byte> data, int start, bool isBigEndian)
{
return isBigEndian
? BinaryPrimitives.ReadInt64BigEndian(data.Slice(start))
: BinaryPrimitives.ReadInt64LittleEndian(data.Slice(start));
}
public static float ReadSingle(this Span<byte> data, int start, bool isBigEndian)
{
return SpanToSingle(data, start, isBigEndian);
}
#if NETFRAMEWORK
public static float SpanToSingle(Span<byte> data, int start, bool isBigEndian)
{
var bytes = data.Slice(start, 4);
if ((isBigEndian && BitConverter.IsLittleEndian) || (!isBigEndian && !BitConverter.IsLittleEndian))
bytes.Reverse();
return BitConverter.ToSingle(bytes.ToArray(), 0);
}
#else
public static float SpanToSingle(Span<byte> data, int start, bool isBigEndian)
{
return isBigEndian
? BinaryPrimitives.ReadSingleBigEndian(data[start..])
: BinaryPrimitives.ReadSingleLittleEndian(data[start..]);
}
#endif
public static string ReadStringToNull(this Span<byte> data, int maxLength = 32767)
{
Span<byte> bytes = stackalloc byte[maxLength];
var count = 0;
while (count != data.Length && count < maxLength)
{
var b = data[count];
if (b == 0)
{
break;
}
bytes[count] = b;
count++;
}
bytes = bytes.Slice(0, count);
#if NETFRAMEWORK
return Encoding.UTF8.GetString(bytes.ToArray());
#else
return Encoding.UTF8.GetString(bytes);
#endif
}
}
}

View File

@ -53,7 +53,11 @@ namespace AssetStudio
count++;
}
bytes = bytes.Slice(0, count);
#if NETFRAMEWORK
return encoding?.GetString(bytes.ToArray()) ?? Encoding.UTF8.GetString(bytes.ToArray());
#else
return encoding?.GetString(bytes) ?? Encoding.UTF8.GetString(bytes);
#endif
}
private static string ReadUnicodeStringToNull(this BinaryReader reader, int maxLength)

View File

@ -1,6 +1,5 @@
using System;
using System.IO;
using static AssetStudio.EndianSpanReader;
namespace AssetStudio
{
@ -14,6 +13,9 @@ namespace AssetStudio
private static readonly byte[] brotliMagic = { 0x62, 0x72, 0x6F, 0x74, 0x6C, 0x69 };
private static readonly byte[] zipMagic = { 0x50, 0x4B, 0x03, 0x04 };
private static readonly byte[] zipSpannedMagic = { 0x50, 0x4B, 0x07, 0x08 };
private static readonly byte[] unityFsMagic = {0x55, 0x6E, 0x69, 0x74, 0x79, 0x46, 0x53};
private static readonly int headerBuffLen = 1152;
private static byte[] headerBuff = new byte[headerBuffLen];
public FileReader(string path) : this(path, File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { }
@ -26,14 +28,19 @@ namespace AssetStudio
private FileType CheckFileType()
{
var signature = this.ReadStringToNull(20);
var buff = headerBuff.AsSpan();
buff.Clear();
var dataLen = Read(headerBuff, 0, headerBuffLen);
Position = 0;
var signature = buff.ReadStringToNull(20);
switch (signature)
{
case "UnityWeb":
case "UnityRaw":
case "UnityArchive":
case "UnityFS":
CheckBundleDataOffset(buff);
return FileType.BundleFile;
case "UnityWebData1.0":
return FileType.WebFile;
@ -41,17 +48,15 @@ namespace AssetStudio
return FileType.WebFile;
default:
{
var buff = ReadBytes(40).AsSpan();
var magic = Span<byte>.Empty;
Position = 0;
magic = buff.Length > 2 ? buff.Slice(0, 2) : magic;
magic = dataLen > 2 ? buff.Slice(0, 2) : magic;
if (magic.SequenceEqual(gzipMagic))
{
return FileType.GZipFile;
}
magic = buff.Length > 38 ? buff.Slice(32, 6) : magic;
magic = dataLen > 38 ? buff.Slice(32, 6) : magic;
if (magic.SequenceEqual(brotliMagic))
{
return FileType.BrotliFile;
@ -62,12 +67,17 @@ namespace AssetStudio
return FileType.AssetsFile;
}
magic = buff.Length > 4 ? buff.Slice(0, 4): magic;
magic = dataLen > 4 ? buff.Slice(0, 4): magic;
if (magic.SequenceEqual(zipMagic) || magic.SequenceEqual(zipSpannedMagic))
{
return FileType.ZipFile;
}
if (CheckBundleDataOffset(buff))
{
return FileType.BundleFile;
}
return FileType.ResourceFile;
}
}
@ -82,10 +92,10 @@ namespace AssetStudio
}
var isBigEndian = Endian == EndianType.BigEndian;
//var m_MetadataSize = SpanToUint32(buff, 0, isBigEndian);
long m_FileSize = SpanToUint32(buff, 4, isBigEndian);
var m_Version = SpanToUint32(buff, 8, isBigEndian);
long m_DataOffset = SpanToUint32(buff, 12, isBigEndian);
//var m_MetadataSize = buff.ReadUInt32(0, isBigEndian);
long m_FileSize = buff.ReadUInt32(4, isBigEndian);
var m_Version = buff.ReadUInt32(8, isBigEndian);
long m_DataOffset = buff.ReadUInt32(12, isBigEndian);
//var m_Endianess = buff[16];
//var m_Reserved = buff.Slice(17, 3);
if (m_Version >= 22)
@ -94,15 +104,44 @@ namespace AssetStudio
{
return false;
}
//m_MetadataSize = SpanToUint32(buff, 20, isBigEndian);
m_FileSize = SpanToInt64(buff, 24, isBigEndian);
m_DataOffset = SpanToInt64(buff, 32, isBigEndian);
//m_MetadataSize = buff.ReadUInt32(20, isBigEndian);
m_FileSize = buff.ReadInt64(24, isBigEndian);
m_DataOffset = buff.ReadInt64(32, isBigEndian);
}
if (m_FileSize != fileSize || m_DataOffset > fileSize)
{
return false;
}
return true;
}
private bool CheckBundleDataOffset(ReadOnlySpan<byte> buff)
{
var lastOffset = buff.LastIndexOf(unityFsMagic);
if (lastOffset <= 0)
return false;
var firstOffset = buff.IndexOf(unityFsMagic);
if (firstOffset == lastOffset || lastOffset - firstOffset < 200)
{
Position = lastOffset;
return true;
}
Position = firstOffset;
_ = this.ReadStringToNull();
_ = this.ReadUInt32();
_ = this.ReadStringToNull();
_ = this.ReadStringToNull();
var bundleSize = this.ReadInt64();
if (bundleSize > 200 && firstOffset + bundleSize < lastOffset)
{
Position = firstOffset;
return true;
}
Position = lastOffset;
return true;
}
}

View File

@ -0,0 +1,92 @@
using System.IO;
namespace AssetStudio
{
public class OffsetStream : Stream
{
private readonly Stream _baseStream;
private long _offset;
public override bool CanRead => _baseStream.CanRead;
public override bool CanSeek => _baseStream.CanSeek;
public override bool CanWrite => false;
public override long Length => _baseStream.Length - _offset;
public override long Position
{
get => _baseStream.Position - _offset;
set => Seek(value, SeekOrigin.Begin);
}
public long BasePosition => _baseStream.Position;
public long Offset
{
get => _offset;
set
{
if (value < 0 || value > _baseStream.Length)
{
throw new IOException($"{nameof(Offset)} is out of stream bound");
}
_offset = value;
Seek(0, SeekOrigin.Begin);
}
}
public OffsetStream(FileReader reader)
{
_baseStream = reader.BaseStream;
Offset = reader.Position;
}
public override void Flush() { }
public override int Read(byte[] buffer, int offset, int count)
{
return _baseStream.Read(buffer, offset, count);
}
public override long Seek(long offset, SeekOrigin origin)
{
if (offset > _baseStream.Length)
{
throw new IOException("Unable to seek beyond stream bound");
}
switch (origin)
{
case SeekOrigin.Begin:
_baseStream.Seek(offset + _offset, SeekOrigin.Begin);
break;
case SeekOrigin.Current:
_baseStream.Seek(offset + Position, SeekOrigin.Begin);
break;
case SeekOrigin.End:
_baseStream.Seek(offset + _baseStream.Length, SeekOrigin.Begin);
break;
}
return Position;
}
public override void SetLength(long value)
{
throw new System.NotImplementedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new System.NotImplementedException();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_baseStream.Dispose();
}
base.Dispose(disposing);
}
}
}