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 @@ + diff --git a/OpenRA.Mods.Common/InstallUtils.cs b/OpenRA.Mods.Common/InstallUtils.cs index 3835bd4daf..607f917761 100644 --- a/OpenRA.Mods.Common/InstallUtils.cs +++ b/OpenRA.Mods.Common/InstallUtils.cs @@ -39,6 +39,12 @@ namespace OpenRA.Mods.Common public readonly string MusicPackageMirrorList = null; public readonly int ShippedSoundtracks = 0; + /// InstallShield .cab File Ids, used to extract Mod specific files + public readonly int[] InstallShieldCABFileIds = { }; + + /// InstallShield .cab File Ids, used to extract Mod specific archives and extract contents of ExtractFilesFromCD + public readonly string[] InstallShieldCABFilePackageIds = { }; + public static Dictionary LoadFilesToExtract(MiniYaml yaml) { var md = yaml.ToDictionary(); diff --git a/OpenRA.Mods.Common/Widgets/Logic/Installation/InstallFromCDLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Installation/InstallFromCDLogic.cs index fa45caae50..01aa6d0745 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Installation/InstallFromCDLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Installation/InstallFromCDLogic.cs @@ -12,6 +12,7 @@ using System; using System.IO; using System.Linq; using System.Threading; +using OpenRA.FileSystem; using OpenRA.Widgets; namespace OpenRA.Mods.Common.Widgets.Logic @@ -51,12 +52,26 @@ namespace OpenRA.Mods.Common.Widgets.Logic return installData.DiskTestFiles.All(f => File.Exists(Path.Combine(diskRoot, f))); } + bool IsTFD(string diskpath) + { + var test = File.Exists(Path.Combine(diskpath, "data1.hdr")); + var i = 0; + + while (test && i < 14) + test &= File.Exists(Path.Combine(diskpath, "data{0}.cab".F(++i))); + + return test; + } + void CheckForDisk() { var path = InstallUtils.GetMountedDisk(IsValidDisk); if (path != null) Install(path); + else if ((installData.InstallShieldCABFileIds.Length != 0 || installData.InstallShieldCABFilePackageIds.Length != 0) + && (path = InstallUtils.GetMountedDisk(IsTFD)) != null) + InstallTFD(Platform.ResolvePath(path, "data1.hdr")); else { insertDiskContainer.IsVisible = () => true; @@ -64,6 +79,63 @@ namespace OpenRA.Mods.Common.Widgets.Logic } } + void InstallTFD(string source) + { + backButton.IsDisabled = () => true; + retryButton.IsDisabled = () => true; + insertDiskContainer.IsVisible = () => false; + installingContainer.IsVisible = () => true; + progressBar.Percentage = 0; + + new Thread(() => + { + using (var cabExtractor = new InstallShieldCABExtractor(source)) + { + var denom = installData.InstallShieldCABFileIds.Length; + var extractFiles = installData.ExtractFilesFromCD; + + if (installData.InstallShieldCABFilePackageIds.Length > 0) + denom += extractFiles.SelectMany(x => x.Value).Count(); + + var installPercent = 100 / denom; + + foreach (uint index in installData.InstallShieldCABFileIds) + { + var filename = cabExtractor.FileName(index); + statusLabel.GetText = () => "Extracting {0}".F(filename); + var dest = Platform.ResolvePath("^", "Content", Game.ModData.Manifest.Mod.Id, filename.ToLowerInvariant()); + cabExtractor.ExtractFile(index, dest); + progressBar.Percentage += installPercent; + } + + var ArchivesToExtract = installData.InstallShieldCABFilePackageIds.Select(x => x.Split(':')); + var destDir = Platform.ResolvePath("^", "Content", Game.ModData.Manifest.Mod.Id); + var onError = (Action)(s => { }); + var overwrite = installData.OverwriteFiles; + + var onProgress = (Action)(s => Game.RunAfterTick(() => + { + progressBar.Percentage += installPercent; + + statusLabel.GetText = () => s; + })); + + foreach (var archive in ArchivesToExtract) + { + var filename = cabExtractor.FileName(uint.Parse(archive[0])); + statusLabel.GetText = () => "Extracting {0}".F(filename); + var destFile = Platform.ResolvePath("^", "Content", Game.ModData.Manifest.Mod.Id, filename.ToLowerInvariant()); + cabExtractor.ExtractFile(uint.Parse(archive[0]), destFile); + var annotation = archive.Length > 1 ? archive[1] : null; + InstallUtils.ExtractFromPackage(source, destFile, annotation, extractFiles, destDir, overwrite, onProgress, onError); + progressBar.Percentage += installPercent; + } + } + + continueLoading(); + }) { IsBackground = true }.Start(); + } + void Install(string source) { backButton.IsDisabled = () => true; diff --git a/mods/cnc/mod.yaml b/mods/cnc/mod.yaml index 49cc307058..e8b4946741 100644 --- a/mods/cnc/mod.yaml +++ b/mods/cnc/mod.yaml @@ -153,6 +153,7 @@ ContentInstaller: .: conquer.mix, desert.mix, general.mix, scores.mix, sounds.mix, temperat.mix, winter.mix ShippedSoundtracks: 2 MusicPackageMirrorList: http://www.openra.net/packages/cnc-music-mirrors.txt + InstallShieldCABFileIds: 66, 45, 42, 65, 68, 67, 71, 47, 49, 60, 75, 73, 53 ServerTraits: LobbyCommands diff --git a/mods/ra/mod.yaml b/mods/ra/mod.yaml index 5b47ef8fd0..203c5c5013 100644 --- a/mods/ra/mod.yaml +++ b/mods/ra/mod.yaml @@ -152,6 +152,8 @@ ContentInstaller: .: INSTALL/REDALERT.MIX ShippedSoundtracks: 2 MusicPackageMirrorList: http://www.openra.net/packages/ra-music-mirrors.txt + InstallShieldCABFilePackageIds: 105 + InstallShieldCABFileIds: 116 ServerTraits: LobbyCommands diff --git a/mods/ts/mod.yaml b/mods/ts/mod.yaml index 9ae2ee8785..aa96accefe 100644 --- a/mods/ts/mod.yaml +++ b/mods/ts/mod.yaml @@ -201,6 +201,8 @@ ContentInstaller: .: cache.mix, conquer.mix, isosnow.mix, isotemp.mix, local.mix, sidec01.mix, sidec02.mix, sno.mix, snow.mix, sounds.mix, speech01.mix, tem.mix, temperat.mix ShippedSoundtracks: 2 MusicPackageMirrorList: http://www.openra.net/packages/ts-music-mirrors.txt + InstallShieldCABFilePackageIds: 332:CRC32 + InstallShieldCABFileIds: 323, 364 ServerTraits: LobbyCommands