using System; using System.Text; using System.IO; using System.Linq; using System.IO.Compression; using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; public class MMArchiveHelper { public static void CheckMagic(uint magic, uint required) { if (magic != required) { throw new Exception(string.Format("Invalid Magic {0:08X} (expected {1:08X})", magic, required)); } } public static string ReadASCII(Stream stream) { var builder = new StringBuilder(); for (int i; (i = stream.ReadByte()) > 0;) { builder.Append((char) i); } return builder.ToString(); } public class ArchiveEntry { public readonly string Name; public readonly long Size; public readonly long RawSize; public readonly long Position; public ArchiveEntry(string name, long size, long rawSize, long fileOffset) { Name = name; Size = size; RawSize = rawSize; Position = fileOffset; } public byte[] GetContents(Stream input) { input.Seek(Position, SeekOrigin.Begin); if (Size != RawSize) { input = new DeflateStream(input, CompressionMode.Decompress, true); } var buffer = new byte[Size]; input.Read(buffer, 0, buffer.Length); return buffer; } public override string ToString() { return Name; } } public class VirtualFileInode { public readonly string Name; public readonly uint DataOffset; public readonly uint Size; public readonly bool IsDirectory; public VirtualFileInode(BinaryReader reader, Stream nameStream) { DataOffset = reader.ReadUInt32(); var field4 = reader.ReadUInt32(); var field8 = reader.ReadUInt32(); Size = (field4 & 0x7FFFFF); IsDirectory = (field8 & 1) != 0; var nameOffset = (field8 >> 14) & 0x3FFFF; var extensionOffset = (field4 >> 23) & 0x1FF; var nameInteger = (field8 >> 1) & 0x1FFF; nameStream.Seek(nameOffset, SeekOrigin.Begin); Name = ReadASCII(nameStream).Replace("\x01", nameInteger.ToString()); if (extensionOffset != 0) { nameStream.Seek(extensionOffset, SeekOrigin.Begin); Name += "." + ReadASCII(nameStream); } } public ArchiveEntry GetArchiveEntry(string parent = "") { if (!IsDirectory) { return new ArchiveEntry(parent + Name, Size, Size, DataOffset); } return null; } public IEnumerable GetChildren(VirtualFileInode[] nodes) { if (IsDirectory) { for (var i = DataOffset; i < DataOffset + Size; ++i) { yield return nodes[i]; } } } public IEnumerable GetFiles(VirtualFileInode[] nodes, string parent = "") { if (IsDirectory) { var name = parent + Name + "\\"; foreach (var node in GetChildren(nodes)) { if (node.IsDirectory) { foreach (var file in node.GetFiles(nodes, name)) { yield return file; } } else { yield return node.GetArchiveEntry(name); } } } } public override string ToString() { return Name; } } public static IEnumerable ReadDaveArchive(BinaryReader reader) { reader.BaseStream.Seek(0, SeekOrigin.Begin); CheckMagic(reader.ReadUInt32(), 0x45564144); var headerCount = reader.ReadUInt32(); var namesOffset = reader.ReadUInt32(); var namesSize = reader.ReadUInt32(); for (var i = 0; i < headerCount; ++i) { reader.BaseStream.Seek(2048 + (i * 16), SeekOrigin.Begin); var nameOffset = reader.ReadUInt32(); var dataOffset = reader.ReadUInt32(); var uncompressedSize = reader.ReadUInt32(); var compressedSize = reader.ReadUInt32(); reader.BaseStream.Seek(2048 + namesOffset + nameOffset, SeekOrigin.Begin); var name = ReadASCII(reader.BaseStream); yield return new ArchiveEntry(name, uncompressedSize, compressedSize, dataOffset); } } public static IEnumerable ReadPKArchive(BinaryReader reader) { reader.BaseStream.Seek(-22, SeekOrigin.End); CheckMagic(reader.ReadUInt32(), 0x06054B50); // ZIPENDLOCATOR var diskNumber = reader.ReadUInt16(); var startDiskNumber = reader.ReadUInt16(); if (diskNumber != startDiskNumber) { throw new Exception("Incomplete Archive"); } var fileCount = reader.ReadUInt16(); var filesInDirectory = reader.ReadUInt16(); var directorySize = reader.ReadUInt32(); var directoryOffset = reader.ReadUInt32(); var fileCommentLength = reader.ReadUInt16(); var currentOffset = directoryOffset; while (true) { reader.BaseStream.Seek(currentOffset, SeekOrigin.Begin); if (reader.ReadUInt32() != 0x02014B50) // ZIPDIRENTRY { break; } var versionMadeBy = reader.ReadUInt16(); var versionToExtract = reader.ReadUInt16(); var flags = reader.ReadUInt16(); var compressionMethod = reader.ReadUInt16(); if (compressionMethod != 0 && compressionMethod != 8) { throw new Exception("Invalid compression method " + compressionMethod); } var fileTime = reader.ReadUInt16(); var fileDate = reader.ReadUInt16(); var crc = reader.ReadUInt32(); var compressedSize = reader.ReadUInt32(); var uncompressedSize = reader.ReadUInt32(); var nameLength = reader.ReadUInt16(); var extraLength = reader.ReadUInt16(); var commentLength = reader.ReadUInt16(); var diskNumberStart = reader.ReadUInt16(); var internalAttributes = reader.ReadUInt16(); var externalAttributes = reader.ReadUInt32(); var dataOffset = reader.ReadUInt32(); // ZIPFILERECORD var name = new string(reader.ReadChars(nameLength)); currentOffset = (uint) reader.BaseStream.Position + extraLength + commentLength; yield return new ArchiveEntry(name, uncompressedSize, compressedSize, dataOffset + nameLength + 30); } } public static IEnumerable ReadARESArchive(BinaryReader reader) { reader.BaseStream.Seek(0, SeekOrigin.Begin); CheckMagic(reader.ReadUInt32(), 0x53455241); var headerCount = reader.ReadUInt32(); var directoryCount = reader.ReadUInt32(); var namesSize = reader.ReadUInt32(); var namesOffset = 16 + headerCount * 12; reader.BaseStream.Seek(namesOffset, SeekOrigin.Begin); var namesStream = new MemoryStream(reader.ReadBytes((int) namesSize)); var nodes = new VirtualFileInode[headerCount]; for (var i = 0; i < headerCount; ++i) { reader.BaseStream.Seek(16 + (i * 12), SeekOrigin.Begin); nodes[i] = new VirtualFileInode(reader, namesStream); } for (var i = 0; i < directoryCount; ++i) { foreach (var file in nodes[i].GetFiles(nodes, "")) { yield return file; } } } public static IEnumerable GetArchiveEntries(Stream archive) { var binaryReader = new BinaryReader(archive); binaryReader.BaseStream.Seek(0, SeekOrigin.Begin); if (binaryReader.ReadUInt32() == 0x45564144) // DAVE { return ReadDaveArchive(binaryReader); } binaryReader.BaseStream.Seek(0, SeekOrigin.Begin); if (binaryReader.ReadUInt32() == 0x53455241) // ARES { return ReadARESArchive(binaryReader); } binaryReader.BaseStream.Seek(-22, SeekOrigin.End); if (binaryReader.ReadUInt32() == 0x06054B50) // ZIPENDLOCATOR { return ReadPKArchive(binaryReader); } throw new ArgumentException("Invalid archive stream"); } } namespace DaveReader { class MMArchiveReader { static string ConsoleClearLine() { return string.Format("\r{0}\r", new string(' ', Console.WindowWidth - 1)); } static void OverwriteConsole(string format, params object[ ] args) { Console.Write(ConsoleClearLine() + format, args); } static void OverwriteConsoleLine(string format, params object[ ] args) { Console.WriteLine(ConsoleClearLine() + format, args); } static void ExtractArchive(string path, bool merge) { using (var fileStream = Stream.Synchronized(File.OpenRead(path))) { var stopwatch = Stopwatch.StartNew(); var tasks = MMArchiveHelper.GetArchiveEntries(fileStream).Select((entry) => { try { return new Task(() => { var contents = entry.GetContents(fileStream); if (contents.Length > 0) { var outputName = merge ? Path.Combine(Environment.CurrentDirectory, "Extracted", entry.Name) : Path.Combine(Environment.CurrentDirectory, "Extracted", Path.GetFileNameWithoutExtension(path), entry.Name); Directory.CreateDirectory(Path.GetDirectoryName(outputName)); using (var output = File.OpenWrite(outputName)) { output.Write(contents, 0, contents.Length); } } }); } catch (Exception e) { Console.WriteLine("Failed to extract {0} - {1}", entry.Name, e); } return null; }).ToArray(); foreach (var task in tasks) { task.Start(); } var endTask = Task.WhenAll(tasks); while (!endTask.Wait(100)) { var completed = tasks.Where(x => x.IsCompleted).ToArray(); OverwriteConsole("{0} : {1} / {2}", path, completed.Length, tasks.Length); } OverwriteConsoleLine("{0} : {1} files in {2} ms", path, tasks.Length, stopwatch.ElapsedMilliseconds); } } static string GetArgOrReadLine(string[] args, uint index) { return args.Length > index ? args[index] : Console.ReadLine(); } static string GetArgOfDefault(string[] args, uint index, string defaultValue) { return args.Length > index ? args[index] : defaultValue; } static void Main(string[] args) { Console.Clear(); Console.ForegroundColor = ConsoleColor.Green; var path = GetArgOrReadLine(args, 0); var merge = bool.Parse(GetArgOfDefault(args, 1, "false")); if (File.Exists(path)) { ExtractArchive(path, merge); } else if (Directory.Exists(path)) { foreach (var file in Directory.GetFiles(path, "*.ar")) { ExtractArchive(file, merge); } } else { Console.WriteLine("Invalid file/path: {0}", path); } Console.WriteLine("Finished"); Console.ReadKey(true); } } }