From 33a2ea0800c50239649f9e062ae295e1b1ae17ef Mon Sep 17 00:00:00 2001 From: Jarl Gullberg Date: Mon, 16 Oct 2017 18:23:30 +0200 Subject: [PATCH] Throw instead of returning null for most MPQ operations. --- libwarcraft/Containers/Terrain/GameWorld.cs | 3 +- libwarcraft/Core/Exceptions.cs | 51 ++++++ libwarcraft/MPQ/MPQ.cs | 158 +++++++----------- .../MPQ/Tables/Block/BlockTableEntry.cs | 9 + libwarcraft/MPQ/Tables/Hash/HashTable.cs | 2 +- .../WMO/RootFile/Chunks/ModelGroupNames.cs | 5 +- 6 files changed, 126 insertions(+), 102 deletions(-) diff --git a/libwarcraft/Containers/Terrain/GameWorld.cs b/libwarcraft/Containers/Terrain/GameWorld.cs index bf25d54..2e3ba40 100644 --- a/libwarcraft/Containers/Terrain/GameWorld.cs +++ b/libwarcraft/Containers/Terrain/GameWorld.cs @@ -20,6 +20,7 @@ // along with this program. If not, see . // +using System; using System.Collections.Generic; using Warcraft.WDT; using Warcraft.ADT; @@ -73,7 +74,7 @@ public TerrainTile LoadTile(uint tileXPosition, uint tileYPosition) if (!this.Package.ContainsFile(tilePath)) { - return null; + throw new ArgumentException("No tile found for the given coordinates."); } byte[] terrainTileData = this.Package.ExtractFile(tilePath); diff --git a/libwarcraft/Core/Exceptions.cs b/libwarcraft/Core/Exceptions.cs index 076a2dc..afbf736 100644 --- a/libwarcraft/Core/Exceptions.cs +++ b/libwarcraft/Core/Exceptions.cs @@ -24,6 +24,57 @@ namespace Warcraft.Core { + public class FileDeletedException : Exception + { + public string FilePath { get; } + + /// + /// Creates a new instance of the class, along with a specified + /// message. + /// + /// The message included in the exception. + public FileDeletedException(string message) + : base(message) + { + } + + /// + /// Creates a new instance of the class, along with a specified + /// message and file path. + /// + /// + /// + public FileDeletedException(string message, string filePath) + : base(message) + { + this.FilePath = filePath; + } + + /// + /// Creates a new instance of the class, along with a specified + /// message and inner exception which caused this exception. + /// + /// The message included in the exception. + /// The exception which caused this exception. + public FileDeletedException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Creates a new instance of the class, along with a specified + /// message, file path, and inner exception which caused this exception. + /// + /// + /// + /// + public FileDeletedException(string message, string filePath, Exception innerException) + : base(message, innerException) + { + this.FilePath = filePath; + } + } + /// /// This exception thrown when an invalid or unknown chunk signature is found during parsing of binary data which /// is expected to be in valid RIFF format. diff --git a/libwarcraft/MPQ/MPQ.cs b/libwarcraft/MPQ/MPQ.cs index 9ba72c8..a2c98b7 100644 --- a/libwarcraft/MPQ/MPQ.cs +++ b/libwarcraft/MPQ/MPQ.cs @@ -211,10 +211,7 @@ private uint PeekHeaderSize() /// true if this archive has file attributes; otherwise, false. public bool HasFileAttributes() { - if (this.IsDisposed) - { - throw new ObjectDisposedException(ToString(), "Cannot use a disposed archive."); - } + ThrowIfDisposed(); return ContainsFile(ExtendedAttributes.InternalFileName); } @@ -224,6 +221,8 @@ public bool HasFileAttributes() /// public ExtendedAttributes GetFileAttributes() { + ThrowIfDisposed(); + if (this.FileAttributes != null) { return this.FileAttributes; @@ -244,14 +243,9 @@ public ExtendedAttributes GetFileAttributes() /// The weak signature. public WeakPackageSignature GetWeakSignature() { - if (ContainsFile(WeakPackageSignature.InternalFilename)) - { - return new WeakPackageSignature(ExtractFile(WeakPackageSignature.InternalFilename)); - } - else - { - return null; - } + ThrowIfDisposed(); + + return new WeakPackageSignature(ExtractFile(WeakPackageSignature.InternalFilename)); } /// @@ -260,10 +254,7 @@ public WeakPackageSignature GetWeakSignature() /// true if this archive has a listfile; otherwise, false. public bool HasFileList() { - if (this.IsDisposed) - { - throw new ObjectDisposedException(ToString(), "Cannot use a disposed archive."); - } + ThrowIfDisposed(); return ContainsFile("(listfile)") || this.ExternalListfile.Count > 0; } @@ -275,39 +266,31 @@ public bool HasFileList() /// The listfile. public IEnumerable GetFileList() { - if (this.IsDisposed) - { - throw new ObjectDisposedException(ToString(), "Cannot use a disposed archive."); - } + ThrowIfDisposed(); if (this.ExternalListfile.Count > 0) { return GetExternalFileList(); } - else - { - return GetInternalFileList(); - } + + return GetInternalFileList(); } /// - /// Gets the internal file list. If no listfile is stored in the archive, this may - /// return null. + /// Gets the internal file list. If no listfile is stored in the archive, this may not return anything. /// /// The internal file list. public IEnumerable GetInternalFileList() { - if (this.IsDisposed) - { - throw new ObjectDisposedException(ToString(), "Cannot use a disposed archive."); - } + ThrowIfDisposed(); - byte[] listfileBytes = ExtractFile("(listfile)"); - if (listfileBytes == null) + if (!ContainsFile("(listfile)")) { yield break; } + byte[] listfileBytes = ExtractFile("(listfile)"); + using (MemoryStream listfileStream = new MemoryStream(listfileBytes)) { using (TextReader tr = new StreamReader(listfileStream)) @@ -328,10 +311,7 @@ public IEnumerable GetInternalFileList() /// The external file list. public IEnumerable GetExternalFileList() { - if (this.IsDisposed) - { - throw new ObjectDisposedException(ToString(), "Cannot use a disposed archive."); - } + ThrowIfDisposed(); return this.ExternalListfile; } @@ -342,10 +322,7 @@ public IEnumerable GetExternalFileList() /// In external listfile. public void SetFileList(List inExternalListfile) { - if (this.IsDisposed) - { - throw new ObjectDisposedException(ToString(), "Cannot use a disposed archive."); - } + ThrowIfDisposed(); this.ExternalListfile = inExternalListfile; } @@ -356,90 +333,73 @@ public void SetFileList(List inExternalListfile) /// public void ResetExternalFileList() { - if (this.IsDisposed) - { - throw new ObjectDisposedException(ToString(), "Cannot use a disposed archive."); - } + ThrowIfDisposed(); this.ExternalListfile.Clear(); } - /// - /// Checks if the specified file path exists in the archive. - /// - /// true, if the file exists, false otherwise. - /// File path. + /// public bool ContainsFile(string filePath) { - if (this.IsDisposed) + ThrowIfDisposed(); + + try { - throw new ObjectDisposedException(ToString(), "Cannot use a disposed archive."); + this.ArchiveHashTable.FindEntry(filePath.ToUpperInvariant()); + } + catch (FileNotFoundException) + { + return false; } - HashTableEntry fileHashEntry = this.ArchiveHashTable.FindEntry(filePath.ToUpperInvariant()); - return fileHashEntry != null; + return true; } - /// - /// Gets the file info of the provided path. - /// - /// The file info, or null if the file doesn't exist in the archive. - /// File path. + /// public MPQFileInfo GetFileInfo(string filePath) { - if (this.IsDisposed) + ThrowIfDisposed(); + + if (!ContainsFile(filePath)) { - throw new ObjectDisposedException(ToString(), "Cannot use a disposed archive."); + throw new FileNotFoundException("The given file was not present in the archive.", filePath); } - if (ContainsFile(filePath)) - { - HashTableEntry hashEntry = this.ArchiveHashTable.FindEntry(filePath); - BlockTableEntry blockEntry = this.ArchiveBlockTable.GetEntry((int)hashEntry.GetBlockEntryIndex()); + HashTableEntry hashEntry = this.ArchiveHashTable.FindEntry(filePath); + BlockTableEntry blockEntry = this.ArchiveBlockTable.GetEntry((int)hashEntry.GetBlockEntryIndex()); - if (HasFileAttributes()) - { - return new MPQFileInfo(filePath, hashEntry, blockEntry); - } - else - { - return new MPQFileInfo(filePath, hashEntry, blockEntry, this.FileAttributes.FileAttributes[(int)hashEntry.GetBlockEntryIndex()]); - } - } - else + if (HasFileAttributes()) { - return null; + return new MPQFileInfo(filePath, hashEntry, blockEntry); } + + return new MPQFileInfo(filePath, hashEntry, blockEntry, this.FileAttributes.FileAttributes[(int)hashEntry.GetBlockEntryIndex()]); } - // TODO: Filter files based on language and platform - /// - /// Extract the file at from the archive. - /// - /// The file as a byte array, or null if the file could not be found. - /// Path to the file in the archive. + /// public byte[] ExtractFile(string filePath) { - if (this.IsDisposed) - { - throw new ObjectDisposedException(ToString(), "Cannot use a disposed archive."); - } + ThrowIfDisposed(); // Reset all positions to be safe this.ArchiveReader.BaseStream.Position = 0; - HashTableEntry fileHashEntry = this.ArchiveHashTable.FindEntry(filePath); - if (fileHashEntry == null) + HashTableEntry fileHashEntry; + try + { + fileHashEntry = this.ArchiveHashTable.FindEntry(filePath); + } + catch (FileNotFoundException fex) { - return null; + throw new FileNotFoundException("No file found at the given path.", filePath, fex); } BlockTableEntry fileBlockEntry = this.ArchiveBlockTable.GetEntry((int)fileHashEntry.GetBlockEntryIndex()); - // Drop out if the file is not actually a file - if (!fileBlockEntry.HasData()) + // Drop out if the file has been deleted + if (fileBlockEntry.IsDeleted()) { - return null; + throw new FileDeletedException("The given file is deleted.", filePath); } // Seek to the beginning of the file's sectors @@ -766,13 +726,15 @@ private uint GetMaxSectorSize() return (uint)(512 * Math.Pow(2, this.Header.GetSectorSizeExponent())); } - /// - /// Releases all resource used by the object. - /// - /// Call when you are finished using the . The - /// method leaves the in an unusable state. After calling , you must - /// release all references to the so the garbage collector can reclaim the memory that - /// the was occupying. + private void ThrowIfDisposed() + { + if (this.IsDisposed) + { + throw new ObjectDisposedException(ToString(), "Cannot use a disposed archive."); + } + } + + /// public void Dispose() { this.Header = null; diff --git a/libwarcraft/MPQ/Tables/Block/BlockTableEntry.cs b/libwarcraft/MPQ/Tables/Block/BlockTableEntry.cs index cfa8ddf..0c555d7 100644 --- a/libwarcraft/MPQ/Tables/Block/BlockTableEntry.cs +++ b/libwarcraft/MPQ/Tables/Block/BlockTableEntry.cs @@ -179,6 +179,15 @@ public bool HasData() return this.Flags.HasFlag(BlockFlags.Exists) && !this.Flags.HasFlag(BlockFlags.IsDeletionMarker); } + /// + /// Determines whether or not this block entry is pointing to a deleted file. + /// + /// + public bool IsDeleted() + { + return this.Flags.HasFlag(BlockFlags.IsDeletionMarker); + } + /// /// Determines whether or not the file data is stored as a single unit. If not, it's in sectors. /// diff --git a/libwarcraft/MPQ/Tables/Hash/HashTable.cs b/libwarcraft/MPQ/Tables/Hash/HashTable.cs index e94dc9f..1594e67 100644 --- a/libwarcraft/MPQ/Tables/Hash/HashTable.cs +++ b/libwarcraft/MPQ/Tables/Hash/HashTable.cs @@ -104,7 +104,7 @@ public HashTableEntry FindEntry(uint hashA, uint hashB, uint entryHomeIndex) } else { - return null; + throw new FileNotFoundException($"No file has ever existed at the home index {entryHomeIndex}."); } // If that file doesn't match (but has existed, or is occupied, let's keep looking down the table. diff --git a/libwarcraft/WMO/RootFile/Chunks/ModelGroupNames.cs b/libwarcraft/WMO/RootFile/Chunks/ModelGroupNames.cs index d692d6e..07376ad 100644 --- a/libwarcraft/WMO/RootFile/Chunks/ModelGroupNames.cs +++ b/libwarcraft/WMO/RootFile/Chunks/ModelGroupNames.cs @@ -20,6 +20,7 @@ // along with this program. If not, see . // +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -76,7 +77,7 @@ public string GetInternalGroupName(ModelGroup modelGroup) return this.GroupNames[internalNameOffset]; } - return null; + throw new ArgumentException("Group name not found.", nameof(modelGroup)); } public string GetInternalDescriptiveGroupName(ModelGroup modelGroup) @@ -87,7 +88,7 @@ public string GetInternalDescriptiveGroupName(ModelGroup modelGroup) return this.GroupNames[internalDescriptiveNameOffset]; } - return null; + throw new ArgumentException("Descriptive group name not found.", nameof(modelGroup)); } public byte[] Serialize()