static void Main(string[] args) { if (args.Length == 0) { Console.WriteLine("usage:"); Console.WriteLine("Extraction:"); Console.WriteLine("\t{0} x input.l7z", AppDomain.CurrentDomain.FriendlyName); Console.WriteLine(); Console.WriteLine("Creation:"); Console.WriteLine("\t{0} c input_folderName output.l7z", AppDomain.CurrentDomain.FriendlyName); Console.WriteLine(); Console.WriteLine("Decompress individual file with mode 0x80:"); Console.WriteLine("\t{0} d input.bin output.bin", AppDomain.CurrentDomain.FriendlyName); Console.WriteLine(); Console.WriteLine("Compress individual file with mode 0x80:"); Console.WriteLine("\t{0} e input.bin output.bin", AppDomain.CurrentDomain.FriendlyName); Environment.Exit(1); } if (args[0] == "c") { PackArchive(args[1], args[2]); } else if (args[0] == "x") { UnpackArchive(args[1]); } else if (args[0] == "d") { var input = args[1]; var output = input + ".out"; if (args.Length >= 2) { output = args[2]; } byte[] data; using (BinaryReader reader = new BinaryReader(File.OpenRead(input))) { var expectedFileSize = reader.ReadUInt32(); if (expectedFileSize == 0x19) // Blank { expectedFileSize = reader.ReadUInt32(); } else { expectedFileSize = (expectedFileSize & 0xffffff00) >> 8; } data = TaikoCompression.Decompress(reader.ReadBytes((int)reader.BaseStream.Length - 4)); if (data.Length != expectedFileSize) { Console.WriteLine("File size didn't match expected output file size. Maybe bad decompression? ({0:x8} != {1:x8})", data.Length, expectedFileSize); } } File.WriteAllBytes(output, data); } else if (args[0] == "e") { var input = args[1]; var output = input + ".out"; if (args.Length >= 2) { output = args[2]; } var data = File.ReadAllBytes(input); using (var writer = new BinaryWriter(File.OpenWrite(output))) { var expectedFileSize = data.Length; if (expectedFileSize > 0xffffff) { writer.Write(0x00000019); writer.Write(expectedFileSize); } else { writer.Write((expectedFileSize << 8) | 0x19); } writer.Write(TaikoCompression.Compress(data)); } } }
static void UnpackArchive(string inputFilename) { using (BinaryReader reader = new BinaryReader(File.OpenRead(inputFilename))) { if (reader.ReadUInt32() != 0x4143374c) { Console.WriteLine("Not a L7CA archive"); Environment.Exit(1); } var header = new L7CAHeader { unk = reader.ReadUInt32(), archiveSize = reader.ReadInt32(), metadataOffset = reader.ReadInt32(), metadataSize = reader.ReadInt32(), unk2 = reader.ReadUInt32(), filesystemEntries = reader.ReadInt32(), folders = reader.ReadInt32(), files = reader.ReadInt32(), chunks = reader.ReadInt32(), stringTableSize = reader.ReadInt32(), unk4 = reader.ReadInt32() }; if (header.unk4 != 0x05) { Console.WriteLine("This archive type is unsupported and most likely won't unpack properly."); } // Read strings var baseOffset = reader.BaseStream.Length - header.stringTableSize; reader.BaseStream.Seek(baseOffset, SeekOrigin.Begin); var strings = new Dictionary <int, string>(); while (reader.PeekChar() != -1) { var offset = (int)(reader.BaseStream.Position - baseOffset); int len = reader.ReadByte(); var str = Encoding.UTF8.GetString(reader.ReadBytes(len)); strings.Add(offset, str); } // Read filesystem entries var entries = new Dictionary <int, L7CAFilesystemEntry>(); reader.BaseStream.Seek(header.metadataOffset, SeekOrigin.Begin); for (var i = 0; i < header.filesystemEntries; i++) { var entry = new L7CAFilesystemEntry { id = reader.ReadInt32(), hash = reader.ReadUInt32(), folderOffset = reader.ReadInt32(), filenameOffset = reader.ReadInt32(), timestamp = reader.ReadInt64() }; entry.filename = entry.id == -1 ? $"{strings[entry.folderOffset]}" : $"{strings[entry.folderOffset]}/{strings[entry.filenameOffset]}"; //Console.WriteLine("{0:x8} {1:x8} {2:x8} {3:x8} {4:x16}", entry.id, entry.hash, entry.folderOffset, entry.filenameOffset, entry.timestamp); if (Crc32.CalculateNamco(entry.filename) != entry.hash) { Console.WriteLine("{0} did not match expected hash", entry.filename); } if (entry.id != -1) { entries.Add(entry.id, entry); } else { // -1 is a folder. // Only create a folder and move on to next entry. // This step probably isn't needed, but just for the sake of completeness I added it. // There might be some game out there that has blank folders but no actual data in it, so those will be accounted for as well. if (!Directory.Exists(entry.filename)) { Directory.CreateDirectory(entry.filename); } } } // Read file information var files = new List <L7CAFileEntry>(); for (var i = 0; i < header.files; i++) { var entry = new L7CAFileEntry { compressedFilesize = reader.ReadInt32(), rawFilesize = reader.ReadInt32(), chunkIdx = reader.ReadInt32(), chunkCount = reader.ReadInt32(), offset = reader.ReadInt32(), crc32 = reader.ReadUInt32() }; //var filename = entries[(uint)i].filename; //Console.WriteLine("{2}\noffset[{0:x8}] fileSize[{1:x8}]\nreal_crc32[{3:x8}] crc32[{4:x8}] crc32[{5:x8}]\n", entry.offset, entry.compressedFileSize, filename, entries[(uint)i].hash, crc32.Value, entry.crc32); files.Add(entry); } // Read chunk information var chunks = new List <L7CAChunkEntry>(); for (var i = 0; i < header.chunks; i++) { var entry = new L7CAChunkEntry { chunkSize = reader.ReadInt32(), unk = reader.ReadUInt16(), chunkId = reader.ReadUInt16() }; //Console.WriteLine("{3:x8} {0:x8} {1:x4} {2:x4}", entry.chunkSize, entry.unk, entry.chunkNum, i); chunks.Add(entry); } for (var i = 0; i < header.files; i++) { var file = files[i]; var entry = entries[i]; Console.WriteLine("Extracting {0}...", entry.filename); //Console.WriteLine("{0:x1} {1:x8} {2:x8} {3:x8} {4:x8} {5:x8}", file.chunkIdx, file.chunkCount, file.offset, file.compressedFileSize, file.rawFileSize, file.crc32); //var output = Path.Combine("output", entry.filename); var output = entry.filename; Directory.CreateDirectory(Path.GetDirectoryName(output)); reader.BaseStream.Seek(file.offset, SeekOrigin.Begin); var origData = reader.ReadBytes(file.compressedFilesize); var data = new List <byte>(); using (var dataStream = new BinaryReader(new MemoryStream(origData))) { if (_debugDecompressionCode) { foreach (var f in Directory.EnumerateFiles(".", "output-chunk-*.bin")) { File.Delete(f); } } for (int x = 0; x < file.chunkCount; x++) { var compMode = (chunks[file.chunkIdx + x].chunkSize >> 24) & 0xff; var len = chunks[file.chunkIdx + x].chunkSize & 0xffffff; var isCompressed = (chunks[file.chunkIdx + x].chunkSize & 0x80000000) != 0; if (isCompressed) { Console.WriteLine("Chunk is compressed."); } if (_debugDecompressionCode) { Console.WriteLine("{0:x8} {1}", len, isCompressed); } var d = dataStream.ReadBytes(len); if (_debugDecompressionCode) { File.WriteAllBytes($"output-chunk-{x}.bin", d); } if (isCompressed) { try { // Decompress chunk switch (compMode) { case 0x80: d = TaikoCompression.Decompress(d, data.ToArray()); data = new List <byte>(d); break; case 0x81: d = TaikoCompression2.Decompress(d, data.ToArray()); data = new List <byte>(d); break; } } catch { // Save compressed data Console.WriteLine("Could not decompress file."); data.AddRange(d); } } else { data.AddRange(d); } //Console.WriteLine(" {0:x8}", d.Length); if (_debugDecompressionCode) { File.WriteAllBytes($"output-chunk-{x}-decomp.bin", d); } } } var crc32 = Crc32.Calculate(data.ToArray()); if (crc32 != file.crc32) { Console.WriteLine("Invalid CRC32: {0:x8} vs {1:x8}", crc32, file.crc32); //File.WriteAllBytes("invalid.bin", data.ToArray()); //Environment.Exit(1); } File.WriteAllBytes(output, data.ToArray()); if (data.Count != file.rawFilesize) { Console.WriteLine("Invalid file size: {0:x8} vs {1:x8}", data.Count, file.rawFilesize); //Environment.Exit(1); } Console.WriteLine(); } } }