Files
OpenRA/OpenRA.Mods.Common/FileFormats/InstallShieldCABCompression.cs
2018-01-17 00:47:34 +01:00

439 lines
12 KiB
C#

#region Copyright & License Information
/*
* Copyright 2007-2018 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<int, Stream> volumes;
uint remainingInArchive;
uint toExtract;
int currentVolumeID;
Stream currentVolume;
public CabExtracter(FileDescriptor file, Dictionary<int, Stream> 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<int> 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 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<string, FileDescriptor> index = new Dictionary<string, FileDescriptor>();
readonly Dictionary<int, Stream> volumes;
public InstallShieldCABCompression(Stream header, Dictionary<int, Stream> 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<FileGroup>();
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<int> 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<int> 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 = {0}, Got = {1}".F(file.ExpandedSize, output.Length));
}
public IReadOnlyDictionary<int, IEnumerable<string>> Contents
{
get
{
var contents = new Dictionary<int, List<string>>();
foreach (var kv in index)
contents.GetOrAdd(kv.Value.Volume).Add(kv.Key);
return new ReadOnlyDictionary<int, IEnumerable<string>>(contents
.ToDictionary(x => x.Key, x => x.Value.AsEnumerable()));
}
}
}
}