#region Copyright & License Information /* * Copyright 2007-2022 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.Collections.ObjectModel; using System.IO; using System.Linq; 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 } readonly 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; } } readonly 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(); } } readonly 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; } } readonly 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; } } readonly 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(); } } readonly 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) { onProgress?.Invoke((int)(100 * output.Position / file.ExpandedSize)); var inflated = inf.Inflate(buffer); output.Write(buffer, 0, inflated); } inf.Reset(); } while (toExtract > 0); } else { do { onProgress?.Invoke((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 {currentVolumeID} is not available"); 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 InvalidDataException("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 = $"{fileGroup.Name}\\{directories[file.DirectoryIndex].Name}\\{file.Filename}"; index[path] = file; } } } public void ExtractFile(string filename, Stream output, Action onProgress = null) { if (!index.TryGetValue(filename, out var 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 InvalidDataException("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 = {file.ExpandedSize}, Got = {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())); } } } }