From b29198b3d49bef6846048a76f779ed1a90623ed0 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Sun, 22 May 2022 23:46:29 -0700 Subject: [PATCH] CHM objectification --- BurnOutSharp/External/libmspack/CHM/CHM.cs | 85 + .../External/libmspack/CHM/Compressor.cs | 44 +- .../External/libmspack/CHM/CompressorImpl.cs | 22 - .../External/libmspack/CHM/Constants.cs | 53 + .../External/libmspack/CHM/DecompressState.cs | 2 +- .../External/libmspack/CHM/Decompressor.cs | 1405 +++++++++++++++- .../libmspack/CHM/DecompressorImpl.cs | 20 - BurnOutSharp/External/libmspack/CHM/Header.cs | 135 -- .../External/libmspack/CHM/Implementation.cs | 1494 ----------------- .../External/libmspack/CHM/Section.cs | 2 +- .../External/libmspack/CHM/_CHMHeader.cs | 118 ++ .../External/libmspack/CHM/_HeaderSection0.cs | 85 + .../External/libmspack/CHM/_HeaderSection1.cs | 184 ++ .../libmspack/CHM/_HeaderSectionTable.cs | 104 ++ BurnOutSharp/External/libmspack/Library.cs | 17 +- 15 files changed, 2009 insertions(+), 1761 deletions(-) create mode 100644 BurnOutSharp/External/libmspack/CHM/CHM.cs delete mode 100644 BurnOutSharp/External/libmspack/CHM/CompressorImpl.cs create mode 100644 BurnOutSharp/External/libmspack/CHM/Constants.cs delete mode 100644 BurnOutSharp/External/libmspack/CHM/DecompressorImpl.cs delete mode 100644 BurnOutSharp/External/libmspack/CHM/Header.cs delete mode 100644 BurnOutSharp/External/libmspack/CHM/Implementation.cs create mode 100644 BurnOutSharp/External/libmspack/CHM/_CHMHeader.cs create mode 100644 BurnOutSharp/External/libmspack/CHM/_HeaderSection0.cs create mode 100644 BurnOutSharp/External/libmspack/CHM/_HeaderSection1.cs create mode 100644 BurnOutSharp/External/libmspack/CHM/_HeaderSectionTable.cs diff --git a/BurnOutSharp/External/libmspack/CHM/CHM.cs b/BurnOutSharp/External/libmspack/CHM/CHM.cs new file mode 100644 index 00000000..6a0c33c2 --- /dev/null +++ b/BurnOutSharp/External/libmspack/CHM/CHM.cs @@ -0,0 +1,85 @@ +/* libmspack -- a library for working with Microsoft compression formats. + * (C) 2003-2019 Stuart Caie + * + * libmspack is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License (LGPL) version 2.1 + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +namespace LibMSPackSharp.CHM +{ + /// + /// A structure which represents a CHM helpfile. + /// + /// All fields are READ ONLY. + /// + public class CHM + { + #region Internal + + /// + /// CHM header information + /// + internal _CHMHeader Header { get; set; } + + /// + /// Header section table information + /// + internal _HeaderSectionTable HeaderSectionTable { get; set; } + + /// + /// Header section 0 information + /// + internal _HeaderSection0 HeaderSection0 { get; set; } + + /// + /// Header section 1 information + /// + internal _HeaderSection1 HeaderSection1 { get; set; } + + #endregion + + /// + /// The filename of the CHM helpfile. This is given by the library user + /// and may be in any format. + /// + public string Filename { get; set; } + + /// + /// A list of all non-system files in the CHM helpfile. + /// + public DecompressFile Files { get; set; } + + /// + /// A list of all system files in the CHM helpfile. + /// + /// System files are files which begin with "::". They are meta-files + /// generated by the CHM creation process. + /// + public DecompressFile SysFiles { get; set; } + + /// + /// The section 0 (uncompressed) data in this CHM helpfile. + /// + public UncompressedSection Sec0 { get; set; } + + /// + /// The section 1 (MSCompressed) data in this CHM helpfile. + /// + public MSCompressedSection Sec1 { get; set; } + + /// + /// A cache of loaded chunks, filled in by mschm_decoder::fast_find(). + /// Available only in CHM decoder version 2 and above. + /// + public byte[][] ChunkCache { get; set; } + } +} diff --git a/BurnOutSharp/External/libmspack/CHM/Compressor.cs b/BurnOutSharp/External/libmspack/CHM/Compressor.cs index 05560832..99614f78 100644 --- a/BurnOutSharp/External/libmspack/CHM/Compressor.cs +++ b/BurnOutSharp/External/libmspack/CHM/Compressor.cs @@ -27,6 +27,20 @@ namespace LibMSPackSharp.CHM /// public class Compressor { + #region Fields + + public SystemImpl System { get; set; } + + public string TempFile { get; set; } + + public bool UseTempFile { get; set; } + + public Error Error { get; set; } + + #endregion + + #region Public Functionality + /// /// Generates a CHM help file. /// @@ -43,10 +57,6 @@ namespace LibMSPackSharp.CHM /// use one. See use_temporary_file() for the behaviour of generate() in /// these two different modes. /// - /// - /// a self-referential pointer to the mschm_compressor - /// instance being called - /// /// /// an array of mschmc_file structures, terminated /// with an entry whose mschmc_file::section field is @@ -64,7 +74,7 @@ namespace LibMSPackSharp.CHM /// an error code, or MSPACK_ERR_OK if successful /// /// - public Func Generate; + public Func Generate; /// /// Specifies whether a temporary file is used during CHM generation. @@ -108,10 +118,6 @@ namespace LibMSPackSharp.CHM /// update, the appropriate sections are seek() ed to and re-written, then /// the output file is closed. /// - /// - /// a self-referential pointer to the mschm_compressor - /// instance being called - /// /// /// non-zero if the temporary file should be used, /// zero if the temporary file should not be used. @@ -124,15 +130,11 @@ namespace LibMSPackSharp.CHM /// /// an error code, or MSPACK_ERR_OK if successful /// - public Func UseTemporaryFile; + public Func UseTemporaryFile; /// /// Sets a CHM compression engine parameter. /// - /// - /// a self-referential pointer to the mschm_compressor - /// instance being called - /// /// the parameter to set /// the value to set the parameter to /// @@ -140,18 +142,8 @@ namespace LibMSPackSharp.CHM /// is a problem with either parameter or value. /// /// - public Func SetParam; + public Func SetParam; - /// - /// Returns the error code set by the most recently called method. - /// - /// - /// a self-referential pointer to the mschm_compressor - /// instance being called - /// - /// the most recent error code - /// - /// - public Func LastError; + #endregion } } diff --git a/BurnOutSharp/External/libmspack/CHM/CompressorImpl.cs b/BurnOutSharp/External/libmspack/CHM/CompressorImpl.cs deleted file mode 100644 index 9c27324a..00000000 --- a/BurnOutSharp/External/libmspack/CHM/CompressorImpl.cs +++ /dev/null @@ -1,22 +0,0 @@ -/* This file is part of libmspack. - * (C) 2003-2004 Stuart Caie. - * - * libmspack is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License (LGPL) version 2.1 - * - * For further details, see the file COPYING.LIB distributed with libmspack - */ - -namespace LibMSPackSharp.CHM -{ - public class CompressorImpl : Compressor - { - public SystemImpl System { get; set; } - - public string TempFile { get; set; } - - public bool UseTempFile { get; set; } - - public Error Error { get; set; } - } -} diff --git a/BurnOutSharp/External/libmspack/CHM/Constants.cs b/BurnOutSharp/External/libmspack/CHM/Constants.cs new file mode 100644 index 00000000..aeb7856a --- /dev/null +++ b/BurnOutSharp/External/libmspack/CHM/Constants.cs @@ -0,0 +1,53 @@ +/* This file is part of libmspack. + * (C) 2003-2004 Stuart Caie. + * + * libmspack is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License (LGPL) version 2.1 + * + * For further details, see the file COPYING.LIB distributed with libmspack + */ + +namespace LibMSPackSharp.CHM +{ + public class Constants + { + // _PMGHeader + internal const int pmgl_Signature = 0x0000; + internal const int pmgl_QuickRefSize = 0x0004; + internal const int pmgl_PMGIEntries = 0x0008; // Unknown1 in PMGL + internal const int pmgl_PrevChunk = 0x000C; // Not in PMGI + internal const int pmgl_NextChunk = 0x0010; // Not in PMGI + internal const int pmgl_PMGLEntries = 0x0014; // Not in PMGI + internal const int pmgl_headerSIZEOF = 0x0014; + internal const int pmgi_headerSIZEOF = 0x000C; + + // _LZXControlData + internal const int lzxcd_Length = 0x0000; + internal const int lzxcd_Signature = 0x0004; + internal const int lzxcd_Version = 0x0008; + internal const int lzxcd_ResetInterval = 0x000C; + internal const int lzxcd_WindowSize = 0x0010; + internal const int lzxcd_CacheSize = 0x0014; + internal const int lzxcd_Unknown1 = 0x0018; + internal const int lzxcd_SIZEOF = 0x001C; + + // _LZXResetTable + internal const int lzxrt_Unknown1 = 0x0000; + internal const int lzxrt_NumEntries = 0x0004; + internal const int lzxrt_EntrySize = 0x0008; + internal const int lzxrt_TableOffset = 0x000C; + internal const int lzxrt_UncompLen = 0x0010; + internal const int lzxrt_CompLen = 0x0018; + internal const int lzxrt_FrameLen = 0x0020; + internal const int lzxrt_Entries = 0x0028; + internal const int lzxrt_headerSIZEOF = 0x0028; + + // Filenames of the system files used for decompression. + // - Content and ControlData are essential. + // - ResetTable is preferred, but SpanInfo can be used if not available + internal const string ContentName = "::DataSpace/Storage/MSCompressed/Content"; + internal const string ControlName = "::DataSpace/Storage/MSCompressed/ControlData"; + internal const string SpanInfoName = "::DataSpace/Storage/MSCompressed/SpanInfo"; + internal const string ResetTableName = "::DataSpace/Storage/MSCompressed/Transform/{7FC28940-9D31-11D0-9B27-00A0C91E9C7C}/InstanceData/ResetTable"; + } +} diff --git a/BurnOutSharp/External/libmspack/CHM/DecompressState.cs b/BurnOutSharp/External/libmspack/CHM/DecompressState.cs index b1d87eec..7c916af2 100644 --- a/BurnOutSharp/External/libmspack/CHM/DecompressState.cs +++ b/BurnOutSharp/External/libmspack/CHM/DecompressState.cs @@ -17,7 +17,7 @@ namespace LibMSPackSharp.CHM /// /// CHM file being decompressed /// - public Header Header { get; set; } + public CHM Header { get; set; } /// /// Uncompressed offset within folder diff --git a/BurnOutSharp/External/libmspack/CHM/Decompressor.cs b/BurnOutSharp/External/libmspack/CHM/Decompressor.cs index 6ddb1ea3..bd5a99b4 100644 --- a/BurnOutSharp/External/libmspack/CHM/Decompressor.cs +++ b/BurnOutSharp/External/libmspack/CHM/Decompressor.cs @@ -15,6 +15,10 @@ */ using System; +using System.IO; +using System.Text; +using LibMSPackSharp.Compression; +using static LibMSPackSharp.CHM.Constants; namespace LibMSPackSharp.CHM { @@ -27,6 +31,18 @@ namespace LibMSPackSharp.CHM /// public class Decompressor { + #region Fields + + public SystemImpl System { get; set; } + + public DecompressState State { get; set; } + + public Error Error { get; set; } + + #endregion + + #region Public Functionality + /// /// Opens a CHM helpfile and reads its contents. /// @@ -40,17 +56,16 @@ namespace LibMSPackSharp.CHM /// The filename pointer should be considered "in use" until close() is /// called on the CHM helpfile. /// - /// - /// a self-referential pointer to the mschm_decompressor - /// instance being called - /// /// /// the filename of the CHM helpfile. This is passed /// directly to mspack_system::open(). /// /// a pointer to a mschmd_header structure, or NULL on failure - /// - public Func Open; + /// + public CHM Open(string filename) + { + return RealOpen(filename, true); + } /// /// Closes a previously opened CHM helpfile. @@ -64,14 +79,34 @@ namespace LibMSPackSharp.CHM /// mschmd_file pointers referencing that CHM are also now invalid, and /// cannot be used again. /// - /// - /// a self-referential pointer to the mschm_decompressor - /// instance being called - /// /// the CHM helpfile to close - /// - /// - public Action Close; + /// + /// + public void Close(CHM chm) + { + Error = Error.MSPACK_ERR_OK; + + // Free files + DecompressFile fi, nfi; + for (fi = chm.Files; fi != null; fi = nfi) + { + nfi = fi.Next; + } + + for (fi = chm.SysFiles; fi != null; fi = nfi) + { + nfi = fi.Next; + } + + // If this CHM was being decompressed, free decompression state + if (State != null && (State.Header == chm)) + { + System.Close(State.InputFileHandle); + System.Close(State.OutputFileHandle); + + State = null; + } + } /// /// Extracts a file from a CHM helpfile. @@ -85,29 +120,140 @@ namespace LibMSPackSharp.CHM /// and mschmd_header::sysfiles list and mschmd_file structures generated /// on the fly by fast_find(). /// - /// - /// a self-referential pointer to the mschm_decompressor - /// instance being called - /// /// the file to be decompressed /// the filename of the file being written to /// an error code, or MSPACK_ERR_OK if successful - public Func Extract; + public Error Extract(DecompressFile file, string filename) + { + if (file == null || file.Section == null) + return Error = Error.MSPACK_ERR_ARGS; - /// - /// Returns the error code set by the most recently called method. - /// - /// This is useful for open() and fast_open(), which do not return an - /// error code directly. - /// - /// - /// a self-referential pointer to the mschm_decompressor - /// instance being called - /// - /// the most recent error code - /// - /// - public Func LastError; + CHM chm = file.Section.Header; + + // Create decompression state if it doesn't exist + if (State == null) + { + State = new DecompressState(); + State.Header = chm; + State.Offset = 0; + State.State = null; + State.Sys = System; + State.Sys.Write = SysWrite; + State.InputFileHandle = null; + State.OutputFileHandle = null; + } + + // Open input chm file if not open, or the open one is a different chm + if (State.InputFileHandle == null || (State.Header != chm)) + { + System.Close(State.InputFileHandle); + System.Close(State.OutputFileHandle); + + State.Header = chm; + State.Offset = 0; + State.State = null; + State.InputFileHandle = System.Open(chm.Filename, OpenMode.MSPACK_SYS_OPEN_READ); + if (State.InputFileHandle == null) + return Error = Error.MSPACK_ERR_OPEN; + } + + // Open file for output + FileStream fh = System.Open(filename, OpenMode.MSPACK_SYS_OPEN_WRITE); + if (fh == null) + return Error = Error.MSPACK_ERR_OPEN; + + // If file is empty, simply creating it is enough + if (file.Length == 0) + { + System.Close(fh); + return Error = Error.MSPACK_ERR_OK; + } + + Error = Error.MSPACK_ERR_OK; + + switch (file.Section.ID) + { + // Uncompressed section file + case 0: + // Simple seek + copy + if (!System.Seek(State.InputFileHandle, file.Section.Header.Sec0.Offset + file.Offset, SeekMode.MSPACK_SYS_SEEK_START)) + { + Error = Error.MSPACK_ERR_SEEK; + } + else + { + byte[] buf = new byte[512]; + long length = file.Length; + while (length > 0) + { + int run = 512; + if (run > length) + run = (int)length; + + if (System.Read(State.InputFileHandle, buf, 0, run) != run) + { + Error = Error.MSPACK_ERR_READ; + break; + } + + if (System.Write(fh, buf, 0, run) != run) + { + Error = Error.MSPACK_ERR_WRITE; + break; + } + + length -= run; + } + } + break; + + // MSCompressed section file + case 1: + // (Re)initialise compression state if we it is not yet initialised, + // or we have advanced too far and have to backtrack + if (State.State == null || (file.Offset < State.Offset)) + { + if (State.State != null) + State.State = null; + + if (InitDecompressor(file) != Error.MSPACK_ERR_OK) + break; + } + + // Seek to input data + if (!System.Seek(State.InputFileHandle, State.InOffset, SeekMode.MSPACK_SYS_SEEK_START)) + { + Error = Error.MSPACK_ERR_SEEK; + break; + } + + // Get to correct offset. + State.OutputFileHandle = null; + long bytes; + if ((bytes = file.Offset - State.Offset) != 0) + Error = LZX.Decompress(State.State, bytes); + + // If getting to the correct offset was error free, unpack file + if (Error == Error.MSPACK_ERR_OK) + { + State.OutputFileHandle = fh; + Error = LZX.Decompress(State.State, file.Length); + } + + // Save offset in input source stream, in case there is a section 0 + // file between now and the next section 1 file extracted + State.InOffset = System.Tell(State.InputFileHandle); + + // If an LZX error occured, the LZX decompressor is now useless + if (Error != Error.MSPACK_ERR_OK) + State.State = null; + + break; + } + + System.Close(fh); + return Error; + } /// /// Opens a CHM helpfile quickly. @@ -124,20 +270,19 @@ namespace LibMSPackSharp.CHM /// The filename pointer should be considered "in use" until close() is /// called on the CHM helpfile. /// - /// - /// a self-referential pointer to the mschm_decompressor - /// instance being called - /// /// /// the filename of the CHM helpfile. This is passed /// directly to mspack_system::open(). /// /// a pointer to a mschmd_header structure, or NULL on failure - /// - /// - /// - /// - public Func FastOpen; + /// + /// + /// + /// + public CHM FastOpen(string filename) + { + return RealOpen(filename, false); + } /// /// Finds file details quickly. @@ -168,18 +313,1178 @@ namespace LibMSPackSharp.CHM /// opened with fast_open(), but it also works with helpfiles opened /// using the regular open(). /// - /// - /// a self-referential pointer to the mschm_decompressor - /// instance being called - /// /// the CHM helpfile to search for the file /// the filename of the file to search for /// a pointer to a caller-provded mschmd_file structure /// an error code, or MSPACK_ERR_OK if successful - /// - /// - /// - /// - public Func FastFind; + /// + /// + /// + /// + public Error FastFind(CHM chm, string filename, DecompressFile f_ptr) + { + // p and end are initialised to prevent MSVC warning about "potentially" + // uninitialised usage. This is provably untrue, but MS won't fix: + // https://developercommunity.visualstudio.com/content/problem/363489/c4701-false-positive-warning.html + byte[] chunk = new byte[0]; + int p = -1, end = -1, result = -1; + Error err = Error.MSPACK_ERR_OK; + uint n, sec; + + if (chm == null || f_ptr == null) + return Error.MSPACK_ERR_ARGS; + + // Clear the results structure + f_ptr = new DecompressFile(); + + FileStream fh = System.Open(chm.Filename, OpenMode.MSPACK_SYS_OPEN_READ); + if (fh == null) + return Error.MSPACK_ERR_OPEN; + + // Go through PMGI chunk hierarchy to reach PMGL chunk + if (chm.HeaderSection1.IndexRoot < chm.HeaderSection1.NumChunks) + { + n = chm.HeaderSection1.IndexRoot; + for (; ; ) + { + if ((chunk = ReadChunk(chm, fh, n)) == null) + { + System.Close(fh); + return Error; + } + + // Search PMGI/PMGL chunk. exit early if no entry found + if ((result = SearchChunk(chm, chunk, filename, ref p, ref end)) <= 0) + break; + + // Found result. loop around for next chunk if this is PMGI + if (chunk[3] == 0x4C) + { + break; + } + else + { + // READ_ENCINT(n) + n = 0; + do + { + if (p >= end) + { + Console.WriteLine("Read beyond end of chunk entries"); + System.Close(fh); + return Error = Error.MSPACK_ERR_DATAFORMAT; + } + + n = (uint)((n << 7) | (chunk[p] & 0x7F)); + } while ((chunk[p++] & 0x80) != 0); + } + } + } + else + { + // PMGL chunks only, search from first_pmgl to last_pmgl + for (n = chm.HeaderSection1.FirstPMGL; n <= chm.HeaderSection1.LastPMGL; n = BitConverter.ToUInt32(chunk, pmgl_NextChunk)) + { + if ((chunk = ReadChunk(chm, fh, n)) == null) + { + err = Error; + break; + } + + // Search PMGL chunk. exit if file found + if ((result = SearchChunk(chm, chunk, filename, ref p, ref end)) > 0) + break; + + // Stop simple infinite loops: can't visit the same chunk twice + if (n == BitConverter.ToUInt32(chunk, pmgl_NextChunk)) + break; + } + } + + // If we found a file, read it + if (result > 0) + { + // READ_ENCINT(sec) + sec = 0; + do + { + if (p >= end) + { + Console.WriteLine("Read beyond end of chunk entries"); + System.Close(fh); + return Error = Error.MSPACK_ERR_DATAFORMAT; + } + + sec = (uint)((sec << 7) | (chunk[p] & 0x7F)); + } while ((chunk[p++] & 0x80) != 0); + + f_ptr.Section = sec == 0 ? chm.Sec0 as Section : chm.Sec1 as Section; + + // READ_ENCINT(f_ptr.Offset) + f_ptr.Offset = 0; + do + { + if (p >= end) + { + Console.WriteLine("Read beyond end of chunk entries"); + System.Close(fh); + return Error = Error.MSPACK_ERR_DATAFORMAT; + } + + f_ptr.Offset = (uint)((f_ptr.Offset << 7) | (chunk[p] & 0x7F)); + } while ((chunk[p++] & 0x80) != 0); + + // READ_ENCINT(f_ptr.Length) + f_ptr.Length = 0; + do + { + if (p >= end) + { + Console.WriteLine("Read beyond end of chunk entries"); + System.Close(fh); + return Error = Error.MSPACK_ERR_DATAFORMAT; + } + + f_ptr.Length = (uint)((f_ptr.Length << 7) | (chunk[p] & 0x7F)); + } while ((chunk[p++] & 0x80) != 0); + } + else if (result < 0) + { + err = Error.MSPACK_ERR_DATAFORMAT; + } + + System.Close(fh); + return Error = err; + } + + #endregion + + #region Decompress State + + /// + /// Initialises the LZX decompressor to decompress the compressed stream, + /// from the nearest reset offset and length that is needed for the given + /// file. + /// + internal Error InitDecompressor(DecompressFile file) + { + int window_size, window_bits, reset_interval, entry; + byte[] data; + + MSCompressedSection sec = file.Section as MSCompressedSection; + + // Ensure we have a mscompressed content section + DecompressFile contentFile = null; + Error err = FindSysFile(sec, ref contentFile, ContentName); + if (err != Error.MSPACK_ERR_OK) + return Error = err; + + sec.Content = contentFile; + + // Ensure we have a ControlData file + DecompressFile controlFile = null; + err = FindSysFile(sec, ref controlFile, ControlName); + if (err != Error.MSPACK_ERR_OK) + return Error = err; + + sec.Control = controlFile; + + // Read ControlData + if (sec.Control.Length != lzxcd_SIZEOF) + { + Console.WriteLine("ControlData file is wrong size"); + return Error = Error.MSPACK_ERR_DATAFORMAT; + } + + if ((data = ReadSysFile(sec.Control)) == null) + { + Console.WriteLine("Can't read mscompressed control data file"); + return Error; + } + + //// Create a new control data based on that + //err = _LZXControlData.Create(data, out _LZXControlData lzxControlData); + //if (err != Error.MSPACK_ERR_OK) + // return err; + + // Check LZXC signature + if (BitConverter.ToUInt32(data, lzxcd_Signature) != 0x43585A4C) + return Error = Error.MSPACK_ERR_SIGNATURE; + + // Read reset_interval and window_size and validate version number + switch (BitConverter.ToUInt32(data, lzxcd_Version)) + { + case 1: + reset_interval = (int)BitConverter.ToUInt32(data, lzxcd_ResetInterval); + window_size = (int)BitConverter.ToUInt32(data, lzxcd_WindowSize); + break; + case 2: + reset_interval = (int)BitConverter.ToUInt32(data, lzxcd_ResetInterval) * LZX.LZX_FRAME_SIZE; + window_size = (int)BitConverter.ToUInt32(data, lzxcd_WindowSize) * LZX.LZX_FRAME_SIZE; + break; + default: + Console.WriteLine("Bad controldata version"); + return Error = Error.MSPACK_ERR_DATAFORMAT; + } + + // Find window_bits from window_size + switch (window_size) + { + case 0x008000: window_bits = 15; break; + case 0x010000: window_bits = 16; break; + case 0x020000: window_bits = 17; break; + case 0x040000: window_bits = 18; break; + case 0x080000: window_bits = 19; break; + case 0x100000: window_bits = 20; break; + case 0x200000: window_bits = 21; break; + default: + Console.WriteLine("Bad controldata window size"); + return Error = Error.MSPACK_ERR_DATAFORMAT; + } + + // Validate reset_interval + if (reset_interval == 0 || (reset_interval % LZX.LZX_FRAME_SIZE) != 0) + { + Console.WriteLine("Bad controldata reset interval"); + return Error = Error.MSPACK_ERR_DATAFORMAT; + } + + // Which reset table entry would we like? + entry = (int)(file.Offset / reset_interval); + + // Convert from reset interval multiple (usually 64k) to 32k frames + entry *= reset_interval / LZX.LZX_FRAME_SIZE; + + // Read the reset table entry + if (ReadResetTable(sec, (uint)entry, out long length, out long offset)) + { + // The uncompressed length given in the reset table is dishonest. + // The uncompressed data is always padded out from the given + // uncompressed length up to the next reset interval + length += reset_interval - 1; + length &= -reset_interval; + } + else + { + // if we can't read the reset table entry, just start from + // the beginning. Use spaninfo to get the uncompressed length + entry = 0; + offset = 0; + err = ReadSpanInfo(sec, out length); + } + + if (err != Error.MSPACK_ERR_OK) + return Error = err; + + // Get offset of compressed data stream: + // = offset of uncompressed section from start of file + // + offset of compressed stream from start of uncompressed section + // + offset of chosen reset interval from start of compressed stream + State.InOffset = file.Section.Header.Sec0.Offset + sec.Content.Offset + offset; + + // Set start offset and overall remaining stream length + State.Offset = entry * LZX.LZX_FRAME_SIZE; + length -= State.Offset; + + // Initialise LZX stream + State.State = LZX.Init(State.Sys, State.InputFileHandle, State.OutputFileHandle, window_bits, reset_interval / LZX.LZX_FRAME_SIZE, 4096, length, false); + + if (State.State == null) + Error = Error.MSPACK_ERR_NOMEMORY; + + return Error; + } + + #endregion + + #region I/O Methods + + /// + /// SysWrite is the internal writer function which the decompressor + /// uses. If either writes data to disk (self.State.OutputFileHandle) with the real + /// sys.write() function, or does nothing with the data when + /// self.State.OutputFileHandle == null. advances self.State.Offset. + /// + private static int SysWrite(object file, byte[] buffer, int offset, int bytes) + { + // Null output file means skip those bytes + if (file == null) + { + return bytes; + } + else if (file is Decompressor self) + { + self.State.Offset += (uint)bytes; + if (self.State.OutputFileHandle != null) + return self.System.Write(self.State.OutputFileHandle, buffer, offset, bytes); + + return bytes; + } + else if (file is FileStream impl) + { + return SystemImpl.DefaultSystem.Write(impl, buffer, offset, bytes); + } + + // Unknown file to write to + return 0; + } + + #endregion + + #region Helpers + + /// + /// Uses chmd_fast_find to locate a system file, and fills out that system + /// file's entry and links it into the list of system files. Returns zero + /// for success, non-zero for both failure and the file not existing. + /// + private Error FindSysFile(MSCompressedSection sec, ref DecompressFile f_ptr, string name) + { + DecompressFile result = null; + + // Already loaded + if (f_ptr != null) + return Error.MSPACK_ERR_OK; + + // Try using fast_find to find the file - return DATAFORMAT error if + // it fails, or successfully doesn't find the file + if (FastFind(sec.Header, name, result) != Error.MSPACK_ERR_OK || result.Section == null) + return Error.MSPACK_ERR_DATAFORMAT; + + f_ptr = new DecompressFile(); + + // Copy result + f_ptr = result; + f_ptr.Filename = name; + + // Link file into sysfiles list + f_ptr.Next = sec.Header.SysFiles; + sec.Header.SysFiles = f_ptr; + + return Error.MSPACK_ERR_OK; + } + + /// + /// Reads the given chunk into memory, storing it in a chunk cache + /// so it doesn't need to be read from disk more than once + /// + private byte[] ReadChunk(CHM chm, FileStream fh, uint chunkNum) + { + // Check arguments - most are already checked by chmd_fast_find + if (chunkNum >= chm.HeaderSection1.NumChunks) + return null; + + // Ensure chunk cache is available + if (chm.ChunkCache == null) + chm.ChunkCache = new byte[chm.HeaderSection1.NumChunks][]; + + // Try to answer out of chunk cache + if (chm.ChunkCache[chunkNum] != null) + return chm.ChunkCache[chunkNum]; + + // Need to read chunk - allocate memory for it + byte[] buf = new byte[chm.HeaderSection1.ChunkSize]; + + // Seek to block and read it + if (!System.Seek(fh, (chm.HeaderSectionTable.OffsetHS1 + (chunkNum * chm.HeaderSection1.ChunkSize)), SeekMode.MSPACK_SYS_SEEK_START)) + { + Error = Error.MSPACK_ERR_SEEK; + return null; + } + + if (System.Read(fh, buf, 0, (int)chm.HeaderSection1.ChunkSize) != (int)chm.HeaderSection1.ChunkSize) + { + Error = Error.MSPACK_ERR_READ; + return null; + } + + // Check the signature. Is is PMGL or PMGI? + if (!((buf[0] == 0x50) && (buf[1] == 0x4D) && (buf[2] == 0x47) && ((buf[3] == 0x4C) || (buf[3] == 0x49)))) + { + Error = Error.MSPACK_ERR_SEEK; + return null; + } + + // All OK. Store chunk in cache and return it + return chm.ChunkCache[chunkNum] = buf; + } + + /// + /// Reads the basic CHM file headers. If the "entire" parameter is + /// non-zero, all file entries will also be read. fills out a pre-existing + /// mschmd_header structure, allocates memory for files as necessary + /// + private Error ReadHeaders(FileStream fh, CHM chm, bool entire) + { + uint section, nameLen, x, errors, numChunks; + byte[] buf = new byte[0x54]; + int name, p, end; + DecompressFile fi, link = null; + long offset, length; + int numEntries; + + // Initialise pointers + chm.Files = null; + chm.SysFiles = null; + chm.ChunkCache = null; + + chm.Sec0.Header = chm; + chm.Sec0.ID = 0; + + chm.Sec1.Header = chm; + chm.Sec1.ID = 1; + chm.Sec1.Content = null; + chm.Sec1.Control = null; + chm.Sec1.SpanInfo = null; + chm.Sec1.ResetTable = null; + + #region Header + + // Read the first header + if (System.Read(fh, buf, 0, _CHMHeader.Size) != _CHMHeader.Size) + return Error.MSPACK_ERR_READ; + + // Create a new header based on that + Error err = _CHMHeader.Create(buf, out _CHMHeader chmHeader); + if (err != Error.MSPACK_ERR_OK) + return err; + + // Assign the header + chm.Header = chmHeader; + if (chm.Header.Version > 3) + System.Message(fh, "WARNING; CHM version > 3"); + + #endregion + + #region Header Section Table + + // Read the header section table + if (System.Read(fh, buf, 0, _HeaderSectionTable.V3Size) != _HeaderSectionTable.V3Size) + return Error.MSPACK_ERR_READ; + + // Create a new secton table based on that + err = _HeaderSectionTable.Create(buf, out _HeaderSectionTable sectionTable); + if (err != Error.MSPACK_ERR_OK) + return err; + + // Assign the section table + chm.HeaderSectionTable = sectionTable; + + // Assign the CS0 value to the Sec0.Offset for later + chm.Sec0.Offset = chm.HeaderSectionTable.OffsetCS0; + + #endregion + + #region Header Section 0 + + // Seek to header section 0 + if (!System.Seek(fh, chm.HeaderSectionTable.OffsetHS0, SeekMode.MSPACK_SYS_SEEK_START)) + return Error.MSPACK_ERR_SEEK; + + // Read header section 0 + if (System.Read(fh, buf, 0, _HeaderSection0.Size) != _HeaderSection0.Size) + return Error.MSPACK_ERR_READ; + + // Create a new secton 0 based on that + err = _HeaderSection0.Create(buf, out _HeaderSection0 section0); + if (err != Error.MSPACK_ERR_OK) + return err; + + // Assign the section 0 + chm.HeaderSection0 = section0; + + #endregion + + #region Header Section 1 + + // Seek to header section 1 + if (!System.Seek(fh, chm.HeaderSectionTable.OffsetHS1, SeekMode.MSPACK_SYS_SEEK_START)) + return Error.MSPACK_ERR_SEEK; + + // Read header section 1 + if (System.Read(fh, buf, 0, _HeaderSection1.Size) != _HeaderSection1.Size) + return Error.MSPACK_ERR_READ; + + // Create a new secton 1 based on that + err = _HeaderSection1.Create(buf, out _HeaderSection1 section1); + if (err != Error.MSPACK_ERR_OK) + return err; + + // Assign the section 1 + chm.HeaderSection1 = section1; + + chm.HeaderSectionTable.OffsetHS1 = System.Tell(fh); + + // Versions before 3 don't have OffsetCS0 + if (chm.Header.Version < 3) + chm.Sec0.Offset = chm.HeaderSectionTable.OffsetHS1 + (chm.HeaderSection1.ChunkSize * chm.HeaderSection1.NumChunks); + + // Check if content offset or file size is wrong + if (chm.Sec0.Offset > chm.HeaderSection0.FileLength) + { + Console.WriteLine("Content section begins after file has ended"); + return Error.MSPACK_ERR_DATAFORMAT; + } + + // Ensure there are chunks and that chunk size is + // large enough for signature and num_entries + if (chm.HeaderSection1.ChunkSize < (pmgl_PMGLEntries + 2)) + { + Console.WriteLine("Chunk size not large enough"); + return Error.MSPACK_ERR_DATAFORMAT; + } + + if (chm.HeaderSection1.NumChunks == 0) + { + Console.WriteLine("No chunks"); + return Error.MSPACK_ERR_DATAFORMAT; + } + + // The ChunkCache data structure is not great; large values for NumChunks + // or NumChunks*ChunkSize can exhaust all memory. Until a better chunk + // cache is implemented, put arbitrary limits on NumChunks and chunk size. + if (chm.HeaderSection1.NumChunks > 100000) + { + Console.WriteLine("More than 100,000 chunks"); + return Error.MSPACK_ERR_DATAFORMAT; + } + + if (chm.HeaderSection1.ChunkSize > 8192) + { + Console.WriteLine("Chunk size over 8192 (get in touch if this is valid)"); + return Error.MSPACK_ERR_DATAFORMAT; + } + + if (chm.HeaderSection1.ChunkSize * (long)chm.HeaderSection1.NumChunks > chm.HeaderSection0.FileLength) + { + Console.WriteLine("Chunks larger than entire file"); + return Error.MSPACK_ERR_DATAFORMAT; + } + + // Common sense checks on header section 1 fields + if (chm.HeaderSection1.ChunkSize != 4096) + System.Message(fh, "WARNING; chunk size is not 4096"); + + if (chm.HeaderSection1.FirstPMGL != 0) + System.Message(fh, "WARNING; first PMGL chunk is not zero"); + + if (chm.HeaderSection1.FirstPMGL > chm.HeaderSection1.LastPMGL) + { + Console.WriteLine("First pmgl chunk is after last pmgl chunk"); + return Error.MSPACK_ERR_DATAFORMAT; + } + + if (chm.HeaderSection1.IndexRoot != 0xFFFFFFFF && chm.HeaderSection1.IndexRoot >= chm.HeaderSection1.NumChunks) + { + Console.WriteLine("IndexRoot outside valid range"); + return Error.MSPACK_ERR_DATAFORMAT; + } + + #endregion + + // If we are doing a quick read, stop here! + if (!entire) + return Error.MSPACK_ERR_OK; + + // Seek to the first PMGL chunk, and reduce the number of chunks to read + if ((x = chm.HeaderSection1.FirstPMGL) != 0) + { + if (!System.Seek(fh, x * chm.HeaderSection1.ChunkSize, SeekMode.MSPACK_SYS_SEEK_CUR)) + return Error.MSPACK_ERR_SEEK; + } + + numChunks = chm.HeaderSection1.LastPMGL - x + 1; + + byte[] chunk = new byte[chm.HeaderSection1.ChunkSize]; + + // Read and process all chunks from FirstPMGL to LastPMGL + errors = 0; + while (numChunks-- != 0) + { + // Read next chunk + if (System.Read(fh, chunk, 0, (int)chm.HeaderSection1.ChunkSize) != (int)chm.HeaderSection1.ChunkSize) + return Error.MSPACK_ERR_READ; + + //// Create a new header based on that + //err = _PMGHeader.Create(buf, out _PMGHeader pmglHeader); + //if (err != Error.MSPACK_ERR_OK) + // return err; + + // Process only directory (PMGL) chunks + if (BitConverter.ToUInt32(chunk, pmgl_Signature) != 0x4C474D50) + continue; + + if (BitConverter.ToUInt32(chunk, pmgl_QuickRefSize) < 2) + System.Message(fh, "WARNING; PMGL quickref area is too small"); + + if (BitConverter.ToUInt32(chunk, pmgl_QuickRefSize) > chm.HeaderSection1.ChunkSize - pmgl_PMGLEntries) + System.Message(fh, "WARNING; PMGL quickref area is too large"); + + p = pmgl_PMGLEntries; + end = (int)(chm.HeaderSection1.ChunkSize - 2); + numEntries = BitConverter.ToUInt16(chunk, end); + + while (numEntries-- != 0) + { + // READ_ENCINT(nameLen) + nameLen = 0; + do + { + if (p >= end) + { + if (numEntries >= 0) + { + Console.WriteLine("Chunk ended before all entries could be read"); + errors++; + } + } + + nameLen = (uint)((nameLen << 7) | (chunk[p] & 0x7F)); + } while ((chunk[p++] & 0x80) != 0); + + if (nameLen > (uint)(end - p)) + { + if (numEntries >= 0) + { + Console.WriteLine("Chunk ended before all entries could be read"); + errors++; + } + } + + name = p; p += (int)nameLen; + + // READ_ENCINT(section) + section = 0; + do + { + if (p >= end) + { + if (numEntries >= 0) + { + Console.WriteLine("Chunk ended before all entries could be read"); + errors++; + } + } + + section = (uint)((section << 7) | (chunk[p] & 0x7F)); + } while ((chunk[p++] & 0x80) != 0); + + // READ_ENCINT(offset) + offset = 0; + do + { + if (p >= end) + { + if (numEntries >= 0) + { + Console.WriteLine("Chunk ended before all entries could be read"); + errors++; + } + } + + offset = (offset << 7) | (chunk[p] & 0x7F); + } while ((chunk[p++] & 0x80) != 0); + + // READ_ENCINT(length) + length = 0; + do + { + if (p >= end) + { + if (numEntries >= 0) + { + Console.WriteLine("Chunk ended before all entries could be read"); + errors++; + } + } + + length = (length << 7) | (chunk[p] & 0x7F); + } while ((chunk[p++] & 0x80) != 0); + + // Ignore blank or one-char (e.g. "/") filenames we'd return as blank + if (nameLen < 2 || chunk[name + 0] == 0x00 || chunk[name + 1] == 0x00) + continue; + + // Empty files and directory names are stored as a file entry at + // offset 0 with length 0. We want to keep empty files, but not + // directory names, which end with a "/" + if ((offset == 0) && (length == 0)) + { + if ((nameLen > 0) && (chunk[name + nameLen - 1] == '/')) + continue; + } + + if (section > 1) + { + System.Message(fh, $"Invalid section number '{section}'."); + continue; + } + + fi = new DecompressFile(); + + fi.Next = null; + fi.Filename = Encoding.UTF8.GetString(chunk, name, (int)nameLen) + "\0"; + fi.Section = (section == 0) ? chm.Sec0 as Section : chm.Sec1 as Section; + fi.Offset = offset; + fi.Length = length; + + if (chunk[name + 0] == ':' && chunk[name + 1] == ':') + { + // System file + if (nameLen == 40 && fi.Filename.Trim().Equals(ContentName)) + chm.Sec1.Content = fi; + + else if (nameLen == 44 && fi.Filename.Trim().Equals(ControlName)) + chm.Sec1.Control = fi; + + else if (nameLen == 41 && fi.Filename.Trim().Equals(SpanInfoName)) + chm.Sec1.SpanInfo = fi; + + else if (nameLen == 105 && fi.Filename.Trim().Equals(ResetTableName)) + chm.Sec1.ResetTable = fi; + + fi.Next = chm.SysFiles; + chm.SysFiles = fi; + } + else + { + // Normal file + if (link != null) + link.Next = fi; + else + chm.Files = fi; + + link = fi; + } + } + } + + return (errors > 0) ? Error.MSPACK_ERR_DATAFORMAT : Error.MSPACK_ERR_OK; + } + + /// + /// Reads one entry out of the reset table. Also reads the uncompressed + /// data length. Writes these to offset_ptr and length_ptr respectively. + /// Returns non-zero for success, zero for failure. + /// + private bool ReadResetTable(MSCompressedSection sec, uint entry, out long length_ptr, out long offset_ptr) + { + length_ptr = 0; offset_ptr = 0; + byte[] data; + + // Do we have a ResetTable file? + DecompressFile resetTable = null; + Error err = FindSysFile(sec, ref resetTable, ResetTableName); + if (err != Error.MSPACK_ERR_OK) + return false; + + sec.ResetTable = resetTable; + + // Read ResetTable file + if (sec.ResetTable.Length < lzxrt_headerSIZEOF) + { + Console.WriteLine("ResetTable file is too short"); + return false; + } + + if (sec.ResetTable.Length > 1000000) + { + // Arbitrary upper limit + Console.WriteLine($"ResetTable >1MB ({sec.ResetTable.Length}), report if genuine"); + return false; + } + + if ((data = ReadSysFile(sec.ResetTable)) == null) + { + Console.WriteLine("can't read reset table"); + return false; + } + + //// Create a new reset data based on that + //err = _LZXResetTable.Create(data, out _LZXResetTable lzxResetTable); + //if (err != Error.MSPACK_ERR_OK) + // return false; + + // Check sanity of reset table + if (BitConverter.ToUInt32(data, lzxrt_FrameLen) != LZX.LZX_FRAME_SIZE) + { + Console.WriteLine("Bad reset table frame length"); + return false; + } + + // Get the uncompressed length of the LZX stream + if ((length_ptr = BitConverter.ToInt64(data, lzxrt_UncompLen)) == 0) + return false; + + uint entrysize = BitConverter.ToUInt32(data, lzxrt_EntrySize); + uint pos = BitConverter.ToUInt32(data, lzxrt_TableOffset) + (entry * entrysize); + + // Ensure reset table entry for this offset exists + if (entry < BitConverter.ToUInt32(data, lzxrt_NumEntries) && pos <= (sec.ResetTable.Length - entrysize)) + { + switch (entrysize) + { + case 4: + offset_ptr = BitConverter.ToUInt32(data, (int)pos); + err = Error.MSPACK_ERR_OK; + break; + case 8: + offset_ptr = BitConverter.ToInt64(data, (int)pos); + break; + default: + Console.WriteLine("Reset table entry size neither 4 nor 8"); + err = Error.MSPACK_ERR_ARGS; + break; + } + } + else + { + Console.WriteLine("Bad reset interval"); + err = Error.MSPACK_ERR_ARGS; + } + + // Return success + return (err == Error.MSPACK_ERR_OK); + } + + /// + /// Reads the uncompressed data length from the spaninfo file. + /// Returns zero for success or a non-zero error code for failure. + /// + private Error ReadSpanInfo(MSCompressedSection sec, out long length_ptr) + { + length_ptr = 0; + + // Find SpanInfo file + DecompressFile spanInfo = null; + Error err = FindSysFile(sec, ref spanInfo, SpanInfoName); + if (err != Error.MSPACK_ERR_OK) + return Error.MSPACK_ERR_DATAFORMAT; + + sec.SpanInfo = spanInfo; + + // Check it's large enough + if (sec.SpanInfo.Length != 8) + { + Console.WriteLine("SpanInfo file is wrong size"); + return Error.MSPACK_ERR_DATAFORMAT; + } + + // Read the SpanInfo file + byte[] data; + if ((data = ReadSysFile(sec.SpanInfo)) == null) + { + Console.WriteLine("can't read SpanInfo file"); + return Error; + } + + // Get the uncompressed length of the LZX stream + length_ptr = BitConverter.ToInt64(data, 0); + if (length_ptr <= 0) + { + Console.WriteLine("output length is invalid"); + return Error.MSPACK_ERR_DATAFORMAT; + } + + return Error.MSPACK_ERR_OK; + } + + /// + /// Allocates memory for a section 0 (uncompressed) file and reads it into memory. + /// + private byte[] ReadSysFile(DecompressFile file) + { + if (file == null || file.Section == null || (file.Section.ID != 0)) + { + Error = Error.MSPACK_ERR_DATAFORMAT; + return null; + } + + int len = (int)file.Length; + byte[] data = new byte[len]; + + if (System.Seek(State.InputFileHandle, file.Section.Header.Sec0.Offset + file.Offset, SeekMode.MSPACK_SYS_SEEK_START)) + { + Error = Error.MSPACK_ERR_SEEK; + return null; + } + + if (System.Read(State.InputFileHandle, data, 0, len) != len) + { + Error = Error.MSPACK_ERR_READ; + return null; + } + + return data; + } + + /// + /// The real implementation of chmd_open() and chmd_fast_open(). It simply + /// passes the "entire" parameter to chmd_read_headers(), which will then + /// either read all headers, or a bare mininum. + /// + private CHM RealOpen(string filename, bool entire) + { + FileStream fh = System.Open(filename, OpenMode.MSPACK_SYS_OPEN_READ); + if (fh != null) + { + CHM chm = new CHM() { Filename = filename }; + Error error = ReadHeaders(fh, chm, entire); + if (error != Error.MSPACK_ERR_OK) + { + // If the error is DATAFORMAT, and there are some results, return + // partial results with a warning, rather than nothing + if (error == Error.MSPACK_ERR_DATAFORMAT && (chm.Files != null || chm.SysFiles != null)) + { + System.Message(fh, "WARNING; contents are corrupt"); + error = Error.MSPACK_ERR_OK; + } + else + { + Close(chm); + chm = null; + } + } + + Error = error; + System.Close(fh); + return chm; + } + else + { + Error = Error.MSPACK_ERR_OPEN; + return null; + } + } + + /// + /// searches a PMGI/PMGL chunk for a given filename entry. Returns -1 on + /// data format error, 0 if entry definitely not found, 1 if entry + /// found.In the latter case, * result and* result_end are set pointing + /// to that entry's data (either the "next chunk" ENCINT for a PMGI or + /// the section, offset and length ENCINTs for a PMGL). + /// + /// In the case of PMGL chunks, the entry has definitely been + /// found.In the case of PMGI chunks, the entry which points to the + /// chunk that may eventually contain that entry has been found. + /// + private int SearchChunk(CHM chm, byte[] chunk, string filename, ref int result, ref int resultEnd) + { + int p; + uint nameLen; + uint left, right, midpoint, entriesOff; + bool is_pmgl; + int cmp; + + // PMGL chunk or PMGI chunk? (note: read_chunk() has already + // checked the rest of the characters in the chunk signature) + if (chunk[3] == 0x4C) + { + is_pmgl = true; + entriesOff = pmgl_PMGLEntries; + } + else + { + is_pmgl = false; + entriesOff = pmgl_PMGIEntries; + } + + // Step 1: binary search first filename of each QR entry + // - target filename == entry + // found file + // - target filename < all entries + // file not found + // - target filename > all entries + // proceed to step 2 using final entry + // - target filename between two searched entries + // Proceed to step 2 + uint qrSize = BitConverter.ToUInt32(chunk, pmgl_QuickRefSize); + int start = (int)(chm.HeaderSection1.ChunkSize - 2); + int end = (int)(chm.HeaderSection1.ChunkSize - qrSize); + ushort numEntries = BitConverter.ToUInt16(chunk, start); + uint qrDensity = 1 + (uint)(1 << (int)chm.HeaderSection1.Density); + uint qrEntries = (numEntries + qrDensity - 1) / qrDensity; + + if (numEntries == 0) + { + Console.Write("chunk has no entries"); + return -1; + } + + if (qrSize > chm.HeaderSection1.ChunkSize) + { + Console.Write("quickref size > chunk size"); + return -1; + } + + resultEnd = end; + + if (((int)qrEntries * 2) > (start - end)) + { + Console.Write("WARNING; more quickrefs than quickref space"); + qrEntries = 0; // But we can live with it + } + + if (qrEntries > 0) + { + left = 0; + right = qrEntries - 1; + do + { + // Pick new midpoint + midpoint = (left + right) >> 1; + + // Compare filename with entry QR points to + p = (int)(entriesOff + (midpoint != 0 ? BitConverter.ToUInt16(chunk, (int)(start - (midpoint << 1))) : 0)); + + // READ_ENCINT(nameLen) + nameLen = 0; + do + { + if (p >= end) + { + Console.WriteLine("reached end of chunk data while searching"); + return -1; + } + + nameLen = (uint)((nameLen << 7) | (chunk[p] & 0x7F)); + } while ((chunk[p++] & 0x80) != 0); + + if (nameLen > (uint)(end - p)) + { + Console.WriteLine("reached end of chunk data while searching"); + return -1; + } + + cmp = string.Compare(filename, Encoding.ASCII.GetString(chunk, p, (int)nameLen), StringComparison.OrdinalIgnoreCase); + + if (cmp == 0) + { + break; + } + else if (cmp < 0) + { + if (midpoint != 0) + right = midpoint - 1; + else + return 0; + } + else if (cmp > 0) + { + left = midpoint + 1; + } + } while (left <= right); + + midpoint = (left + right) >> 1; + + if (cmp == 0) + { + // Exact match! + p += (int)nameLen; + result = p; + return 1; + } + + // Otherwise, read the group of entries for QR entry M + p = (int)(entriesOff + (midpoint != 0 ? BitConverter.ToUInt16(chunk, (int)(start - (midpoint << 1))) : 0)); + numEntries -= (ushort)(midpoint * qrDensity); + if (numEntries > qrDensity) + numEntries = (ushort)qrDensity; + } + else + { + p = (int)entriesOff; + } + + // Step 2: linear search through the set of entries reached in step 1. + // - filename == any entry + // found entry + // - filename < all entries (PMGI) or any entry (PMGL) + // entry not found, stop now + // - filename > all entries + // entry not found (PMGL) / maybe found (PMGI) + result = -1; + while (numEntries-- > 0) + { + // READ_ENCINT(nameLen) + nameLen = 0; + do + { + if (p >= end) + { + Console.WriteLine("reached end of chunk data while searching"); + return -1; + } + + nameLen = (uint)((nameLen << 7) | (chunk[p] & 0x7F)); + } while ((chunk[p++] & 0x80) != 0); + + if (nameLen > (uint)(end - p)) + { + Console.WriteLine("reached end of chunk data while searching"); + return -1; + } + + cmp = string.Compare(filename, Encoding.ASCII.GetString(chunk, p, (int)nameLen), StringComparison.OrdinalIgnoreCase); + p += (int)nameLen; + + if (cmp == 0) + { + // Entry found + result = p; + return 1; + } + + if (cmp < 0) + { + // Entry not found (PMGL) / maybe found (PMGI) + break; + } + + // Read and ignore the rest of this entry + if (is_pmgl) + { + // Skip section, offset, and length + for (int i = 0; i < 3; i++) + { + // READ_ENCINT(R) + right = 0; + do + { + if (p >= end) + { + Console.WriteLine("reached end of chunk data while searching"); + return -1; + } + + right = (uint)((right << 7) | (chunk[p] & 0x7F)); + } while ((chunk[p++] & 0x80) != 0); + } + } + else + { + result = p; // Store potential final result + + // Skip chunk number + // READ_ENCINT(R) + right = 0; + do + { + if (p >= end) + { + Console.WriteLine("reached end of chunk data while searching"); + return -1; + } + + right = (uint)((right << 7) | (chunk[p] & 0x7F)); + } while ((chunk[p++] & 0x80) != 0); + } + } + + // PMGL? not found. PMGI? maybe found + return (is_pmgl) ? 0 : (result != 0 ? 1 : 0); + } + + #endregion } } diff --git a/BurnOutSharp/External/libmspack/CHM/DecompressorImpl.cs b/BurnOutSharp/External/libmspack/CHM/DecompressorImpl.cs deleted file mode 100644 index f9805ee9..00000000 --- a/BurnOutSharp/External/libmspack/CHM/DecompressorImpl.cs +++ /dev/null @@ -1,20 +0,0 @@ -/* This file is part of libmspack. - * (C) 2003-2004 Stuart Caie. - * - * libmspack is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License (LGPL) version 2.1 - * - * For further details, see the file COPYING.LIB distributed with libmspack - */ - -namespace LibMSPackSharp.CHM -{ - public class DecompressorImpl : Decompressor - { - public SystemImpl System { get; set; } - - public DecompressState State { get; set; } - - public Error Error { get; set; } - } -} diff --git a/BurnOutSharp/External/libmspack/CHM/Header.cs b/BurnOutSharp/External/libmspack/CHM/Header.cs deleted file mode 100644 index ab8ac3b8..00000000 --- a/BurnOutSharp/External/libmspack/CHM/Header.cs +++ /dev/null @@ -1,135 +0,0 @@ -/* libmspack -- a library for working with Microsoft compression formats. - * (C) 2003-2019 Stuart Caie - * - * libmspack is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License (LGPL) version 2.1 - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - */ - -namespace LibMSPackSharp.CHM -{ - /// - /// A structure which represents a CHM helpfile. - /// - /// All fields are READ ONLY. - /// - public class Header - { - /// - /// The version of the CHM file format used in this file. - /// - public uint Version { get; set; } - - /// - /// The "timestamp" of the CHM helpfile. - /// - /// It is the lower 32 bits of a 64-bit value representing the number of - /// centiseconds since 1601-01-01 00:00:00 UTC, plus 42. It is not useful - /// as a timestamp, but it is useful as a semi-unique ID. - /// - public uint Timestamp { get; set; } - - /// - /// The default Language and Country ID (LCID) of the user who ran the - /// HTMLHelp Compiler. This is not the language of the CHM file itself. - /// - public uint Language { get; set; } - - /// - /// The filename of the CHM helpfile. This is given by the library user - /// and may be in any format. - /// - public string Filename { get; set; } - - /// - /// The length of the CHM helpfile, in bytes. - /// - public long Length { get; set; } - - /// - /// A list of all non-system files in the CHM helpfile. - /// - public DecompressFile Files { get; set; } - - /// - /// A list of all system files in the CHM helpfile. - /// - /// System files are files which begin with "::". They are meta-files - /// generated by the CHM creation process. - /// - public DecompressFile SysFiles { get; set; } - - /// - /// The section 0 (uncompressed) data in this CHM helpfile. - /// - public UncompressedSection Sec0 { get; set; } - - /// - /// The section 1 (MSCompressed) data in this CHM helpfile. - /// - public MSCompressedSection Sec1 { get; set; } - - /// - /// The file offset of the first PMGL/PMGI directory chunk. - /// - public long DirOffset { get; set; } - - /// - /// The number of PMGL/PMGI directory chunks in this CHM helpfile. - /// - public uint NumChunks { get; set; } - - /// - /// The size of each PMGL/PMGI chunk, in bytes. - /// - public uint ChunkSize { get; set; } - - /// - /// The "density" of the quick-reference section in PMGL/PMGI chunks. - /// - public uint Density { get; set; } - - /// - /// The depth of the index tree. - /// - /// - if 1, there are no PMGI chunks, only PMGL chunks. - /// - if 2, there is 1 PMGI chunk. All chunk indices point to PMGL chunks. - /// - if 3, the root PMGI chunk points to secondary PMGI chunks, which in turn point to PMGL chunks. - /// - and so on... - /// - public uint Depth { get; set; } - - /// - /// The number of the root PMGI chunk. - /// - /// If there is no index in the CHM helpfile, this will be 0xFFFFFFFF. - /// - public uint IndexRoot { get; set; } - - /// - /// The number of the first PMGL chunk. Usually zero. - /// Available only in CHM decoder version 2 and above. - /// - public uint FirstPMGL { get; set; } - - /// - /// The number of the last PMGL chunk. Usually num_chunks-1. - /// Available only in CHM decoder version 2 and above. - /// - public uint LastPMGL { get; set; } - - /// - /// A cache of loaded chunks, filled in by mschm_decoder::fast_find(). - /// Available only in CHM decoder version 2 and above. - /// - public byte[][] ChunkCache { get; set; } - } -} diff --git a/BurnOutSharp/External/libmspack/CHM/Implementation.cs b/BurnOutSharp/External/libmspack/CHM/Implementation.cs deleted file mode 100644 index 4ac9a862..00000000 --- a/BurnOutSharp/External/libmspack/CHM/Implementation.cs +++ /dev/null @@ -1,1494 +0,0 @@ -/* This file is part of libmspack. - * (C) 2003-2004 Stuart Caie. - * - * libmspack is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License (LGPL) version 2.1 - * - * For further details, see the file COPYING.LIB distributed with libmspack - */ - -using System; -using System.IO; -using System.Linq; -using System.Text; -using LibMSPackSharp.Compression; - -namespace LibMSPackSharp.CHM -{ - public class Implementation - { - #region Generic CHM Definitions - - #region Structure Offsets - - private const int chmhead_Signature = 0x0000; - private const int chmhead_Version = 0x0004; - private const int chmhead_HeaderLen = 0x0008; - private const int chmhead_Unknown1 = 0x000C; - private const int chmhead_Timestamp = 0x0010; - private const int chmhead_LanguageID = 0x0014; - private const int chmhead_GUID1 = 0x0018; - private const int chmhead_GUID2 = 0x0028; - private const int chmhead_SIZEOF = 0x0038; - - private const int chmhst_OffsetHS0 = 0x0000; - private const int chmhst_LengthHS0 = 0x0008; - private const int chmhst_OffsetHS1 = 0x0010; - private const int chmhst_LengthHS1 = 0x0018; - private const int chmhst_SIZEOF = 0x0020; - private const int chmhst3_OffsetCS0 = 0x0020; - private const int chmhst3_SIZEOF = 0x0028; - - private const int chmhs0_Unknown1 = 0x0000; - private const int chmhs0_Unknown2 = 0x0004; - private const int chmhs0_FileLen = 0x0008; - private const int chmhs0_Unknown3 = 0x0010; - private const int chmhs0_Unknown4 = 0x0014; - private const int chmhs0_SIZEOF = 0x0018; - - private const int chmhs1_Signature = 0x0000; - private const int chmhs1_Version = 0x0004; - private const int chmhs1_HeaderLen = 0x0008; - private const int chmhs1_Unknown1 = 0x000C; - private const int chmhs1_ChunkSize = 0x0010; - private const int chmhs1_Density = 0x0014; - private const int chmhs1_Depth = 0x0018; - private const int chmhs1_IndexRoot = 0x001C; - private const int chmhs1_FirstPMGL = 0x0020; - private const int chmhs1_LastPMGL = 0x0024; - private const int chmhs1_Unknown2 = 0x0028; - private const int chmhs1_NumChunks = 0x002C; - private const int chmhs1_LanguageID = 0x0030; - private const int chmhs1_GUID = 0x0034; - private const int chmhs1_Unknown3 = 0x0044; - private const int chmhs1_Unknown4 = 0x0048; - private const int chmhs1_Unknown5 = 0x004C; - private const int chmhs1_Unknown6 = 0x0050; - private const int chmhs1_SIZEOF = 0x0054; - - private const int pmgl_Signature = 0x0000; - private const int pmgl_QuickRefSize = 0x0004; - private const int pmgl_Unknown1 = 0x0008; - private const int pmgl_PrevChunk = 0x000C; - private const int pmgl_NextChunk = 0x0010; - private const int pmgl_Entries = 0x0014; - private const int pmgl_headerSIZEOF = 0x0014; - - private const int pmgi_Signature = 0x0000; - private const int pmgi_QuickRefSize = 0x0004; - private const int pmgi_Entries = 0x0008; - private const int pmgi_headerSIZEOF = 0x000C; - - private const int lzxcd_Length = 0x0000; - private const int lzxcd_Signature = 0x0004; - private const int lzxcd_Version = 0x0008; - private const int lzxcd_ResetInterval = 0x000C; - private const int lzxcd_WindowSize = 0x0010; - private const int lzxcd_CacheSize = 0x0014; - private const int lzxcd_Unknown1 = 0x0018; - private const int lzxcd_SIZEOF = 0x001C; - - private const int lzxrt_Unknown1 = 0x0000; - private const int lzxrt_NumEntries = 0x0004; - private const int lzxrt_EntrySize = 0x0008; - private const int lzxrt_TableOffset = 0x000C; - private const int lzxrt_UncompLen = 0x0010; - private const int lzxrt_CompLen = 0x0018; - private const int lzxrt_FrameLen = 0x0020; - private const int lzxrt_Entries = 0x0028; - private const int lzxrt_headerSIZEOF = 0x0028; - - #endregion - - // filenames of the system files used for decompression. - // Content and ControlData are essential. - // ResetTable is preferred, but SpanInfo can be used if not available - public const string ContentName = "::DataSpace/Storage/MSCompressed/Content"; - public const string ControlName = "::DataSpace/Storage/MSCompressed/ControlData"; - public const string SpanInfoName = "::DataSpace/Storage/MSCompressed/SpanInfo"; - public const string ResetTableName = "::DataSpace/Storage/MSCompressed/Transform/{7FC28940-9D31-11D0-9B27-00A0C91E9C7C}/InstanceData/ResetTable"; - - #endregion - - #region CHMD_OPEN - - /// - /// Opens a file and tries to read it as a CHM file. - /// Calls RealOpen() with entire=1. - /// - public static Header Open(Decompressor decompressor, string filename) - { - return RealOpen(decompressor, filename, true); - } - - #endregion - - #region CHMD_FAST_OPEN - - /// - /// Opens a file and tries to read it as a CHM file, but does not read - /// the file headers. Calls chmd_real_open() with entire=0 - /// - public static Header FastOpen(Decompressor decompressor, string filename) - { - return RealOpen(decompressor, filename, false); - } - - #endregion - - #region CHMD_REAL_OPEN - - /// - /// The real implementation of chmd_open() and chmd_fast_open(). It simply - /// passes the "entire" parameter to chmd_read_headers(), which will then - /// either read all headers, or a bare mininum. - /// - private static Header RealOpen(Decompressor d, string filename, bool entire) - { - DecompressorImpl self = d as DecompressorImpl; - Header chm = null; - - if (d == null) - return null; - - SystemImpl sys = self.System; - - FileStream fh; - if ((fh = sys.Open(filename, OpenMode.MSPACK_SYS_OPEN_READ)) != null) - { - chm = new Header(); - chm.Filename = filename; - Error error = ReadHeaders(sys, fh, chm, entire); - if (error != Error.MSPACK_ERR_OK) - { - // If the error is DATAFORMAT, and there are some results, return - // partial results with a warning, rather than nothing - if (error == Error.MSPACK_ERR_DATAFORMAT && (chm.Files != null || chm.SysFiles != null)) - { - sys.Message(fh, "WARNING; contents are corrupt"); - error = Error.MSPACK_ERR_OK; - } - else - { - Close(d, chm); - chm = null; - } - } - - self.Error = error; - sys.Close(fh); - } - else - { - self.Error = Error.MSPACK_ERR_OPEN; - } - - return chm; - } - - #endregion - - #region CHMD_CLOSE - - /// - /// Frees all memory associated with a given mschmd_header - /// - public static void Close(Decompressor d, Header chm) - { - DecompressorImpl self = d as DecompressorImpl; - DecompressFile fi, nfi; - uint i; - - if (d == null) - return; - - SystemImpl sys = self.System; - - self.Error = Error.MSPACK_ERR_OK; - - // Free files - for (fi = chm.Files; fi != null; fi = nfi) - { - nfi = fi.Next; - } - - for (fi = chm.SysFiles; fi != null; fi = nfi) - { - nfi = fi.Next; - } - - // If this CHM was being decompressed, free decompression state - if (self.State != null && (self.State.Header == chm)) - { - sys.Close(self.State.InputFileHandle); - sys.Close(self.State.OutputFileHandle); - - self.State = null; - } - } - - #endregion - - #region CHMD_READ_HEADERS - - /// - /// The GUIDs found in CHM headers - /// - private static readonly byte[] guids = - { - /* {7C01FD10-7BAA-11D0-9E0C-00A0-C922-E6EC} */ - 0x10, 0xFD, 0x01, 0x7C, 0xAA, 0x7B, 0xD0, 0x11, - 0x9E, 0x0C, 0x00, 0xA0, 0xC9, 0x22, 0xE6, 0xEC, - - /* {7C01FD11-7BAA-11D0-9E0C-00A0-C922-E6EC} */ - 0x11, 0xFD, 0x01, 0x7C, 0xAA, 0x7B, 0xD0, 0x11, - 0x9E, 0x0C, 0x00, 0xA0, 0xC9, 0x22, 0xE6, 0xEC - }; - - /// - /// Reads the basic CHM file headers. If the "entire" parameter is - /// non-zero, all file entries will also be read. fills out a pre-existing - /// mschmd_header structure, allocates memory for files as necessary - /// - public static Error ReadHeaders(SystemImpl sys, FileStream fh, Header chm, bool entire) - { - uint section, nameLen, x, errors, numChunks; - byte[] buf = new byte[0x54]; - int name, p, end; - DecompressFile fi, link = null; - long offset, length; - int numEntries; - - // Initialise pointers - chm.Files = null; - chm.SysFiles = null; - chm.ChunkCache = null; - - chm.Sec0.Header = chm; - chm.Sec0.ID = 0; - - chm.Sec1.Header = chm; - chm.Sec1.ID = 1; - chm.Sec1.Content = null; - chm.Sec1.Control = null; - chm.Sec1.SpanInfo = null; - chm.Sec1.ResetTable = null; - - // Read the first header - if (sys.Read(fh, buf, 0, chmhead_SIZEOF) != chmhead_SIZEOF) - return Error.MSPACK_ERR_READ; - - // Check ITSF signature - if (BitConverter.ToUInt32(buf, chmhead_Signature) != 0x46535449) - return Error.MSPACK_ERR_SIGNATURE; - - // Check both header GUIDs - if (!buf.Skip(chmhead_GUID1).Take(32).SequenceEqual(guids)) - { - Console.WriteLine("incorrect GUIDs"); - return Error.MSPACK_ERR_SIGNATURE; - } - - chm.Version = BitConverter.ToUInt32(buf, chmhead_Version); - chm.Timestamp = BitConverter.ToUInt32(buf, chmhead_Timestamp); - chm.Language = BitConverter.ToUInt32(buf, chmhead_LanguageID); - if (chm.Version > 3) - sys.Message(fh, "WARNING; CHM version > 3"); - - // Read the header section table - if (sys.Read(fh, buf, 0, chmhst3_SIZEOF) != chmhst3_SIZEOF) - return Error.MSPACK_ERR_READ; - - // chmhst3_OffsetCS0 does not exist in version 1 or 2 CHM files. - // The offset will be corrected later, once HS1 is read. - if ((offset = BitConverter.ToInt64(buf, chmhst_OffsetHS0)) != 0 - || (chm.DirOffset = BitConverter.ToInt64(buf, chmhst_OffsetHS1)) != 0 - || (chm.Sec0.Offset = BitConverter.ToInt64(buf, chmhst3_OffsetCS0)) != 0) - { - return Error.MSPACK_ERR_DATAFORMAT; - } - - // Seek to header section 0 - if (!sys.Seek(fh, offset, SeekMode.MSPACK_SYS_SEEK_START)) - return Error.MSPACK_ERR_SEEK; - - // Read header section 0 - if (sys.Read(fh, buf, 0, chmhs0_SIZEOF) != chmhs0_SIZEOF) - return Error.MSPACK_ERR_READ; - - if ((chm.Length = BitConverter.ToInt64(buf, chmhs0_FileLen)) != 0) - return Error.MSPACK_ERR_DATAFORMAT; - - // Seek to header section 1 - if (!sys.Seek(fh, chm.DirOffset, SeekMode.MSPACK_SYS_SEEK_START)) - return Error.MSPACK_ERR_SEEK; - - // Read header section 1 - if (sys.Read(fh, buf, 0, chmhs1_SIZEOF) != chmhs1_SIZEOF) - return Error.MSPACK_ERR_READ; - - chm.DirOffset = sys.Tell(fh); - chm.ChunkSize = BitConverter.ToUInt32(buf, chmhs1_ChunkSize); - chm.Density = BitConverter.ToUInt32(buf, chmhs1_Density); - chm.Depth = BitConverter.ToUInt32(buf, chmhs1_Depth); - chm.IndexRoot = BitConverter.ToUInt32(buf, chmhs1_IndexRoot); - chm.NumChunks = BitConverter.ToUInt32(buf, chmhs1_NumChunks); - chm.FirstPMGL = BitConverter.ToUInt32(buf, chmhs1_FirstPMGL); - chm.LastPMGL = BitConverter.ToUInt32(buf, chmhs1_LastPMGL); - - if (chm.Version < 3) - { - // Versions before 3 don't have chmhst3_OffsetCS0 - chm.Sec0.Offset = chm.DirOffset + (chm.ChunkSize * chm.NumChunks); - } - - // Check if content offset or file size is wrong - if (chm.Sec0.Offset > chm.Length) - { - Console.WriteLine("content section begins after file has ended"); - return Error.MSPACK_ERR_DATAFORMAT; - } - - // Ensure there are chunks and that chunk size is - // large enough for signature and num_entries - if (chm.ChunkSize < (pmgl_Entries + 2)) - { - Console.WriteLine("chunk size not large enough"); - return Error.MSPACK_ERR_DATAFORMAT; - } - - if (chm.NumChunks == 0) - { - Console.WriteLine("no chunks"); - return Error.MSPACK_ERR_DATAFORMAT; - } - - // The ChunkCache data structure is not great; large values for NumChunks - // or NumChunks*ChunkSize can exhaust all memory. Until a better chunk - // cache is implemented, put arbitrary limits on NumChunks and chunk size. - if (chm.NumChunks > 100000) - { - Console.WriteLine("more than 100,000 chunks"); - return Error.MSPACK_ERR_DATAFORMAT; - } - if (chm.ChunkSize > 8192) - { - Console.WriteLine("chunk size over 8192 (get in touch if this is valid)"); - return Error.MSPACK_ERR_DATAFORMAT; - } - if (chm.ChunkSize * (long)chm.NumChunks > chm.Length) - { - Console.WriteLine("chunks larger than entire file"); - return Error.MSPACK_ERR_DATAFORMAT; - } - - // Common sense checks on header section 1 fields - if (chm.ChunkSize != 4096) - sys.Message(fh, "WARNING; chunk size is not 4096"); - - if (chm.FirstPMGL != 0) - sys.Message(fh, "WARNING; first PMGL chunk is not zero"); - - if (chm.FirstPMGL > chm.LastPMGL) - { - Console.WriteLine("first pmgl chunk is after last pmgl chunk"); - return Error.MSPACK_ERR_DATAFORMAT; - } - - if (chm.IndexRoot != 0xFFFFFFFF && chm.IndexRoot >= chm.NumChunks) - { - Console.WriteLine("IndexRoot outside valid range"); - return Error.MSPACK_ERR_DATAFORMAT; - } - - // If we are doing a quick read, stop here! - if (!entire) - return Error.MSPACK_ERR_OK; - - // Seek to the first PMGL chunk, and reduce the number of chunks to read - if ((x = chm.FirstPMGL) != 0) - { - if (!sys.Seek(fh, x * chm.ChunkSize, SeekMode.MSPACK_SYS_SEEK_CUR)) - return Error.MSPACK_ERR_SEEK; - } - - numChunks = chm.LastPMGL - x + 1; - - byte[] chunk = new byte[chm.ChunkSize]; - - // Read and process all chunks from FirstPMGL to LastPMGL - errors = 0; - while (numChunks-- != 0) - { - // Read next chunk - if (sys.Read(fh, chunk, 0, (int)chm.ChunkSize) != (int)chm.ChunkSize) - return Error.MSPACK_ERR_READ; - - // Process only directory (PMGL) chunks - if (BitConverter.ToUInt32(chunk, pmgl_Signature) != 0x4C474D50) - continue; - - if (BitConverter.ToUInt32(chunk, pmgl_QuickRefSize) < 2) - sys.Message(fh, "WARNING; PMGL quickref area is too small"); - - if (BitConverter.ToUInt32(chunk, pmgl_QuickRefSize) > chm.ChunkSize - pmgl_Entries) - sys.Message(fh, "WARNING; PMGL quickref area is too large"); - - p = pmgl_Entries; - end = (int)(chm.ChunkSize - 2); - numEntries = BitConverter.ToUInt16(chunk, end); - - while (numEntries-- != 0) - { - // READ_ENCINT(nameLen) - nameLen = 0; - do - { - if (p >= end) - goto chunk_end; - - nameLen = (uint)((nameLen << 7) | (chunk[p] & 0x7F)); - } while ((chunk[p++] & 0x80) != 0); - - if (nameLen > (uint)(end - p)) - goto chunk_end; - - name = p; p += (int)nameLen; - - // READ_ENCINT(section) - section = 0; - do - { - if (p >= end) - goto chunk_end; - - section = (uint)((section << 7) | (chunk[p] & 0x7F)); - } while ((chunk[p++] & 0x80) != 0); - - // READ_ENCINT(offset) - offset = 0; - do - { - if (p >= end) - goto chunk_end; - - offset = (offset << 7) | (chunk[p] & 0x7F); - } while ((chunk[p++] & 0x80) != 0); - - // READ_ENCINT(length) - length = 0; - do - { - if (p >= end) - goto chunk_end; - - length = (length << 7) | (chunk[p] & 0x7F); - } while ((chunk[p++] & 0x80) != 0); - - // Ignore blank or one-char (e.g. "/") filenames we'd return as blank - if (nameLen < 2 || chunk[name + 0] == 0x00 || chunk[name + 1] == 0x00) - continue; - - // Empty files and directory names are stored as a file entry at - // offset 0 with length 0. We want to keep empty files, but not - // directory names, which end with a "/" - if ((offset == 0) && (length == 0)) - { - if ((nameLen > 0) && (chunk[name + nameLen - 1] == '/')) - continue; - } - - if (section > 1) - { - sys.Message(fh, $"invalid section number '{section}'."); - continue; - } - - fi = new DecompressFile(); - - fi.Next = null; - fi.Filename = Encoding.UTF8.GetString(chunk, name, (int)nameLen) + "\0"; - fi.Section = (section == 0) ? chm.Sec0 as Section : chm.Sec1 as Section; - fi.Offset = offset; - fi.Length = length; - - if (chunk[name + 0] == ':' && chunk[name + 1] == ':') - { - // System file - if (nameLen == 40 && fi.Filename.Trim().Equals(ContentName)) - chm.Sec1.Content = fi; - - else if (nameLen == 44 && fi.Filename.Trim().Equals(ControlName)) - chm.Sec1.Control = fi; - - else if (nameLen == 41 && fi.Filename.Trim().Equals(SpanInfoName)) - chm.Sec1.SpanInfo = fi; - - else if (nameLen == 105 && fi.Filename.Trim().Equals(ResetTableName)) - chm.Sec1.ResetTable = fi; - - fi.Next = chm.SysFiles; - chm.SysFiles = fi; - } - else - { - // Normal file - if (link != null) - link.Next = fi; - else - chm.Files = fi; - - link = fi; - } - } - - // This is reached either when num_entries runs out, or if - // reading data from the chunk reached a premature end of chunk - chunk_end: - if (numEntries >= 0) - { - Console.WriteLine("chunk ended before all entries could be read"); - errors++; - } - } - - return (errors > 0) ? Error.MSPACK_ERR_DATAFORMAT : Error.MSPACK_ERR_OK; - } - - #endregion - - #region CHMD_FAST_FIND - - /// - /// uses PMGI index chunks and quickref data to quickly locate a file - /// directly from the on-disk index. - /// - /// TODO: protect against infinite loops in chunks (where pgml_NextChunk - /// or a PMGI index entry point to an already visited chunk) - /// - public static Error FastFind(Decompressor d, Header chm, string filename, DecompressFile f_ptr) - { - DecompressorImpl self = d as DecompressorImpl; - SystemImpl sys; - FileStream fh; - - // p and end are initialised to prevent MSVC warning about "potentially" - // uninitialised usage. This is provably untrue, but MS won't fix: - // https://developercommunity.visualstudio.com/content/problem/363489/c4701-false-positive-warning.html - byte[] chunk = new byte[0]; - int p = -1, end = -1, result = -1; - Error err = Error.MSPACK_ERR_OK; - uint n, sec; - - if (self == null || chm == null || f_ptr == null) - return Error.MSPACK_ERR_ARGS; - - sys = self.System; - - // Clear the results structure - f_ptr = new DecompressFile(); - - if ((fh = sys.Open(chm.Filename, OpenMode.MSPACK_SYS_OPEN_READ)) == null) - return Error.MSPACK_ERR_OPEN; - - // Go through PMGI chunk hierarchy to reach PMGL chunk - if (chm.IndexRoot < chm.NumChunks) - { - n = chm.IndexRoot; - for (; ; ) - { - if ((chunk = ReadChunk(self, chm, fh, n)) == null) - { - sys.Close(fh); - return self.Error; - } - - // Search PMGI/PMGL chunk. exit early if no entry found - if ((result = SearchChunk(chm, chunk, filename, ref p, ref end)) <= 0) - break; - - /* found result. loop around for next chunk if this is PMGI */ - if (chunk[3] == 0x4C) - { - break; - } - else - { - // READ_ENCINT(n) - n = 0; - do - { - if (p >= end) - goto chunk_end; - - n = (uint)((n << 7) | (chunk[p] & 0x7F)); - } while ((chunk[p++] & 0x80) != 0); - } - } - } - else - { - // PMGL chunks only, search from first_pmgl to last_pmgl - for (n = chm.FirstPMGL; n <= chm.LastPMGL; n = BitConverter.ToUInt32(chunk, pmgl_NextChunk)) - { - if ((chunk = ReadChunk(self, chm, fh, n)) == null) - { - err = self.Error; - break; - } - - // Search PMGL chunk. exit if file found - if ((result = SearchChunk(chm, chunk, filename, ref p, ref end)) > 0) - break; - - // Stop simple infinite loops: can't visit the same chunk twice - if (n == BitConverter.ToUInt32(chunk, pmgl_NextChunk)) - break; - } - } - - // If we found a file, read it - if (result > 0) - { - // READ_ENCINT(sec) - sec = 0; - do - { - if (p >= end) - goto chunk_end; - - sec = (uint)((sec << 7) | (chunk[p] & 0x7F)); - } while ((chunk[p++] & 0x80) != 0); - - f_ptr.Section = sec == 0 ? chm.Sec0 as Section : chm.Sec1 as Section; - - // READ_ENCINT(f_ptr.Offset) - f_ptr.Offset = 0; - do - { - if (p >= end) - goto chunk_end; - - f_ptr.Offset = (uint)((f_ptr.Offset << 7) | (chunk[p] & 0x7F)); - } while ((chunk[p++] & 0x80) != 0); - - // READ_ENCINT(f_ptr.Length) - f_ptr.Length = 0; - do - { - if (p >= end) - goto chunk_end; - - f_ptr.Length = (uint)((f_ptr.Length << 7) | (chunk[p] & 0x7F)); - } while ((chunk[p++] & 0x80) != 0); - } - else if (result < 0) - { - err = Error.MSPACK_ERR_DATAFORMAT; - } - - sys.Close(fh); - return self.Error = err; - - chunk_end: - Console.WriteLine("read beyond end of chunk entries"); - sys.Close(fh); - return self.Error = Error.MSPACK_ERR_DATAFORMAT; - } - - /// - /// Reads the given chunk into memory, storing it in a chunk cache - /// so it doesn't need to be read from disk more than once - /// - public static byte[] ReadChunk(DecompressorImpl self, Header chm, FileStream fh, uint chunkNum) - { - SystemImpl sys = self.System; - - // Check arguments - most are already checked by chmd_fast_find - if (chunkNum >= chm.NumChunks) - return null; - - // ensure chunk cache is available - if (chm.ChunkCache == null) - chm.ChunkCache = new byte[chm.NumChunks][]; - - // try to answer out of chunk cache */ - if (chm.ChunkCache[chunkNum] != null) - return chm.ChunkCache[chunkNum]; - - // Need to read chunk - allocate memory for it - byte[] buf = new byte[chm.ChunkSize]; - - // Seek to block and read it - if (!sys.Seek(fh, (chm.DirOffset + (chunkNum * chm.ChunkSize)), SeekMode.MSPACK_SYS_SEEK_START)) - { - self.Error = Error.MSPACK_ERR_SEEK; - return null; - } - - if (sys.Read(fh, buf, 0, (int)chm.ChunkSize) != (int)chm.ChunkSize) - { - self.Error = Error.MSPACK_ERR_READ; - return null; - } - - // Check the signature. Is is PMGL or PMGI? - if (!((buf[0] == 0x50) && (buf[1] == 0x4D) && (buf[2] == 0x47) && ((buf[3] == 0x4C) || (buf[3] == 0x49)))) - { - self.Error = Error.MSPACK_ERR_SEEK; - return null; - } - - // All OK. Store chunk in cache and return it - return chm.ChunkCache[chunkNum] = buf; - } - - /// - /// searches a PMGI/PMGL chunk for a given filename entry. Returns -1 on - /// data format error, 0 if entry definitely not found, 1 if entry - /// found.In the latter case, * result and* result_end are set pointing - /// to that entry's data (either the "next chunk" ENCINT for a PMGI or - /// the section, offset and length ENCINTs for a PMGL). - /// - /// In the case of PMGL chunks, the entry has definitely been - /// found.In the case of PMGI chunks, the entry which points to the - /// chunk that may eventually contain that entry has been found. - /// - public static int SearchChunk(Header chm, byte[] chunk, string filename, ref int result, ref int resultEnd) - { - int p; - uint nameLen; - uint left, right, midpoint, entriesOff; - bool is_pmgl; - int cmp; - - // PMGL chunk or PMGI chunk? (note: read_chunk() has already - // checked the rest of the characters in the chunk signature) - if (chunk[3] == 0x4C) - { - is_pmgl = true; - entriesOff = pmgl_Entries; - } - else - { - is_pmgl = false; - entriesOff = pmgi_Entries; - } - - // Step 1: binary search first filename of each QR entry - // - target filename == entry - // found file - // - target filename < all entries - // file not found - // - target filename > all entries - // proceed to step 2 using final entry - // - target filename between two searched entries - // Proceed to step 2 - uint qrSize = BitConverter.ToUInt32(chunk, pmgl_QuickRefSize); - int start = (int)(chm.ChunkSize - 2); - int end = (int)(chm.ChunkSize - qrSize); - ushort numEntries = BitConverter.ToUInt16(chunk, start); - uint qrDensity = 1 + (uint)(1 << (int)chm.Density); - uint qrEntries = (numEntries + qrDensity - 1) / qrDensity; - - if (numEntries == 0) - { - Console.Write("chunk has no entries"); - return -1; - } - - if (qrSize > chm.ChunkSize) - { - Console.Write("quickref size > chunk size"); - return -1; - } - - resultEnd = end; - - if (((int)qrEntries * 2) > (start - end)) - { - Console.Write("WARNING; more quickrefs than quickref space"); - qrEntries = 0; // But we can live with it - } - - if (qrEntries > 0) - { - left = 0; - right = qrEntries - 1; - do - { - // Pick new midpoint - midpoint = (left + right) >> 1; - - // Compare filename with entry QR points to - p = (int)(entriesOff + (midpoint != 0 ? BitConverter.ToUInt16(chunk, (int)(start - (midpoint << 1))) : 0)); - - // READ_ENCINT(nameLen) - nameLen = 0; - do - { - if (p >= end) - goto chunk_end; - - nameLen = (uint)((nameLen << 7) | (chunk[p] & 0x7F)); - } while ((chunk[p++] & 0x80) != 0); - - if (nameLen > (uint)(end - p)) - goto chunk_end; - - cmp = string.Compare(filename, Encoding.ASCII.GetString(chunk, p, (int)nameLen), StringComparison.OrdinalIgnoreCase); - - if (cmp == 0) - { - break; - } - else if (cmp < 0) - { - if (midpoint != 0) - right = midpoint - 1; - else - return 0; - } - else if (cmp > 0) - { - left = midpoint + 1; - } - } while (left <= right); - - midpoint = (left + right) >> 1; - - if (cmp == 0) - { - // Exact match! - p += (int)nameLen; - result = p; - return 1; - } - - // Otherwise, read the group of entries for QR entry M - p = (int)(entriesOff + (midpoint != 0 ? BitConverter.ToUInt16(chunk, (int)(start - (midpoint << 1))) : 0)); - numEntries -= (ushort)(midpoint * qrDensity); - if (numEntries > qrDensity) - numEntries = (ushort)qrDensity; - } - else - { - p = (int)entriesOff; - } - - // Step 2: linear search through the set of entries reached in step 1. - // - filename == any entry - // found entry - // - filename < all entries (PMGI) or any entry (PMGL) - // entry not found, stop now - // - filename > all entries - // entry not found (PMGL) / maybe found (PMGI) - result = -1; - while (numEntries-- > 0) - { - // READ_ENCINT(nameLen) - nameLen = 0; - do - { - if (p >= end) - goto chunk_end; - - nameLen = (uint)((nameLen << 7) | (chunk[p] & 0x7F)); - } while ((chunk[p++] & 0x80) != 0); - - if (nameLen > (uint)(end - p)) - goto chunk_end; - - cmp = string.Compare(filename, Encoding.ASCII.GetString(chunk, p, (int)nameLen), StringComparison.OrdinalIgnoreCase); - p += (int)nameLen; - - if (cmp == 0) - { - // Entry found - result = p; - return 1; - } - - if (cmp < 0) - { - // Entry not found (PMGL) / maybe found (PMGI) - break; - } - - // Read and ignore the rest of this entry - if (is_pmgl) - { - // Skip section, offset, and length - for (int i = 0; i < 3; i++) - { - // READ_ENCINT(R) - right = 0; - do - { - if (p >= end) - goto chunk_end; - - right = (uint)((right << 7) | (chunk[p] & 0x7F)); - } while ((chunk[p++] & 0x80) != 0); - } - } - else - { - result = p; // Store potential final result - - // Skip chunk number - // READ_ENCINT(R) - right = 0; - do - { - if (p >= end) - goto chunk_end; - - right = (uint)((right << 7) | (chunk[p] & 0x7F)); - } while ((chunk[p++] & 0x80) != 0); - } - } - - // PMGL? not found. PMGI? maybe found - return (is_pmgl) ? 0 : (result != 0 ? 1 : 0); - - chunk_end: - Console.WriteLine("reached end of chunk data while searching"); - return -1; - } - - #endregion - - #region CHMD_EXTRACT - - /// - /// Extracts a file from a CHM helpfile - /// - public static Error Extract(Decompressor d, DecompressFile file, string filename) - { - DecompressorImpl self = d as DecompressorImpl; - if (self == null) - return Error.MSPACK_ERR_ARGS; - - if (file == null || file.Section == null) - return self.Error = Error.MSPACK_ERR_ARGS; - - SystemImpl sys = self.System; - Header chm = file.Section.Header; - - // Create decompression state if it doesn't exist - if (self.State == null) - { - self.State = new DecompressState(); - self.State.Header = chm; - self.State.Offset = 0; - self.State.State = null; - self.State.Sys = sys; - self.State.Sys.Write = SysWrite; - self.State.InputFileHandle = null; - self.State.OutputFileHandle = null; - } - - // Open input chm file if not open, or the open one is a different chm - if (self.State.InputFileHandle == null || (self.State.Header != chm)) - { - sys.Close(self.State.InputFileHandle); - sys.Close(self.State.OutputFileHandle); - - self.State.Header = chm; - self.State.Offset = 0; - self.State.State = null; - self.State.InputFileHandle = sys.Open(chm.Filename, OpenMode.MSPACK_SYS_OPEN_READ); - if (self.State.InputFileHandle == null) - return self.Error = Error.MSPACK_ERR_OPEN; - } - - // Open file for output - FileStream fh; - if ((fh = sys.Open(filename, OpenMode.MSPACK_SYS_OPEN_WRITE)) == null) - return self.Error = Error.MSPACK_ERR_OPEN; - - // If file is empty, simply creating it is enough - if (file.Length == 0) - { - sys.Close(fh); - return self.Error = Error.MSPACK_ERR_OK; - } - - self.Error = Error.MSPACK_ERR_OK; - - switch (file.Section.ID) - { - // Uncompressed section file - case 0: - // Simple seek + copy - if (!sys.Seek(self.State.InputFileHandle, file.Section.Header.Sec0.Offset + file.Offset, SeekMode.MSPACK_SYS_SEEK_START)) - { - self.Error = Error.MSPACK_ERR_SEEK; - } - else - { - byte[] buf = new byte[512]; - long length = file.Length; - while (length > 0) - { - int run = 512; - if (run > length) - run = (int)length; - - if (sys.Read(self.State.InputFileHandle, buf, 0, run) != run) - { - self.Error = Error.MSPACK_ERR_READ; - break; - } - - if (sys.Write(fh, buf, 0, run) != run) - { - self.Error = Error.MSPACK_ERR_WRITE; - break; - } - - length -= run; - } - } - break; - - // MSCompressed section file - case 1: - // (Re)initialise compression state if we it is not yet initialised, - // or we have advanced too far and have to backtrack - if (self.State.State == null || (file.Offset < self.State.Offset)) - { - if (self.State.State != null) - self.State.State = null; - - if (InitDecompressor(self, file) != Error.MSPACK_ERR_OK) - break; - } - - // Seek to input data - if (!sys.Seek(self.State.InputFileHandle, self.State.InOffset, SeekMode.MSPACK_SYS_SEEK_START)) - { - self.Error = Error.MSPACK_ERR_SEEK; - break; - } - - // Get to correct offset. - self.State.OutputFileHandle = null; - long bytes; - if ((bytes = file.Offset - self.State.Offset) != 0) - self.Error = LZX.Decompress(self.State.State, bytes); - - // If getting to the correct offset was error free, unpack file - if (self.Error == Error.MSPACK_ERR_OK) - { - self.State.OutputFileHandle = fh; - self.Error = LZX.Decompress(self.State.State, file.Length); - } - - // Save offset in input source stream, in case there is a section 0 - // file between now and the next section 1 file extracted - self.State.InOffset = sys.Tell(self.State.InputFileHandle); - - // If an LZX error occured, the LZX decompressor is now useless - if (self.Error != Error.MSPACK_ERR_OK) - self.State.State = null; - - break; - } - - sys.Close(fh); - return self.Error; - } - - #endregion - - #region CHMD_SYS_WRITE - - /// - /// chmd_sys_write is the internal writer function which the decompressor - /// uses. If either writes data to disk (self.State.OutputFileHandle) with the real - /// sys.write() function, or does nothing with the data when - /// self.State.OutputFileHandle == null. advances self.State.Offset. - /// - private static int SysWrite(object file, byte[] buffer, int offset, int bytes) - { - // Null output file means skip those bytes - if (file == null) - { - return bytes; - } - else if (file is DecompressorImpl self) - { - self.State.Offset += (uint)bytes; - if (self.State.OutputFileHandle != null) - return self.System.Write(self.State.OutputFileHandle, buffer, offset, bytes); - - return bytes; - } - else if (file is FileStream impl) - { - return SystemImpl.DefaultSystem.Write(impl, buffer, offset, bytes); - } - - // Unknown file to write to - return 0; - } - - #endregion - - #region CHMD_INIT_DECOMP - - /// - /// Initialises the LZX decompressor to decompress the compressed stream, - /// from the nearest reset offset and length that is needed for the given - /// file. - /// - public static Error InitDecompressor(DecompressorImpl self, DecompressFile file) - { - int window_size, window_bits, reset_interval, entry; - SystemImpl sys = self.System; - byte[] data; - - MSCompressedSection sec = file.Section as MSCompressedSection; - - // Ensure we have a mscompressed content section - DecompressFile contentFile = null; - Error err = FindSysFile(self, sec, ref contentFile, ContentName); - if (err != Error.MSPACK_ERR_OK) - return self.Error = err; - - sec.Content = contentFile; - - // Ensure we have a ControlData file - DecompressFile controlFile = null; - err = FindSysFile(self, sec, ref controlFile, ControlName); - if (err != Error.MSPACK_ERR_OK) - return self.Error = err; - - sec.Control = controlFile; - - // Read ControlData - if (sec.Control.Length != lzxcd_SIZEOF) - { - Console.WriteLine("ControlData file is wrong size"); - return self.Error = Error.MSPACK_ERR_DATAFORMAT; - } - - if ((data = ReadSysFile(self, sec.Control)) == null) - { - Console.WriteLine("can't read mscompressed control data file"); - return self.Error; - } - - // Check LZXC signature - if (BitConverter.ToUInt32(data, lzxcd_Signature) != 0x43585A4C) - { - return self.Error = Error.MSPACK_ERR_SIGNATURE; - } - - // Read reset_interval and window_size and validate version number - switch (BitConverter.ToUInt32(data, lzxcd_Version)) - { - case 1: - reset_interval = (int)BitConverter.ToUInt32(data, lzxcd_ResetInterval); - window_size = (int)BitConverter.ToUInt32(data, lzxcd_WindowSize); - break; - case 2: - reset_interval = (int)BitConverter.ToUInt32(data, lzxcd_ResetInterval) * LZX.LZX_FRAME_SIZE; - window_size = (int)BitConverter.ToUInt32(data, lzxcd_WindowSize) * LZX.LZX_FRAME_SIZE; - break; - default: - Console.WriteLine("bad controldata version"); - return self.Error = Error.MSPACK_ERR_DATAFORMAT; - } - - // Find window_bits from window_size - switch (window_size) - { - case 0x008000: window_bits = 15; break; - case 0x010000: window_bits = 16; break; - case 0x020000: window_bits = 17; break; - case 0x040000: window_bits = 18; break; - case 0x080000: window_bits = 19; break; - case 0x100000: window_bits = 20; break; - case 0x200000: window_bits = 21; break; - default: - Console.WriteLine("bad controldata window size"); - return self.Error = Error.MSPACK_ERR_DATAFORMAT; - } - - // Validate reset_interval - if (reset_interval == 0 || (reset_interval % LZX.LZX_FRAME_SIZE) != 0) - { - Console.WriteLine("bad controldata reset interval"); - return self.Error = Error.MSPACK_ERR_DATAFORMAT; - } - - // Which reset table entry would we like? - entry = (int)(file.Offset / reset_interval); - - // Convert from reset interval multiple (usually 64k) to 32k frames - entry *= reset_interval / LZX.LZX_FRAME_SIZE; - - // Read the reset table entry - if (ReadResetTable(self, sec, (uint)entry, out long length, out long offset)) - { - // The uncompressed length given in the reset table is dishonest. - // The uncompressed data is always padded out from the given - // uncompressed length up to the next reset interval - length += reset_interval - 1; - length &= -reset_interval; - } - else - { - // if we can't read the reset table entry, just start from - // the beginning. Use spaninfo to get the uncompressed length - entry = 0; - offset = 0; - err = ReadSpanInfo(self, sec, out length); - } - - if (err != Error.MSPACK_ERR_OK) - return self.Error = err; - - // Get offset of compressed data stream: - // = offset of uncompressed section from start of file - // + offset of compressed stream from start of uncompressed section - // + offset of chosen reset interval from start of compressed stream - self.State.InOffset = file.Section.Header.Sec0.Offset + sec.Content.Offset + offset; - - // Set start offset and overall remaining stream length - self.State.Offset = entry * LZX.LZX_FRAME_SIZE; - length -= self.State.Offset; - - // Initialise LZX stream - self.State.State = LZX.Init(self.State.Sys, self.State.InputFileHandle, self.State.OutputFileHandle, window_bits, reset_interval / LZX.LZX_FRAME_SIZE, 4096, length, false); - - if (self.State.State == null) - self.Error = Error.MSPACK_ERR_NOMEMORY; - - return self.Error; - } - - #endregion - - #region READ_RESET_TABLE - - /// - /// Reads one entry out of the reset table. Also reads the uncompressed - /// data length. Writes these to offset_ptr and length_ptr respectively. - /// Returns non-zero for success, zero for failure. - /// - public static bool ReadResetTable(DecompressorImpl self, MSCompressedSection sec, uint entry, out long length_ptr, out long offset_ptr) - { - length_ptr = 0; offset_ptr = 0; - SystemImpl sys = self.System; - byte[] data; - - // Do we have a ResetTable file? - DecompressFile resetTable = null; - Error err = FindSysFile(self, sec, ref resetTable, ResetTableName); - if (err != Error.MSPACK_ERR_OK) - return false; - - sec.ResetTable = resetTable; - - // Read ResetTable file - if (sec.ResetTable.Length < lzxrt_headerSIZEOF) - { - Console.WriteLine("ResetTable file is too short"); - return false; - } - - if (sec.ResetTable.Length > 1000000) - { - // Arbitrary upper limit - Console.WriteLine($"ResetTable >1MB ({sec.ResetTable.Length}), report if genuine"); - return false; - } - - if ((data = ReadSysFile(self, sec.ResetTable)) == null) - { - Console.WriteLine("can't read reset table"); - return false; - } - - // Check sanity of reset table - if (BitConverter.ToUInt32(data, lzxrt_FrameLen) != LZX.LZX_FRAME_SIZE) - { - Console.WriteLine(("bad reset table frame length")); - return false; - } - - // Get the uncompressed length of the LZX stream - if ((length_ptr = BitConverter.ToInt64(data, lzxrt_UncompLen)) == 0) - return false; - - uint entrysize = BitConverter.ToUInt32(data, lzxrt_EntrySize); - uint pos = BitConverter.ToUInt32(data, lzxrt_TableOffset) + (entry * entrysize); - - // Ensure reset table entry for this offset exists - if (entry < BitConverter.ToUInt32(data, lzxrt_NumEntries) && pos <= (sec.ResetTable.Length - entrysize)) - { - switch (entrysize) - { - case 4: - offset_ptr = BitConverter.ToUInt32(data, (int)pos); - err = 0; - break; - case 8: - offset_ptr = BitConverter.ToInt64(data, (int)pos); - break; - default: - Console.WriteLine("reset table entry size neither 4 nor 8"); - err = Error.MSPACK_ERR_ARGS; - break; - } - } - else - { - Console.WriteLine("bad reset interval"); - err = Error.MSPACK_ERR_ARGS; - } - - // Return success - return (err == Error.MSPACK_ERR_OK); - } - - #endregion - - #region READ_SPANINFO - - /// - /// Reads the uncompressed data length from the spaninfo file. - /// Returns zero for success or a non-zero error code for failure. - /// - public static Error ReadSpanInfo(DecompressorImpl self, MSCompressedSection sec, out long length_ptr) - { - length_ptr = 0; - SystemImpl sys = self.System; - - // Find SpanInfo file - DecompressFile spanInfo = null; - Error err = FindSysFile(self, sec, ref spanInfo, SpanInfoName); - if (err != Error.MSPACK_ERR_OK) - return Error.MSPACK_ERR_DATAFORMAT; - - sec.SpanInfo = spanInfo; - - // Check it's large enough - if (sec.SpanInfo.Length != 8) - { - Console.WriteLine("SpanInfo file is wrong size"); - return Error.MSPACK_ERR_DATAFORMAT; - } - - // Read the SpanInfo file - byte[] data; - if ((data = ReadSysFile(self, sec.SpanInfo)) == null) - { - Console.WriteLine("can't read SpanInfo file"); - return self.Error; - } - - // Get the uncompressed length of the LZX stream - length_ptr = BitConverter.ToInt64(data, 0); - if (length_ptr <= 0) - { - Console.WriteLine("output length is invalid"); - return Error.MSPACK_ERR_DATAFORMAT; - } - - return Error.MSPACK_ERR_OK; - } - - #endregion - - #region FIND_SYS_FILE - - /// - /// Uses chmd_fast_find to locate a system file, and fills out that system - /// file's entry and links it into the list of system files. Returns zero - /// for success, non-zero for both failure and the file not existing. - /// - public static Error FindSysFile(DecompressorImpl self, MSCompressedSection sec, ref DecompressFile f_ptr, string name) - { - SystemImpl sys = self.System; - DecompressFile result = null; - - // Already loaded - if (f_ptr != null) - return Error.MSPACK_ERR_OK; - - // Try using fast_find to find the file - return DATAFORMAT error if - // it fails, or successfully doesn't find the file - if (FastFind(self, sec.Header, name, result) != Error.MSPACK_ERR_OK || result.Section == null) - return Error.MSPACK_ERR_DATAFORMAT; - - f_ptr = new DecompressFile(); - - // Copy result - f_ptr = result; - f_ptr.Filename = name; - - // Link file into sysfiles list - f_ptr.Next = sec.Header.SysFiles; - sec.Header.SysFiles = f_ptr; - - return Error.MSPACK_ERR_OK; - } - - #endregion - - #region READ_SYS_FILE - - /// - /// Allocates memory for a section 0 (uncompressed) file and reads it into memory. - /// - public static byte[] ReadSysFile(DecompressorImpl self, DecompressFile file) - { - SystemImpl sys = self.System; - - if (file == null || file.Section == null || (file.Section.ID != 0)) - { - self.Error = Error.MSPACK_ERR_DATAFORMAT; - return null; - } - - int len = (int)file.Length; - byte[] data = new byte[len]; - - if (sys.Seek(self.State.InputFileHandle, file.Section.Header.Sec0.Offset + file.Offset, SeekMode.MSPACK_SYS_SEEK_START)) - { - self.Error = Error.MSPACK_ERR_SEEK; - return null; - } - - if (sys.Read(self.State.InputFileHandle, data, 0, len) != len) - { - self.Error = Error.MSPACK_ERR_READ; - return null; - } - - return data; - } - - #endregion - - #region CHMD_ERROR - - /// - /// Returns the last error that occurred - /// - /// - /// - public static Error LastError(Decompressor d) - { - DecompressorImpl self = d as DecompressorImpl; - return (self != null ? self.Error : Error.MSPACK_ERR_ARGS); - } - - #endregion - } -} diff --git a/BurnOutSharp/External/libmspack/CHM/Section.cs b/BurnOutSharp/External/libmspack/CHM/Section.cs index 968adff2..80f99a1c 100644 --- a/BurnOutSharp/External/libmspack/CHM/Section.cs +++ b/BurnOutSharp/External/libmspack/CHM/Section.cs @@ -29,7 +29,7 @@ namespace LibMSPackSharp.CHM /// /// A pointer to the CHM helpfile that contains this section. /// - public Header Header { get; set; } + public CHM Header { get; set; } /// /// The section ID. Either 0 for the uncompressed section diff --git a/BurnOutSharp/External/libmspack/CHM/_CHMHeader.cs b/BurnOutSharp/External/libmspack/CHM/_CHMHeader.cs new file mode 100644 index 00000000..d46af784 --- /dev/null +++ b/BurnOutSharp/External/libmspack/CHM/_CHMHeader.cs @@ -0,0 +1,118 @@ +/* This file is part of libmspack. + * (C) 2003-2004 Stuart Caie. + * + * libmspack is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License (LGPL) version 2.1 + * + * For further details, see the file COPYING.LIB distributed with libmspack + */ + +using System; + +namespace LibMSPackSharp.CHM +{ + internal class _CHMHeader + { + #region Fields + + /// + /// "ITSF" + /// + /// 0x0000 + public uint Signature { get; private set; } + + /// + /// The version of the CHM file format used in this file. + /// + /// 0x0004 + public uint Version { get; private set; } + + /// + /// UNKNOWN + /// + /// 0x0008 + public uint HeaderLen { get; private set; } + + /// + /// UNKNOWN + /// + /// 0x000C + public uint Unknown1 { get; private set; } + + /// + /// The "timestamp" of the CHM helpfile. + /// + /// + /// 0x0010 + /// + /// It is the lower 32 bits of a 64-bit value representing the number of + /// centiseconds since 1601-01-01 00:00:00 UTC, plus 42. It is not useful + /// as a timestamp, but it is useful as a semi-unique ID. + /// + public uint Timestamp { get; private set; } + + /// + /// The default Language and Country ID (LCID) of the user who ran the + /// HTMLHelp Compiler. This is not the language of the CHM file itself. + /// + /// 0x0014 + public uint LanguageID { get; private set; } + + /// + /// Header GUID 1 + /// + /// 0x0018 + public Guid GUID1 { get; private set; } + + /// + /// Header GUID 2 + /// + /// 0x0028 + public Guid GUID2 { get; private set; } + + /// + /// Total size of the CHM header in bytes + /// + public const int Size = 0x0038; + + #endregion + + /// + /// Private constructor + /// + private _CHMHeader() { } + + /// + /// Create a _CHMHeader from a byte array, if possible + /// + public static Error Create(byte[] buffer, out _CHMHeader header) + { + header = null; + if (buffer == null || buffer.Length < Size) + return Error.MSPACK_ERR_READ; + + header = new _CHMHeader(); + + header.Signature = BitConverter.ToUInt32(buffer, 0x0000); + if (header.Signature != 0x46535449) + return Error.MSPACK_ERR_SIGNATURE; + + // Expect version less than or equal to 3, but don't validate + header.Version = BitConverter.ToUInt32(buffer, 0x0004); + header.HeaderLen = BitConverter.ToUInt32(buffer, 0x0008); + header.Unknown1 = BitConverter.ToUInt32(buffer, 0x000C); + header.Timestamp = BitConverter.ToUInt32(buffer, 0x0010); + header.LanguageID = BitConverter.ToUInt32(buffer, 0x0014); + + header.GUID1 = new Guid(new ReadOnlySpan(buffer, 0x0018, 0x10).ToArray()); + if (header.GUID1 != Guid.Parse("7C01FD10-7BAA-11D0-9E0C-00A0-C922-E6EC")) + return Error.MSPACK_ERR_SIGNATURE; + + header.GUID2 = new Guid(new ReadOnlySpan(buffer, 0x0028, 0x10).ToArray()); + if (header.GUID2 != Guid.Parse("7C01FD11-7BAA-11D0-9E0C-00A0-C922-E6EC")) + return Error.MSPACK_ERR_SIGNATURE; + + return Error.MSPACK_ERR_OK; + } + } +} diff --git a/BurnOutSharp/External/libmspack/CHM/_HeaderSection0.cs b/BurnOutSharp/External/libmspack/CHM/_HeaderSection0.cs new file mode 100644 index 00000000..bcc8bdeb --- /dev/null +++ b/BurnOutSharp/External/libmspack/CHM/_HeaderSection0.cs @@ -0,0 +1,85 @@ +/* This file is part of libmspack. + * (C) 2003-2004 Stuart Caie. + * + * libmspack is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License (LGPL) version 2.1 + * + * For further details, see the file COPYING.LIB distributed with libmspack + */ + +using System; + +namespace LibMSPackSharp.CHM +{ + internal class _HeaderSection0 + { + #region Fields + + /// + /// UNKNOWN + /// + /// 0x0000 + public uint Unknown1 { get; private set; } + + /// + /// UNKNOWN + /// + /// 0x0004 + public uint Unknown2 { get; private set; } + + /// + /// The length of the CHM helpfile, in bytes. + /// + /// 0x0008 + public long FileLength { get; private set; } + + /// + /// UNKNOWN + /// + /// 0x0010 + public uint Unknown3 { get; private set; } + + /// + /// UNKNOWN + /// + /// 0x0014 + public uint Unknown4 { get; private set; } + + /// + /// Total size of the header section 0 in bytes + /// + public const int Size = 0x0018; + + #endregion + + /// + /// Private constructor + /// + private _HeaderSection0() { } + + /// + /// Create a _HeaderSection0 from a byte array, if possible + /// + public static Error Create(byte[] buffer, out _HeaderSection0 headerSection) + { + headerSection = null; + if (buffer == null || buffer.Length < Size) + return Error.MSPACK_ERR_READ; + + headerSection = new _HeaderSection0(); + + headerSection.Unknown1 = BitConverter.ToUInt32(buffer, 0x0000); + headerSection.Unknown2 = BitConverter.ToUInt32(buffer, 0x0004); + headerSection.FileLength = BitConverter.ToInt64(buffer, 0x0008); + + // TODO: Is this supposed to be == 0? + if (headerSection.FileLength != 0) + return Error.MSPACK_ERR_DATAFORMAT; + + headerSection.Unknown3 = BitConverter.ToUInt32(buffer, 0x0010); + headerSection.Unknown4 = BitConverter.ToUInt32(buffer, 0x0014); + + return Error.MSPACK_ERR_OK; + } + } +} diff --git a/BurnOutSharp/External/libmspack/CHM/_HeaderSection1.cs b/BurnOutSharp/External/libmspack/CHM/_HeaderSection1.cs new file mode 100644 index 00000000..bb810b06 --- /dev/null +++ b/BurnOutSharp/External/libmspack/CHM/_HeaderSection1.cs @@ -0,0 +1,184 @@ +/* This file is part of libmspack. + * (C) 2003-2004 Stuart Caie. + * + * libmspack is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License (LGPL) version 2.1 + * + * For further details, see the file COPYING.LIB distributed with libmspack + */ + +using System; + +namespace LibMSPackSharp.CHM +{ + internal class _HeaderSection1 + { + #region Fields + + /// + /// UNKNOWN + /// + /// 0x0000 + public uint Signature { get; private set; } + + /// + /// UNKNOWN + /// + /// 0x0004 + public uint Version { get; private set; } + + /// + /// UNKNOWN + /// + /// 0x0008 + public uint HeaderLen { get; private set; } + + /// + /// UNKNOWN + /// + /// 0x000C + public uint Unknown1 { get; private set; } + + /// + /// The size of each PMGL/PMGI chunk, in bytes. + /// + /// 0x0010 + public uint ChunkSize { get; private set; } + + /// + /// The "density" of the quick-reference section in PMGL/PMGI chunks. + /// + /// 0x0014 + public uint Density { get; private set; } + + /// + /// The depth of the index tree. + /// + /// + /// 0x0018 + /// + /// - if 1, there are no PMGI chunks, only PMGL chunks. + /// - if 2, there is 1 PMGI chunk. All chunk indices point to PMGL chunks. + /// - if 3, the root PMGI chunk points to secondary PMGI chunks, which in turn point to PMGL chunks. + /// - and so on... + /// + public uint Depth { get; private set; } + + /// + /// The number of the root PMGI chunk. + /// + /// + /// 0x001C + /// + /// If there is no index in the CHM helpfile, this will be 0xFFFFFFFF. + /// + public uint IndexRoot { get; private set; } + + /// + /// The number of the first PMGL chunk. Usually zero. + /// Available only in CHM decoder version 2 and above. + /// + /// 0x0020 + public uint FirstPMGL { get; private set; } + + /// + /// The number of the last PMGL chunk. Usually num_chunks-1. + /// Available only in CHM decoder version 2 and above. + /// + /// 0x0024 + public uint LastPMGL { get; private set; } + + /// + /// UNKNOWN + /// + /// 0x0028 + public uint Unknown2 { get; private set; } + + /// + /// The number of PMGL/PMGI directory chunks in this CHM helpfile. + /// + /// 0x002C + public uint NumChunks { get; private set; } + + /// + /// UNKNOWN + /// + /// 0x0030 + public uint LanguageID { get; private set; } + + /// + /// UNKNOWN + /// + /// 0x0034 + public Guid GUID { get; private set; } + + /// + /// UNKNOWN + /// + /// 0x0044 + public uint Unknown3 { get; private set; } + + /// + /// UNKNOWN + /// + /// 0x0048 + public uint Unknown4 { get; private set; } + + /// + /// UNKNOWN + /// + /// 0x004C + public uint Unknown5 { get; private set; } + + /// + /// UNKNOWN + /// + /// 0x0050 + public uint Unknown6 { get; private set; } + + /// + /// Total size of the header section 1 in bytes + /// + public const int Size = 0x0054; + + #endregion + + /// + /// Private constructor + /// + private _HeaderSection1() { } + + /// + /// Create a _HeaderSection1 from a byte array, if possible + /// + public static Error Create(byte[] buffer, out _HeaderSection1 headerSection) + { + headerSection = null; + if (buffer == null || buffer.Length < Size) + return Error.MSPACK_ERR_READ; + + headerSection = new _HeaderSection1(); + + headerSection.Signature = BitConverter.ToUInt32(buffer, 0x0000); + headerSection.Version = BitConverter.ToUInt32(buffer, 0x0004); + headerSection.HeaderLen = BitConverter.ToUInt32(buffer, 0x0008); + headerSection.Unknown1 = BitConverter.ToUInt32(buffer, 0x000C); + headerSection.ChunkSize = BitConverter.ToUInt32(buffer, 0x0010); + headerSection.Density = BitConverter.ToUInt32(buffer, 0x0014); + headerSection.Depth = BitConverter.ToUInt32(buffer, 0x0018); + headerSection.IndexRoot = BitConverter.ToUInt32(buffer, 0x001C); + headerSection.FirstPMGL = BitConverter.ToUInt32(buffer, 0x0020); + headerSection.LastPMGL = BitConverter.ToUInt32(buffer, 0x0024); + headerSection.Unknown2 = BitConverter.ToUInt32(buffer, 0x0028); + headerSection.NumChunks = BitConverter.ToUInt32(buffer, 0x002C); + headerSection.LanguageID = BitConverter.ToUInt32(buffer, 0x0030); + headerSection.GUID = new Guid(new ReadOnlySpan(buffer, 0x0034, 0x10).ToArray()); + headerSection.Unknown3 = BitConverter.ToUInt32(buffer, 0x0044); + headerSection.Unknown4 = BitConverter.ToUInt32(buffer, 0x0048); + headerSection.Unknown5 = BitConverter.ToUInt32(buffer, 0x004C); + headerSection.Unknown6 = BitConverter.ToUInt32(buffer, 0x0050); + + return Error.MSPACK_ERR_OK; + } + } +} diff --git a/BurnOutSharp/External/libmspack/CHM/_HeaderSectionTable.cs b/BurnOutSharp/External/libmspack/CHM/_HeaderSectionTable.cs new file mode 100644 index 00000000..824c932b --- /dev/null +++ b/BurnOutSharp/External/libmspack/CHM/_HeaderSectionTable.cs @@ -0,0 +1,104 @@ +/* This file is part of libmspack. + * (C) 2003-2004 Stuart Caie. + * + * libmspack is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License (LGPL) version 2.1 + * + * For further details, see the file COPYING.LIB distributed with libmspack + */ + +using System; + +namespace LibMSPackSharp.CHM +{ + internal class _HeaderSectionTable + { + #region Regular Table + + /// + /// Header section 0 offset + /// + /// 0x0000 + public long OffsetHS0 { get; private set; } + + /// + /// Header section 0 length + /// + /// 0x0008 + public long LengthHS0 { get; private set; } + + /// + /// Header section 1 offset + /// The file offset of the first PMGL/PMGI directory chunk. + /// + /// + /// 0x0010 + /// + /// This is internally settable because it has to be corrected in some cases + /// + public long OffsetHS1 { get; internal set; } + + /// + /// Header section 1 length + /// + /// 0x0018 + public long LengthHS1 { get; private set; } + + /// + /// Total size of the version 1 and 2 header section table in bytes + /// + public const int Size = 0x0020; + + #endregion + + #region Version 3 Table + + /// + /// Header section 0 offset correct + /// + /// 0x0020 + public long OffsetCS0 { get; private set; } + + /// + /// Total size of the version 3 header section table in bytes + /// + public const int V3Size = 0x0028; + + #endregion + + /// + /// Private constructor + /// + private _HeaderSectionTable() { } + + /// + /// Create a _HeaderSectionTable from a byte array, if possible + /// + public static Error Create(byte[] buffer, out _HeaderSectionTable headerSectionTable) + { + headerSectionTable = null; + if (buffer == null || buffer.Length < Size) + return Error.MSPACK_ERR_READ; + + headerSectionTable = new _HeaderSectionTable(); + + headerSectionTable.OffsetHS0 = BitConverter.ToInt64(buffer, 0x0000); + headerSectionTable.LengthHS0 = BitConverter.ToInt64(buffer, 0x0008); + headerSectionTable.OffsetHS1 = BitConverter.ToInt64(buffer, 0x0010); + headerSectionTable.LengthHS1 = BitConverter.ToInt64(buffer, 0x0018); + + if (buffer.Length >= V3Size) + { + headerSectionTable.OffsetCS0 = BitConverter.ToInt64(buffer, 0x0020); + } + + // OffsetCS0 does not exist in version 1 or 2 CHM files. + // The offset will be corrected later, once HS1 is read. + // TODO: Are these supposed to be == 0? + if (headerSectionTable.OffsetHS0 != 0 || headerSectionTable.OffsetHS1 != 0 || headerSectionTable.OffsetCS0 != 0) + return Error.MSPACK_ERR_DATAFORMAT; + + return Error.MSPACK_ERR_OK; + } + } +} diff --git a/BurnOutSharp/External/libmspack/Library.cs b/BurnOutSharp/External/libmspack/Library.cs index 125d5824..4295679a 100644 --- a/BurnOutSharp/External/libmspack/Library.cs +++ b/BurnOutSharp/External/libmspack/Library.cs @@ -225,14 +225,8 @@ namespace LibMSPackSharp if (!SystemImpl.ValidSystem(sys)) return null; - return new CHM.DecompressorImpl() + return new CHM.Decompressor() { - Open = CHM.Implementation.Open, - Close = CHM.Implementation.Close, - Extract = CHM.Implementation.Extract, - LastError = CHM.Implementation.LastError, - FastOpen = CHM.Implementation.FastOpen, - FastFind = CHM.Implementation.FastFind, System = sys, Error = Error.MSPACK_ERR_OK, State = null, @@ -242,8 +236,8 @@ namespace LibMSPackSharp /// /// Destroys an existing CHM compressor. /// - /// the to destroy - public static void DestroyCHMCompressor(CHM.Compressor c) + /// the to destroy + public static void DestroyCHMCompressor(CHM.Compressor self) { // TODO } @@ -251,10 +245,9 @@ namespace LibMSPackSharp /// /// Destroys an existing CHM decompressor. /// - /// the to destroy - public static void DestroyCHMDecompressor(CHM.Decompressor d) + /// the to destroy + public static void DestroyCHMDecompressor(CHM.Decompressor self) { - CHM.DecompressorImpl self = d as CHM.DecompressorImpl; if (self != null) { SystemImpl sys = self.System;