diff --git a/OpenRA.Mods.Common/FileFormats/InstallShieldCABCompression.cs b/OpenRA.Mods.Common/FileFormats/InstallShieldCABCompression.cs new file mode 100644 index 0000000000..53b970a483 --- /dev/null +++ b/OpenRA.Mods.Common/FileFormats/InstallShieldCABCompression.cs @@ -0,0 +1,438 @@ +#region Copyright & License Information +/* + * Copyright 2007-2016 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, either version 3 of + * the License, or (at your option) any later version. 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; + +namespace OpenRA.Mods.Common.FileFormats +{ + public sealed class InstallShieldCABCompression + { + const uint MaxFileGroupCount = 71; + + enum CABFlags : ushort + { + FileSplit = 0x1, + FileObfuscated = 0x2, + FileCompressed = 0x4, + FileInvalid = 0x8, + } + + enum LinkFlags : byte + { + Prev = 0x1, + Next = 0x2 + } + + struct FileGroup + { + public readonly string Name; + public readonly uint FirstFile; + public readonly uint LastFile; + + public FileGroup(Stream stream, long offset) + { + var nameOffset = stream.ReadUInt32(); + stream.Position += 18; + FirstFile = stream.ReadUInt32(); + LastFile = stream.ReadUInt32(); + + var pos = stream.Position; + stream.Position = offset + nameOffset; + Name = stream.ReadASCIIZ(); + stream.Position = pos; + } + } + + 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 stream) + { + FileTableOffset = stream.ReadUInt32(); + stream.Position += 4; + FileTableSize = stream.ReadUInt32(); + FileTableSize2 = stream.ReadUInt32(); + DirectoryCount = stream.ReadUInt32(); + stream.Position += 8; + FileCount = stream.ReadUInt32(); + FileTableOffset2 = stream.ReadUInt32(); + } + } + + struct DirectoryDescriptor + { + public readonly string Name; + + public DirectoryDescriptor(Stream stream, long nameTableOffset) + { + var nameOffset = stream.ReadUInt32(); + var pos = stream.Position; + + stream.Position = nameTableOffset + nameOffset; + + Name = stream.ReadASCIIZ(); + stream.Position = pos; + } + } + + struct FileDescriptor + { + public readonly uint Index; + public readonly CABFlags 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 LinkFlags LinkFlags; + public readonly ushort Volume; + public readonly string Filename; + + public FileDescriptor(Stream stream, uint index, long tableOffset) + { + Index = index; + Flags = (CABFlags)stream.ReadUInt16(); + ExpandedSize = stream.ReadUInt32(); + stream.Position += 4; + CompressedSize = stream.ReadUInt32(); + + stream.Position += 4; + DataOffset = stream.ReadUInt32(); + stream.Position += 4; + MD5 = stream.ReadBytes(16); + + stream.Position += 16; + NameOffset = stream.ReadUInt32(); + DirectoryIndex = stream.ReadUInt16(); + stream.Position += 12; + LinkToPrevious = stream.ReadUInt32(); + LinkToNext = stream.ReadUInt32(); + + LinkFlags = (LinkFlags)stream.ReadUInt8(); + Volume = stream.ReadUInt16(); + + var pos = stream.Position; + stream.Position = tableOffset + NameOffset; + Filename = stream.ReadASCIIZ(); + stream.Position = pos; + } + } + + 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 stream) + { + Version = stream.ReadUInt32(); + VolumeInfo = stream.ReadUInt32(); + CabDescriptorOffset = stream.ReadUInt32(); + CabDescriptorSize = stream.ReadUInt32(); + } + } + + 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 stream) + { + DataOffset = stream.ReadUInt32(); + DataOffsetHigh = stream.ReadUInt32(); + + FirstFileIndex = stream.ReadUInt32(); + LastFileIndex = stream.ReadUInt32(); + FirstFileOffset = stream.ReadUInt32(); + FirstFileOffsetHigh = stream.ReadUInt32(); + + FirstFileSizeExpanded = stream.ReadUInt32(); + FirstFileSizeExpandedHigh = stream.ReadUInt32(); + FirstFileSizeCompressed = stream.ReadUInt32(); + FirstFileSizeCompressedHigh = stream.ReadUInt32(); + + LastFileOffset = stream.ReadUInt32(); + LastFileOffsetHigh = stream.ReadUInt32(); + LastFileSizeExpanded = stream.ReadUInt32(); + LastFileSizeExpandedHigh = stream.ReadUInt32(); + + LastFileSizeCompressed = stream.ReadUInt32(); + LastFileSizeCompressedHigh = stream.ReadUInt32(); + } + } + + class CabExtracter + { + readonly FileDescriptor file; + readonly Dictionary volumes; + + uint remainingInArchive; + uint toExtract; + + int currentVolumeID; + Stream currentVolume; + + public CabExtracter(FileDescriptor file, Dictionary volumes) + { + this.file = file; + this.volumes = volumes; + + remainingInArchive = 0; + toExtract = file.Flags.HasFlag(CABFlags.FileCompressed) ? file.CompressedSize : file.ExpandedSize; + + SetVolume(file.Volume); + } + + public void CopyTo(Stream output, Action onProgress) + { + if (file.Flags.HasFlag(CABFlags.FileCompressed)) + { + var inf = new Inflater(true); + var buffer = new byte[165535]; + do + { + var bytesToExtract = currentVolume.ReadUInt16(); + remainingInArchive -= 2; + toExtract -= 2; + inf.SetInput(GetBytes(bytesToExtract)); + toExtract -= bytesToExtract; + while (!inf.IsNeedingInput) + { + if (onProgress != null) + onProgress((int)(100 * output.Position / file.ExpandedSize)); + + var inflated = inf.Inflate(buffer); + output.Write(buffer, 0, inflated); + } + + inf.Reset(); + } while (toExtract > 0); + } + else + { + do + { + if (onProgress != null) + onProgress((int)(100 * output.Position / file.ExpandedSize)); + + toExtract -= remainingInArchive; + output.Write(GetBytes(remainingInArchive), 0, (int)remainingInArchive); + } while (toExtract > 0); + } + } + + public byte[] GetBytes(uint count) + { + if (count < remainingInArchive) + { + remainingInArchive -= count; + return currentVolume.ReadBytes((int)count); + } + else + { + var outArray = new byte[count]; + var read = currentVolume.Read(outArray, 0, (int)remainingInArchive); + if (toExtract > remainingInArchive) + { + SetVolume(currentVolumeID + 1); + remainingInArchive -= (uint)currentVolume.Read(outArray, read, (int)count - read); + } + + return outArray; + } + } + + void SetVolume(int newVolume) + { + currentVolumeID = newVolume; + if (!volumes.TryGetValue(currentVolumeID, out currentVolume)) + throw new FileNotFoundException("Volume {0} is not available".F(currentVolumeID)); + + currentVolume.Position = 0; + if (currentVolume.ReadUInt32() != 0x28635349) + throw new InvalidDataException("Not an Installshield CAB package"); + + uint fileOffset; + if (file.Flags.HasFlag(CABFlags.FileSplit)) + { + currentVolume.Position += CommonHeader.Size; + var head = new VolumeHeader(currentVolume); + if (file.Index == head.LastFileIndex) + { + if (file.Flags.HasFlag(CABFlags.FileCompressed)) + remainingInArchive = head.LastFileSizeCompressed; + else + remainingInArchive = head.LastFileSizeExpanded; + + fileOffset = head.LastFileOffset; + } + else if (file.Index == head.FirstFileIndex) + { + if (file.Flags.HasFlag(CABFlags.FileCompressed)) + remainingInArchive = head.FirstFileSizeCompressed; + else + remainingInArchive = head.FirstFileSizeExpanded; + + fileOffset = head.FirstFileOffset; + } + else + throw new Exception("Cannot Resolve Remaining Stream"); + } + else + { + if (file.Flags.HasFlag(CABFlags.FileCompressed)) + remainingInArchive = file.CompressedSize; + else + remainingInArchive = file.ExpandedSize; + + fileOffset = file.DataOffset; + } + + currentVolume.Position = fileOffset; + } + } + + readonly Dictionary index = new Dictionary(); + readonly Dictionary volumes; + + public InstallShieldCABCompression(Stream header, Dictionary volumes) + { + this.volumes = volumes; + + if (header.ReadUInt32() != 0x28635349) + throw new InvalidDataException("Not an Installshield CAB package"); + + header.Position += 8; + var cabDescriptorOffset = header.ReadUInt32(); + header.Position = cabDescriptorOffset + 12; + var cabDescriptor = new CabDescriptor(header); + header.Position += 14; + + var fileGroupOffsets = new uint[MaxFileGroupCount]; + for (var i = 0; i < MaxFileGroupCount; i++) + fileGroupOffsets[i] = header.ReadUInt32(); + + header.Position = cabDescriptorOffset + cabDescriptor.FileTableOffset; + var directories = new DirectoryDescriptor[cabDescriptor.DirectoryCount]; + for (var i = 0; i < directories.Length; i++) + directories[i] = new DirectoryDescriptor(header, cabDescriptorOffset + cabDescriptor.FileTableOffset); + + var fileGroups = new List(); + foreach (var offset in fileGroupOffsets) + { + var nextOffset = offset; + while (nextOffset != 0) + { + header.Position = cabDescriptorOffset + (long)nextOffset + 4; + var descriptorOffset = header.ReadUInt32(); + nextOffset = header.ReadUInt32(); + header.Position = cabDescriptorOffset + descriptorOffset; + + fileGroups.Add(new FileGroup(header, cabDescriptorOffset)); + } + } + + header.Position = cabDescriptorOffset + cabDescriptor.FileTableOffset + cabDescriptor.FileTableOffset2; + foreach (var fileGroup in fileGroups) + { + for (var i = fileGroup.FirstFile; i <= fileGroup.LastFile; i++) + { + header.Position = cabDescriptorOffset + cabDescriptor.FileTableOffset + cabDescriptor.FileTableOffset2 + i * 0x57; + var file = new FileDescriptor(header, i, cabDescriptorOffset + cabDescriptor.FileTableOffset); + var path = "{0}\\{1}\\{2}".F(fileGroup.Name, directories[file.DirectoryIndex].Name, file.Filename); + index[path] = file; + } + } + } + + public void ExtractFile(string filename, Stream output, Action onProgress = null) + { + FileDescriptor file; + if (!index.TryGetValue(filename, out file)) + throw new FileNotFoundException(filename); + + ExtractFile(file, output, onProgress); + } + + void ExtractFile(FileDescriptor file, Stream output, Action onProgress = null) + { + if (file.Flags.HasFlag(CABFlags.FileInvalid)) + throw new Exception("File Invalid"); + + if (file.LinkFlags.HasFlag(LinkFlags.Prev)) + { + var prev = index.Values.First(f => f.Index == file.LinkToPrevious); + ExtractFile(prev, output, onProgress); + return; + } + + if (file.Flags.HasFlag(CABFlags.FileObfuscated)) + throw new NotImplementedException("Obfuscated files are not supported"); + + var extracter = new CabExtracter(file, volumes); + extracter.CopyTo(output, onProgress); + + if (output.Length != file.ExpandedSize) + throw new InvalidDataException("File expanded to wrong length. Expected = {0}, Got = {1}".F(file.ExpandedSize, output.Length)); + } + + public IReadOnlyDictionary> Contents + { + get + { + var contents = new Dictionary>(); + foreach (var kv in index) + contents.GetOrAdd(kv.Value.Volume).Add(kv.Key); + + return new ReadOnlyDictionary>(contents + .ToDictionary(x => x.Key, x => x.Value.AsEnumerable())); + } + } + } +} diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index aa47b17679..44c49a43f9 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -775,6 +775,8 @@ + + diff --git a/OpenRA.Mods.Common/UtilityCommands/ListInstallShieldCabContentsCommand.cs b/OpenRA.Mods.Common/UtilityCommands/ListInstallShieldCabContentsCommand.cs new file mode 100644 index 0000000000..04e9610e50 --- /dev/null +++ b/OpenRA.Mods.Common/UtilityCommands/ListInstallShieldCabContentsCommand.cs @@ -0,0 +1,41 @@ +#region Copyright & License Information +/* + * Copyright 2007-2016 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, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using OpenRA.Mods.Common.FileFormats; + +namespace OpenRA.Mods.Common.UtilityCommands +{ + class ListInstallShieldCabContentsCommand : IUtilityCommand + { + public string Name { get { return "--list-installshield-cab"; } } + + public bool ValidateArguments(string[] args) + { + return args.Length == 2; + } + + [Desc("DATA.HDR", "Lists the filenames contained within an Installshield CAB volume set")] + public void Run(ModData modData, string[] args) + { + var package = new InstallShieldCABCompression(File.OpenRead(args[1]), null); + foreach (var volume in package.Contents.OrderBy(kv => kv.Key)) + { + Console.WriteLine("Volume: {0}", volume.Key); + foreach (var filename in volume.Value) + Console.WriteLine("\t{0}", filename); + } + } + } +} diff --git a/OpenRA.Mods.Common/Widgets/Logic/Installation/InstallFromDiscLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Installation/InstallFromDiscLogic.cs index 8583959e37..5fabfccd88 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Installation/InstallFromDiscLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Installation/InstallFromDiscLogic.cs @@ -234,6 +234,12 @@ namespace OpenRA.Mods.Common.Widgets.Logic break; } + case "extract-iscab": + { + ExtractFromISCab(path, i.Value, extracted, m => message = m); + break; + } + default: Log.Write("debug", "Unknown installation command {0} - ignoring", i.Key); break; @@ -370,6 +376,59 @@ namespace OpenRA.Mods.Common.Widgets.Logic } } + static void ExtractFromISCab(string path, MiniYaml actionYaml, List extractedFiles, Action updateMessage) + { + var sourcePath = Path.Combine(path, actionYaml.Value); + var volumeNode = actionYaml.Nodes.FirstOrDefault(n => n.Key == "Volumes"); + if (volumeNode == null) + throw new InvalidDataException("extract-iscab entry doesn't define a Volumes node"); + + var extractNode = actionYaml.Nodes.FirstOrDefault(n => n.Key == "Extract"); + if (extractNode == null) + throw new InvalidDataException("extract-iscab entry doesn't define an Extract node"); + + var volumes = new Dictionary(); + try + { + foreach (var node in volumeNode.Value.Nodes) + { + var volume = FieldLoader.GetValue("(key)", node.Key); + var stream = File.OpenRead(Path.Combine(path, node.Value.Value)); + volumes.Add(volume, stream); + } + + using (var source = File.OpenRead(sourcePath)) + { + var reader = new InstallShieldCABCompression(source, volumes); + foreach (var node in extractNode.Value.Nodes) + { + var targetPath = Platform.ResolvePath(node.Key); + + if (File.Exists(targetPath)) + { + Log.Write("install", "Skipping installed file " + targetPath); + continue; + } + + extractedFiles.Add(targetPath); + Directory.CreateDirectory(Path.GetDirectoryName(targetPath)); + using (var target = File.OpenWrite(targetPath)) + { + Log.Write("install", "Extracting {0} -> {1}".F(sourcePath, targetPath)); + var displayFilename = Path.GetFileName(Path.GetFileName(targetPath)); + Action onProgress = percent => updateMessage("Extracting {0} ({1}%)".F(displayFilename, percent)); + reader.ExtractFile(node.Value.Value, target, onProgress); + } + } + } + } + finally + { + foreach (var kv in volumes) + kv.Value.Dispose(); + } + } + string FindSourcePath(ModContent.ModSource source, IEnumerable volumes) { if (source.Type == ModContent.SourceType.Install)