From 8468ce5fe61d09722cea1365d302c1809f043854 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 21 Jul 2015 21:11:38 +0100 Subject: [PATCH] added InstallShield CAB Extractor --- OpenRA.Game/FileSystem/GlobalFileSystem.cs | 4 + .../FileSystem/InstallShieldCABExtractor.cs | 526 ++++++++++++++++++ OpenRA.Game/OpenRA.Game.csproj | 1 + 3 files changed, 531 insertions(+) create mode 100644 OpenRA.Game/FileSystem/InstallShieldCABExtractor.cs diff --git a/OpenRA.Game/FileSystem/GlobalFileSystem.cs b/OpenRA.Game/FileSystem/GlobalFileSystem.cs index 183216ded0..fe92e513e7 100644 --- a/OpenRA.Game/FileSystem/GlobalFileSystem.cs +++ b/OpenRA.Game/FileSystem/GlobalFileSystem.cs @@ -74,6 +74,8 @@ namespace OpenRA.FileSystem throw new NotImplementedException("The creation of .PAK archives is unimplemented"); if (filename.EndsWith(".big", StringComparison.InvariantCultureIgnoreCase)) throw new NotImplementedException("The creation of .big archives is unimplemented"); + if (filename.EndsWith(".cab", StringComparison.InvariantCultureIgnoreCase)) + throw new NotImplementedException("The creation of .cab archives is unimplemented"); return new Folder(filename, order, content); } @@ -103,6 +105,8 @@ namespace OpenRA.FileSystem return new BigFile(filename, order); if (filename.EndsWith(".bag", StringComparison.InvariantCultureIgnoreCase)) return new BagFile(filename, order); + if (filename.EndsWith(".hdr", StringComparison.InvariantCultureIgnoreCase)) + return new InstallShieldCABExtractor(filename, order); return new Folder(filename, order); } diff --git a/OpenRA.Game/FileSystem/InstallShieldCABExtractor.cs b/OpenRA.Game/FileSystem/InstallShieldCABExtractor.cs new file mode 100644 index 0000000000..8f7ecc044a --- /dev/null +++ b/OpenRA.Game/FileSystem/InstallShieldCABExtractor.cs @@ -0,0 +1,526 @@ +#region Copyright & License Information +/* + * Copyright 2007-2015 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation. For more information, + * see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using ICSharpCode.SharpZipLib.Zip.Compression; +using ICSharpCode.SharpZipLib.Zip.Compression.Streams; + +namespace OpenRA.FileSystem +{ + public class InstallShieldCABExtractor : IDisposable, IFolder + { + const uint FileSplit = 0x1; + const uint FileObfuscated = 0x2; + const uint FileCompressed = 0x4; + const uint FileInvalid = 0x8; + + const uint LinkPrev = 0x1; + const uint LinkNext = 0x2; + const uint MaxFileGroupCount = 71; + +#region Nested Structs + + struct FileGroup + { + public readonly string Name; + public readonly uint FirstFile; + public readonly uint LastFile; + + public FileGroup(Stream reader, long offset) + { + var nameOffset = reader.ReadUInt32(); + /* unknown */ reader.ReadBytes(18); + FirstFile = reader.ReadUInt32(); + LastFile = reader.ReadUInt32(); + + reader.Seek(offset + (long)nameOffset, SeekOrigin.Begin); + Name = reader.ReadASCIIZ(); + } + } + + struct VolumeHeader + { + public readonly uint DataOffset; + public readonly uint DataOffsetHigh; + public readonly uint FirstFileIndex; + public readonly uint LastFileIndex; + + public readonly uint FirstFileOffset; + public readonly uint FirstFileOffsetHigh; + public readonly uint FirstFileSizeExpanded; + public readonly uint FirstFileSizeExpandedHigh; + + public readonly uint FirstFileSizeCompressed; + public readonly uint FirstFileSizeCompressedHigh; + public readonly uint LastFileOffset; + public readonly uint LastFileOffsetHigh; + + public readonly uint LastFileSizeExpanded; + public readonly uint LastFileSizeExpandedHigh; + public readonly uint LastFileSizeCompressed; + public readonly uint LastFileSizeCompressedHigh; + + public VolumeHeader(Stream reader) + { + DataOffset = reader.ReadUInt32(); + DataOffsetHigh = reader.ReadUInt32(); + + FirstFileIndex = reader.ReadUInt32(); + LastFileIndex = reader.ReadUInt32(); + FirstFileOffset = reader.ReadUInt32(); + FirstFileOffsetHigh = reader.ReadUInt32(); + + FirstFileSizeExpanded = reader.ReadUInt32(); + FirstFileSizeExpandedHigh = reader.ReadUInt32(); + FirstFileSizeCompressed = reader.ReadUInt32(); + FirstFileSizeCompressedHigh = reader.ReadUInt32(); + + LastFileOffset = reader.ReadUInt32(); + LastFileOffsetHigh = reader.ReadUInt32(); + LastFileSizeExpanded = reader.ReadUInt32(); + LastFileSizeExpandedHigh = reader.ReadUInt32(); + + LastFileSizeCompressed = reader.ReadUInt32(); + LastFileSizeCompressedHigh = reader.ReadUInt32(); + } + } + + struct CommonHeader + { + public const long Size = 16; + public readonly uint Version; + public readonly uint VolumeInfo; + public readonly long CabDescriptorOffset; + public readonly uint CabDescriptorSize; + + public CommonHeader(Stream reader) + { + Version = reader.ReadUInt32(); + VolumeInfo = reader.ReadUInt32(); + CabDescriptorOffset = (long)reader.ReadUInt32(); + CabDescriptorSize = reader.ReadUInt32(); + } + } + + struct CabDescriptor + { + public readonly long FileTableOffset; + public readonly uint FileTableSize; + public readonly uint FileTableSize2; + public readonly uint DirectoryCount; + + public readonly uint FileCount; + public readonly long FileTableOffset2; + + public CabDescriptor(Stream reader, CommonHeader commonHeader) + { + reader.Seek(commonHeader.CabDescriptorOffset + 12, SeekOrigin.Begin); + FileTableOffset = (long)reader.ReadUInt32(); + /* unknown */ reader.ReadUInt32(); + FileTableSize = reader.ReadUInt32(); + + FileTableSize2 = reader.ReadUInt32(); + DirectoryCount = reader.ReadUInt32(); + /* unknown */ reader.ReadBytes(8); + FileCount = reader.ReadUInt32(); + + FileTableOffset2 = (long)reader.ReadUInt32(); + } + } + + struct FileDescriptor + { + public readonly ushort Flags; + public readonly uint ExpandedSize; + public readonly uint CompressedSize; + public readonly uint DataOffset; + + public readonly byte[] MD5; + public readonly uint NameOffset; + public readonly ushort DirectoryIndex; + public readonly uint LinkToPrevious; + + public readonly uint LinkToNext; + public readonly byte LinkFlags; + public readonly ushort Volume; + public readonly string Filename; + + public FileDescriptor(Stream reader, long tableOffset) + { + Flags = reader.ReadUInt16(); + ExpandedSize = reader.ReadUInt32(); + /* unknown */ reader.ReadUInt32(); + CompressedSize = reader.ReadUInt32(); + + /* unknown */ reader.ReadUInt32(); + DataOffset = reader.ReadUInt32(); + /* unknown */ reader.ReadUInt32(); + MD5 = reader.ReadBytes(16); + + /* unknown */ reader.ReadBytes(16); + NameOffset = reader.ReadUInt32(); + DirectoryIndex = reader.ReadUInt16(); + /* unknown */ reader.ReadBytes(12); + LinkToPrevious = reader.ReadUInt32(); + LinkToNext = reader.ReadUInt32(); + + LinkFlags = reader.ReadBytes(1)[0]; + Volume = reader.ReadUInt16(); + var posSave = reader.Position; + + reader.Seek(tableOffset + NameOffset, SeekOrigin.Begin); + Filename = reader.ReadASCIIZ(); + reader.Seek(posSave, SeekOrigin.Begin); + } + } + + class CabReader : IDisposable + { + readonly FileDescriptor fileDes; + public uint RemainingArchiveStream; + public uint RemainingFileStream; + readonly uint index; + readonly string commonName; + ushort volumeNumber; + Stream cabFile; + + public CabReader(FileDescriptor fileDes, uint index, string commonName) + { + this.fileDes = fileDes; + this.index = index; + this.commonName = commonName; + volumeNumber = (ushort)((uint)fileDes.Volume - 1u); + RemainingArchiveStream = 0; + if ((fileDes.Flags & FileCompressed) > 0) + RemainingFileStream = fileDes.CompressedSize; + else + RemainingFileStream = fileDes.ExpandedSize; + + cabFile = null; + NextFile(); + } + + public void CopyTo(Stream dest) + { + if ((fileDes.Flags & FileCompressed) != 0) + { + var inf = new Inflater(true); + var buffer = new byte[165535]; + do + { + var bytesToExtract = cabFile.ReadUInt16(); + RemainingArchiveStream -= 2u; + RemainingFileStream -= 2u; + inf.SetInput(GetBytes(bytesToExtract)); + RemainingFileStream -= bytesToExtract; + while (!inf.IsNeedingInput) + { + var inflated = inf.Inflate(buffer); + dest.Write(buffer, 0, inflated); + } + + inf.Reset(); + } + while (RemainingFileStream > 0); + } + else + { + do + { + RemainingFileStream -= RemainingArchiveStream; + dest.Write(GetBytes(RemainingArchiveStream), 0, (int)RemainingArchiveStream); + } + while (RemainingFileStream > 0); + } + } + + public byte[] GetBytes(uint count) + { + if (count < RemainingArchiveStream) + { + RemainingArchiveStream -= count; + return cabFile.ReadBytes((int)count); + } + else + { + var outArray = new byte[count]; + var read = cabFile.Read(outArray, 0, (int)RemainingArchiveStream); + if (RemainingFileStream > RemainingArchiveStream) + { + NextFile(); + RemainingArchiveStream -= (uint)cabFile.Read(outArray, read, (int)count - read); + } + + return outArray; + } + } + + public void Dispose() + { + cabFile.Dispose(); + } + + public void NextFile() + { + if (cabFile != null) + cabFile.Dispose(); + + ++volumeNumber; + cabFile = GlobalFileSystem.Open("{0}{1}.cab".F(commonName, volumeNumber)); + if (cabFile.ReadUInt32() != 0x28635349) + throw new InvalidDataException("Not an Installshield CAB package"); + + uint fileOffset; + if ((fileDes.Flags & FileSplit) != 0) + { + cabFile.Seek(CommonHeader.Size, SeekOrigin.Current); + var head = new VolumeHeader(cabFile); + if (index == head.LastFileIndex) + { + if ((fileDes.Flags & FileCompressed) != 0) + RemainingArchiveStream = head.LastFileSizeCompressed; + else + RemainingArchiveStream = head.LastFileSizeExpanded; + + fileOffset = head.LastFileOffset; + } + else if (index == head.FirstFileIndex) + { + if ((fileDes.Flags & FileCompressed) != 0) + RemainingArchiveStream = head.FirstFileSizeCompressed; + else + RemainingArchiveStream = head.FirstFileSizeExpanded; + + fileOffset = head.FirstFileOffset; + } + else + throw new Exception("Cannot Resolve Remaining Stream"); + } + else + { + if ((fileDes.Flags & FileCompressed) != 0) + RemainingArchiveStream = fileDes.CompressedSize; + else + RemainingArchiveStream = fileDes.ExpandedSize; + + fileOffset = fileDes.DataOffset; + } + + cabFile.Seek(fileOffset, SeekOrigin.Begin); + } + } +#endregion + + readonly Stream hdrFile; + readonly CommonHeader commonHeader; + readonly CabDescriptor cabDescriptor; + readonly List directoryTable; + readonly Dictionary directoryNames = new Dictionary(); + readonly Dictionary fileDescriptors = new Dictionary(); + readonly Dictionary fileLookup = new Dictionary(); + int priority; + string commonName; + public int Priority { get { return priority; } } + + public string Name { get { return commonName; } } + + public InstallShieldCABExtractor(string hdrFilename, int priority = -1) + { + var fileGroups = new List(); + var fileGroupOffsets = new List(); + + this.priority = priority; + hdrFile = GlobalFileSystem.Open(hdrFilename); + + // Strips archive number AND file extension + commonName = Regex.Replace(hdrFilename, @"\d*\.[^\.]*$", ""); + var signature = hdrFile.ReadUInt32(); + + if (signature != 0x28635349) + throw new InvalidDataException("Not an Installshield CAB package"); + + commonHeader = new CommonHeader(hdrFile); + cabDescriptor = new CabDescriptor(hdrFile, commonHeader); + /* unknown */ hdrFile.ReadBytes(14); + + for (var i = 0U; i < MaxFileGroupCount; ++i) + fileGroupOffsets.Add(hdrFile.ReadUInt32()); + + hdrFile.Seek(commonHeader.CabDescriptorOffset + cabDescriptor.FileTableOffset, SeekOrigin.Begin); + directoryTable = new List(); + + for (var i = 0U; i < cabDescriptor.DirectoryCount; ++i) + directoryTable.Add(hdrFile.ReadUInt32()); + + foreach (var offset in fileGroupOffsets) + { + var nextOffset = offset; + while (nextOffset != 0) + { + hdrFile.Seek((long)nextOffset + 4 + commonHeader.CabDescriptorOffset, SeekOrigin.Begin); + var descriptorOffset = hdrFile.ReadUInt32(); + nextOffset = hdrFile.ReadUInt32(); + hdrFile.Seek((long)descriptorOffset + commonHeader.CabDescriptorOffset, SeekOrigin.Begin); + + fileGroups.Add(new FileGroup(hdrFile, commonHeader.CabDescriptorOffset)); + } + } + + hdrFile.Seek(commonHeader.CabDescriptorOffset + cabDescriptor.FileTableOffset + cabDescriptor.FileTableOffset2, SeekOrigin.Begin); + foreach (var fileGroup in fileGroups) + { + for (var index = fileGroup.FirstFile; index <= fileGroup.LastFile; ++index) + { + AddFileDescriptorToList(index); + var fileDescriptor = fileDescriptors[index]; + var fullFilePath = "{0}\\{1}\\{2}".F(fileGroup.Name, DirectoryName((uint)fileDescriptor.DirectoryIndex), fileDescriptor.Filename); + fileLookup.Add(fullFilePath, index); + } + } + } + + public string DirectoryName(uint index) + { + if (directoryNames.ContainsKey(index)) + return directoryNames[index]; + + hdrFile.Seek(commonHeader.CabDescriptorOffset + + cabDescriptor.FileTableOffset + + directoryTable[(int)index], + SeekOrigin.Begin); + + var test = hdrFile.ReadASCIIZ(); + return test; + } + + public bool Exists(string filename) + { + return fileLookup.ContainsKey(filename); + } + + public uint DirectoryCount() + { + return cabDescriptor.DirectoryCount; + } + + public string FileName(uint index) + { + if (!fileDescriptors.ContainsKey(index)) + AddFileDescriptorToList(index); + + return fileDescriptors[index].Filename; + } + + void AddFileDescriptorToList(uint index) + { + hdrFile.Seek(commonHeader.CabDescriptorOffset + + cabDescriptor.FileTableOffset + + cabDescriptor.FileTableOffset2 + + index * 0x57, + SeekOrigin.Begin); + + var fd = new FileDescriptor(hdrFile, + commonHeader.CabDescriptorOffset + cabDescriptor.FileTableOffset); + + fileDescriptors.Add(index, fd); + } + + public uint FileCount() + { + return cabDescriptor.FileCount; + } + + public void ExtractFile(uint index, string fileName) + { + using (var destfile = File.Open(fileName, FileMode.Create)) + GetContentById(index, destfile); + } + + public void Write(Dictionary input) + { + throw new NotImplementedException("Cannot Add Files To Cab"); + } + + public IEnumerable ClassicHashes() + { + return fileLookup.Keys.Select(k => PackageEntry.HashFilename(k, PackageHashType.Classic)); + } + + public Stream GetContentById(uint index) + { + var fileDes = fileDescriptors[index]; + if ((fileDes.Flags & FileInvalid) != 0) + throw new Exception("File Invalid"); + + if ((fileDes.LinkFlags & LinkPrev) != 0) + return GetContentById(fileDes.LinkToPrevious); + + if ((fileDes.Flags & FileObfuscated) != 0) + throw new NotImplementedException("Haven't implemented obfustcated files"); + + var output = new MemoryStream((int)fileDes.ExpandedSize); + + using (var reader = new CabReader(fileDes, index, commonName)) + reader.CopyTo(output); + + if (output.Length != fileDes.ExpandedSize) + throw new Exception("Did not fully extract Expected = {0}, Got = {1}".F(fileDes.ExpandedSize, output.Length)); + + output.Position = 0; + return output; + } + + public void GetContentById(uint index, Stream output) + { + var fileDes = fileDescriptors[index]; + if ((fileDes.Flags & FileInvalid) != 0) + throw new Exception("File Invalid"); + + if ((fileDes.LinkFlags & LinkPrev) != 0) + { + GetContentById(fileDes.LinkToPrevious, output); + return; + } + + if ((fileDes.Flags & FileObfuscated) != 0) + throw new NotImplementedException("Haven't implemented obfustcated files"); + + using (var reader = new CabReader(fileDes, index, commonName)) + reader.CopyTo(output); + + if (output.Length != fileDes.ExpandedSize) + throw new Exception("Did not fully extract Expected = {0}, Got = {1}".F(fileDes.ExpandedSize, output.Length)); + } + + public Stream GetContent(string fileName) + { + return GetContentById(fileLookup[fileName]); + } + + public IEnumerable CrcHashes() + { + yield break; + } + + public IEnumerable AllFileNames() + { + return fileLookup.Keys; + } + + public void Dispose() + { + hdrFile.Dispose(); + } + } +} diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index 477deb934d..ea74fbb26a 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -253,6 +253,7 @@ +