static void Pack(string inPath, string outFile, bool eofEntry) { List <SubFile?> entries = new List <SubFile?>(); // Parse every file. foreach (string file in Directory.GetFiles(inPath)) { // Parse filename. string fileName = Path.GetFileName(file); int dotPos = fileName.IndexOf('.'); if (dotPos < 0) { dotPos = fileName.Length; } int uscPos = fileName.LastIndexOf('_', dotPos); string numString = fileName.Substring(uscPos + 1, dotPos - uscPos - 1); if (!Int32.TryParse(numString, out int fileNum)) { // No number string found; skip this file. continue; } // Expand file entries if necessary. while (entries.Count <= fileNum) { entries.Add(null); } // Read the file. byte[] buffer; long size; bool compressed; using (FileStream fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read)) { // Check if the file is compressed. using (MemoryStream ms = new MemoryStream()) { fs.Position = 0; ms.Position = 0; if (LZ77.Decompress(fs, ms) && ms.Position > 8) { compressed = true; size = ms.Position; } else { compressed = false; size = fs.Length; } } if (size > int.MaxValue) { if (compressed) { throw new IOException("Uncompressed size of file " + fileName + " exceeds " + int.MaxValue + " bytes."); } else { throw new IOException("Size of file " + fileName + " exceeds " + int.MaxValue + " bytes."); } } buffer = new byte[fs.Length]; fs.Position = 0; if (fs.Read(buffer, 0, buffer.Length) != fs.Length) { throw new IOException("Could not read entirety of input file " + fileName + "."); } } SubFile entry = new SubFile() { Size = (int)size, Compressed = compressed, Data = buffer }; entries[fileNum] = entry; } if (eofEntry) { // Append EOF entry. entries.Add(null); } // Create directory for the output file. string dir = Path.GetDirectoryName(Path.GetFullPath(outFile)); if (dir.Length > 0) { Directory.CreateDirectory(dir); } // Create the output file. using (FileStream fs = new FileStream(outFile, FileMode.Create, FileAccess.Write, FileShare.Write)) { BinaryWriter bw = new BinaryWriter(fs); // Write the header. (Account for terminator entry.) long filePos = (entries.Count + 1) * 8; for (int i = 0; i < entries.Count; i++) { if (entries[i] == null) { // Write empty entry. bw.Write((uint)filePos); bw.Write((uint)0); continue; } // Update entry with offset. SubFile entry = (SubFile)entries[i]; entry.Offset = (uint)filePos; entries[i] = entry; // Write entry. bw.Write((uint)entry.Offset); bw.Write((uint)((uint)entry.Size | (entry.Compressed ? 0x80000000 : 0))); // If file was compressed, round up to multiple of 4. long size = entry.Data.Length; if (entry.Compressed && size % 4 != 0) { size += 4 - (size % 4); } filePos += entry.Data.LongLength; // Round next file offset up to multiple of 4. (Optional?) if (true && filePos % 4 != 0) { filePos += 4 - (filePos % 4); } if (filePos > uint.MaxValue) { throw new IOException("Maximum file size for archive exceeded."); } } // Write terminator entry. bw.Write((uint)filePos); bw.Write((uint)0xFFFF); // Write the files. for (int i = 0; i < entries.Count; i++) { if (entries[i] is null) { continue; } SubFile entry = (SubFile)entries[i]; // Advance to actual offset of file. uint offset = entry.Offset; while (fs.Position < offset) { fs.WriteByte(0); } // Write the file data. fs.Write(entry.Data, 0, entry.Data.Length); } // Advance to end of file. while (fs.Position < filePos) { fs.WriteByte(0); } } Console.WriteLine("Created archive " + Path.GetFileName(outFile) + " with " + entries.Count + " subfiles."); }
static void Extract(string inFile, string outPath, bool decompress, bool eofEntry) { List <SubFile> entries = new List <SubFile>(); using (FileStream arcFile = new FileStream(inFile, FileMode.Open, FileAccess.Read, FileShare.Read)) using (MemoryStream uncompressed = new MemoryStream()) { BinaryReader br = new BinaryReader(arcFile); long headerEnd = arcFile.Length; long maxUncompressedSize = 0; // Load header. SubFile entry = default; while (arcFile.Position < headerEnd) { uint offset = br.ReadUInt32(); uint size = br.ReadUInt32(); entry = new SubFile() { Offset = offset, Size = (int)(size & 0x7FFFFFFF), Compressed = (size & 0x80000000) != 0 }; entries.Add(entry); if (entry.Compressed) { maxUncompressedSize = Math.Max(entry.Size, maxUncompressedSize); } headerEnd = Math.Min(entry.Offset, headerEnd); } if (arcFile.Position != headerEnd || (eofEntry && (entry.Size > 0 || entry.Compressed))) { throw new InvalidDataException("Invalid archive file header."); } if (eofEntry) { // Remove EOF entry. entries.RemoveAt(entries.Count - 1); } // Create directory to hold files. if (!outPath.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal)) { outPath += Path.DirectorySeparatorChar; } Directory.CreateDirectory(outPath); int digits = (entries.Count - 1).ToString().Length; // Pre-allocate decompression buffer. uncompressed.SetLength(maxUncompressedSize); // Extract the files. for (int i = 0; i < entries.Count; i++) { entry = entries[i]; long start = entry.Offset; // Skip last size 0xFFFF entry. if (i == entries.Count - 1 && start == arcFile.Length && entry.Size == 0xFFFF && !entry.Compressed) { entries.RemoveAt(i); continue; } string outFilePath = outPath + Path.GetFileNameWithoutExtension(inFile) + "_" + i.ToString().PadLeft(digits, '0') + ".bin"; // Get size of the file. long size; if (entry.Compressed) { // Get compressed size by decompressing, and check if decompressed size matches size in file entry. arcFile.Position = start; uncompressed.Position = 0; if (!LZ77.Decompress(arcFile, uncompressed) || uncompressed.Position != entry.Size) { throw new InvalidDataException("Could not read subfile " + i + ": invalid LZ77 compressed data."); } size = decompress ? uncompressed.Position : (arcFile.Position - start); uncompressed.Position = 0; } else { size = entry.Size; } // Read the file. arcFile.Position = start; entry.Data = new byte[size]; if ((decompress && entry.Compressed ? (Stream)uncompressed : (Stream)arcFile).Read(entry.Data, 0, (int)size) < size) { throw new InvalidDataException("Could not read subfile " + i + ": invalid size."); } // Write the file. using (FileStream subFile = new FileStream(outFilePath, FileMode.Create, FileAccess.Write, FileShare.Read)) { subFile.Write(entry.Data, 0, entry.Data.Length); } } } Console.WriteLine("Extracted " + entries.Count + " subfiles from archive " + Path.GetFileName(inFile) + "."); }