diff --git a/AssetStudio/AssetsManager.cs b/AssetStudio/AssetsManager.cs index 0707bbf..3414bd2 100644 --- a/AssetStudio/AssetsManager.cs +++ b/AssetStudio/AssetsManager.cs @@ -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}"); } } } diff --git a/AssetStudio/BundleFile.cs b/AssetStudio/BundleFile.cs index 5e56826..70a3074 100644 --- a/AssetStudio/BundleFile.cs +++ b/AssetStudio/BundleFile.cs @@ -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) diff --git a/AssetStudio/CubismMoc.cs b/AssetStudio/CubismMoc.cs index 07c6ed1..3d93695 100644 --- a/AssetStudio/CubismMoc.cs +++ b/AssetStudio/CubismMoc.cs @@ -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 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 ReadMocStringHashSet(ReadOnlySpan data, int index, int count) + private static HashSet ReadMocStrings(Span data, int index, int count) { const int strLen = 64; var strHashSet = new HashSet(); @@ -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; diff --git a/AssetStudio/EndianBinaryReader.cs b/AssetStudio/EndianBinaryReader.cs index 9beb801..12097d2 100644 --- a/AssetStudio/EndianBinaryReader.cs +++ b/AssetStudio/EndianBinaryReader.cs @@ -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 } } diff --git a/AssetStudio/EndianSpanReader.cs b/AssetStudio/EndianSpanReader.cs index 9a946a9..622848e 100644 --- a/AssetStudio/EndianSpanReader.cs +++ b/AssetStudio/EndianSpanReader.cs @@ -1,29 +1,90 @@ using System; using System.Buffers.Binary; +using System.Text; namespace AssetStudio { public static class EndianSpanReader { - public static uint SpanToUint32(Span data, int start, bool isBigEndian) + public static uint ReadUInt32(this Span data, int start, bool isBigEndian) + { + return SpanToUInt32(data, start, isBigEndian); + } + + public static uint SpanToUInt32(Span data, int start, bool isBigEndian) { return isBigEndian ? BinaryPrimitives.ReadUInt32BigEndian(data.Slice(start)) : BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(start)); } - public static uint SpanToUint16(Span data, int start, bool isBigEndian) + public static long ReadUInt16(this Span data, int start, bool isBigEndian) + { + return SpanToUInt16(data, start, isBigEndian); + } + + public static uint SpanToUInt16(Span data, int start, bool isBigEndian) { return isBigEndian ? BinaryPrimitives.ReadUInt16BigEndian(data.Slice(start)) : BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(start)); } + public static long ReadInt64(this Span data, int start, bool isBigEndian) + { + return SpanToInt64(data, start, isBigEndian); + } + public static long SpanToInt64(Span data, int start, bool isBigEndian) { return isBigEndian ? BinaryPrimitives.ReadInt64BigEndian(data.Slice(start)) : BinaryPrimitives.ReadInt64LittleEndian(data.Slice(start)); } + + public static float ReadSingle(this Span data, int start, bool isBigEndian) + { + return SpanToSingle(data, start, isBigEndian); + } + +#if NETFRAMEWORK + public static float SpanToSingle(Span 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 data, int start, bool isBigEndian) + { + return isBigEndian + ? BinaryPrimitives.ReadSingleBigEndian(data[start..]) + : BinaryPrimitives.ReadSingleLittleEndian(data[start..]); + } +#endif + + public static string ReadStringToNull(this Span data, int maxLength = 32767) + { + Span 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 + } } } diff --git a/AssetStudio/Extensions/BinaryReaderExtensions.cs b/AssetStudio/Extensions/BinaryReaderExtensions.cs index df86cc3..2e967ce 100644 --- a/AssetStudio/Extensions/BinaryReaderExtensions.cs +++ b/AssetStudio/Extensions/BinaryReaderExtensions.cs @@ -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) diff --git a/AssetStudio/FileReader.cs b/AssetStudio/FileReader.cs index 76f384d..05fd8af 100644 --- a/AssetStudio/FileReader.cs +++ b/AssetStudio/FileReader.cs @@ -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.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 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; } } diff --git a/AssetStudio/OffsetStream.cs b/AssetStudio/OffsetStream.cs new file mode 100644 index 0000000..a0f768c --- /dev/null +++ b/AssetStudio/OffsetStream.cs @@ -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); + } + } +}