From 58e0e9ab8df88382b3f31d63c22566b5a7b2d095 Mon Sep 17 00:00:00 2001 From: AXiX-official <2879710747@qq.com> Date: Sat, 18 May 2024 00:29:40 +0800 Subject: [PATCH] Init commit --- .gitignore | 5 + LICENSE | 21 + README.md | 3 + UnityAsset.NET.sln | 22 + .../BundleFile/BlocksAndDirectoryInfo.cs | 197 +++++++++ UnityAsset.NET/BundleFile/BundleFile.cs | 390 ++++++++++++++++++ UnityAsset.NET/BundleFile/CABInfo.cs | 41 ++ UnityAsset.NET/BundleFile/Header.cs | 65 +++ UnityAsset.NET/BundleFile/StorageBlockInfo.cs | 40 ++ UnityAsset.NET/BundleFile/UnityCN.cs | 237 +++++++++++ UnityAsset.NET/CRC32.cs | 82 ++++ UnityAsset.NET/Compression.cs | 61 +++ UnityAsset.NET/Enums/ArchiveFlags.cs | 12 + UnityAsset.NET/Enums/CompressionType.cs | 9 + UnityAsset.NET/Enums/CryptoType.cs | 7 + .../Enums/SerializedFileFormatVersion.cs | 80 ++++ UnityAsset.NET/Enums/StorageBlockFlags.cs | 8 + .../Extensions/ByteArrayExtensions.cs | 17 + UnityAsset.NET/Extensions/StreamExtensions.cs | 23 ++ UnityAsset.NET/IO/AssetReader.cs | 104 +++++ UnityAsset.NET/IO/AssetWriter.cs | 54 +++ UnityAsset.NET/SerializedFile/AssetsFile.cs | 6 + .../SerializedFile/SerializedFile.cs | 18 + .../SerializedFile/SerializedFileHeader.cs | 41 ++ UnityAsset.NET/UnityAsset.NET.csproj | 15 + 25 files changed, 1558 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 UnityAsset.NET.sln create mode 100644 UnityAsset.NET/BundleFile/BlocksAndDirectoryInfo.cs create mode 100644 UnityAsset.NET/BundleFile/BundleFile.cs create mode 100644 UnityAsset.NET/BundleFile/CABInfo.cs create mode 100644 UnityAsset.NET/BundleFile/Header.cs create mode 100644 UnityAsset.NET/BundleFile/StorageBlockInfo.cs create mode 100644 UnityAsset.NET/BundleFile/UnityCN.cs create mode 100644 UnityAsset.NET/CRC32.cs create mode 100644 UnityAsset.NET/Compression.cs create mode 100644 UnityAsset.NET/Enums/ArchiveFlags.cs create mode 100644 UnityAsset.NET/Enums/CompressionType.cs create mode 100644 UnityAsset.NET/Enums/CryptoType.cs create mode 100644 UnityAsset.NET/Enums/SerializedFileFormatVersion.cs create mode 100644 UnityAsset.NET/Enums/StorageBlockFlags.cs create mode 100644 UnityAsset.NET/Extensions/ByteArrayExtensions.cs create mode 100644 UnityAsset.NET/Extensions/StreamExtensions.cs create mode 100644 UnityAsset.NET/IO/AssetReader.cs create mode 100644 UnityAsset.NET/IO/AssetWriter.cs create mode 100644 UnityAsset.NET/SerializedFile/AssetsFile.cs create mode 100644 UnityAsset.NET/SerializedFile/SerializedFile.cs create mode 100644 UnityAsset.NET/SerializedFile/SerializedFileHeader.cs create mode 100644 UnityAsset.NET/UnityAsset.NET.csproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..add57be --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..03bdd08 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 AXiX + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a5caab6 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# UnityAsset.NET + +A .NET library for reading and modifying Unity assets and bundles. \ No newline at end of file diff --git a/UnityAsset.NET.sln b/UnityAsset.NET.sln new file mode 100644 index 0000000..12dfa57 --- /dev/null +++ b/UnityAsset.NET.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityAsset.NET", "UnityAsset.NET\UnityAsset.NET.csproj", "{B83CD1B5-07A2-4DB7-92C2-AD1D692945DE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "Test\Test.csproj", "{3B0C7B83-05AC-41A2-BFD2-9B7CDD4547EA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B83CD1B5-07A2-4DB7-92C2-AD1D692945DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B83CD1B5-07A2-4DB7-92C2-AD1D692945DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B83CD1B5-07A2-4DB7-92C2-AD1D692945DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B83CD1B5-07A2-4DB7-92C2-AD1D692945DE}.Release|Any CPU.Build.0 = Release|Any CPU + {3B0C7B83-05AC-41A2-BFD2-9B7CDD4547EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3B0C7B83-05AC-41A2-BFD2-9B7CDD4547EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3B0C7B83-05AC-41A2-BFD2-9B7CDD4547EA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3B0C7B83-05AC-41A2-BFD2-9B7CDD4547EA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/UnityAsset.NET/BundleFile/BlocksAndDirectoryInfo.cs b/UnityAsset.NET/BundleFile/BlocksAndDirectoryInfo.cs new file mode 100644 index 0000000..80130d2 --- /dev/null +++ b/UnityAsset.NET/BundleFile/BlocksAndDirectoryInfo.cs @@ -0,0 +1,197 @@ +using UnityAsset.NET.Enums; +using UnityAsset.NET.IO; + +namespace UnityAsset.NET.BundleFile; + +public class BlocksAndDirectoryInfo +{ + public byte[] UncompressedDataHash; + + public List BlocksInfo; + + public List DirectoryInfo; + + private uint uncompressedSize; + + public List BlocksInfoBytes; + + public BlocksAndDirectoryInfo(AssetReader reader, Header header, ref bool blocksInfoAtTheEnd) + { + if ((header.flags & ArchiveFlags.BlocksInfoAtTheEnd) != 0) //kArchiveBlocksInfoAtTheEnd + { + blocksInfoAtTheEnd = true; + long position = reader.Position; + reader.Position = reader.BaseStream.Length - header.compressedBlocksInfoSize; + BlocksInfoBytes = reader.ReadBytes((int)header.compressedBlocksInfoSize).ToList(); + reader.Position = position; + } + else //0x40 BlocksAndDirectoryInfoCombined + { + BlocksInfoBytes = reader.ReadBytes((int)header.compressedBlocksInfoSize).ToList(); + } + + ReadOnlySpan blocksInfoCompressedData = BlocksInfoBytes.ToArray(); + var compressionType = (CompressionType)(header.flags & ArchiveFlags.CompressionTypeMask); + uncompressedSize = header.uncompressedBlocksInfoSize; + MemoryStream blocksInfoUncompresseddStream = new MemoryStream((int)(uncompressedSize)); + switch (compressionType) //kArchiveCompressionTypeMask + { + case CompressionType.None: //None + { + blocksInfoUncompresseddStream = new MemoryStream(blocksInfoCompressedData.ToArray()); + break; + } + case CompressionType.Lzma: //LZMA + { + Compression.DecompressToStream(blocksInfoCompressedData, blocksInfoUncompresseddStream, uncompressedSize, "lzma"); + blocksInfoUncompresseddStream.Position = 0; + break; + } + case CompressionType.Lz4: //LZ4 + case CompressionType.Lz4HC: //LZ4HC + { + Compression.DecompressToStream(blocksInfoCompressedData, blocksInfoUncompresseddStream, uncompressedSize, "lz4"); + blocksInfoUncompresseddStream.Position = 0; + break; + } + default: + throw new IOException($"Unsupported compression type {compressionType}"); + } + + using AssetReader blocksInfoReader = new AssetReader(blocksInfoUncompresseddStream); + + UncompressedDataHash = blocksInfoReader.ReadBytes(16);// 除了ENCR + var blocksInfoCount = blocksInfoReader.ReadInt32(); + BlocksInfo = new List(); + for (int i = 0; i < blocksInfoCount; i++) + { + BlocksInfo.Add(new StorageBlockInfo(blocksInfoReader)); + } + + var directoryInfoCount = blocksInfoReader.ReadInt32(); + DirectoryInfo = new List(); + for (int i = 0; i < directoryInfoCount; i++) + { + DirectoryInfo.Add(new CABInfo(blocksInfoReader)); + } + } + + public void uncompresseFlags() + { + foreach (var block in BlocksInfo) + { + block.flags &= ~StorageBlockFlags.CompressionTypeMask; + block.compressedSize = block.uncompressedSize; + } + Update(); + } + + public void merge() + { + List newBlocksInfo = new List(); + + foreach (var block in BlocksInfo) + { + if (newBlocksInfo.Count > 0) + { + if (newBlocksInfo.Last().flags != block.flags) + { + throw new Exception("Expected all blocks to have the same flags"); + } + long newSize = (long)newBlocksInfo.Last().uncompressedSize + block.uncompressedSize; + if (newSize <= uint.MaxValue) + { + newBlocksInfo.Last().uncompressedSize = (uint)newSize; + newBlocksInfo.Last().compressedSize += block.compressedSize; + } + else + { + newBlocksInfo.Add(block); + } + } + else + { + newBlocksInfo.Add(block); + } + } + + BlocksInfo = newBlocksInfo; + Update(); + } + + public void Update(string compressionType = "none") + { + // 写入数据到BlocksInfoBytes + var BlocksInfoStream = new MemoryStream(); + using AssetWriter writer = new AssetWriter(BlocksInfoStream); + writer.Write(UncompressedDataHash); + writer.WriteInt32(BlocksInfo.Count); + foreach (var block in BlocksInfo) + { + block.Write(writer); + } + writer.WriteInt32(DirectoryInfo.Count); + foreach (var node in DirectoryInfo) + { + node.Write(writer); + } + + uncompressedSize = (uint)BlocksInfoStream.Length; + BlocksInfoBytes = Compression.CompressStream(BlocksInfoStream, compressionType); + } + + public void calculateSize(ref uint uncompressedBlocksInfoSize, ref uint compressedBlocksInfoSize) + { + uncompressedBlocksInfoSize = uncompressedSize; + compressedBlocksInfoSize = (uint)BlocksInfoBytes.Count; + } + + public void FixSize(int diff) + { + + if (DirectoryInfo[^1].size + diff <= UInt32.MaxValue && DirectoryInfo[^1].size + diff > 0) + { + if (BlocksInfo[^1].uncompressedSize + diff <= UInt32.MaxValue && BlocksInfo[^1].uncompressedSize + diff > 0) + { + if (diff > 0) + { + BlocksInfo[^1].uncompressedSize += (uint)diff; + BlocksInfo[^1].compressedSize += (uint)diff; + DirectoryInfo[^1].size += (uint)diff; + } + else + { + BlocksInfo[^1].uncompressedSize -= (uint)-diff; + BlocksInfo[^1].compressedSize -= (uint)-diff; + DirectoryInfo[^1].size -= (uint)-diff; + } + } + else + { + if (BlocksInfo[^1].uncompressedSize + diff <= UInt32.MaxValue) + { + BlocksInfo.Add(new StorageBlockInfo((uint)diff, (uint)diff ,BlocksInfo[^1].flags)); + DirectoryInfo[^1].size += (uint)diff; + } + else + { + diff += (int)BlocksInfo[^1].uncompressedSize; + diff = -diff; + BlocksInfo.Remove(BlocksInfo[^1]); + BlocksInfo[^1].uncompressedSize -= (uint)diff; + BlocksInfo[^1].compressedSize -= (uint)diff; + DirectoryInfo[^1].size -= (uint)diff; + } + } + } + else + { + throw new Exception("DirectoryInfo size overflow"); + } + } + + public void Write(AssetWriter writer) + { + writer.Write(BlocksInfoBytes.ToArray()); + } +} \ No newline at end of file diff --git a/UnityAsset.NET/BundleFile/BundleFile.cs b/UnityAsset.NET/BundleFile/BundleFile.cs new file mode 100644 index 0000000..c22c7d2 --- /dev/null +++ b/UnityAsset.NET/BundleFile/BundleFile.cs @@ -0,0 +1,390 @@ +using System.Text.RegularExpressions; + +using UnityAsset.NET.Enums; +using UnityAsset.NET.Extensions; +using UnityAsset.NET.IO; + +namespace UnityAsset.NET.BundleFile; + +public sealed class BundleFile +{ + public Header Header; + + public string? UnityCNKey; + + public UnityCN? UnityCNInfo; + + public BlocksAndDirectoryInfo DataInfo; + + public List cabStreams; + + public uint crc32; + + public bool BlocksInfoAtTheEnd = false; + + public bool HasBlockInfoNeedPaddingAtStart; + + private ArchiveFlags mask; + + public BundleFile(Stream input, bool original = false, string? key = null) + { + using AssetReader reader = new AssetReader(input); + + UnityCNKey = key; + + Header = new Header(reader); + + if (Header.version >= 7) + { + reader.AlignStream(16); + } + + var version = ParseVersion(); + if (version[0] < 2020 || //2020 and earlier + (version[0] == 2020 && version[1] == 3 && version[2] <= 34) || //2020.3.34 and earlier + (version[0] == 2021 && version[1] == 3 && version[2] <= 2) || //2021.3.2 and earlier + (version[0] == 2022 && version[1] == 3 && version[2] <= 1)) //2022.3.1 and earlier + { + mask = ArchiveFlags.BlockInfoNeedPaddingAtStart; + HasBlockInfoNeedPaddingAtStart = false; + } + else + { + mask = ArchiveFlags.UnityCNEncryption; + HasBlockInfoNeedPaddingAtStart = true; + } + if ((Header.flags & mask) != 0) + { + Console.WriteLine($"Encryption flag exist, file is encrypted, attempting to decrypt"); + if (UnityCNKey == null) + { + throw new Exception("UnityCN key is required for decryption"); + } + UnityCNInfo = new UnityCN(reader, UnityCNKey); + Header.flags &= (ArchiveFlags)~mask; + } + + DataInfo = new BlocksAndDirectoryInfo(reader, Header, ref BlocksInfoAtTheEnd); + + foreach (var blockInfo in DataInfo.BlocksInfo) + { + blockInfo.flags &= (StorageBlockFlags)~0x100; + } + + if (HasBlockInfoNeedPaddingAtStart && (Header.flags & ArchiveFlags.BlockInfoNeedPaddingAtStart) != 0) + { + reader.AlignStream(16); + } + + ReadBlocks(reader); + + DataInfo.uncompresseFlags(); + Header.flags &= (ArchiveFlags)~StorageBlockFlags.CompressionTypeMask; + DataInfo.calculateSize(ref Header.uncompressedBlocksInfoSize,ref Header.compressedBlocksInfoSize); + + crc32 = CalculateCRC32(); + } + + private uint CalculateCRC32() + { + uint crc = 0; + foreach (var cab in cabStreams) + { + crc = CRC32.CalculateCRC32(cab, crc); + } + return crc; + } + + private string BlocksInfoCompressionType + { + get + { + var compressionType = (CompressionType)(Header.flags & ArchiveFlags.CompressionTypeMask); + switch (compressionType) + { + case CompressionType.None: + return "none"; + case CompressionType.Lzma: + return "lzma"; + case CompressionType.Lz4: + return "lz4"; + case CompressionType.Lz4HC: + return "lz4hc"; + default: + throw new IOException($"Unsupported compression type {compressionType}"); + } + } + + set + { + Header.flags &= (ArchiveFlags)~StorageBlockFlags.CompressionTypeMask; + switch (value) + { + case "none": + Header.flags |= (ArchiveFlags)CompressionType.None; + break; + case "lzma": + Header.flags |= (ArchiveFlags)CompressionType.Lzma; + break; + case "lz4": + Header.flags |= (ArchiveFlags)CompressionType.Lz4; + break; + case "lz4hc": + Header.flags |= (ArchiveFlags)CompressionType.Lz4HC; + break; + default: + throw new IOException($"Unsupported compression type {value}"); + } + } + } + + private string BlocksCompressionType + { + set + { + var newFlag = CompressionType.None; + switch (value) + { + case "none": + break; + case "lzma": + newFlag = CompressionType.Lzma; + break; + case "lz4": + newFlag = CompressionType.Lz4; + break; + case "lz4hc": + newFlag = CompressionType.Lz4HC; + break; + default: + throw new IOException($"Unsupported compression type {value}"); + } + + foreach (var block in DataInfo.BlocksInfo) + { + block.flags &= (StorageBlockFlags)~StorageBlockFlags.CompressionTypeMask; + block.flags |= (StorageBlockFlags)newFlag; + } + } + } + + private void ReadBlocks(AssetReader reader) + { + MemoryStream BlocksStream = new MemoryStream(); + for (int i = 0; i < DataInfo.BlocksInfo.Count; i++) + { + var blockInfo = DataInfo.BlocksInfo[i]; + var compressionType = (CompressionType)(blockInfo.flags & StorageBlockFlags.CompressionTypeMask); + var encryptedData = reader.ReadBytes((int)blockInfo.compressedSize); + if (UnityCNInfo != null) + { + UnityCNInfo.DecryptBlock(encryptedData, encryptedData.Length, i); + } + ReadOnlySpan compressedData = encryptedData; + switch (compressionType) + { + case CompressionType.None: + { + BlocksStream.Write(compressedData); + break; + } + case CompressionType.Lzma: + { + Compression.DecompressToStream(compressedData, BlocksStream, blockInfo.uncompressedSize, "lzma"); + break; + } + case CompressionType.Lz4: + case CompressionType.Lz4HC: + { + Compression.DecompressToStream(compressedData, BlocksStream, blockInfo.uncompressedSize, "lz4"); + break; + } + default: + throw new IOException($"Unsupported compression type {compressionType}"); + } + } + BlocksStream.Position = 0; + + cabStreams = new List(); + + foreach (var cab in DataInfo.DirectoryInfo) + { + MemoryStream cabStream = new MemoryStream(); + cabStreams.Add(cabStream); + BlocksStream.Position = cab.offset; + BlocksStream.CopyTo(cabStream, cab.size); + cabStream.Position = 0; + } + } + + public void Write(Stream output, string infoPacker = "none", string dataPacker = "none", bool unityCN = false) + { + MemoryStream compressedStream = new MemoryStream(); + + //Bumbo(); + + fixCRC(crc32, CalculateCRC32()); + + BlocksCompressionType = dataPacker; + + if (unityCN && !(dataPacker == "lz4" || dataPacker == "lz4hc")) + { + throw new Exception("UnityCN encryption requires lz4 or lz4hc compression type"); + } + + //DataInfo.merge(); + + MemoryStream BlocksStream = new MemoryStream(); + + foreach (var cab in cabStreams) + { + cab.Position = 0; + cab.CopyTo(BlocksStream); + } + + BlocksStream.Position = 0; + + // compress blocks + foreach (var block in DataInfo.BlocksInfo) + { + MemoryStream uncompressedStream = new MemoryStream(); + BlocksStream.CopyTo(uncompressedStream, block.uncompressedSize); + var compressedData = Compression.CompressStream(uncompressedStream, dataPacker); + block.compressedSize = (uint)compressedData.Count; + compressedStream.Write(compressedData.ToArray(), 0, compressedData.Count); + } + + if (unityCN) + { + if (UnityCNKey == null) + { + throw new Exception("UnityCN key is required for encryption"); + } + if (UnityCNInfo == null) + { + throw new Exception("TODO: UnityCNInfo is null"); + } + UnityCNInfo.reset(); + compressedStream.Position = 0; + using MemoryStream encryptedStream = new MemoryStream(); + for (int i =0; i < DataInfo.BlocksInfo.Count; i++) + { + var block = DataInfo.BlocksInfo[i]; + block.flags |= (StorageBlockFlags)0x100; + var blockData = new byte[block.compressedSize]; + compressedStream.Read(blockData, 0, blockData.Length); + UnityCNInfo.EncryptBlock(blockData, blockData.Length, i); + encryptedStream.Write(blockData); + } + encryptedStream.Position = 0; + compressedStream.SetLength(0); + encryptedStream.CopyTo(compressedStream); + Header.flags |= mask; + } + + BlocksInfoCompressionType = infoPacker; + DataInfo.Update(BlocksInfoCompressionType); + calculateSize(compressedStream.Length, unityCN); + + using AssetWriter writer = new AssetWriter(output); + + Header.Write(writer); + + if (Header.version >= 7) + { + writer.AlignStream(16); + } + + if (unityCN && UnityCNInfo != null) + { + UnityCNInfo.Write(writer); + } + + if (!BlocksInfoAtTheEnd) + { + DataInfo.Write(writer); + } + + if (HasBlockInfoNeedPaddingAtStart && (Header.flags & ArchiveFlags.BlockInfoNeedPaddingAtStart) != 0) + { + writer.AlignStream(16); + } + compressedStream.Position = 0; + + writer.WriteStream(compressedStream); + + if (BlocksInfoAtTheEnd) + { + DataInfo.Write(writer); + } + } + + private void fixCRC(uint targetCRC, uint currentCRC) + { + if (targetCRC == currentCRC) + { + return; + } + uint append = CRC32.rCRC(targetCRC, currentCRC); + var fixCRCBytes = BitConverter.GetBytes(append); + Array.Reverse(fixCRCBytes); + + cabStreams[^1].SetLength(cabStreams[^1].Length + 4); + cabStreams[^1].Position = cabStreams[^1].Length - 4; + cabStreams[^1].Write(fixCRCBytes); + cabStreams[^1].Position = 0; + DataInfo.FixSize(4); + } + + private void calculateSize(long BlocksStreamLength, bool unityCN) + { + DataInfo.calculateSize(ref Header.uncompressedBlocksInfoSize,ref Header.compressedBlocksInfoSize); + long size = 0; + size += Header.CalculateSize(); + if (Header.version >= 7) + { + var a = size % 16; + if (a != 0) + { + size += 16 - a; + } + } + + if (unityCN) + { + size += 0x46; + } + + size += Header.compressedBlocksInfoSize; + + if (HasBlockInfoNeedPaddingAtStart && (Header.flags & ArchiveFlags.BlockInfoNeedPaddingAtStart) != 0) + { + var a = size % 16; + if (a != 0) + { + size += 16 - a; + } + } + + size += BlocksStreamLength; + Header.size = size; + } + + public void Bumbo() + { + var blockFlag = DataInfo.BlocksInfo[^1].flags; + var blockSize = int.MaxValue / 2; + DataInfo.BlocksInfo.Add(new StorageBlockInfo((uint)blockSize, (uint)blockSize, blockFlag)); + DataInfo.DirectoryInfo[^1].size += blockSize; + cabStreams[^1].SetLength(cabStreams[^1].Length + blockSize); + cabStreams[^1].Position = cabStreams[^1].Length - blockSize; + cabStreams[^1].Write(new byte[blockSize]); + cabStreams[^1].Position = 0; + } + + private int[] ParseVersion() + { + var versionSplit = Regex.Replace(Header.unityRevision, @"\D", ".").Split(new[] { "." }, StringSplitOptions.RemoveEmptyEntries); + return versionSplit.Select(int.Parse).ToArray(); + } +} \ No newline at end of file diff --git a/UnityAsset.NET/BundleFile/CABInfo.cs b/UnityAsset.NET/BundleFile/CABInfo.cs new file mode 100644 index 0000000..2a55571 --- /dev/null +++ b/UnityAsset.NET/BundleFile/CABInfo.cs @@ -0,0 +1,41 @@ +using UnityAsset.NET.IO; + +namespace UnityAsset.NET.BundleFile; + +public sealed class CABInfo +{ + public long offset; + public long size; + public uint flags; + public string path; + + public CABInfo(AssetReader reader) + { + offset = reader.ReadInt64(); + size = reader.ReadInt64(); + flags = reader.ReadUInt32(); + path = reader.ReadStringToNull(); + } + + public void Write(AssetWriter writer) + { + writer.WriteInt64(offset); + writer.WriteInt64(size); + writer.WriteUInt32(flags); + writer.WriteStringToNull(path); + } + + public override string ToString() + { + return + $"offset: 0x{offset:X8} | " + + $"size: 0x{size:X8} | " + + $"flags: {flags} | " + + $"path: {path}"; + } + + public long CalculateSize() + { + return 8 + 8 + 4 + path.Length + 1; + } +} \ No newline at end of file diff --git a/UnityAsset.NET/BundleFile/Header.cs b/UnityAsset.NET/BundleFile/Header.cs new file mode 100644 index 0000000..8fc3f98 --- /dev/null +++ b/UnityAsset.NET/BundleFile/Header.cs @@ -0,0 +1,65 @@ +using UnityAsset.NET.Enums; +using UnityAsset.NET.IO; + +namespace UnityAsset.NET.BundleFile; + +public sealed 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 Header(AssetReader reader) + { + signature = reader.ReadStringToNull(20); + switch (signature) + { + case "UnityFS": + version = reader.ReadUInt32(); + unityVersion = reader.ReadStringToNull(); + unityRevision = reader.ReadStringToNull(); + size = reader.ReadInt64(); + compressedBlocksInfoSize = reader.ReadUInt32(); + uncompressedBlocksInfoSize = reader.ReadUInt32(); + flags = (ArchiveFlags)reader.ReadUInt32(); + break; + default: + throw new Exception("Invalid signature"); + } + } + + public void Write(AssetWriter writer) + { + writer.WriteStringToNull(signature); + writer.WriteUInt32(version); + writer.WriteStringToNull(unityVersion); + writer.WriteStringToNull(unityRevision); + writer.WriteInt64(size); + writer.WriteUInt32(compressedBlocksInfoSize); + writer.WriteUInt32(uncompressedBlocksInfoSize); + writer.WriteUInt32((uint)flags); + } + + public override string ToString() + { + return + $"signature: {signature} | " + + $"version: {version} | " + + $"unityVersion: {unityVersion} | " + + $"unityRevision: {unityRevision} | " + + $"size: 0x{size:X8} | " + + $"compressedBlocksInfoSize: 0x{compressedBlocksInfoSize:X8} | " + + $"uncompressedBlocksInfoSize: 0x{uncompressedBlocksInfoSize:X8} | " + + $"flags: 0x{(int)flags:X8}"; + } + + public long CalculateSize() + { + return signature.Length + 1 + 4 + unityVersion.Length + 1 + unityRevision.Length + 1 + 8 + 4 + 4 + 4; + } + } \ No newline at end of file diff --git a/UnityAsset.NET/BundleFile/StorageBlockInfo.cs b/UnityAsset.NET/BundleFile/StorageBlockInfo.cs new file mode 100644 index 0000000..c6f4717 --- /dev/null +++ b/UnityAsset.NET/BundleFile/StorageBlockInfo.cs @@ -0,0 +1,40 @@ +using UnityAsset.NET.Enums; +using UnityAsset.NET.IO; + +namespace UnityAsset.NET.BundleFile; + +public sealed class StorageBlockInfo +{ + public uint compressedSize; + public uint uncompressedSize; + public StorageBlockFlags flags; + + public StorageBlockInfo(uint compressedSize, uint uncompressedSize, StorageBlockFlags flags) + { + this.compressedSize = compressedSize; + this.uncompressedSize = uncompressedSize; + this.flags = flags; + } + + public StorageBlockInfo(AssetReader reader) + { + uncompressedSize = reader.ReadUInt32(); + compressedSize = reader.ReadUInt32(); + flags = (StorageBlockFlags)reader.ReadUInt16(); + } + + public void Write(AssetWriter writer) + { + writer.WriteUInt32(uncompressedSize); + writer.WriteUInt32(compressedSize); + writer.WriteUInt16((ushort)flags); + } + + public override string ToString() + { + return + $"compressedSize: 0x{compressedSize:X8} | " + + $"uncompressedSize: 0x{uncompressedSize:X8} | " + + $"flags: 0x{(int)flags:X8}"; + } +} \ No newline at end of file diff --git a/UnityAsset.NET/BundleFile/UnityCN.cs b/UnityAsset.NET/BundleFile/UnityCN.cs new file mode 100644 index 0000000..0fe5838 --- /dev/null +++ b/UnityAsset.NET/BundleFile/UnityCN.cs @@ -0,0 +1,237 @@ +using System.Text; +using System.Security.Cryptography; + +using UnityAsset.NET.Extensions; +using UnityAsset.NET.IO; + +namespace UnityAsset.NET.BundleFile; + +public sealed class UnityCN +{ + private const string Signature = "#$unity3dchina!@"; + + private ICryptoTransform Encryptor; + + uint value; + + public byte[] InfoBytes = new byte[0x10]; + public byte[] InfoKey = new byte[0x10]; + + public byte[] SignatureBytes = new byte[0x10]; + public byte[] SignatureKey = new byte[0x10]; + + public byte[] Index = new byte[0x10]; + public byte[] Sub = new byte[0x10]; + + public UnityCN(AssetReader reader, string key) + { + SetKey(key); + + value = reader.ReadUInt32(); + + InfoBytes = reader.ReadBytes(0x10); + InfoKey = reader.ReadBytes(0x10); + reader.Position += 1; + + SignatureBytes = reader.ReadBytes(0x10); + SignatureKey = reader.ReadBytes(0x10); + reader.Position += 1; + + reset(); + } + + public void reset() + { + var infoBytes = InfoBytes.ToArray(); + var infoKey = InfoKey.ToArray(); + var signatureBytes = SignatureBytes.ToArray(); + var signatureKey = SignatureKey.ToArray(); + + DecryptKey(signatureKey, signatureBytes); + + var str = Encoding.UTF8.GetString(signatureBytes); + if (str != Signature) + { + throw new Exception($"Invalid Signature, Expected {Signature} but found {str} instead"); + } + + DecryptKey(infoKey, infoBytes); + + infoBytes = infoBytes.ToUInt4Array(); + infoBytes.AsSpan(0, 0x10).CopyTo(Index); + var subBytes = infoBytes.AsSpan(0x10, 0x10); + for (var i = 0; i < subBytes.Length; i++) + { + var idx = (i % 4 * 4) + (i / 4); + Sub[idx] = subBytes[i]; + } + } + + public void Write(AssetWriter writer) + { + writer.WriteUInt32(value); + writer.Write(InfoBytes); + writer.Write(InfoKey); + writer.Write((byte)0); + writer.Write(SignatureBytes); + writer.Write(SignatureKey); + writer.Write((byte)0); + } + + public bool SetKey(string key) + { + try + { + using var aes = Aes.Create(); + aes.Mode = CipherMode.ECB; + aes.Key = Convert.FromHexString(key); + + Encryptor = aes.CreateEncryptor(); + } + catch (Exception e) + { + Console.WriteLine($"[UnityCN] Invalid key !!\n{e.Message}"); + return false; + } + return true; + } + + public void DecryptBlock(Span bytes, int size, int index) + { + var offset = 0; + while (offset < size) + { + offset += Decrypt(bytes.Slice(offset), index++, size - offset); + } + } + + public void EncryptBlock(Span bytes, int size, int index) + { + var offset = 0; + while (offset < size) + { + offset += Encrypt(bytes.Slice(offset), index++, size - offset); + } + } + + private void DecryptKey(byte[] key, byte[] data) + { + if (Encryptor != null) + { + key = Encryptor.TransformFinalBlock(key, 0, key.Length); + for (int i = 0; i < 0x10; i++) + data[i] ^= key[i]; + } + } + + private int DecryptByte(Span bytes, ref int offset, ref int index) + { + var b = Sub[((index >> 2) & 3) + 4] + Sub[index & 3] + Sub[((index >> 4) & 3) + 8] + Sub[((byte)index >> 6) + 12]; + bytes[offset] = (byte)((Index[bytes[offset] & 0xF] - b) & 0xF | 0x10 * (Index[bytes[offset] >> 4] - b)); + b = bytes[offset]; + offset++; + index++; + return b; + } + + private int EncryptByte(Span bytes, ref int offset, ref int index) + { + byte currentByte = bytes[offset]; + var low = currentByte & 0xF; + var high = currentByte >> 4; + + var b = Sub[((index >> 2) & 3) + 4] + Sub[index & 3] + Sub[((index >> 4) & 3) + 8] + Sub[((byte)index >> 6) + 12]; + + int i = 0; + while (((Index[i] - b) & 0xF) != low && i < 0x10) + { + i++; + } + low = i; + i = 0; + while (((Index[i] - b) & 0xF) != high && i < 0x10) + { + i++; + } + high = i; + + bytes[offset] = (byte)(low | (high << 4)); + offset++; + index++; + return currentByte; + } + + private int Decrypt(Span bytes, int index, int remaining) + { + var offset = 0; + + var curByte = DecryptByte(bytes, ref offset, ref index); + var byteHigh = curByte >> 4; + var byteLow = curByte & 0xF; + + if (byteHigh == 0xF) + { + int b; + do + { + b = DecryptByte(bytes, ref offset, ref index); + byteHigh += b; + } while (b == 0xFF); + } + + offset += byteHigh; + + if (offset < remaining) + { + DecryptByte(bytes, ref offset, ref index); + DecryptByte(bytes, ref offset, ref index); + if (byteLow == 0xF) + { + int b; + do + { + b = DecryptByte(bytes, ref offset, ref index); + } while (b == 0xFF); + } + } + + return offset; + } + + private int Encrypt(Span bytes, int index, int remaining) + { + var offset = 0; + + var curByte = EncryptByte(bytes, ref offset, ref index); + var byteHigh = curByte >> 4; + var byteLow = curByte & 0xF; + + if (byteHigh == 0xF) + { + int b; + do + { + b = EncryptByte(bytes, ref offset, ref index); + byteHigh += b; + } while (b == 0xFF); + } + + offset += byteHigh; + + if (offset < remaining) + { + EncryptByte(bytes, ref offset, ref index); + EncryptByte(bytes, ref offset, ref index); + if (byteLow == 0xF) + { + int b; + do + { + b = EncryptByte(bytes, ref offset, ref index); + } while (b == 0xFF); + } + } + + return offset; + } +} \ No newline at end of file diff --git a/UnityAsset.NET/CRC32.cs b/UnityAsset.NET/CRC32.cs new file mode 100644 index 0000000..0c1e459 --- /dev/null +++ b/UnityAsset.NET/CRC32.cs @@ -0,0 +1,82 @@ +namespace UnityAsset.NET; + +public static class CRC32 +{ + private static readonly uint[] Crc32Table = [0, 1996959894, 3993919788, 2567524794, 124634137, 1886057615, 3915621685, 2657392035, 249268274, 2044508324, 3772115230, 2547177864, 162941995, 2125561021, 3887607047, 2428444049, 498536548, 1789927666, 4089016648, 2227061214, 450548861, 1843258603, 4107580753, 2211677639, 325883990, 1684777152, 4251122042, 2321926636, 335633487, 1661365465, 4195302755, 2366115317, 997073096, 1281953886, 3579855332, 2724688242, 1006888145, 1258607687, 3524101629, 2768942443, 901097722, 1119000684, 3686517206, 2898065728, 853044451, 1172266101, 3705015759, 2882616665, 651767980, 1373503546, 3369554304, 3218104598, 565507253, 1454621731, 3485111705, 3099436303, 671266974, 1594198024, 3322730930, 2970347812, 795835527, 1483230225, 3244367275, 3060149565, 1994146192, 31158534, 2563907772, 4023717930, 1907459465, 112637215, 2680153253, 3904427059, 2013776290, 251722036, 2517215374, 3775830040, 2137656763, 141376813, 2439277719, 3865271297, 1802195444, 476864866, 2238001368, 4066508878, 1812370925, 453092731, 2181625025, 4111451223, 1706088902, 314042704, 2344532202, 4240017532, 1658658271, 366619977, 2362670323, 4224994405, 1303535960, 984961486, 2747007092, 3569037538, 1256170817, 1037604311, 2765210733, 3554079995, 1131014506, 879679996, 2909243462, 3663771856, 1141124467, 855842277, 2852801631, 3708648649, 1342533948, 654459306, 3188396048, 3373015174, 1466479909, 544179635, 3110523913, 3462522015, 1591671054, 702138776, 2966460450, 3352799412, 1504918807, 783551873, 3082640443, 3233442989, 3988292384, 2596254646, 62317068, 1957810842, 3939845945, 2647816111, 81470997, 1943803523, 3814918930, 2489596804, 225274430, 2053790376, 3826175755, 2466906013, 167816743, 2097651377, 4027552580, 2265490386, 503444072, 1762050814, 4150417245, 2154129355, 426522225, 1852507879, 4275313526, 2312317920, 282753626, 1742555852, 4189708143, 2394877945, 397917763, 1622183637, 3604390888, 2714866558, 953729732, 1340076626, 3518719985, 2797360999, 1068828381, 1219638859, 3624741850, 2936675148, 906185462, 1090812512, 3747672003, 2825379669, 829329135, 1181335161, 3412177804, 3160834842, 628085408, 1382605366, 3423369109, 3138078467, 570562233, 1426400815, 3317316542, 2998733608, 733239954, 1555261956, 3268935591, 3050360625, 752459403, 1541320221, 2607071920, 3965973030, 1969922972, 40735498, 2617837225, 3943577151, 1913087877, 83908371, 2512341634, 3803740692, 2075208622, 213261112, 2463272603, 3855990285, 2094854071, 198958881, 2262029012, 4057260610, 1759359992, 534414190, 2176718541, 4139329115, 1873836001, 414664567, 2282248934, 4279200368, 1711684554, 285281116, 2405801727, 4167216745, 1634467795, 376229701, 2685067896, 3608007406, 1308918612, 956543938, 2808555105, 3495958263, 1231636301, 1047427035, 2932959818, 3654703836, 1088359270, 936918000, 2847714899, 3736837829, 1202900863, 817233897, 3183342108, 3401237130, 1404277552, 615818150, 3134207493, 3453421203, 1423857449, 601450431, 3009837614, 3294710456, 1567103746, 711928724, 3020668471, 3272380065, 1510334235, 755167117]; + + private static uint HIBYTE(uint X) => (X >> 8) & 0xFF; + + private static uint LOBYTE(uint X) => X & 0xFF; + + private static uint HIWORD(uint X) => (X >> 16) & 0xFFFF; + + private static uint LOWORD(uint X) => X & 0xFFFF; + + private static uint MAKEWORD(uint X, uint Y) => (Y << 8) | X; + + private static uint MAKELONG(uint X, uint Y) => (Y << 16) | X; + + private static uint RF(uint X) + { + for(uint i = 0; i < 256; i++) + { + if(HIBYTE(HIWORD(Crc32Table[i])) == X) + { + return i; + } + } + return 0; + } + + private static uint F(uint X) => HIBYTE(HIWORD(Crc32Table[X])); + + private static uint G(uint X) => LOBYTE(HIWORD(Crc32Table[X])); + + private static uint H(uint X) => HIBYTE(LOWORD(Crc32Table[X])); + + private static uint I(uint X) => LOBYTE(LOWORD(Crc32Table[X])); + + public static uint rCRC(uint targetCrc, uint originalCrc) + { + targetCrc ^= 0xFFFFFFFF; + originalCrc ^= 0xFFFFFFFF; + + uint W = HIBYTE(HIWORD(targetCrc)); + uint X = LOBYTE(HIWORD(targetCrc)); + uint Y = HIBYTE(LOWORD(targetCrc)); + uint Z = LOBYTE(LOWORD(targetCrc)); + + uint A = HIBYTE(HIWORD(originalCrc)); + uint B = LOBYTE(HIWORD(originalCrc)); + uint C = HIBYTE(LOWORD(originalCrc)); + uint D = LOBYTE(LOWORD(originalCrc)); + + uint p = RF(W); + uint o = RF(X ^ G(p)); + uint n = RF(Y ^ G(o) ^ H(p)); + uint m = RF(Z ^ G(n) ^ H(o) ^ I(p)); + + uint d = m ^ D; + uint c = n ^ C ^ I(m); + uint b = o ^ B ^ H(m) ^ I(n); + uint a = p ^ A ^ G(m) ^ H(n) ^ I(o); + + return MAKELONG(MAKEWORD(a, b), MAKEWORD(c, d)); + } + + public static uint CalculateCRC32(byte[] data, uint initialCrc = 0) + { + uint crc = ~initialCrc; + foreach (var b in data) + { + crc = (crc >> 8) ^ Crc32Table[(crc ^ b) & 0xFF]; + } + return ~crc; + } + + public static uint CalculateCRC32(Stream data, uint initialCrc = 0) + { + data.Position = 0; + return CalculateCRC32(new BinaryReader(data).ReadBytes((int)data.Length), initialCrc); + } +} \ No newline at end of file diff --git a/UnityAsset.NET/Compression.cs b/UnityAsset.NET/Compression.cs new file mode 100644 index 0000000..fc7215c --- /dev/null +++ b/UnityAsset.NET/Compression.cs @@ -0,0 +1,61 @@ +using SevenZip.Compression.LZMA; +using K4os.Compression.LZ4; + +namespace UnityAsset.NET; + +public static class Compression +{ + public static void DecompressToStream(ReadOnlySpan compressedData, Stream decompressedStream, + long decompressedSize, string compressionType) + { + switch (compressionType) + { + case "lz4": + byte[] decompressedData = new byte[decompressedSize]; + LZ4Codec.Decode(compressedData, new Span(decompressedData)); + decompressedStream.Write(decompressedData, 0, decompressedData.Length); + break; + case "lzma": + var properties = new byte[5]; + if (compressedData.Length < 5) + throw new Exception("input .lzma is too short"); + compressedData.Slice(0, 5).CopyTo(properties); + var decoder = new Decoder(); + decoder.SetDecoderProperties(properties); + MemoryStream compressedStream = new MemoryStream(compressedData.Slice(5).ToArray()); + decoder.Code(compressedStream, decompressedStream, compressedData.Length - 5, decompressedSize, null); + break; + default: + throw new ArgumentException($"Unsupported compression type {compressionType}"); + } + } + + public static List CompressStream(MemoryStream uncompressedStream, string compressionType) + { + byte[] uncompressedData = uncompressedStream.ToArray(); + switch (compressionType) + { + case "none": + return uncompressedData.ToList(); + case "lz4": + byte[] compressedData = new byte[LZ4Codec.MaximumOutputSize(uncompressedData.Length)]; + int compressedSize = LZ4Codec.Encode(uncompressedData, compressedData); + return compressedData.Take(compressedSize).ToList(); + case "lz4hc": + byte[] compressedDataHC = new byte[LZ4Codec.MaximumOutputSize(uncompressedData.Length)]; + int compressedSizeHC = LZ4Codec.Encode(uncompressedData, compressedDataHC, LZ4Level.L09_HC); + return compressedDataHC.Take(compressedSizeHC).ToList(); + case "lzma": + var encoder = new Encoder(); + MemoryStream compressedStream = new MemoryStream(); + encoder.WriteCoderProperties(compressedStream); + long fileSize = uncompressedStream.Length; + uncompressedStream.Position = 0; + encoder.Code(uncompressedStream, compressedStream, -1, -1, null); + Console.WriteLine($"Compressed {fileSize} bytes to {compressedStream.Length} bytes"); + return compressedStream.ToArray().ToList(); + default: + throw new ArgumentException($"Unsupported compression type {compressionType}"); + } + } +} \ No newline at end of file diff --git a/UnityAsset.NET/Enums/ArchiveFlags.cs b/UnityAsset.NET/Enums/ArchiveFlags.cs new file mode 100644 index 0000000..eac5d09 --- /dev/null +++ b/UnityAsset.NET/Enums/ArchiveFlags.cs @@ -0,0 +1,12 @@ +namespace UnityAsset.NET.Enums; + +[Flags] +public enum ArchiveFlags +{ + CompressionTypeMask = 0x3f, + BlocksAndDirectoryInfoCombined = 0x40, + BlocksInfoAtTheEnd = 0x80, + OldWebPluginCompatibility = 0x100, + BlockInfoNeedPaddingAtStart = 0x200, + UnityCNEncryption = 0x400 +} \ No newline at end of file diff --git a/UnityAsset.NET/Enums/CompressionType.cs b/UnityAsset.NET/Enums/CompressionType.cs new file mode 100644 index 0000000..02ad56f --- /dev/null +++ b/UnityAsset.NET/Enums/CompressionType.cs @@ -0,0 +1,9 @@ +namespace UnityAsset.NET.Enums; + +public enum CompressionType +{ + None, + Lzma, + Lz4, + Lz4HC +} \ No newline at end of file diff --git a/UnityAsset.NET/Enums/CryptoType.cs b/UnityAsset.NET/Enums/CryptoType.cs new file mode 100644 index 0000000..20d0185 --- /dev/null +++ b/UnityAsset.NET/Enums/CryptoType.cs @@ -0,0 +1,7 @@ +namespace UnityAsset.NET.Enums; + +public enum CryptoType +{ + None, + UnityCN +} \ No newline at end of file diff --git a/UnityAsset.NET/Enums/SerializedFileFormatVersion.cs b/UnityAsset.NET/Enums/SerializedFileFormatVersion.cs new file mode 100644 index 0000000..8ba5ad8 --- /dev/null +++ b/UnityAsset.NET/Enums/SerializedFileFormatVersion.cs @@ -0,0 +1,80 @@ +namespace UnityAsset.NET.Enums; + +public enum SerializedFileFormatVersion +{ + Unsupported = 1, + Unknown_2 = 2, + Unknown_3 = 3, + /// + /// 1.2.0 to 2.0.0 + /// + Unknown_5 = 5, + /// + /// 2.1.0 to 2.6.1 + /// + Unknown_6 = 6, + /// + /// 3.0.0b + /// + Unknown_7 = 7, + /// + /// 3.0.0 to 3.4.2 + /// + Unknown_8 = 8, + /// + /// 3.5.0 to 4.7.2 + /// + Unknown_9 = 9, + /// + /// 5.0.0aunk1 + /// + Unknown_10 = 10, + /// + /// 5.0.0aunk2 + /// + HasScriptTypeIndex = 11, + /// + /// 5.0.0aunk3 + /// + Unknown_12 = 12, + /// + /// 5.0.0aunk4 + /// + HasTypeTreeHashes = 13, + /// + /// 5.0.0unk + /// + Unknown_14 = 14, + /// + /// 5.0.1 to 5.4.0 + /// + SupportsStrippedObject = 15, + /// + /// 5.5.0a + /// + RefactoredClassId = 16, + /// + /// 5.5.0unk to 2018.4 + /// + RefactorTypeData = 17, + /// + /// 2019.1a + /// + RefactorShareableTypeTreeData = 18, + /// + /// 2019.1unk + /// + TypeTreeNodeWithTypeFlags = 19, + /// + /// 2019.2 + /// + SupportsRefObject = 20, + /// + /// 2019.3 to 2019.4 + /// + StoresTypeDependencies = 21, + /// + /// 2020.1 to x + /// + LargeFilesSupport = 22 +} \ No newline at end of file diff --git a/UnityAsset.NET/Enums/StorageBlockFlags.cs b/UnityAsset.NET/Enums/StorageBlockFlags.cs new file mode 100644 index 0000000..d7e1b3c --- /dev/null +++ b/UnityAsset.NET/Enums/StorageBlockFlags.cs @@ -0,0 +1,8 @@ +namespace UnityAsset.NET.Enums; + +[Flags] +public enum StorageBlockFlags +{ + CompressionTypeMask = 0x3f, + Streamed = 0x40, +} \ No newline at end of file diff --git a/UnityAsset.NET/Extensions/ByteArrayExtensions.cs b/UnityAsset.NET/Extensions/ByteArrayExtensions.cs new file mode 100644 index 0000000..d2faf0f --- /dev/null +++ b/UnityAsset.NET/Extensions/ByteArrayExtensions.cs @@ -0,0 +1,17 @@ +namespace UnityAsset.NET.Extensions; + +public static class ByteArrayExtensions +{ + public static byte[] ToUInt4Array(this byte[] source) => ToUInt4Array(source, 0, source.Length); + public static byte[] ToUInt4Array(this byte[] source, int offset, int size) + { + var buffer = new byte[size * 2]; + for (var i = 0; i < size; i++) + { + var idx = i * 2; + buffer[idx] = (byte)(source[offset + i] >> 4); + buffer[idx + 1] = (byte)(source[offset + i] & 0xF); + } + return buffer; + } +} \ No newline at end of file diff --git a/UnityAsset.NET/Extensions/StreamExtensions.cs b/UnityAsset.NET/Extensions/StreamExtensions.cs new file mode 100644 index 0000000..68fc921 --- /dev/null +++ b/UnityAsset.NET/Extensions/StreamExtensions.cs @@ -0,0 +1,23 @@ +using System.IO; + +namespace UnityAsset.NET.Extensions; + +public static class StreamExtensions +{ + private const int BufferSize = 81920; + + public static void CopyTo(this Stream source, Stream destination, long size) + { + var buffer = new byte[BufferSize]; + for (var left = size; left > 0; left -= BufferSize) + { + int toRead = BufferSize < left ? BufferSize : (int)left; + int read = source.Read(buffer, 0, toRead); + destination.Write(buffer, 0, read); + if (read != toRead) + { + return; + } + } + } +} \ No newline at end of file diff --git a/UnityAsset.NET/IO/AssetReader.cs b/UnityAsset.NET/IO/AssetReader.cs new file mode 100644 index 0000000..ea1900a --- /dev/null +++ b/UnityAsset.NET/IO/AssetReader.cs @@ -0,0 +1,104 @@ +using System.Buffers.Binary; +using System.Text; + +namespace UnityAsset.NET.IO; + +public class AssetReader : BinaryReader +{ + public bool BigEndian; + + public long Position + { + get => BaseStream.Position; + set => BaseStream.Position = value; + } + + private readonly byte[] buffer; + + public AssetReader(Stream stream, bool isBigEndian = true) : base(stream) + { + buffer = new byte[8]; + BigEndian = isBigEndian; + } + + public void AlignStream(int alignment) + { + var pos = Position; + var mod = pos % alignment; + if (mod != 0) + { + Position += alignment - mod; + } + } + + public override int ReadInt32() + { + if (BigEndian) + { + Read(buffer, 0, 4); + return BinaryPrimitives.ReadInt32BigEndian(buffer); + } + else + { + return base.ReadInt32(); + } + } + + public override long ReadInt64() + { + if (BigEndian) + { + Read(buffer, 0, 8); + return BinaryPrimitives.ReadInt64BigEndian(buffer); + } + else + { + return base.ReadInt64(); + } + } + + public override ushort ReadUInt16() + { + if (BigEndian) + { + Read(buffer, 0, 2); + return BinaryPrimitives.ReadUInt16BigEndian(buffer); + } + else + { + return base.ReadUInt16(); + } + } + + public override uint ReadUInt32() + { + if (BigEndian) + { + Read(buffer, 0, 4); + return BinaryPrimitives.ReadUInt32BigEndian(buffer); + } + else + { + return base.ReadUInt32(); + } + } + + + + public string ReadStringToNull(int maxLength = 32767) + { + var bytes = new List(); + int count = 0; + while (count < maxLength) + { + var b = ReadByte(); + if (b == 0) + { + break; + } + bytes.Add(b); + count++; + } + return Encoding.UTF8.GetString(bytes.ToArray()); + } +} \ No newline at end of file diff --git a/UnityAsset.NET/IO/AssetWriter.cs b/UnityAsset.NET/IO/AssetWriter.cs new file mode 100644 index 0000000..9e6d726 --- /dev/null +++ b/UnityAsset.NET/IO/AssetWriter.cs @@ -0,0 +1,54 @@ +using System.Buffers.Binary; +using System.Text; + +namespace UnityAsset.NET.IO; + +public class AssetWriter : BinaryWriter +{ + public bool BigEndian; + + public AssetWriter(Stream stream, bool isBigEndian = true) : base(stream) + { + BigEndian = isBigEndian; + } + + public void AlignStream(int alignment) + { + int offset = (int)BaseStream.Position % alignment; + if (offset != 0) + { + Write(new byte[alignment - offset]); + } + } + + public void WriteInt32(int value) + { + Write(BigEndian ? BinaryPrimitives.ReverseEndianness(value) : value); + } + + public void WriteInt64(long value) + { + Write(BigEndian ? BinaryPrimitives.ReverseEndianness(value) : value); + } + + public void WriteUInt16(ushort value) + { + Write(BigEndian ? BinaryPrimitives.ReverseEndianness(value) : value); + } + + public void WriteUInt32(uint value) + { + Write(BigEndian ? BinaryPrimitives.ReverseEndianness(value) : value); + } + + public void WriteStringToNull(string str) + { + Write(Encoding.UTF8.GetBytes(str)); + Write((byte)0); + } + + public void WriteStream(Stream stream) + { + stream.CopyTo(BaseStream); + } +} \ No newline at end of file diff --git a/UnityAsset.NET/SerializedFile/AssetsFile.cs b/UnityAsset.NET/SerializedFile/AssetsFile.cs new file mode 100644 index 0000000..1165199 --- /dev/null +++ b/UnityAsset.NET/SerializedFile/AssetsFile.cs @@ -0,0 +1,6 @@ +namespace UnityAsset.NET.AssetsFile; + +public sealed class AssetsFile +{ + +} \ No newline at end of file diff --git a/UnityAsset.NET/SerializedFile/SerializedFile.cs b/UnityAsset.NET/SerializedFile/SerializedFile.cs new file mode 100644 index 0000000..4c6c1cc --- /dev/null +++ b/UnityAsset.NET/SerializedFile/SerializedFile.cs @@ -0,0 +1,18 @@ +using UnityAsset.NET.IO; + +namespace UnityAsset.NET.SerializedFile; + +public sealed class SerializedFile +{ + public SerializedFileHeader Header; + + public SerializedFile(Stream data) + { + using AssetReader reader = new AssetReader(data); + Header = new SerializedFileHeader(reader); + if (Header.Endianess == 0) + { + reader.BigEndian = false; + } + } +} \ No newline at end of file diff --git a/UnityAsset.NET/SerializedFile/SerializedFileHeader.cs b/UnityAsset.NET/SerializedFile/SerializedFileHeader.cs new file mode 100644 index 0000000..fd2e504 --- /dev/null +++ b/UnityAsset.NET/SerializedFile/SerializedFileHeader.cs @@ -0,0 +1,41 @@ +using UnityAsset.NET.Enums; +using UnityAsset.NET.IO; + +namespace UnityAsset.NET.SerializedFile; + +public sealed class SerializedFileHeader +{ + public uint MetadataSize; + public long FileSize; + public SerializedFileFormatVersion Version; + public long DataOffset; + public byte Endianess; + public byte[] Reserved; + + public SerializedFileHeader(AssetReader reader) + { + MetadataSize = reader.ReadUInt32(); + FileSize = reader.ReadUInt32(); + Version = (SerializedFileFormatVersion)reader.ReadUInt32(); + DataOffset = reader.ReadUInt32(); + + if (Version >= SerializedFileFormatVersion.Unknown_9) + { + Endianess = reader.ReadByte(); + Reserved = reader.ReadBytes(3); + } + else + { + reader.Position = FileSize - MetadataSize; + Endianess = reader.ReadByte(); + } + + if (Version >= SerializedFileFormatVersion.LargeFilesSupport) + { + MetadataSize = reader.ReadUInt32(); + FileSize = reader.ReadUInt32(); + DataOffset = reader.ReadUInt32(); + reader.ReadInt64(); // unknown + } + } +} \ No newline at end of file diff --git a/UnityAsset.NET/UnityAsset.NET.csproj b/UnityAsset.NET/UnityAsset.NET.csproj new file mode 100644 index 0000000..38e594c --- /dev/null +++ b/UnityAsset.NET/UnityAsset.NET.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + + + + + + + + +