diff --git a/OpenRA.Game/FileSystem/BagFile.cs b/OpenRA.Game/FileSystem/BagFile.cs index dbb297df3c..0f6e9dfe94 100644 --- a/OpenRA.Game/FileSystem/BagFile.cs +++ b/OpenRA.Game/FileSystem/BagFile.cs @@ -11,117 +11,143 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; -using System.Linq; using System.Text; using OpenRA.FileFormats; using OpenRA.Primitives; namespace OpenRA.FileSystem { - public sealed class BagFile : IReadOnlyPackage + public class AudioBagLoader : IPackageLoader { - public string Name { get; private set; } - public IEnumerable Contents { get { return index.Keys; } } - - readonly Stream s; - readonly Dictionary index; - - public BagFile(FileSystem context, string filename) + sealed class BagFile : IReadOnlyPackage { - Name = filename; + public string Name { get; private set; } + public IEnumerable Contents { get { return index.Keys; } } + + readonly Stream s; + readonly Dictionary index; + + public BagFile(Stream s, List entries, string filename) + { + Name = filename; + this.s = s; + + index = entries.ToDictionaryWithConflictLog(x => x.Filename, + "{0} (bag format)".F(filename), + null, x => "(offs={0}, len={1})".F(x.Offset, x.Length)); + } + + public Stream GetStream(string filename) + { + IdxEntry entry; + if (!index.TryGetValue(filename, out entry)) + return null; + + s.Seek(entry.Offset, SeekOrigin.Begin); + + var waveHeaderMemoryStream = new MemoryStream(); + + var channels = (entry.Flags & 1) > 0 ? 2 : 1; + + if ((entry.Flags & 2) > 0) + { + // PCM + waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("RIFF")); + waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.Length + 36)); + waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("WAVE")); + waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("fmt ")); + waveHeaderMemoryStream.Write(BitConverter.GetBytes(16)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)1)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)channels)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.SampleRate)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes(2 * channels * entry.SampleRate)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)(2 * channels))); + waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)16)); + waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("data")); + waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.Length)); + } + + if ((entry.Flags & 8) > 0) + { + // IMA ADPCM + var samplesPerChunk = (2 * (entry.ChunkSize - 4)) + 1; + var bytesPerSec = (int)Math.Floor(((double)(2 * entry.ChunkSize) / samplesPerChunk) * ((double)entry.SampleRate / 2)); + var chunkSize = entry.ChunkSize > entry.Length ? entry.Length : entry.ChunkSize; + + waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("RIFF")); + waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.Length + 52)); + waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("WAVE")); + waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("fmt ")); + waveHeaderMemoryStream.Write(BitConverter.GetBytes(20)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)17)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)channels)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.SampleRate)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes(bytesPerSec)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)chunkSize)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)4)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)2)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)samplesPerChunk)); + waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("fact")); + waveHeaderMemoryStream.Write(BitConverter.GetBytes(4)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes(4 * entry.Length)); + waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("data")); + waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.Length)); + } + + waveHeaderMemoryStream.Seek(0, SeekOrigin.Begin); + + // Construct a merged stream + var mergedStream = new MergedStream(waveHeaderMemoryStream, s); + mergedStream.SetLength(waveHeaderMemoryStream.Length + entry.Length); + + return mergedStream; + } + + public bool Contains(string filename) + { + return index.ContainsKey(filename); + } + + public IReadOnlyPackage OpenPackage(string filename, FileSystem context) + { + // Not implemented + return null; + } + + public void Dispose() + { + s.Dispose(); + } + } + + bool IPackageLoader.TryParsePackage(Stream s, string filename, FileSystem context, out IReadOnlyPackage package) + { + if (!filename.EndsWith(".bag", StringComparison.InvariantCultureIgnoreCase)) + { + package = null; + return false; + } // A bag file is always accompanied with an .idx counterpart // For example: audio.bag requires the audio.idx file var indexFilename = Path.ChangeExtension(filename, ".idx"); + List entries = null; - // Build the index and dispose the stream, it is no longer needed after this - List entries; - using (var indexStream = context.Open(indexFilename)) - entries = new IdxReader(indexStream).Entries; - - index = entries.ToDictionaryWithConflictLog(x => x.Filename, - "{0} (bag format)".F(filename), - null, x => "(offs={0}, len={1})".F(x.Offset, x.Length)); - - s = context.Open(filename); - } - - public Stream GetStream(string filename) - { - IdxEntry entry; - if (!index.TryGetValue(filename, out entry)) - return null; - - s.Seek(entry.Offset, SeekOrigin.Begin); - - var waveHeaderMemoryStream = new MemoryStream(); - - var channels = (entry.Flags & 1) > 0 ? 2 : 1; - - if ((entry.Flags & 2) > 0) + try { - // PCM - waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("RIFF")); - waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.Length + 36)); - waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("WAVE")); - waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("fmt ")); - waveHeaderMemoryStream.Write(BitConverter.GetBytes(16)); - waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)1)); - waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)channels)); - waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.SampleRate)); - waveHeaderMemoryStream.Write(BitConverter.GetBytes(2 * channels * entry.SampleRate)); - waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)(2 * channels))); - waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)16)); - waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("data")); - waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.Length)); + // Build the index and dispose the stream, it is no longer needed after this + using (var indexStream = context.Open(indexFilename)) + entries = new IdxReader(indexStream).Entries; + } + catch + { + package = null; + return false; } - if ((entry.Flags & 8) > 0) - { - // IMA ADPCM - var samplesPerChunk = (2 * (entry.ChunkSize - 4)) + 1; - var bytesPerSec = (int)Math.Floor(((double)(2 * entry.ChunkSize) / samplesPerChunk) * ((double)entry.SampleRate / 2)); - var chunkSize = entry.ChunkSize > entry.Length ? entry.Length : entry.ChunkSize; - - waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("RIFF")); - waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.Length + 52)); - waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("WAVE")); - waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("fmt ")); - waveHeaderMemoryStream.Write(BitConverter.GetBytes(20)); - waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)17)); - waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)channels)); - waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.SampleRate)); - waveHeaderMemoryStream.Write(BitConverter.GetBytes(bytesPerSec)); - waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)chunkSize)); - waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)4)); - waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)2)); - waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)samplesPerChunk)); - waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("fact")); - waveHeaderMemoryStream.Write(BitConverter.GetBytes(4)); - waveHeaderMemoryStream.Write(BitConverter.GetBytes(4 * entry.Length)); - waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("data")); - waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.Length)); - } - - waveHeaderMemoryStream.Seek(0, SeekOrigin.Begin); - - // Construct a merged stream - var mergedStream = new MergedStream(waveHeaderMemoryStream, s); - mergedStream.SetLength(waveHeaderMemoryStream.Length + entry.Length); - - return mergedStream; - } - - public bool Contains(string filename) - { - return index.ContainsKey(filename); - } - - public void Dispose() - { - s.Dispose(); + package = new BagFile(s, entries, filename); + return true; } } } diff --git a/OpenRA.Game/FileSystem/BigFile.cs b/OpenRA.Game/FileSystem/BigFile.cs index 1ae45a41ed..f5e6ac7016 100644 --- a/OpenRA.Game/FileSystem/BigFile.cs +++ b/OpenRA.Game/FileSystem/BigFile.cs @@ -12,94 +12,117 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; namespace OpenRA.FileSystem { - public sealed class BigFile : IReadOnlyPackage + public class BigLoader : IPackageLoader { - public string Name { get; private set; } - public IEnumerable Contents { get { return index.Keys; } } - - readonly Dictionary index = new Dictionary(); - readonly Stream s; - - public BigFile(FileSystem context, string filename) + sealed class BigFile : IReadOnlyPackage { - Name = filename; + public string Name { get; private set; } + public IEnumerable Contents { get { return index.Keys; } } - s = context.Open(filename); - try - { - if (s.ReadASCII(4) != "BIGF") - throw new InvalidDataException("Header is not BIGF"); - - // Total archive size. - s.ReadUInt32(); - - var entryCount = s.ReadUInt32(); - if (BitConverter.IsLittleEndian) - entryCount = int2.Swap(entryCount); - - // First entry offset? This is apparently bogus for EA's .big files - // and we don't have to try seeking there since the entries typically start next in EA's .big files. - s.ReadUInt32(); - - for (var i = 0; i < entryCount; i++) - { - var entry = new Entry(s); - index.Add(entry.Path, entry); - } - } - catch - { - Dispose(); - throw; - } - } - - class Entry - { + readonly Dictionary index = new Dictionary(); readonly Stream s; - readonly uint offset; - readonly uint size; - public readonly string Path; - public Entry(Stream s) + public BigFile(Stream s, string filename) { + Name = filename; this.s = s; - offset = s.ReadUInt32(); - size = s.ReadUInt32(); - if (BitConverter.IsLittleEndian) + try { - offset = int2.Swap(offset); - size = int2.Swap(size); + /* var signature = */ s.ReadASCII(4); + + // Total archive size. + s.ReadUInt32(); + + var entryCount = s.ReadUInt32(); + if (BitConverter.IsLittleEndian) + entryCount = int2.Swap(entryCount); + + // First entry offset? This is apparently bogus for EA's .big files + // and we don't have to try seeking there since the entries typically start next in EA's .big files. + s.ReadUInt32(); + + for (var i = 0; i < entryCount; i++) + { + var entry = new Entry(s); + index.Add(entry.Path, entry); + } + } + catch + { + Dispose(); + throw; + } + } + + class Entry + { + readonly Stream s; + readonly uint offset; + readonly uint size; + public readonly string Path; + + public Entry(Stream s) + { + this.s = s; + + offset = s.ReadUInt32(); + size = s.ReadUInt32(); + if (BitConverter.IsLittleEndian) + { + offset = int2.Swap(offset); + size = int2.Swap(size); + } + + Path = s.ReadASCIIZ(); } - Path = s.ReadASCIIZ(); + public Stream GetData() + { + s.Position = offset; + return new MemoryStream(s.ReadBytes((int)size)); + } } - public Stream GetData() + public Stream GetStream(string filename) { - s.Position = offset; - return new MemoryStream(s.ReadBytes((int)size)); + return index[filename].GetData(); + } + + public bool Contains(string filename) + { + return index.ContainsKey(filename); + } + + public IReadOnlyPackage OpenPackage(string filename, FileSystem context) + { + // Not implemented + return null; + } + + public void Dispose() + { + s.Dispose(); } } - public Stream GetStream(string filename) + bool IPackageLoader.TryParsePackage(Stream s, string filename, FileSystem context, out IReadOnlyPackage package) { - return index[filename].GetData(); - } + // Take a peek at the file signature + var signature = s.ReadASCII(4); + s.Position -= 4; - public bool Contains(string filename) - { - return index.ContainsKey(filename); - } + if (signature != "BIGF") + { + package = null; + return false; + } - public void Dispose() - { - s.Dispose(); + package = new BigFile(s, filename); + return true; } } -} \ No newline at end of file +} diff --git a/OpenRA.Game/FileSystem/D2kSoundResources.cs b/OpenRA.Game/FileSystem/D2kSoundResources.cs index b99de84c09..5dea955653 100644 --- a/OpenRA.Game/FileSystem/D2kSoundResources.cs +++ b/OpenRA.Game/FileSystem/D2kSoundResources.cs @@ -9,73 +9,94 @@ */ #endregion +using System; using System.Collections.Generic; using System.IO; -using OpenRA.Primitives; namespace OpenRA.FileSystem { - public sealed class D2kSoundResources : IReadOnlyPackage + public class D2kSoundResourcesLoader : IPackageLoader { - struct Entry + sealed class D2kSoundResources : IReadOnlyPackage { - public readonly uint Offset; - public readonly uint Length; - - public Entry(uint offset, uint length) + struct Entry { - Offset = offset; - Length = length; - } - } + public readonly uint Offset; + public readonly uint Length; - public string Name { get; private set; } - public IEnumerable Contents { get { return index.Keys; } } - - readonly Stream s; - readonly Dictionary index = new Dictionary(); - - public D2kSoundResources(FileSystem context, string filename) - { - Name = filename; - - s = context.Open(filename); - try - { - var headerLength = s.ReadUInt32(); - while (s.Position < headerLength + 4) + public Entry(uint offset, uint length) { - var name = s.ReadASCIIZ(); - var offset = s.ReadUInt32(); - var length = s.ReadUInt32(); - index.Add(name, new Entry(offset, length)); + Offset = offset; + Length = length; } } - catch + + public string Name { get; private set; } + public IEnumerable Contents { get { return index.Keys; } } + + readonly Stream s; + readonly Dictionary index = new Dictionary(); + + public D2kSoundResources(Stream s, string filename) { - Dispose(); - throw; + Name = filename; + this.s = s; + + try + { + var headerLength = s.ReadUInt32(); + while (s.Position < headerLength + 4) + { + var name = s.ReadASCIIZ(); + var offset = s.ReadUInt32(); + var length = s.ReadUInt32(); + index.Add(name, new Entry(offset, length)); + } + } + catch + { + Dispose(); + throw; + } + } + + public Stream GetStream(string filename) + { + Entry e; + if (!index.TryGetValue(filename, out e)) + return null; + + s.Seek(e.Offset, SeekOrigin.Begin); + return new MemoryStream(s.ReadBytes((int)e.Length)); + } + + public IReadOnlyPackage OpenPackage(string filename, FileSystem context) + { + // Not implemented + return null; + } + + public bool Contains(string filename) + { + return index.ContainsKey(filename); + } + + public void Dispose() + { + s.Dispose(); } } - public Stream GetStream(string filename) + bool IPackageLoader.TryParsePackage(Stream s, string filename, FileSystem context, out IReadOnlyPackage package) { - Entry e; - if (!index.TryGetValue(filename, out e)) - return null; + if (!filename.EndsWith(".rs", StringComparison.InvariantCultureIgnoreCase)) + { + package = null; + return false; + } - s.Seek(e.Offset, SeekOrigin.Begin); - return new MemoryStream(s.ReadBytes((int)e.Length)); - } - - public bool Contains(string filename) - { - return index.ContainsKey(filename); - } - - public void Dispose() - { - s.Dispose(); + package = new D2kSoundResources(s, filename); + return true; } } } diff --git a/OpenRA.Game/FileSystem/FileSystem.cs b/OpenRA.Game/FileSystem/FileSystem.cs index 11b3baae10..bb839b849e 100644 --- a/OpenRA.Game/FileSystem/FileSystem.cs +++ b/OpenRA.Game/FileSystem/FileSystem.cs @@ -34,80 +34,69 @@ namespace OpenRA.FileSystem // Mod packages that should not be disposed readonly List modPackages = new List(); readonly IReadOnlyDictionary installedMods; + readonly IPackageLoader[] packageLoaders; Cache> fileIndex = new Cache>(_ => new List()); - public FileSystem(IReadOnlyDictionary installedMods) + public FileSystem(IReadOnlyDictionary installedMods, IPackageLoader[] packageLoaders) { this.installedMods = installedMods; + this.packageLoaders = packageLoaders + .Append(new ZipFileLoader()) + .ToArray(); + } + + public bool TryParsePackage(Stream stream, string filename, out IReadOnlyPackage package) + { + package = null; + foreach (var packageLoader in packageLoaders) + if (packageLoader.TryParsePackage(stream, filename, this, out package)) + return true; + + return false; } public IReadOnlyPackage OpenPackage(string filename) { - if (filename.EndsWith(".mix", StringComparison.InvariantCultureIgnoreCase)) - return new MixFile(this, filename); - if (filename.EndsWith(".zip", StringComparison.InvariantCultureIgnoreCase)) - return new ZipFile(this, filename); - if (filename.EndsWith(".oramap", StringComparison.InvariantCultureIgnoreCase)) - return new ZipFile(this, filename); - if (filename.EndsWith(".RS", StringComparison.InvariantCultureIgnoreCase)) - return new D2kSoundResources(this, filename); - if (filename.EndsWith(".Z", StringComparison.InvariantCultureIgnoreCase)) - return new InstallShieldPackage(this, filename); - if (filename.EndsWith(".PAK", StringComparison.InvariantCultureIgnoreCase)) - return new PakFile(this, filename); - if (filename.EndsWith(".big", StringComparison.InvariantCultureIgnoreCase)) - return new BigFile(this, filename); - if (filename.EndsWith(".bag", StringComparison.InvariantCultureIgnoreCase)) - return new BagFile(this, filename); + // Raw directories are the easiest and one of the most common cases, so try these first + var resolvedPath = Platform.ResolvePath(filename); + if (!filename.Contains("|") && Directory.Exists(resolvedPath)) + return new Folder(resolvedPath); + // Children of another package require special handling IReadOnlyPackage parent; string subPath = null; if (TryGetPackageContaining(filename, out parent, out subPath)) - return OpenPackage(subPath, parent); + return parent.OpenPackage(subPath, this); - return new Folder(Platform.ResolvePath(filename)); - } + // Try and open it normally + IReadOnlyPackage package; + var stream = Open(filename); + if (TryParsePackage(stream, filename, out package)) + return package; - public IReadOnlyPackage OpenPackage(string filename, IReadOnlyPackage parent) - { - // HACK: limit support to zip and folder until we generalize the PackageLoader support - if (filename.EndsWith(".zip", StringComparison.InvariantCultureIgnoreCase) || - filename.EndsWith(".oramap", StringComparison.InvariantCultureIgnoreCase)) - { - using (var s = parent.GetStream(filename)) - return new ZipFile(s, filename, parent); - } - - if (parent is ZipFile) - return new ZipFolder(this, (ZipFile)parent, filename, filename); - - if (parent is ZipFolder) - { - var folder = (ZipFolder)parent; - return new ZipFolder(this, folder.Parent, folder.Name + "/" + filename, filename); - } - - if (parent is Folder) - { - var subFolder = Platform.ResolvePath(Path.Combine(parent.Name, filename)); - if (Directory.Exists(subFolder)) - return new Folder(subFolder); - } + // No package loaders took ownership of the stream, so clean it up + stream.Dispose(); return null; } + public IReadOnlyPackage OpenPackage(string filename, IReadOnlyPackage parent) + { + // TODO: Make legacy callers access the parent package directly + return parent.OpenPackage(filename, this); + } + public void Mount(string name, string explicitName = null) { - var optional = name.StartsWith("~"); + var optional = name.StartsWith("~", StringComparison.Ordinal); if (optional) name = name.Substring(1); try { IReadOnlyPackage package; - if (name.StartsWith("$")) + if (name.StartsWith("$", StringComparison.Ordinal)) { name = name.Substring(1); diff --git a/OpenRA.Game/FileSystem/Folder.cs b/OpenRA.Game/FileSystem/Folder.cs index b13cd663df..0d21358108 100644 --- a/OpenRA.Game/FileSystem/Folder.cs +++ b/OpenRA.Game/FileSystem/Folder.cs @@ -60,13 +60,16 @@ namespace OpenRA.FileSystem // Other package types can be loaded normally IReadOnlyPackage package; var s = GetStream(filename); + if (s == null) + return null; + if (context.TryParsePackage(s, filename, out package)) return package; s.Dispose(); return null; } - + public void Update(string filename, byte[] contents) { // HACK: ZipFiles can't be loaded as read-write from a stream, so we are diff --git a/OpenRA.Game/FileSystem/IPackage.cs b/OpenRA.Game/FileSystem/IPackage.cs index c2d0670569..8e95c5844d 100644 --- a/OpenRA.Game/FileSystem/IPackage.cs +++ b/OpenRA.Game/FileSystem/IPackage.cs @@ -15,12 +15,23 @@ using System.IO; namespace OpenRA.FileSystem { + public interface IPackageLoader + { + /// + /// Attempt to parse a stream as this type of package. + /// If successful, the loader is expected to take ownership of `s` and dispose it once done. + /// If unsuccessful, the loader is expected to return the stream position to where it started. + /// + bool TryParsePackage(Stream s, string filename, FileSystem context, out IReadOnlyPackage package); + } + public interface IReadOnlyPackage : IDisposable { string Name { get; } IEnumerable Contents { get; } Stream GetStream(string filename); bool Contains(string filename); + IReadOnlyPackage OpenPackage(string filename, FileSystem context); } public interface IReadWritePackage : IReadOnlyPackage diff --git a/OpenRA.Game/FileSystem/InstallShieldPackage.cs b/OpenRA.Game/FileSystem/InstallShieldPackage.cs index 67b342953c..fbf42ae608 100644 --- a/OpenRA.Game/FileSystem/InstallShieldPackage.cs +++ b/OpenRA.Game/FileSystem/InstallShieldPackage.cs @@ -16,122 +16,144 @@ using OpenRA.FileFormats; namespace OpenRA.FileSystem { - public sealed class InstallShieldPackage : IReadOnlyPackage + public class InstallShieldLoader : IPackageLoader { - public struct Entry + public sealed class InstallShieldPackage : IReadOnlyPackage { - public readonly uint Offset; - public readonly uint Length; - - public Entry(uint offset, uint length) + public struct Entry { - Offset = offset; - Length = length; - } - } + public readonly uint Offset; + public readonly uint Length; - public string Name { get; private set; } - public IEnumerable Contents { get { return index.Keys; } } - - readonly Dictionary index = new Dictionary(); - readonly Stream s; - readonly long dataStart = 255; - - public InstallShieldPackage(FileSystem context, string filename) - { - Name = filename; - - s = context.Open(filename); - try - { - // Parse package header - var signature = s.ReadUInt32(); - if (signature != 0x8C655D13) - throw new InvalidDataException("Not an Installshield package"); - - s.Position += 8; - /*var FileCount = */s.ReadUInt16(); - s.Position += 4; - /*var ArchiveSize = */s.ReadUInt32(); - s.Position += 19; - var tocAddress = s.ReadInt32(); - s.Position += 4; - var dirCount = s.ReadUInt16(); - - // Parse the directory list - s.Position = tocAddress; - - // Parse directories - var directories = new Dictionary(); - for (var i = 0; i < dirCount; i++) + public Entry(uint offset, uint length) { - // Parse directory header - var fileCount = s.ReadUInt16(); - var chunkSize = s.ReadUInt16(); - var nameLength = s.ReadUInt16(); - var dirName = s.ReadASCII(nameLength); - - // Skip to the end of the chunk - s.ReadBytes(chunkSize - nameLength - 6); - directories.Add(dirName, fileCount); + Offset = offset; + Length = length; } - - // Parse files - foreach (var dir in directories) - for (var i = 0; i < dir.Value; i++) - ParseFile(s, dir.Key); } - catch + + public string Name { get; private set; } + public IEnumerable Contents { get { return index.Keys; } } + + readonly Dictionary index = new Dictionary(); + readonly Stream s; + readonly long dataStart = 255; + + public InstallShieldPackage(Stream s, string filename) { - Dispose(); - throw; + Name = filename; + this.s = s; + + try + { + // Parse package header + /*var signature = */s.ReadUInt32(); + s.Position += 8; + /*var FileCount = */s.ReadUInt16(); + s.Position += 4; + /*var ArchiveSize = */s.ReadUInt32(); + s.Position += 19; + var tocAddress = s.ReadInt32(); + s.Position += 4; + var dirCount = s.ReadUInt16(); + + // Parse the directory list + s.Position = tocAddress; + + // Parse directories + var directories = new Dictionary(); + for (var i = 0; i < dirCount; i++) + { + // Parse directory header + var fileCount = s.ReadUInt16(); + var chunkSize = s.ReadUInt16(); + var nameLength = s.ReadUInt16(); + var dirName = s.ReadASCII(nameLength); + + // Skip to the end of the chunk + s.ReadBytes(chunkSize - nameLength - 6); + directories.Add(dirName, fileCount); + } + + // Parse files + foreach (var dir in directories) + for (var i = 0; i < dir.Value; i++) + ParseFile(dir.Key); + } + catch + { + Dispose(); + throw; + } + } + + uint accumulatedData = 0; + void ParseFile(string dirName) + { + s.Position += 7; + var compressedSize = s.ReadUInt32(); + s.Position += 12; + var chunkSize = s.ReadUInt16(); + s.Position += 4; + var nameLength = s.ReadByte(); + var fileName = dirName + "\\" + s.ReadASCII(nameLength); + + // Use index syntax to overwrite any duplicate entries with the last value + index[fileName] = new Entry(accumulatedData, compressedSize); + accumulatedData += compressedSize; + + // Skip to the end of the chunk + s.Position += chunkSize - nameLength - 30; + } + + public Stream GetStream(string filename) + { + Entry e; + if (!index.TryGetValue(filename, out e)) + return null; + + s.Seek(dataStart + e.Offset, SeekOrigin.Begin); + + var ret = new MemoryStream(); + Blast.Decompress(s, ret); + ret.Seek(0, SeekOrigin.Begin); + + return ret; + } + + public IReadOnlyPackage OpenPackage(string filename, FileSystem context) + { + // Not implemented + return null; + } + + public bool Contains(string filename) + { + return index.ContainsKey(filename); + } + + public IReadOnlyDictionary Index { get { return new ReadOnlyDictionary(index); } } + + public void Dispose() + { + s.Dispose(); } } - uint accumulatedData = 0; - void ParseFile(Stream s, string dirName) + bool IPackageLoader.TryParsePackage(Stream s, string filename, FileSystem context, out IReadOnlyPackage package) { - s.Position += 7; - var compressedSize = s.ReadUInt32(); - s.Position += 12; - var chunkSize = s.ReadUInt16(); - s.Position += 4; - var nameLength = s.ReadByte(); - var fileName = dirName + "\\" + s.ReadASCII(nameLength); + // Take a peek at the file signature + var signature = s.ReadUInt32(); + s.Position -= 4; - // Use index syntax to overwrite any duplicate entries with the last value - index[fileName] = new Entry(accumulatedData, compressedSize); - accumulatedData += compressedSize; + if (signature != 0x8C655D13) + { + package = null; + return false; + } - // Skip to the end of the chunk - s.Position += chunkSize - nameLength - 30; - } - - public Stream GetStream(string filename) - { - Entry e; - if (!index.TryGetValue(filename, out e)) - return null; - - s.Seek(dataStart + e.Offset, SeekOrigin.Begin); - - var ret = new MemoryStream(); - Blast.Decompress(s, ret); - ret.Seek(0, SeekOrigin.Begin); - - return ret; - } - - public bool Contains(string filename) - { - return index.ContainsKey(filename); - } - - public IReadOnlyDictionary Index { get { return new ReadOnlyDictionary(index); } } - - public void Dispose() - { - s.Dispose(); + package = new InstallShieldPackage(s, filename); + return true; } } } diff --git a/OpenRA.Game/FileSystem/MixFile.cs b/OpenRA.Game/FileSystem/MixFile.cs index 2126ecb99a..7585b6f9ec 100644 --- a/OpenRA.Game/FileSystem/MixFile.cs +++ b/OpenRA.Game/FileSystem/MixFile.cs @@ -11,7 +11,6 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using OpenRA.FileFormats; @@ -19,225 +18,248 @@ using OpenRA.Primitives; namespace OpenRA.FileSystem { - public sealed class MixFile : IReadOnlyPackage + public class MixLoader : IPackageLoader { - public string Name { get; private set; } - public IEnumerable Contents { get { return index.Keys; } } - - readonly Dictionary index; - readonly long dataStart; - readonly Stream s; - readonly FileSystem context; - - public MixFile(FileSystem context, string filename) + public sealed class MixFile : IReadOnlyPackage { - Name = filename; - this.context = context; + public string Name { get; private set; } + public IEnumerable Contents { get { return index.Keys; } } - s = context.Open(filename); - try + readonly Dictionary index; + readonly long dataStart; + readonly Stream s; + + public MixFile(Stream s, string filename, HashSet allPossibleFilenames) { - // Detect format type - var isCncMix = s.ReadUInt16() != 0; + Name = filename; + this.s = s; - // The C&C mix format doesn't contain any flags or encryption - var isEncrypted = false; - if (!isCncMix) - isEncrypted = (s.ReadUInt16() & 0x2) != 0; - - List entries; - if (isEncrypted) + try { - long unused; - entries = ParseHeader(DecryptHeader(s, 4, out dataStart), 0, out unused); - } - else - entries = ParseHeader(s, isCncMix ? 0 : 4, out dataStart); + // Detect format type + var isCncMix = s.ReadUInt16() != 0; - index = ParseIndex(entries.ToDictionaryWithConflictLog(x => x.Hash, - "{0} ({1} format, Encrypted: {2}, DataStart: {3})".F(filename, isCncMix ? "C&C" : "RA/TS/RA2", isEncrypted, dataStart), - null, x => "(offs={0}, len={1})".F(x.Offset, x.Length))); + // The C&C mix format doesn't contain any flags or encryption + var isEncrypted = false; + if (!isCncMix) + isEncrypted = (s.ReadUInt16() & 0x2) != 0; + + List entries; + if (isEncrypted) + { + long unused; + entries = ParseHeader(DecryptHeader(s, 4, out dataStart), 0, out unused); + } + else + entries = ParseHeader(s, isCncMix ? 0 : 4, out dataStart); + + index = ParseIndex(entries.ToDictionaryWithConflictLog(x => x.Hash, + "{0} ({1} format, Encrypted: {2}, DataStart: {3})".F(filename, isCncMix ? "C&C" : "RA/TS/RA2", isEncrypted, dataStart), + null, x => "(offs={0}, len={1})".F(x.Offset, x.Length)), allPossibleFilenames); + } + catch (Exception) + { + Dispose(); + throw; + } } - catch (Exception) + + Dictionary ParseIndex(Dictionary entries, HashSet allPossibleFilenames) { - Dispose(); - throw; + var classicIndex = new Dictionary(); + var crcIndex = new Dictionary(); + + // Try and find a local mix database + var dbNameClassic = PackageEntry.HashFilename("local mix database.dat", PackageHashType.Classic); + var dbNameCRC = PackageEntry.HashFilename("local mix database.dat", PackageHashType.CRC32); + foreach (var kv in entries) + { + if (kv.Key == dbNameClassic || kv.Key == dbNameCRC) + { + using (var content = GetContent(kv.Value)) + { + var db = new XccLocalDatabase(content); + foreach (var e in db.Entries) + allPossibleFilenames.Add(e); + } + + break; + } + } + + foreach (var filename in allPossibleFilenames) + { + var classicHash = PackageEntry.HashFilename(filename, PackageHashType.Classic); + var crcHash = PackageEntry.HashFilename(filename, PackageHashType.CRC32); + PackageEntry e; + + if (entries.TryGetValue(classicHash, out e)) + classicIndex.Add(filename, e); + + if (entries.TryGetValue(crcHash, out e)) + crcIndex.Add(filename, e); + } + + var bestIndex = crcIndex.Count > classicIndex.Count ? crcIndex : classicIndex; + + var unknown = entries.Count - bestIndex.Count; + if (unknown > 0) + Log.Write("debug", "{0}: failed to resolve filenames for {1} unknown hashes".F(Name, unknown)); + + return bestIndex; + } + + static List ParseHeader(Stream s, long offset, out long headerEnd) + { + s.Seek(offset, SeekOrigin.Begin); + var numFiles = s.ReadUInt16(); + /*uint dataSize = */s.ReadUInt32(); + + var items = new List(); + for (var i = 0; i < numFiles; i++) + items.Add(new PackageEntry(s)); + + headerEnd = offset + 6 + numFiles * PackageEntry.Size; + return items; + } + + static MemoryStream DecryptHeader(Stream s, long offset, out long headerEnd) + { + s.Seek(offset, SeekOrigin.Begin); + + // Decrypt blowfish key + var keyblock = s.ReadBytes(80); + var blowfishKey = new BlowfishKeyProvider().DecryptKey(keyblock); + var fish = new Blowfish(blowfishKey); + + // Decrypt first block to work out the header length + var ms = Decrypt(ReadBlocks(s, offset + 80, 1), fish); + var numFiles = ms.ReadUInt16(); + + // Decrypt the full header - round bytes up to a full block + var blockCount = (13 + numFiles * PackageEntry.Size) / 8; + headerEnd = offset + 80 + blockCount * 8; + + return Decrypt(ReadBlocks(s, offset + 80, blockCount), fish); + } + + static MemoryStream Decrypt(uint[] h, Blowfish fish) + { + var decrypted = fish.Decrypt(h); + + var ms = new MemoryStream(); + var writer = new BinaryWriter(ms); + foreach (var t in decrypted) + writer.Write(t); + writer.Flush(); + + ms.Seek(0, SeekOrigin.Begin); + return ms; + } + + static uint[] ReadBlocks(Stream s, long offset, int count) + { + if (offset < 0) + throw new ArgumentOutOfRangeException("offset", "Non-negative number required."); + + if (count < 0) + throw new ArgumentOutOfRangeException("count", "Non-negative number required."); + + if (offset + (count * 2) > s.Length) + throw new ArgumentException("Bytes to read {0} and offset {1} greater than stream length {2}.".F(count * 2, offset, s.Length)); + + s.Seek(offset, SeekOrigin.Begin); + + // A block is a single encryption unit (represented as two 32-bit integers) + var ret = new uint[2 * count]; + for (var i = 0; i < ret.Length; i++) + ret[i] = s.ReadUInt32(); + + return ret; + } + + public Stream GetContent(PackageEntry entry) + { + Stream parentStream; + var baseOffset = dataStart + entry.Offset; + var nestedOffset = baseOffset + SegmentStream.GetOverallNestedOffset(s, out parentStream); + + // Special case FileStream - instead of creating an in-memory copy, + // just reference the portion of the on-disk file that we need to save memory. + // We use GetType instead of 'is' here since we can't handle any derived classes of FileStream. + if (parentStream.GetType() == typeof(FileStream)) + { + var path = ((FileStream)parentStream).Name; + return new SegmentStream(File.OpenRead(path), nestedOffset, entry.Length); + } + + // For all other streams, create a copy in memory. + // This uses more memory but is the only way in general to ensure the returned streams won't clash. + s.Seek(baseOffset, SeekOrigin.Begin); + return new MemoryStream(s.ReadBytes((int)entry.Length)); + } + + public Stream GetStream(string filename) + { + PackageEntry e; + if (!index.TryGetValue(filename, out e)) + return null; + + return GetContent(e); + } + + public IReadOnlyDictionary Index + { + get + { + var absoluteIndex = index.ToDictionary(e => e.Key, e => new PackageEntry(e.Value.Hash, (uint)(e.Value.Offset + dataStart), e.Value.Length)); + return new ReadOnlyDictionary(absoluteIndex); + } + } + + public bool Contains(string filename) + { + return index.ContainsKey(filename); + } + + public IReadOnlyPackage OpenPackage(string filename, FileSystem context) + { + IReadOnlyPackage package; + var childStream = GetStream(filename); + if (childStream == null) + return null; + + if (context.TryParsePackage(childStream, filename, out package)) + return package; + + childStream.Dispose(); + return null; + } + + public void Dispose() + { + s.Dispose(); } } - Dictionary ParseIndex(Dictionary entries) + bool IPackageLoader.TryParsePackage(Stream s, string filename, FileSystem context, out IReadOnlyPackage package) { - var classicIndex = new Dictionary(); - var crcIndex = new Dictionary(); - var allPossibleFilenames = new HashSet(); - - // Try and find a local mix database - var dbNameClassic = PackageEntry.HashFilename("local mix database.dat", PackageHashType.Classic); - var dbNameCRC = PackageEntry.HashFilename("local mix database.dat", PackageHashType.CRC32); - foreach (var kv in entries) + if (!filename.EndsWith(".mix", StringComparison.InvariantCultureIgnoreCase)) { - if (kv.Key == dbNameClassic || kv.Key == dbNameCRC) - { - using (var content = GetContent(kv.Value)) - { - var db = new XccLocalDatabase(content); - foreach (var e in db.Entries) - allPossibleFilenames.Add(e); - } - - break; - } + package = null; + return false; } // Load the global mix database - // TODO: This should be passed to the mix file ctor - if (context.Exists("global mix database.dat")) - { - using (var db = new XccGlobalDatabase(context.Open("global mix database.dat"))) - { + Stream mixDatabase; + var allPossibleFilenames = new HashSet(); + if (context.TryOpen("global mix database.dat", out mixDatabase)) + using (var db = new XccGlobalDatabase(mixDatabase)) foreach (var e in db.Entries) allPossibleFilenames.Add(e); - } - } - foreach (var filename in allPossibleFilenames) - { - var classicHash = PackageEntry.HashFilename(filename, PackageHashType.Classic); - var crcHash = PackageEntry.HashFilename(filename, PackageHashType.CRC32); - PackageEntry e; - - if (entries.TryGetValue(classicHash, out e)) - classicIndex.Add(filename, e); - - if (entries.TryGetValue(crcHash, out e)) - crcIndex.Add(filename, e); - } - - var bestIndex = crcIndex.Count > classicIndex.Count ? crcIndex : classicIndex; - - var unknown = entries.Count - bestIndex.Count; - if (unknown > 0) - Log.Write("debug", "{0}: failed to resolve filenames for {1} unknown hashes".F(Name, unknown)); - - return bestIndex; - } - - static List ParseHeader(Stream s, long offset, out long headerEnd) - { - s.Seek(offset, SeekOrigin.Begin); - var numFiles = s.ReadUInt16(); - /*uint dataSize = */s.ReadUInt32(); - - var items = new List(); - for (var i = 0; i < numFiles; i++) - items.Add(new PackageEntry(s)); - - headerEnd = offset + 6 + numFiles * PackageEntry.Size; - return items; - } - - static MemoryStream DecryptHeader(Stream s, long offset, out long headerEnd) - { - s.Seek(offset, SeekOrigin.Begin); - - // Decrypt blowfish key - var keyblock = s.ReadBytes(80); - var blowfishKey = new BlowfishKeyProvider().DecryptKey(keyblock); - var fish = new Blowfish(blowfishKey); - - // Decrypt first block to work out the header length - var ms = Decrypt(ReadBlocks(s, offset + 80, 1), fish); - var numFiles = ms.ReadUInt16(); - - // Decrypt the full header - round bytes up to a full block - var blockCount = (13 + numFiles * PackageEntry.Size) / 8; - headerEnd = offset + 80 + blockCount * 8; - - return Decrypt(ReadBlocks(s, offset + 80, blockCount), fish); - } - - static MemoryStream Decrypt(uint[] h, Blowfish fish) - { - var decrypted = fish.Decrypt(h); - - var ms = new MemoryStream(); - var writer = new BinaryWriter(ms); - foreach (var t in decrypted) - writer.Write(t); - writer.Flush(); - - ms.Seek(0, SeekOrigin.Begin); - return ms; - } - - static uint[] ReadBlocks(Stream s, long offset, int count) - { - if (offset < 0) - throw new ArgumentOutOfRangeException("offset", "Non-negative number required."); - - if (count < 0) - throw new ArgumentOutOfRangeException("count", "Non-negative number required."); - - if (offset + (count * 2) > s.Length) - throw new ArgumentException("Bytes to read {0} and offset {1} greater than stream length {2}.".F(count * 2, offset, s.Length)); - - s.Seek(offset, SeekOrigin.Begin); - - // A block is a single encryption unit (represented as two 32-bit integers) - var ret = new uint[2 * count]; - for (var i = 0; i < ret.Length; i++) - ret[i] = s.ReadUInt32(); - - return ret; - } - - public Stream GetContent(PackageEntry entry) - { - Stream parentStream; - var baseOffset = dataStart + entry.Offset; - var nestedOffset = baseOffset + SegmentStream.GetOverallNestedOffset(s, out parentStream); - - // Special case FileStream - instead of creating an in-memory copy, - // just reference the portion of the on-disk file that we need to save memory. - // We use GetType instead of 'is' here since we can't handle any derived classes of FileStream. - if (parentStream.GetType() == typeof(FileStream)) - { - var path = ((FileStream)parentStream).Name; - return new SegmentStream(File.OpenRead(path), nestedOffset, entry.Length); - } - - // For all other streams, create a copy in memory. - // This uses more memory but is the only way in general to ensure the returned streams won't clash. - s.Seek(baseOffset, SeekOrigin.Begin); - return new MemoryStream(s.ReadBytes((int)entry.Length)); - } - - public Stream GetStream(string filename) - { - PackageEntry e; - if (!index.TryGetValue(filename, out e)) - return null; - - return GetContent(e); - } - - public IReadOnlyDictionary Index - { - get - { - var absoluteIndex = index.ToDictionary(e => e.Key, e => new PackageEntry(e.Value.Hash, (uint)(e.Value.Offset + dataStart), e.Value.Length)); - return new ReadOnlyDictionary(absoluteIndex); - } - } - - public bool Contains(string filename) - { - return index.ContainsKey(filename); - } - - public void Dispose() - { - s.Dispose(); + package = new MixFile(s, filename, allPossibleFilenames); + return true; } } } diff --git a/OpenRA.Game/FileSystem/Pak.cs b/OpenRA.Game/FileSystem/Pak.cs index a602522ffc..76692c61ac 100644 --- a/OpenRA.Game/FileSystem/Pak.cs +++ b/OpenRA.Game/FileSystem/Pak.cs @@ -9,76 +9,96 @@ */ #endregion +using System; using System.Collections.Generic; using System.IO; namespace OpenRA.FileSystem { - struct Entry + public class PakFileLoader : IPackageLoader { - public uint Offset; - public uint Length; - public string Filename; - } - - public sealed class PakFile : IReadOnlyPackage - { - public string Name { get; private set; } - public IEnumerable Contents { get { return index.Keys; } } - - readonly Dictionary index; - readonly Stream stream; - - public PakFile(FileSystem context, string filename) + struct Entry { - Name = filename; - index = new Dictionary(); + public uint Offset; + public uint Length; + public string Filename; + } - stream = context.Open(filename); - try + sealed class PakFile : IReadOnlyPackage + { + public string Name { get; private set; } + public IEnumerable Contents { get { return index.Keys; } } + + readonly Dictionary index = new Dictionary(); + readonly Stream stream; + + public PakFile(Stream stream, string filename) { - index = new Dictionary(); - var offset = stream.ReadUInt32(); - while (offset != 0) + Name = filename; + this.stream = stream; + + try { - var file = stream.ReadASCIIZ(); - var next = stream.ReadUInt32(); - var length = (next == 0 ? (uint)stream.Length : next) - offset; + var offset = stream.ReadUInt32(); + while (offset != 0) + { + var file = stream.ReadASCIIZ(); + var next = stream.ReadUInt32(); + var length = (next == 0 ? (uint)stream.Length : next) - offset; - // Ignore duplicate files - if (index.ContainsKey(file)) - continue; + // Ignore duplicate files + if (index.ContainsKey(file)) + continue; - index.Add(file, new Entry { Offset = offset, Length = length, Filename = file }); - offset = next; + index.Add(file, new Entry { Offset = offset, Length = length, Filename = file }); + offset = next; + } + } + catch + { + Dispose(); + throw; } } - catch + + public Stream GetStream(string filename) { - Dispose(); - throw; + Entry entry; + if (!index.TryGetValue(filename, out entry)) + return null; + + stream.Seek(entry.Offset, SeekOrigin.Begin); + var data = stream.ReadBytes((int)entry.Length); + return new MemoryStream(data); + } + + public bool Contains(string filename) + { + return index.ContainsKey(filename); + } + + public IReadOnlyPackage OpenPackage(string filename, FileSystem context) + { + // Not implemented + return null; + } + + public void Dispose() + { + stream.Dispose(); } } - public Stream GetStream(string filename) + bool IPackageLoader.TryParsePackage(Stream s, string filename, FileSystem context, out IReadOnlyPackage package) { - Entry entry; - if (!index.TryGetValue(filename, out entry)) - return null; + if (!filename.EndsWith(".pak", StringComparison.InvariantCultureIgnoreCase)) + { + package = null; + return false; + } - stream.Seek(entry.Offset, SeekOrigin.Begin); - var data = stream.ReadBytes((int)entry.Length); - return new MemoryStream(data); - } - - public bool Contains(string filename) - { - return index.ContainsKey(filename); - } - - public void Dispose() - { - stream.Dispose(); + package = new PakFile(s, filename); + return true; } } } diff --git a/OpenRA.Game/FileSystem/ZipFile.cs b/OpenRA.Game/FileSystem/ZipFile.cs index d66edc5a50..cecdb08033 100644 --- a/OpenRA.Game/FileSystem/ZipFile.cs +++ b/OpenRA.Game/FileSystem/ZipFile.cs @@ -9,7 +9,7 @@ */ #endregion -using System.Collections; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -19,142 +19,227 @@ using SZipFile = ICSharpCode.SharpZipLib.Zip.ZipFile; namespace OpenRA.FileSystem { - public sealed class ZipFile : IReadWritePackage + public class ZipFileLoader : IPackageLoader { - public IReadWritePackage Parent { get; private set; } - public string Name { get; private set; } - readonly Stream pkgStream; - readonly SZipFile pkg; + static readonly string[] Extensions = { ".zip", ".oramap" }; - static ZipFile() + sealed class ZipFile : IReadWritePackage { - ZipConstants.DefaultCodePage = Encoding.UTF8.CodePage; + public IReadWritePackage Parent { get; private set; } + public string Name { get; private set; } + readonly Stream pkgStream; + readonly SZipFile pkg; + + static ZipFile() + { + ZipConstants.DefaultCodePage = Encoding.UTF8.CodePage; + } + + public ZipFile(Stream stream, string name, IReadOnlyPackage parent = null) + { + // SharpZipLib breaks when asked to update archives loaded from outside streams or files + // We can work around this by creating a clean in-memory-only file, cutting all outside references + pkgStream = new MemoryStream(); + stream.CopyTo(pkgStream); + pkgStream.Position = 0; + + Name = name; + Parent = parent as IReadWritePackage; + pkg = new SZipFile(pkgStream); + } + + public ZipFile(string filename, IReadWritePackage parent) + { + pkgStream = new MemoryStream(); + + Name = filename; + Parent = parent; + pkg = SZipFile.Create(pkgStream); + } + + public Stream GetStream(string filename) + { + var entry = pkg.GetEntry(filename); + if (entry == null) + return null; + + using (var z = pkg.GetInputStream(entry)) + { + var ms = new MemoryStream(); + z.CopyTo(ms); + ms.Seek(0, SeekOrigin.Begin); + return ms; + } + } + + public IEnumerable Contents + { + get + { + foreach (ZipEntry entry in pkg) + yield return entry.Name; + } + } + + public bool Contains(string filename) + { + return pkg.GetEntry(filename) != null; + } + + void Commit() + { + if (Parent == null) + throw new InvalidDataException("Cannot update ZipFile without writable parent"); + + var pos = pkgStream.Position; + pkgStream.Position = 0; + Parent.Update(Name, pkgStream.ReadBytes((int)pkgStream.Length)); + pkgStream.Position = pos; + } + + public void Update(string filename, byte[] contents) + { + pkg.BeginUpdate(); + pkg.Add(new StaticStreamDataSource(new MemoryStream(contents)), filename); + pkg.CommitUpdate(); + Commit(); + } + + public void Delete(string filename) + { + pkg.BeginUpdate(); + pkg.Delete(filename); + pkg.CommitUpdate(); + Commit(); + } + + public void Dispose() + { + if (pkg != null) + pkg.Close(); + + if (pkgStream != null) + pkgStream.Dispose(); + } + + public IReadOnlyPackage OpenPackage(string filename, FileSystem context) + { + // Directories are stored with a trailing "/" in the index + var entry = pkg.GetEntry(filename) ?? pkg.GetEntry(filename + "/"); + if (entry == null) + return null; + + if (entry.IsDirectory) + return new ZipFolder(this, filename); + + if (Extensions.Any(e => filename.EndsWith(e, StringComparison.InvariantCultureIgnoreCase))) + return new ZipFile(GetStream(filename), filename, this); + + // Other package types can be loaded normally + IReadOnlyPackage package; + var s = GetStream(filename); + if (s == null) + return null; + + if (context.TryParsePackage(s, filename, out package)) + return package; + + s.Dispose(); + return null; + } } - public ZipFile(Stream stream, string name, IReadOnlyPackage parent = null) + sealed class ZipFolder : IReadOnlyPackage { - // SharpZipLib breaks when asked to update archives loaded from outside streams or files - // We can work around this by creating a clean in-memory-only file, cutting all outside references - pkgStream = new MemoryStream(); - stream.CopyTo(pkgStream); - pkgStream.Position = 0; + public string Name { get { return path; } } + public ZipFile Parent { get; private set; } + readonly string path; - Name = name; - Parent = parent as IReadWritePackage; - pkg = new SZipFile(pkgStream); + static ZipFolder() + { + ZipConstants.DefaultCodePage = Encoding.UTF8.CodePage; + } + + public ZipFolder(ZipFile parent, string path) + { + if (path.EndsWith("/", StringComparison.Ordinal)) + path = path.Substring(0, path.Length - 1); + + Parent = parent; + this.path = path; + } + + public Stream GetStream(string filename) + { + // Zip files use '/' as a path separator + return Parent.GetStream(path + '/' + filename); + } + + public IEnumerable Contents + { + get + { + foreach (var entry in Parent.Contents) + { + if (entry.StartsWith(path, StringComparison.Ordinal) && entry != path) + { + var filename = entry.Substring(path.Length + 1); + var dirLevels = filename.Split('/').Count(c => !string.IsNullOrEmpty(c)); + if (dirLevels == 1) + yield return filename; + } + } + } + } + + public bool Contains(string filename) + { + return Parent.Contains(path + '/' + filename); + } + + public IReadOnlyPackage OpenPackage(string filename, FileSystem context) + { + return Parent.OpenPackage(path + '/' + filename, context); + } + + public void Dispose() { /* nothing to do */ } } - public ZipFile(IReadOnlyFileSystem context, string filename) + class StaticStreamDataSource : IStaticDataSource { + readonly Stream s; + public StaticStreamDataSource(Stream s) + { + this.s = s; + } + + public Stream GetSource() + { + return s; + } + } + + public bool TryParsePackage(Stream s, string filename, FileSystem context, out IReadOnlyPackage package) + { + if (!Extensions.Any(e => filename.EndsWith(e, StringComparison.InvariantCultureIgnoreCase))) + { + package = null; + return false; + } + string name; IReadOnlyPackage p; - if (!context.TryGetPackageContaining(filename, out p, out name)) - throw new FileNotFoundException("Unable to find parent package for " + filename); + if (context.TryGetPackageContaining(filename, out p, out name)) + package = new ZipFile(p.GetStream(name), name, p); + else + package = new ZipFile(s, filename, null); - Name = name; - Parent = p as IReadWritePackage; - - // SharpZipLib breaks when asked to update archives loaded from outside streams or files - // We can work around this by creating a clean in-memory-only file, cutting all outside references - pkgStream = new MemoryStream(); - using (var sourceStream = p.GetStream(name)) - sourceStream.CopyTo(pkgStream); - pkgStream.Position = 0; - - pkg = new SZipFile(pkgStream); + return true; } - ZipFile(string filename, IReadWritePackage parent) - { - pkgStream = new MemoryStream(); - - Name = filename; - Parent = parent; - pkg = SZipFile.Create(pkgStream); - } - - public static ZipFile Create(string filename, IReadWritePackage parent) + public static IReadWritePackage Create(string filename, IReadWritePackage parent) { return new ZipFile(filename, parent); } - - public Stream GetStream(string filename) - { - var entry = pkg.GetEntry(filename); - if (entry == null) - return null; - - using (var z = pkg.GetInputStream(entry)) - { - var ms = new MemoryStream(); - z.CopyTo(ms); - ms.Seek(0, SeekOrigin.Begin); - return ms; - } - } - - public IEnumerable Contents - { - get - { - foreach (ZipEntry entry in pkg) - yield return entry.Name; - } - } - - public bool Contains(string filename) - { - return pkg.GetEntry(filename) != null; - } - - void Commit() - { - if (Parent == null) - throw new InvalidDataException("Cannot update ZipFile without writable parent"); - - var pos = pkgStream.Position; - pkgStream.Position = 0; - Parent.Update(Name, pkgStream.ReadBytes((int)pkgStream.Length)); - pkgStream.Position = pos; - } - - public void Update(string filename, byte[] contents) - { - pkg.BeginUpdate(); - pkg.Add(new StaticStreamDataSource(new MemoryStream(contents)), filename); - pkg.CommitUpdate(); - Commit(); - } - - public void Delete(string filename) - { - pkg.BeginUpdate(); - pkg.Delete(filename); - pkg.CommitUpdate(); - Commit(); - } - - public void Dispose() - { - if (pkg != null) - pkg.Close(); - - if (pkgStream != null) - pkgStream.Dispose(); - } - } - - class StaticStreamDataSource : IStaticDataSource - { - readonly Stream s; - public StaticStreamDataSource(Stream s) - { - this.s = s; - } - - public Stream GetSource() - { - return s; - } } } diff --git a/OpenRA.Game/FileSystem/ZipFolder.cs b/OpenRA.Game/FileSystem/ZipFolder.cs deleted file mode 100644 index 02909978ef..0000000000 --- a/OpenRA.Game/FileSystem/ZipFolder.cs +++ /dev/null @@ -1,76 +0,0 @@ -#region Copyright & License Information -/* - * Copyright 2007-2017 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; -using ICSharpCode.SharpZipLib.Zip; -using SZipFile = ICSharpCode.SharpZipLib.Zip.ZipFile; - -namespace OpenRA.FileSystem -{ - public sealed class ZipFolder : IReadOnlyPackage - { - public string Name { get; private set; } - public ZipFile Parent { get; private set; } - readonly string path; - - static ZipFolder() - { - ZipConstants.DefaultCodePage = Encoding.UTF8.CodePage; - } - - public ZipFolder(FileSystem context, ZipFile parent, string path, string filename) - { - if (filename.EndsWith("/")) - filename = filename.Substring(0, filename.Length - 1); - - Name = filename; - Parent = parent; - if (path.EndsWith("/")) - path = path.Substring(0, path.Length - 1); - - this.path = path; - } - - public Stream GetStream(string filename) - { - // Zip files use '/' as a path separator - return Parent.GetStream(path + '/' + filename); - } - - public IEnumerable Contents - { - get - { - foreach (var entry in Parent.Contents) - { - if (entry.StartsWith(path) && entry != path) - { - var filename = entry.Substring(path.Length + 1); - var dirLevels = filename.Split('/').Count(c => !string.IsNullOrEmpty(c)); - if (dirLevels == 1) - yield return filename; - } - } - } - } - - public bool Contains(string filename) - { - return Parent.Contains(path + '/' + filename); - } - - public void Dispose() { /* nothing to do */ } - } -} diff --git a/OpenRA.Game/Manifest.cs b/OpenRA.Game/Manifest.cs index 309aa22fb8..ba617f5320 100644 --- a/OpenRA.Game/Manifest.cs +++ b/OpenRA.Game/Manifest.cs @@ -57,12 +57,13 @@ namespace OpenRA public readonly string[] SoundFormats = { }; public readonly string[] SpriteFormats = { }; + public readonly string[] PackageFormats = { }; readonly string[] reservedModuleNames = { "Metadata", "Folders", "MapFolders", "Packages", "Rules", "Sequences", "VoxelSequences", "Cursors", "Chrome", "Assemblies", "ChromeLayout", "Weapons", "Voices", "Notifications", "Music", "Translations", "TileSets", "ChromeMetrics", "Missions", "ServerTraits", "LoadScreen", "Fonts", "SupportsMapsFrom", "SoundFormats", "SpriteFormats", - "RequiresMods" }; + "RequiresMods", "PackageFormats" }; readonly TypeDictionary modules = new TypeDictionary(); readonly Dictionary yaml; @@ -119,6 +120,9 @@ namespace OpenRA MapCompatibility = compat.ToArray(); + if (yaml.ContainsKey("PackageFormats")) + PackageFormats = FieldLoader.GetValue("PackageFormats", yaml["PackageFormats"].Value); + if (yaml.ContainsKey("SoundFormats")) SoundFormats = FieldLoader.GetValue("SoundFormats", yaml["SoundFormats"].Value); diff --git a/OpenRA.Game/ModData.cs b/OpenRA.Game/ModData.cs index d73ccda644..a087892b3c 100644 --- a/OpenRA.Game/ModData.cs +++ b/OpenRA.Game/ModData.cs @@ -13,7 +13,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Reflection; using OpenRA.FileSystem; using OpenRA.Graphics; using OpenRA.Widgets; @@ -27,6 +26,7 @@ namespace OpenRA public readonly ObjectCreator ObjectCreator; public readonly WidgetLoader WidgetLoader; public readonly MapCache MapCache; + public readonly IPackageLoader[] PackageLoaders; public readonly ISoundLoader[] SoundLoaders; public readonly ISpriteLoader[] SpriteLoaders; public readonly ISpriteSequenceLoader SpriteSequenceLoader; @@ -49,12 +49,12 @@ namespace OpenRA { Languages = new string[0]; - // Take a local copy of the manifest Manifest = new Manifest(mod.Id, mod.Package); ObjectCreator = new ObjectCreator(Manifest, mods); + PackageLoaders = ObjectCreator.GetLoaders(Manifest.PackageFormats, "package"); - ModFiles = new FS(mods); + ModFiles = new FS(mods, PackageLoaders); ModFiles.LoadFromManifest(Manifest); Manifest.LoadCustomData(ObjectCreator); diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index a6ab5f21dd..cc3b1b4405 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -254,7 +254,6 @@ - diff --git a/OpenRA.Mods.Cnc/UtilityCommands/ImportTSMapCommand.cs b/OpenRA.Mods.Cnc/UtilityCommands/ImportTSMapCommand.cs index 1cdfa27a29..84ad6b8dde 100644 --- a/OpenRA.Mods.Cnc/UtilityCommands/ImportTSMapCommand.cs +++ b/OpenRA.Mods.Cnc/UtilityCommands/ImportTSMapCommand.cs @@ -271,7 +271,7 @@ namespace OpenRA.Mods.Cnc.UtilityCommands map.PlayerDefinitions = mapPlayers.ToMiniYaml(); var dest = Path.GetFileNameWithoutExtension(args[1]) + ".oramap"; - map.Save(ZipFile.Create(dest, new Folder("."))); + map.Save(ZipFileLoader.Create(dest, new Folder("."))); Console.WriteLine(dest + " saved."); } diff --git a/OpenRA.Mods.Common/UtilityCommands/ImportLegacyMapCommand.cs b/OpenRA.Mods.Common/UtilityCommands/ImportLegacyMapCommand.cs index cc6a0d8748..f67e7a5a0f 100644 --- a/OpenRA.Mods.Common/UtilityCommands/ImportLegacyMapCommand.cs +++ b/OpenRA.Mods.Common/UtilityCommands/ImportLegacyMapCommand.cs @@ -103,7 +103,7 @@ namespace OpenRA.Mods.Common.UtilityCommands var dest = Path.GetFileNameWithoutExtension(args[1]) + ".oramap"; - Map.Save(ZipFile.Create(dest, new Folder("."))); + Map.Save(ZipFileLoader.Create(dest, new Folder("."))); Console.WriteLine(dest + " saved."); } diff --git a/OpenRA.Mods.Common/UtilityCommands/ListInstallShieldContentsCommand.cs b/OpenRA.Mods.Common/UtilityCommands/ListInstallShieldContentsCommand.cs index 3eb062b6a1..a080ba1bd1 100644 --- a/OpenRA.Mods.Common/UtilityCommands/ListInstallShieldContentsCommand.cs +++ b/OpenRA.Mods.Common/UtilityCommands/ListInstallShieldContentsCommand.cs @@ -27,13 +27,7 @@ namespace OpenRA.Mods.Common.UtilityCommands [Desc("ARCHIVE.Z", "Lists the content ranges for a InstallShield V3 file")] void IUtilityCommand.Run(Utility utility, string[] args) { - var filename = Path.GetFileName(args[1]); - var path = Path.GetDirectoryName(args[1]); - - var fs = new FileSystem.FileSystem(utility.Mods); - fs.Mount(path, "parent"); - var package = new InstallShieldPackage(fs, "parent|" + filename); - + var package = new InstallShieldLoader.InstallShieldPackage(File.OpenRead(args[1]), args[1]); foreach (var kv in package.Index) { Console.WriteLine("{0}:", kv.Key); diff --git a/OpenRA.Mods.Common/UtilityCommands/ListMixContentsCommand.cs b/OpenRA.Mods.Common/UtilityCommands/ListMixContentsCommand.cs index 565ff3ad2d..2821ee5c4b 100644 --- a/OpenRA.Mods.Common/UtilityCommands/ListMixContentsCommand.cs +++ b/OpenRA.Mods.Common/UtilityCommands/ListMixContentsCommand.cs @@ -10,8 +10,10 @@ #endregion using System; +using System.Collections.Generic; using System.IO; using System.Linq; +using OpenRA.FileFormats; using OpenRA.FileSystem; namespace OpenRA.Mods.Common.UtilityCommands @@ -22,23 +24,18 @@ namespace OpenRA.Mods.Common.UtilityCommands bool IUtilityCommand.ValidateArguments(string[] args) { - return args.Length == 2; + return args.Length == 3; } - [Desc("ARCHIVE.MIX", "Lists the content ranges for a mix file")] + [Desc("ARCHIVE.MIX", "MIXDATABASE.DAT", "Lists the content ranges for a mix file")] void IUtilityCommand.Run(Utility utility, string[] args) { - var filename = Path.GetFileName(args[1]); - var path = Path.GetDirectoryName(args[1]); - - var fs = new FileSystem.FileSystem(utility.Mods); - - // Needed to access the global mix database - fs.LoadFromManifest(utility.ModData.Manifest); - - fs.Mount(path, "parent"); - var package = new MixFile(fs, "parent|" + filename); + var allPossibleFilenames = new HashSet(); + using (var db = new XccGlobalDatabase(File.OpenRead(args[2]))) + foreach (var e in db.Entries) + allPossibleFilenames.Add(e); + var package = new MixLoader.MixFile(File.OpenRead(args[1]), args[1], allPossibleFilenames); foreach (var kv in package.Index.OrderBy(kv => kv.Value.Offset)) { Console.WriteLine("{0}:", kv.Key); diff --git a/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs index b5a97c4892..4490db75de 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Editor/SaveMapLogic.cs @@ -123,7 +123,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic directoryDropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 210, writableDirectories, setupItem); } - var mapIsUnpacked = map.Package != null && (map.Package is Folder || map.Package is ZipFolder); + var mapIsUnpacked = map.Package != null && map.Package is Folder; var filename = widget.Get("FILENAME"); filename.Text = map.Package == null ? "" : mapIsUnpacked ? Path.GetFileName(map.Package.Name) : Path.GetFileNameWithoutExtension(map.Package.Name); @@ -185,7 +185,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic { selectedDirectory.Folder.Delete(combinedPath); if (fileType == MapFileType.OraMap) - package = ZipFile.Create(combinedPath, selectedDirectory.Folder); + package = ZipFileLoader.Create(combinedPath, selectedDirectory.Folder); else package = new Folder(combinedPath); } diff --git a/OpenRA.Mods.Common/Widgets/Logic/Installation/ModContentLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Installation/ModContentLogic.cs index 668667f2bd..f4b3a39c66 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Installation/ModContentLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Installation/ModContentLogic.cs @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; using System.Linq; +using OpenRA.FileSystem; using OpenRA.Widgets; namespace OpenRA.Mods.Common.Widgets.Logic @@ -28,13 +29,15 @@ namespace OpenRA.Mods.Common.Widgets.Logic bool discAvailable; [ObjectCreator.UseCtor] - public ModContentLogic(Widget widget, Manifest mod, ModContent content, Action onCancel) + public ModContentLogic(Widget widget, ModData modData, Manifest mod, ModContent content, Action onCancel) { this.content = content; var panel = widget.Get("CONTENT_PANEL"); - var modFileSystem = new FileSystem.FileSystem(Game.Mods); + var modObjectCreator = new ObjectCreator(mod, Game.Mods); + var modPackageLoaders = modObjectCreator.GetLoaders(mod.PackageFormats, "package"); + var modFileSystem = new FileSystem.FileSystem(Game.Mods, modPackageLoaders); modFileSystem.LoadFromManifest(mod); var sourceYaml = MiniYaml.Load(modFileSystem, content.Sources, null); diff --git a/OpenRA.Mods.Common/Widgets/Logic/Installation/ModContentPromptLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Installation/ModContentPromptLogic.cs index ab57f27801..e6b51cc74d 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Installation/ModContentPromptLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Installation/ModContentPromptLogic.cs @@ -11,6 +11,7 @@ using System; using System.Linq; +using OpenRA.FileSystem; using OpenRA.Widgets; namespace OpenRA.Mods.Common.Widgets.Logic @@ -18,7 +19,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic public class ModContentPromptLogic : ChromeLogic { [ObjectCreator.UseCtor] - public ModContentPromptLogic(Widget widget, Manifest mod, ModContent content, Action continueLoading) + public ModContentPromptLogic(Widget widget, ModData modData, Manifest mod, ModContent content, Action continueLoading) { var panel = widget.Get("CONTENT_PROMPT_PANEL"); @@ -55,8 +56,11 @@ namespace OpenRA.Mods.Common.Widgets.Logic quickButton.Bounds.Y += headerHeight; quickButton.OnClick = () => { - var modFileSystem = new FileSystem.FileSystem(Game.Mods); + var modObjectCreator = new ObjectCreator(mod, Game.Mods); + var modPackageLoaders = modObjectCreator.GetLoaders(mod.PackageFormats, "package"); + var modFileSystem = new FileSystem.FileSystem(Game.Mods, modPackageLoaders); modFileSystem.LoadFromManifest(mod); + var downloadYaml = MiniYaml.Load(modFileSystem, content.Downloads, null); modFileSystem.UnmountAll(); diff --git a/OpenRA.Mods.D2k/UtilityCommands/ImportD2kMapCommand.cs b/OpenRA.Mods.D2k/UtilityCommands/ImportD2kMapCommand.cs index 607f9de99d..5dd6ea204d 100644 --- a/OpenRA.Mods.D2k/UtilityCommands/ImportD2kMapCommand.cs +++ b/OpenRA.Mods.D2k/UtilityCommands/ImportD2kMapCommand.cs @@ -37,7 +37,7 @@ namespace OpenRA.Mods.D2k.UtilityCommands return; var dest = Path.GetFileNameWithoutExtension(args[1]) + ".oramap"; - map.Save(ZipFile.Create(dest, new Folder("."))); + map.Save(ZipFileLoader.Create(dest, new Folder("."))); Console.WriteLine(dest + " saved."); } } diff --git a/mods/cnc/mod.yaml b/mods/cnc/mod.yaml index 71adfdc2b3..f29ee495e5 100644 --- a/mods/cnc/mod.yaml +++ b/mods/cnc/mod.yaml @@ -2,6 +2,8 @@ Metadata: Title: Tiberian Dawn Version: {DEV_VERSION} +PackageFormats: Mix + Packages: ~^Content/cnc ~^Content/cnc/movies diff --git a/mods/d2k/mod.yaml b/mods/d2k/mod.yaml index 551975e49e..5c60cdb37e 100644 --- a/mods/d2k/mod.yaml +++ b/mods/d2k/mod.yaml @@ -2,6 +2,8 @@ Metadata: Title: Dune 2000 Version: {DEV_VERSION} +PackageFormats: D2kSoundResources + Packages: ~^Content/d2k/v2/ ~^Content/d2k/v2/GAMESFX diff --git a/mods/ra/mod.yaml b/mods/ra/mod.yaml index 28d6effc5e..34a6ae3a3c 100644 --- a/mods/ra/mod.yaml +++ b/mods/ra/mod.yaml @@ -2,6 +2,8 @@ Metadata: Title: Red Alert Version: {DEV_VERSION} +PackageFormats: Mix + Packages: ~^Content/ra/v2/ ~^Content/ra/v2/expand diff --git a/mods/ts/mod.yaml b/mods/ts/mod.yaml index 4b40483f67..17e221e5a0 100644 --- a/mods/ts/mod.yaml +++ b/mods/ts/mod.yaml @@ -2,6 +2,8 @@ Metadata: Title: Tiberian Sun Version: {DEV_VERSION} +PackageFormats: Mix + Packages: ~^Content/ts ~^Content/ts/firestorm