Compare commits

..

100 Commits
1.9.0 ... 1.9.2

Author SHA1 Message Date
Matt Nadareski
d15b4d7d23 Bump version 2025-09-20 10:47:38 -04:00
Matt Nadareski
60e6a75d5e Make public again 2025-09-20 10:40:19 -04:00
Matt Nadareski
484415d0e5 Ensure the resource table has been parsed for version info 2025-09-20 10:36:09 -04:00
Matt Nadareski
0ef9b447c4 Remove extraneous semicolon 2025-09-20 10:34:08 -04:00
Matt Nadareski
8f64e2defd Minor cleanup to previous commit 2025-09-20 10:32:53 -04:00
HeroponRikiBestest
fbdadce129 Add Matroschka processing. (#23)
* Made changes

* Temporary hack to not rely on models without significantly changing current code. Revert all of this with offset-based reading later.

Also added unnecessary casting in wrapperfactory so serialization will build locally. Revert this, since I assume it somehow builds fine for GA/sabre/etc.

* small fixes

* Store matroschka section as PE extension

* Move extractor out of deserializer, remove weird hack

* Potential GA fix

* More potential GA fixes.

* I have no idea why GA hits that error but not me

* Giving up on GA for now

* fix locking issues

* Fix GA building; thank you sabre

* Minor improvements all around

* Catch some braced single-line if statements

* Use var more

* Seperate deserializer into helper methods

* Make file path reading much more sane

* Removed MatroschkaHeaderType enum

* Removed MatroschkaGapType enum, further simplify matgaphelper.

* Remove MatroschkaHasUnknown enum, further simplify Unknown value reading.

* Cache initial offset.

* Remove TryCreate patterns.

* Rename matroschka variable to package

* Newline after object

* Rename to obj

* Remove a few unecessary TODOs

* Seperate hexstring byte read to another line.

* Fix documentation.

* More private static

* Changed data.position setting to seeking. NTS: check if this broke anything later

* rename entries to obj

* MatroschkaEntry to var

* Newline

* Alphabetical

* More alphabetical.

* section to package

* Move private variables.

* Move to extension properties.

* Revert section finding.

* Remove uneeded _dataSource lock and access.

* combine lines and make var

* Combine two null checks.

* Packaged files, some past commits I think I forgot to push.

* Missed two

* newline

* space

* newline

* Combine two lines

* Removed comment

* Return false explicitly

* Change hashing string implementation

* Fix order.

* Use offset reading instead of filedataarray

* Change file reading around a little preemptively for BOS

---------

Co-authored-by: Matt Nadareski <mnadareski@outlook.com>
2025-09-20 10:00:54 -04:00
Matt Nadareski
d3e7abfaa3 Normalize ReadRangeFromSource use 2025-09-20 09:49:42 -04:00
Matt Nadareski
b2279e97b2 CHD is all big-endian 2025-09-18 21:18:43 -04:00
Matt Nadareski
7cf969336f Fix this bumble 2025-09-18 20:54:30 -04:00
Matt Nadareski
f73d48166a Source isn't needed here anymore 2025-09-18 09:46:01 -04:00
Matt Nadareski
53af618fe4 Proof-of-concept Wise section caching 2025-09-18 09:40:15 -04:00
Matt Nadareski
5d2cf58477 Fix this being finicky 2025-09-17 12:56:32 -04:00
Matt Nadareski
664e7dce28 Greater than but not equal 2025-09-17 10:30:35 -04:00
Matt Nadareski
14a8f00864 Clean up nonstandard deserializers 2025-09-16 23:24:49 -04:00
Matt Nadareski
0b889fdc06 Remove weird holdover property 2025-09-16 23:11:14 -04:00
Matt Nadareski
e336efc149 Do the same for serializers 2025-09-16 22:29:52 -04:00
Matt Nadareski
4cd52162eb Static implementations using reflection go away 2025-09-16 22:25:40 -04:00
Matt Nadareski
eab9fff711 One slipped through the cracks 2025-09-16 22:22:32 -04:00
Matt Nadareski
d4f3511060 These don't need to call the reflection one 2025-09-16 22:21:22 -04:00
Matt Nadareski
ed12bbb35c Avoid hidden reflection call for most cases 2025-09-16 22:20:13 -04:00
Matt Nadareski
aa4629fe99 MPQ needs a distinction 2025-09-16 22:12:18 -04:00
Matt Nadareski
1950f23cf4 This should actually be a different exception type 2025-09-16 22:08:50 -04:00
Matt Nadareski
ca7c88cef6 Add better summaries for things 2025-09-16 22:07:05 -04:00
Matt Nadareski
10848e6c51 Fix issue with seeking by introducing more constructors 2025-09-16 21:54:26 -04:00
Matt Nadareski
f5d0f065c1 Handle unknown AACS records a bit better for now 2025-09-16 20:11:41 -04:00
Matt Nadareski
17b0573b0b Handle memory-padded resources tables and non-local resources 2025-09-12 10:50:22 -04:00
Matt Nadareski
7f1d843d96 Minor bugfix in name retrieval 2025-09-12 09:28:27 -04:00
Matt Nadareski
cc4837c1d1 More partial classes for reasonable things 2025-09-12 09:09:40 -04:00
Matt Nadareski
588ee5bfe4 Make partial classes for extraction 2025-09-12 09:02:03 -04:00
Matt Nadareski
e9b1b2750f Fill out the placeholder 2025-09-11 12:32:44 -04:00
Matt Nadareski
1d6fa06e97 Placeholder for section table trailer data 2025-09-11 12:24:56 -04:00
Matt Nadareski
2c22924239 Seek to section table to match docs 2025-09-11 12:17:48 -04:00
Matt Nadareski
eb01dd1e25 Add note for later 2025-09-11 12:14:07 -04:00
Matt Nadareski
0a3cb79b1c Fix issue found in encrypted and obfuscated PE 2025-09-11 12:07:22 -04:00
Matt Nadareski
da9eace8cc Slight tweak to printing again 2025-09-11 11:47:07 -04:00
Matt Nadareski
526a02b8b6 Slight tweak to printing again 2025-09-11 11:44:58 -04:00
Matt Nadareski
658c7a1c3b Add another safeguard? 2025-09-11 11:10:11 -04:00
Matt Nadareski
af84474795 Fix invalid base relocation table parsing 2025-09-11 10:58:15 -04:00
Matt Nadareski
42913c6732 Invalid export should be null, not empty 2025-09-11 10:49:01 -04:00
Matt Nadareski
2cdf544518 Fix an oddly-reading format 2025-09-11 10:45:17 -04:00
Matt Nadareski
652ec58238 Fix certificate table info printing 2025-09-11 10:41:09 -04:00
Matt Nadareski
f8531daa5c Ensure overlay accounts for certificates properly 2025-09-11 10:40:09 -04:00
Matt Nadareski
e9e89b0b43 This has been consistently wrong 2025-09-11 10:33:10 -04:00
Matt Nadareski
55e788a894 Ignore invalid certificate entries 2025-09-11 10:21:51 -04:00
Matt Nadareski
b28bb93ccb Handle non-section data with valid RVA 2025-09-11 10:11:05 -04:00
Matt Nadareski
367aab0f83 Add placeholder for figuring something out later 2025-09-11 09:59:42 -04:00
Matt Nadareski
9dcf3b9e0a The offset needs to be passed all the way 2025-09-11 09:42:48 -04:00
Matt Nadareski
3c514110ce The offset needs to be passed fully 2025-09-11 09:29:46 -04:00
Matt Nadareski
c9b0c2dace Deliberately don't retain position 2025-09-11 09:27:56 -04:00
Matt Nadareski
d575b6977e Correctly parse resource data 2025-09-11 09:21:31 -04:00
Matt Nadareski
a00e6a5e2d Start cleaning up resource parsing more 2025-09-11 09:09:56 -04:00
Matt Nadareski
1b9ae83e8c Don't pad most tables to aligned size 2025-09-11 08:35:49 -04:00
Matt Nadareski
8b91eb1caf Bound the import and export tables 2025-09-11 08:33:59 -04:00
Matt Nadareski
2a6a7b5e9a Pass in the correct data 2025-09-11 08:14:24 -04:00
Matt Nadareski
a85943866e Start using table data only in already-bounded tables 2025-09-11 08:12:27 -04:00
Matt Nadareski
797fb519c1 Pass table data in, mostly unused 2025-09-11 07:49:17 -04:00
Matt Nadareski
3ba9d56363 Read table data directly 2025-09-11 07:44:28 -04:00
Matt Nadareski
04cd4e4056 Start wiring through size bounding on table reads 2025-09-11 07:41:17 -04:00
Matt Nadareski
348e170654 There 2025-09-10 21:54:10 -04:00
Matt Nadareski
f5a4ca6276 Finally figure out what I was doing 2025-09-10 21:37:05 -04:00
Matt Nadareski
672c010aa7 Fix a stupid issue 2025-09-10 21:04:09 -04:00
Matt Nadareski
2459d88951 Found the real issue 2025-09-10 20:46:32 -04:00
Matt Nadareski
350d1c8d31 I guess this can be null? 2025-09-10 20:29:26 -04:00
Matt Nadareski
98a3842a3e Fixx off-by-one error 2025-09-10 20:26:17 -04:00
Matt Nadareski
b52a4469ee Remove alignment, add TODO and comments 2025-09-10 11:21:34 -04:00
Matt Nadareski
e3143e21ba Fix comment to be more accurate 2025-09-10 11:18:53 -04:00
Matt Nadareski
1bf2181fd3 Make check a little nicer 2025-09-09 18:56:50 -04:00
Matt Nadareski
1460635aab Move hidden resources parsing to make method nicer 2025-09-09 18:52:35 -04:00
Matt Nadareski
935ec00c86 Notes about hidden resources 2025-09-09 17:15:25 -04:00
Matt Nadareski
473b6de09b Slight cleanup 2025-09-09 16:42:50 -04:00
Matt Nadareski
ba75f2ac2c Try to fix weird resource parsing 2025-09-09 14:54:54 -04:00
Matt Nadareski
a230b39fbc Make relocation block parsing safer 2025-09-09 13:51:40 -04:00
Matt Nadareski
8e963ac62a Fix a couple of potential logic bugs 2025-09-09 13:42:36 -04:00
Matt Nadareski
eaaa89847d Rename to pex and nex for readability 2025-09-09 13:11:27 -04:00
Matt Nadareski
ef76166978 Clean up a few more PE things 2025-09-09 13:11:09 -04:00
Matt Nadareski
72912586a1 Clean up COFF symbol table parsing 2025-09-09 12:18:46 -04:00
Matt Nadareski
fb241a4036 Make things easier to read, add some helpers 2025-09-09 09:57:53 -04:00
Matt Nadareski
368c8b0533 Add section table note 2025-09-09 09:37:03 -04:00
Matt Nadareski
4010325e65 Make note from Models 2025-09-09 09:31:53 -04:00
Matt Nadareski
11dd75ad95 Make import table easier to read 2025-09-08 23:21:45 -04:00
Matt Nadareski
d0480a1311 Make export table easier to read 2025-09-08 22:51:46 -04:00
Matt Nadareski
2be33b845d Be even more careful 2025-09-08 22:09:12 -04:00
Matt Nadareski
2ad42e3a0f Seek and ye shall find 2025-09-08 21:41:46 -04:00
Matt Nadareski
5d1f83800b Add SecuROM AddD deserializer 2025-09-08 21:20:47 -04:00
Matt Nadareski
30e89a7943 Clean this up 2025-09-08 21:13:34 -04:00
Matt Nadareski
61f5dc4cf2 Extract even more types of embedded data 2025-09-08 20:08:43 -04:00
Matt Nadareski
d056c179ed Add embedded UHA support 2025-09-08 08:51:40 -04:00
Matt Nadareski
b9c4bfc67e Expand the search window again 2025-09-08 08:17:01 -04:00
Matt Nadareski
6ab5ee0ae0 Add regions here for maybe future work 2025-09-08 08:03:07 -04:00
Matt Nadareski
94c1a86702 Add AssemblyName extension property to PE 2025-09-08 07:56:40 -04:00
Matt Nadareski
af6dd6a7fc Check for BZip2 and XZ in hidden places too 2025-09-08 07:52:41 -04:00
Matt Nadareski
45d4926d4c Toss the filename at the top of the infoprint output 2025-09-07 20:52:02 -04:00
Matt Nadareski
ce016c5eb0 Bump version 2025-09-06 08:18:43 -04:00
Matt Nadareski
2225c1f2d8 Update Nuget packages 2025-09-05 10:57:14 -04:00
Matt Nadareski
2d0c0d5845 Make a bunch of things cache more safer 2025-09-05 08:32:40 -04:00
Matt Nadareski
60f1756cbb Wrap places where ReadFrom was not being used but still could be parallel 2025-09-05 07:45:55 -04:00
Matt Nadareski
738a1d250a Add inherent locking the the data source in wrappers 2025-09-05 07:36:15 -04:00
Matt Nadareski
c8e65e1e30 Add section string lock 2025-09-03 13:46:12 -04:00
Matt Nadareski
ecb09ce6f2 Make sure source data isn't locked unnecessarily 2025-09-02 23:56:29 -04:00
Matt Nadareski
72a1484a71 More granular locks 2025-09-02 23:51:02 -04:00
110 changed files with 9204 additions and 7763 deletions

View File

@@ -10,7 +10,7 @@
<Nullable>enable</Nullable>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>1.9.0</Version>
<Version>1.9.2</Version>
</PropertyGroup>
<!-- Support All Frameworks -->
@@ -66,7 +66,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="SabreTools.IO" Version="1.7.1" />
<PackageReference Include="SabreTools.IO" Version="1.7.2" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.8" Condition="!$(TargetFramework.StartsWith(`net2`)) AND !$(TargetFramework.StartsWith(`net3`)) AND !$(TargetFramework.StartsWith(`net40`)) AND !$(TargetFramework.StartsWith(`net452`))" />
</ItemGroup>

View File

@@ -10,7 +10,7 @@
<Nullable>enable</Nullable>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>1.9.0</Version>
<Version>1.9.2</Version>
</PropertyGroup>
<!-- Support All Frameworks -->
@@ -32,7 +32,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="SabreTools.IO" Version="1.7.1" />
<PackageReference Include="SabreTools.IO" Version="1.7.2" />
<PackageReference Include="SabreTools.Hashing" Version="1.5.0" />
</ItemGroup>

View File

@@ -137,6 +137,8 @@ namespace InfoPrint
Console.WriteLine(builder);
using var sw = new StreamWriter(File.OpenWrite($"{filenameBase}.txt"));
sw.WriteLine(file);
sw.WriteLine();
sw.WriteLine(builder.ToString());
sw.Flush();
}

7
LICENSE Normal file
View File

@@ -0,0 +1,7 @@
Copyright (c) 2018-2025 Matt Nadareski
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -28,7 +28,7 @@
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="SabreTools.Hashing" Version="1.5.0" />
<PackageReference Include="SabreTools.Models" Version="1.7.0" />
<PackageReference Include="SabreTools.Models" Version="1.7.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -28,7 +28,7 @@ namespace SabreTools.Serialization.Deserializers
// Try to parse the record
var record = ParseRecord(data);
if (record == null)
return null;
continue;
// Add the record
records.Add(record);
@@ -64,27 +64,31 @@ namespace SabreTools.Serialization.Deserializers
/// <returns>Filled Record on success, null on error</returns>
private static Record? ParseRecord(Stream data)
{
// The first byte is the type
// The first 4 bytes are the type and length
RecordType type = (RecordType)data.ReadByteValue();
data.Seek(-1, SeekOrigin.Current);
uint recordLength = data.ReadUInt24LittleEndian();
data.Seek(-4, SeekOrigin.Current);
// Create a record based on the type
return type switch
switch (type)
{
// Known record types
RecordType.EndOfMediaKeyBlock => ParseEndOfMediaKeyBlockRecord(data),
RecordType.ExplicitSubsetDifference => ParseExplicitSubsetDifferenceRecord(data),
RecordType.MediaKeyData => ParseMediaKeyDataRecord(data),
RecordType.SubsetDifferenceIndex => ParseSubsetDifferenceIndexRecord(data),
RecordType.TypeAndVersion => ParseTypeAndVersionRecord(data),
RecordType.DriveRevocationList => ParseDriveRevocationListRecord(data),
RecordType.HostRevocationList => ParseHostRevocationListRecord(data),
RecordType.VerifyMediaKey => ParseVerifyMediaKeyRecord(data),
RecordType.Copyright => ParseCopyrightRecord(data),
case RecordType.EndOfMediaKeyBlock: return ParseEndOfMediaKeyBlockRecord(data);
case RecordType.ExplicitSubsetDifference: return ParseExplicitSubsetDifferenceRecord(data);
case RecordType.MediaKeyData: return ParseMediaKeyDataRecord(data);
case RecordType.SubsetDifferenceIndex: return ParseSubsetDifferenceIndexRecord(data);
case RecordType.TypeAndVersion: return ParseTypeAndVersionRecord(data);
case RecordType.DriveRevocationList: return ParseDriveRevocationListRecord(data);
case RecordType.HostRevocationList: return ParseHostRevocationListRecord(data);
case RecordType.VerifyMediaKey: return ParseVerifyMediaKeyRecord(data);
case RecordType.Copyright: return ParseCopyrightRecord(data);
// Unknown record type
_ => null,
};
default:
if (recordLength > 4)
_ = data.ReadBytes((int)recordLength - 4);
return null;
}
}
/// <summary>

View File

@@ -1,6 +1,4 @@
using System;
using System.IO;
using System.Reflection;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Deserializers
@@ -9,17 +7,15 @@ namespace SabreTools.Serialization.Deserializers
/// Base class for all binary deserializers
/// </summary>
/// <typeparam name="TModel">Type of the model to deserialize</typeparam>
/// <remarks>These methods assume there is a concrete implementation of the deserialzier for the model available</remarks>
/// <remarks>
/// This class allows all inheriting types to only implement <see cref="IStreamDeserializer<>"/>
/// and still implicitly implement <see cref="IByteDeserializer<>"/> and <see cref="IFileDeserializer<>"/>
/// </remarks>
public abstract class BaseBinaryDeserializer<TModel> :
IByteDeserializer<TModel>,
IFileDeserializer<TModel>,
IStreamDeserializer<TModel>
{
/// <summary>
/// Indicates if compressed files should be decompressed before processing
/// </summary>
protected virtual bool SkipCompression => false;
#region IByteDeserializer
/// <inheritdoc/>
@@ -35,7 +31,7 @@ namespace SabreTools.Serialization.Deserializers
// Create a memory stream and parse that
var dataStream = new MemoryStream(data, offset, data.Length - offset);
return DeserializeStream(dataStream);
return Deserialize(dataStream);
}
#endregion
@@ -45,8 +41,8 @@ namespace SabreTools.Serialization.Deserializers
/// <inheritdoc/>
public virtual TModel? Deserialize(string? path)
{
using var stream = PathProcessor.OpenStream(path, SkipCompression);
return DeserializeStream(stream);
using var stream = PathProcessor.OpenStream(path);
return Deserialize(stream);
}
#endregion
@@ -57,110 +53,5 @@ namespace SabreTools.Serialization.Deserializers
public abstract TModel? Deserialize(Stream? data);
#endregion
#region Static Implementations
/// <inheritdoc cref="IByteDeserializer.Deserialize(byte[]?, int)"/>
public static TModel? DeserializeBytes(byte[]? data, int offset)
{
var deserializer = GetType<IByteDeserializer<TModel>>();
if (deserializer == null)
return default;
return deserializer.Deserialize(data, offset);
}
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
public static TModel? DeserializeFile(string? path)
{
var deserializer = GetType<IFileDeserializer<TModel>>();
if (deserializer == null)
return default;
return deserializer.Deserialize(path);
}
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
public static TModel? DeserializeStream(Stream? data)
{
var deserializer = GetType<IStreamDeserializer<TModel>>();
if (deserializer == null)
return default;
return deserializer.Deserialize(data);
}
#endregion
#region Helpers
/// <summary>
/// Get a constructed instance of a type, if possible
/// </summary>
/// <typeparam name="TDeserializer">Deserializer type to construct</typeparam>
/// <returns>Deserializer of the requested type, null on error</returns>
private static TDeserializer? GetType<TDeserializer>()
{
// If the deserializer type is invalid
string? deserializerName = typeof(TDeserializer)?.Name;
if (deserializerName == null)
return default;
// If the deserializer has no generic arguments
var genericArgs = typeof(TDeserializer).GetGenericArguments();
if (genericArgs.Length == 0)
return default;
// Loop through all loaded assemblies
Type modelType = genericArgs[0];
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
// If the assembly is invalid
if (assembly == null)
return default;
// If not all types can be loaded, use the ones that could be
Type?[] assemblyTypes = [];
try
{
assemblyTypes = assembly.GetTypes();
}
catch (ReflectionTypeLoadException rtle)
{
assemblyTypes = rtle.Types ?? [];
}
// Loop through all types
foreach (Type? type in assemblyTypes)
{
// If the type is invalid
if (type == null)
continue;
// If the type isn't a class
if (!type.IsClass)
continue;
// If the type doesn't implement the interface
var interfaceType = type.GetInterface(deserializerName);
if (interfaceType == null)
continue;
// If the interface doesn't use the correct type parameter
var genericTypes = interfaceType.GetGenericArguments();
if (genericTypes.Length != 1 || genericTypes[0] != modelType)
continue;
// Try to create a concrete instance of the type
var instance = (TDeserializer?)Activator.CreateInstance(type);
if (instance != null)
return instance;
}
}
return default;
}
#endregion
}
}

View File

@@ -64,7 +64,7 @@ namespace SabreTools.Serialization.Deserializers
return headerV3;
case 4:
var headerV4 = ParseHeaderV1(data);
var headerV4 = ParseHeaderV4(data);
if (headerV4.Tag != Constants.SignatureString)
return null;
@@ -76,7 +76,7 @@ namespace SabreTools.Serialization.Deserializers
return headerV4;
case 5:
var headerV5 = ParseHeaderV1(data);
var headerV5 = ParseHeaderV5(data);
if (headerV5.Tag != Constants.SignatureString)
return null;
@@ -150,15 +150,15 @@ namespace SabreTools.Serialization.Deserializers
byte[] tag = data.ReadBytes(8);
obj.Tag = Encoding.ASCII.GetString(tag);
obj.Length = data.ReadUInt32LittleEndian();
obj.Version = data.ReadUInt32LittleEndian();
obj.Flags = (Flags)data.ReadUInt32LittleEndian();
obj.Compression = (CompressionType)data.ReadUInt32LittleEndian();
obj.HunkSize = data.ReadUInt32LittleEndian();
obj.TotalHunks = data.ReadUInt32LittleEndian();
obj.Cylinders = data.ReadUInt32LittleEndian();
obj.Heads = data.ReadUInt32LittleEndian();
obj.Sectors = data.ReadUInt32LittleEndian();
obj.Length = data.ReadUInt32BigEndian();
obj.Version = data.ReadUInt32BigEndian();
obj.Flags = (Flags)data.ReadUInt32BigEndian();
obj.Compression = (CompressionType)data.ReadUInt32BigEndian();
obj.HunkSize = data.ReadUInt32BigEndian();
obj.TotalHunks = data.ReadUInt32BigEndian();
obj.Cylinders = data.ReadUInt32BigEndian();
obj.Heads = data.ReadUInt32BigEndian();
obj.Sectors = data.ReadUInt32BigEndian();
obj.MD5 = data.ReadBytes(16);
obj.ParentMD5 = data.ReadBytes(16);
@@ -174,18 +174,18 @@ namespace SabreTools.Serialization.Deserializers
byte[] tag = data.ReadBytes(8);
obj.Tag = Encoding.ASCII.GetString(tag);
obj.Length = data.ReadUInt32LittleEndian();
obj.Version = data.ReadUInt32LittleEndian();
obj.Flags = (Flags)data.ReadUInt32LittleEndian();
obj.Compression = (CompressionType)data.ReadUInt32LittleEndian();
obj.HunkSize = data.ReadUInt32LittleEndian();
obj.TotalHunks = data.ReadUInt32LittleEndian();
obj.Cylinders = data.ReadUInt32LittleEndian();
obj.Heads = data.ReadUInt32LittleEndian();
obj.Sectors = data.ReadUInt32LittleEndian();
obj.Length = data.ReadUInt32BigEndian();
obj.Version = data.ReadUInt32BigEndian();
obj.Flags = (Flags)data.ReadUInt32BigEndian();
obj.Compression = (CompressionType)data.ReadUInt32BigEndian();
obj.HunkSize = data.ReadUInt32BigEndian();
obj.TotalHunks = data.ReadUInt32BigEndian();
obj.Cylinders = data.ReadUInt32BigEndian();
obj.Heads = data.ReadUInt32BigEndian();
obj.Sectors = data.ReadUInt32BigEndian();
obj.MD5 = data.ReadBytes(16);
obj.ParentMD5 = data.ReadBytes(16);
obj.BytesPerSector = data.ReadUInt32LittleEndian();
obj.BytesPerSector = data.ReadUInt32BigEndian();
return obj;
}
@@ -199,16 +199,16 @@ namespace SabreTools.Serialization.Deserializers
byte[] tag = data.ReadBytes(8);
obj.Tag = Encoding.ASCII.GetString(tag);
obj.Length = data.ReadUInt32LittleEndian();
obj.Version = data.ReadUInt32LittleEndian();
obj.Flags = (Flags)data.ReadUInt32LittleEndian();
obj.Compression = (CompressionType)data.ReadUInt32LittleEndian();
obj.TotalHunks = data.ReadUInt32LittleEndian();
obj.LogicalBytes = data.ReadUInt64LittleEndian();
obj.MetaOffset = data.ReadUInt64LittleEndian();
obj.Length = data.ReadUInt32BigEndian();
obj.Version = data.ReadUInt32BigEndian();
obj.Flags = (Flags)data.ReadUInt32BigEndian();
obj.Compression = (CompressionType)data.ReadUInt32BigEndian();
obj.TotalHunks = data.ReadUInt32BigEndian();
obj.LogicalBytes = data.ReadUInt64BigEndian();
obj.MetaOffset = data.ReadUInt64BigEndian();
obj.MD5 = data.ReadBytes(16);
obj.ParentMD5 = data.ReadBytes(16);
obj.HunkBytes = data.ReadUInt32LittleEndian();
obj.HunkBytes = data.ReadUInt32BigEndian();
obj.SHA1 = data.ReadBytes(20);
obj.ParentSHA1 = data.ReadBytes(20);
@@ -218,20 +218,20 @@ namespace SabreTools.Serialization.Deserializers
/// <summary>
/// Parse a Stream into a V4 header
/// </summary>
public static HeaderV4? ParseHeaderV4(Stream data)
public static HeaderV4 ParseHeaderV4(Stream data)
{
var obj = new HeaderV4();
byte[] tag = data.ReadBytes(8);
obj.Tag = Encoding.ASCII.GetString(tag);
obj.Length = data.ReadUInt32LittleEndian();
obj.Version = data.ReadUInt32LittleEndian();
obj.Flags = (Flags)data.ReadUInt32LittleEndian();
obj.Compression = (CompressionType)data.ReadUInt32LittleEndian();
obj.TotalHunks = data.ReadUInt32LittleEndian();
obj.LogicalBytes = data.ReadUInt64LittleEndian();
obj.MetaOffset = data.ReadUInt64LittleEndian();
obj.HunkBytes = data.ReadUInt32LittleEndian();
obj.Length = data.ReadUInt32BigEndian();
obj.Version = data.ReadUInt32BigEndian();
obj.Flags = (Flags)data.ReadUInt32BigEndian();
obj.Compression = (CompressionType)data.ReadUInt32BigEndian();
obj.TotalHunks = data.ReadUInt32BigEndian();
obj.LogicalBytes = data.ReadUInt64BigEndian();
obj.MetaOffset = data.ReadUInt64BigEndian();
obj.HunkBytes = data.ReadUInt32BigEndian();
obj.SHA1 = data.ReadBytes(20);
obj.ParentSHA1 = data.ReadBytes(20);
obj.RawSHA1 = data.ReadBytes(20);
@@ -248,18 +248,18 @@ namespace SabreTools.Serialization.Deserializers
byte[] tag = data.ReadBytes(8);
obj.Tag = Encoding.ASCII.GetString(tag);
obj.Length = data.ReadUInt32LittleEndian();
obj.Version = data.ReadUInt32LittleEndian();
obj.Length = data.ReadUInt32BigEndian();
obj.Version = data.ReadUInt32BigEndian();
obj.Compressors = new CodecType[4];
for (int i = 0; i < 4; i++)
{
obj.Compressors[i] = (CodecType)data.ReadUInt32LittleEndian();
obj.Compressors[i] = (CodecType)data.ReadUInt32BigEndian();
}
obj.LogicalBytes = data.ReadUInt64LittleEndian();
obj.MapOffset = data.ReadUInt64LittleEndian();
obj.MetaOffset = data.ReadUInt64LittleEndian();
obj.HunkBytes = data.ReadUInt32LittleEndian();
obj.UnitBytes = data.ReadUInt32LittleEndian();
obj.LogicalBytes = data.ReadUInt64BigEndian();
obj.MapOffset = data.ReadUInt64BigEndian();
obj.MetaOffset = data.ReadUInt64BigEndian();
obj.HunkBytes = data.ReadUInt32BigEndian();
obj.UnitBytes = data.ReadUInt32BigEndian();
obj.RawSHA1 = data.ReadBytes(20);
obj.SHA1 = data.ReadBytes(20);
obj.ParentSHA1 = data.ReadBytes(20);

View File

@@ -11,18 +11,11 @@ namespace SabreTools.Serialization.Deserializers
{
#region IByteDeserializer
/// <inheritdoc cref="IByteDeserializer.Deserialize(byte[]?, int)"/>
public static MetadataFile? DeserializeBytes(byte[]? data, int offset, bool quotes = true)
{
var deserializer = new ClrMamePro();
return deserializer.Deserialize(data, offset, quotes);
}
/// <inheritdoc/>
public override MetadataFile? Deserialize(byte[]? data, int offset)
=> Deserialize(data, offset, true);
/// <inheritdoc/>
/// <inheritdoc cref="Deserialize(byte[], int)"/>
public MetadataFile? Deserialize(byte[]? data, int offset, bool quotes)
{
// If the data is invalid
@@ -35,42 +28,28 @@ namespace SabreTools.Serialization.Deserializers
// Create a memory stream and parse that
var dataStream = new MemoryStream(data, offset, data.Length - offset);
return DeserializeStream(dataStream, quotes);
return Deserialize(dataStream, quotes);
}
#endregion
#region IFileDeserializer
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
public static MetadataFile? DeserializeFile(string? path, bool quotes = true)
{
var deserializer = new ClrMamePro();
return deserializer.Deserialize(path, quotes);
}
/// <inheritdoc/>
public override MetadataFile? Deserialize(string? path)
=> Deserialize(path, true);
/// <inheritdoc/>
/// <inheritdoc cref="Deserialize(string?)"/>
public MetadataFile? Deserialize(string? path, bool quotes)
{
using var stream = PathProcessor.OpenStream(path);
return DeserializeStream(stream, quotes);
return Deserialize(stream, quotes);
}
#endregion
#region IStreamDeserializer
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
public static MetadataFile? DeserializeStream(Stream? data, bool quotes = true)
{
var deserializer = new ClrMamePro();
return deserializer.Deserialize(data, quotes);
}
/// <inheritdoc/>
public override MetadataFile? Deserialize(Stream? data)
=> Deserialize(data, true);

View File

@@ -8,9 +8,6 @@ namespace SabreTools.Serialization.Deserializers
{
public class GZip : BaseBinaryDeserializer<Archive>
{
/// <inheritdoc/>
protected override bool SkipCompression => true;
/// <inheritdoc/>
public override Archive? Deserialize(Stream? data)
{

View File

@@ -10,18 +10,11 @@ namespace SabreTools.Serialization.Deserializers
{
#region IByteDeserializer
/// <inheritdoc cref="IByteDeserializer.Deserialize(byte[]?, int)"/>
public static Models.Hashfile.Hashfile? DeserializeBytes(byte[]? data, int offset, HashType hash = HashType.CRC32)
{
var deserializer = new Hashfile();
return deserializer.Deserialize(data, offset, hash);
}
/// <inheritdoc/>
public override Models.Hashfile.Hashfile? Deserialize(byte[]? data, int offset)
=> Deserialize(data, offset, HashType.CRC32);
/// <inheritdoc/>
/// <inheritdoc cref="Deserialize(byte[], int)"/>
public Models.Hashfile.Hashfile? Deserialize(byte[]? data, int offset, HashType hash)
{
// If the data is invalid
@@ -34,43 +27,28 @@ namespace SabreTools.Serialization.Deserializers
// Create a memory stream and parse that
var dataStream = new MemoryStream(data, offset, data.Length - offset);
return DeserializeStream(dataStream, hash);
return Deserialize(dataStream, hash);
}
#endregion
#region IFileDeserializer
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
public static Models.Hashfile.Hashfile? DeserializeFile(string? path, HashType hash = HashType.CRC32)
{
var deserializer = new Hashfile();
return deserializer.Deserialize(path, hash);
}
/// <inheritdoc/>
public override Models.Hashfile.Hashfile? Deserialize(string? path)
=> Deserialize(path, HashType.CRC32);
/// <inheritdoc/>
/// <inheritdoc cref="Deserialize(string?)"/>
public Models.Hashfile.Hashfile? Deserialize(string? path, HashType hash)
{
using var stream = PathProcessor.OpenStream(path);
return DeserializeStream(stream, hash);
return Deserialize(stream, hash);
}
#endregion
#region IStreamDeserializer
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
public static Models.Hashfile.Hashfile? DeserializeStream(Stream? data, HashType hash = HashType.CRC32)
{
var deserializer = new Hashfile();
return deserializer.Deserialize(data, hash);
}
/// <inheritdoc/>
public override Models.Hashfile.Hashfile? Deserialize(Stream? data)
=> Deserialize(data, HashType.CRC32);

View File

@@ -22,7 +22,7 @@ namespace SabreTools.Serialization.Deserializers
long initialOffset = data.Position;
// Create a new executable to fill
var executable = new Executable();
var nex = new Executable();
#region MS-DOS Stub
@@ -32,20 +32,25 @@ namespace SabreTools.Serialization.Deserializers
return null;
// Set the MS-DOS stub
executable.Stub = stub;
nex.Stub = stub;
#endregion
#region Executable Header
// Get the new executable offset
long newExeOffset = initialOffset + stub.Header.NewExeHeaderAddr;
if (newExeOffset < initialOffset || newExeOffset > data.Length)
return null;
// Try to parse the executable header
data.Seek(initialOffset + stub.Header.NewExeHeaderAddr, SeekOrigin.Begin);
data.Seek(newExeOffset, SeekOrigin.Begin);
var header = ParseExecutableHeader(data);
if (header.Magic != SignatureString)
return null;
// Set the executable header
executable.Header = header;
nex.Header = header;
#endregion
@@ -54,16 +59,16 @@ namespace SabreTools.Serialization.Deserializers
// If the offset for the segment table doesn't exist
long tableAddress = initialOffset + stub.Header.NewExeHeaderAddr + header.SegmentTableOffset;
if (tableAddress >= data.Length)
return executable;
return nex;
// Seek to the segment table
data.Seek(tableAddress, SeekOrigin.Begin);
// Set the segment table
executable.SegmentTable = new SegmentTableEntry[header.FileSegmentCount];
nex.SegmentTable = new SegmentTableEntry[header.FileSegmentCount];
for (int i = 0; i < header.FileSegmentCount; i++)
{
executable.SegmentTable[i] = ParseSegmentTableEntry(data, initialOffset);
nex.SegmentTable[i] = ParseSegmentTableEntry(data, initialOffset);
}
#endregion
@@ -73,13 +78,13 @@ namespace SabreTools.Serialization.Deserializers
// If the offset for the segment table doesn't exist
tableAddress = initialOffset + stub.Header.NewExeHeaderAddr + header.ResourceTableOffset;
if (tableAddress >= data.Length)
return executable;
return nex;
// Seek to the resource table
data.Seek(tableAddress, SeekOrigin.Begin);
// Set the resource table
executable.ResourceTable = ParseResourceTable(data, header.ResourceEntriesCount);
nex.ResourceTable = ParseResourceTable(data, header.ResourceEntriesCount);
#endregion
@@ -89,13 +94,13 @@ namespace SabreTools.Serialization.Deserializers
tableAddress = initialOffset + stub.Header.NewExeHeaderAddr + header.ResidentNameTableOffset;
long endOffset = initialOffset + stub.Header.NewExeHeaderAddr + header.ModuleReferenceTableOffset;
if (tableAddress >= data.Length)
return executable;
return nex;
// Seek to the resident-name table
data.Seek(tableAddress, SeekOrigin.Begin);
// Set the resident-name table
executable.ResidentNameTable = ParseResidentNameTable(data, endOffset);
nex.ResidentNameTable = ParseResidentNameTable(data, endOffset);
#endregion
@@ -104,16 +109,16 @@ namespace SabreTools.Serialization.Deserializers
// If the offset for the module-reference table doesn't exist
tableAddress = initialOffset + stub.Header.NewExeHeaderAddr + header.ModuleReferenceTableOffset;
if (tableAddress >= data.Length)
return executable;
return nex;
// Seek to the module-reference table
data.Seek(tableAddress, SeekOrigin.Begin);
// Set the module-reference table
executable.ModuleReferenceTable = new ModuleReferenceTableEntry[header.ModuleReferenceTableSize];
nex.ModuleReferenceTable = new ModuleReferenceTableEntry[header.ModuleReferenceTableSize];
for (int i = 0; i < header.ModuleReferenceTableSize; i++)
{
executable.ModuleReferenceTable[i] = ParseModuleReferenceTableEntry(data);
nex.ModuleReferenceTable[i] = ParseModuleReferenceTableEntry(data);
}
#endregion
@@ -124,13 +129,13 @@ namespace SabreTools.Serialization.Deserializers
tableAddress = initialOffset + stub.Header.NewExeHeaderAddr + header.ImportedNamesTableOffset;
endOffset = initialOffset + stub.Header.NewExeHeaderAddr + header.EntryTableOffset;
if (tableAddress >= data.Length)
return executable;
return nex;
// Seek to the imported-name table
data.Seek(tableAddress, SeekOrigin.Begin);
// Set the imported-name table
executable.ImportedNameTable = ParseImportedNameTable(data, endOffset);
nex.ImportedNameTable = ParseImportedNameTable(data, endOffset);
#endregion
@@ -140,13 +145,13 @@ namespace SabreTools.Serialization.Deserializers
tableAddress = initialOffset + stub.Header.NewExeHeaderAddr + header.EntryTableOffset;
endOffset = initialOffset + stub.Header.NewExeHeaderAddr + header.EntryTableOffset + header.EntryTableSize;
if (tableAddress >= data.Length)
return executable;
return nex;
// Seek to the imported-name table
data.Seek(tableAddress, SeekOrigin.Begin);
// Set the entry table
executable.EntryTable = ParseEntryTable(data, endOffset);
nex.EntryTable = ParseEntryTable(data, endOffset);
#endregion
@@ -156,17 +161,17 @@ namespace SabreTools.Serialization.Deserializers
tableAddress = initialOffset + header.NonResidentNamesTableOffset;
endOffset = initialOffset + header.NonResidentNamesTableOffset + header.NonResidentNameTableSize;
if (tableAddress >= data.Length)
return executable;
return nex;
// Seek to the nonresident-name table
data.Seek(tableAddress, SeekOrigin.Begin);
// Set the nonresident-name table
executable.NonResidentNameTable = ParseNonResidentNameTable(data, endOffset);
nex.NonResidentNameTable = ParseNonResidentNameTable(data, endOffset);
#endregion
return executable;
return nex;
}
catch
{

View File

@@ -10,9 +10,6 @@ namespace SabreTools.Serialization.Deserializers
{
public class PKZIP : BaseBinaryDeserializer<Archive>
{
/// <inheritdoc/>
protected override bool SkipCompression => true;
/// <inheritdoc/>
public override Archive? Deserialize(Stream? data)
{
@@ -45,7 +42,7 @@ namespace SabreTools.Serialization.Deserializers
{
// Central Directory File Header
case CentralDirectoryFileHeaderSignature:
var cdr = ParseCentralDirectoryFileHeader(data, out _);
var cdr = ParseCentralDirectoryFileHeader(data);
if (cdr == null)
return null;
@@ -170,10 +167,9 @@ namespace SabreTools.Serialization.Deserializers
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled central directory file header on success, null on error</returns>
public static CentralDirectoryFileHeader? ParseCentralDirectoryFileHeader(Stream data, out ExtensibleDataField[]? extraFields)
public static CentralDirectoryFileHeader? ParseCentralDirectoryFileHeader(Stream data)
{
var obj = new CentralDirectoryFileHeader();
extraFields = null;
obj.Signature = data.ReadUInt32LittleEndian();
if (obj.Signature != CentralDirectoryFileHeaderSignature)
@@ -220,8 +216,7 @@ namespace SabreTools.Serialization.Deserializers
if (extraBytes.Length != obj.ExtraFieldLength)
return null;
// TODO: This should live on the model instead of the byte representation
extraFields = ParseExtraFields(obj, extraBytes);
obj.ExtraFields = ParseExtraFields(obj, extraBytes);
}
if (obj.FileCommentLength > 0 && data.Position + obj.FileCommentLength <= data.Length)
{
@@ -416,7 +411,7 @@ namespace SabreTools.Serialization.Deserializers
#region Local File Header
// Try to read the header
var localFileHeader = ParseLocalFileHeader(data, out var extraFields);
var localFileHeader = ParseLocalFileHeader(data);
if (localFileHeader == null)
return null;
@@ -424,9 +419,9 @@ namespace SabreTools.Serialization.Deserializers
obj.LocalFileHeader = localFileHeader;
ulong compressedSize = localFileHeader.CompressedSize;
if (extraFields != null)
if (localFileHeader.ExtraFields != null)
{
foreach (var field in extraFields)
foreach (var field in localFileHeader.ExtraFields)
{
if (field is not Zip64ExtendedInformationExtraField infoField)
continue;
@@ -532,10 +527,9 @@ namespace SabreTools.Serialization.Deserializers
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled local file header on success, null on error</returns>
public static LocalFileHeader? ParseLocalFileHeader(Stream data, out ExtensibleDataField[]? extraFields)
public static LocalFileHeader? ParseLocalFileHeader(Stream data)
{
var obj = new LocalFileHeader();
extraFields = null;
obj.Signature = data.ReadUInt32LittleEndian();
if (obj.Signature != LocalFileHeaderSignature)
@@ -575,8 +569,7 @@ namespace SabreTools.Serialization.Deserializers
if (extraBytes.Length != obj.ExtraFieldLength)
return null;
// TODO: This should live on the model instead of the byte representation
extraFields = ParseExtraFields(obj, extraBytes);
obj.ExtraFields = ParseExtraFields(obj, extraBytes);
}
return obj;

View File

@@ -1,6 +1,4 @@
using System;
using System.IO;
using System.IO.Compression;
namespace SabreTools.Serialization.Deserializers
{
@@ -11,7 +9,7 @@ namespace SabreTools.Serialization.Deserializers
/// </summary>
/// <param name="path">Path to open as a stream</param>
/// <returns>Stream representing the file, null on error</returns>
public static Stream? OpenStream(string? path, bool skipCompression = false)
public static Stream? OpenStream(string? path)
{
try
{
@@ -20,25 +18,7 @@ namespace SabreTools.Serialization.Deserializers
return null;
// Open the file for deserialization
var stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// Get the extension to determine if additional handling is needed
string ext = Path.GetExtension(path).TrimStart('.');
// Determine what we do based on the extension
if (!skipCompression && string.Equals(ext, "gz", StringComparison.OrdinalIgnoreCase))
{
return new GZipStream(stream, CompressionMode.Decompress);
}
else if (!skipCompression && string.Equals(ext, "zip", StringComparison.OrdinalIgnoreCase))
{
// TODO: Support zip-compressed files
return null;
}
else
{
return stream;
}
return File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
}
catch
{

View File

@@ -32,13 +32,16 @@ namespace SabreTools.Serialization.Deserializers
#region Audio Files
// Create the audio file deserializer
var audioDeserializer = new PlayJAudio();
// Create the audio files array
playlist.AudioFiles = new AudioFile[playlistHeader.TrackCount];
// Try to parse the audio files
for (int i = 0; i < playlist.AudioFiles.Length; i++)
{
var entryHeader = PlayJAudio.DeserializeStream(data);
var entryHeader = audioDeserializer.Deserialize(data);
if (entryHeader == null)
continue;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,85 @@
using System.IO;
using System.Text;
using SabreTools.IO.Extensions;
using SabreTools.Models.PortableExecutable;
namespace SabreTools.Serialization.Deserializers
{
public class SecuROMAddD : BaseBinaryDeserializer<Models.PortableExecutable.SecuROMAddD>
{
/// <inheritdoc/>
public override Models.PortableExecutable.SecuROMAddD? Deserialize(Stream? data)
{
// If the data is invalid
if (data == null || !data.CanRead)
return null;
try
{
// Cache the current offset
long initialOffset = data.Position;
var addD = ParseSecuROMAddD(data);
if (addD.Signature != 0x44646441)
return null;
return addD;
}
catch
{
// Ignore the actual error
return null;
}
}
/// <summary>
/// Parse a Stream into an SecuROMAddD
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled SecuROMAddD on success, null on error</returns>
private static Models.PortableExecutable.SecuROMAddD ParseSecuROMAddD(Stream data)
{
var obj = new Models.PortableExecutable.SecuROMAddD();
obj.Signature = data.ReadUInt32LittleEndian();
obj.EntryCount = data.ReadUInt32LittleEndian();
obj.Version = data.ReadNullTerminatedAnsiString();
byte[] buildBytes = data.ReadBytes(4);
string buildStr = Encoding.ASCII.GetString(buildBytes);
obj.Build = buildStr.ToCharArray();
obj.Unknown14h = data.ReadBytes(1); // TODO: Figure out how to determine how many bytes are here consistently
obj.Entries = new SecuROMAddDEntry[obj.EntryCount];
for (int i = 0; i < obj.Entries.Length; i++)
{
var entry = ParseSecuROMAddDEntry(data);
obj.Entries[i] = entry;
}
return obj;
}
/// <summary>
/// Parse a Stream into an SecuROMAddDEntry
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled SecuROMAddDEntry on success, null on error</returns>
private static SecuROMAddDEntry ParseSecuROMAddDEntry(Stream data)
{
var obj = new SecuROMAddDEntry();
obj.PhysicalOffset = data.ReadUInt32LittleEndian();
obj.Length = data.ReadUInt32LittleEndian();
obj.Unknown08h = data.ReadUInt32LittleEndian();
obj.Unknown0Ch = data.ReadUInt32LittleEndian();
obj.Unknown10h = data.ReadUInt32LittleEndian();
obj.Unknown14h = data.ReadUInt32LittleEndian();
obj.Unknown18h = data.ReadUInt32LittleEndian();
obj.Unknown1Ch = data.ReadUInt32LittleEndian();
obj.FileName = data.ReadNullTerminatedAnsiString();
obj.Unknown2Ch = data.ReadUInt32LittleEndian();
return obj;
}
}
}

View File

@@ -0,0 +1,138 @@
using System.IO;
using System.Text;
using SabreTools.IO.Extensions;
using SabreTools.Matching;
using SabreTools.Models.SecuROM;
using static SabreTools.Models.SecuROM.Constants;
namespace SabreTools.Serialization.Deserializers
{
public class SecuROMMatroschkaPackage : BaseBinaryDeserializer<MatroshkaPackage>
{
/// <inheritdoc/>
/// TODO: Unify matroschka spelling to "Matroschka"
public override MatroshkaPackage? Deserialize(Stream? data)
{
// If the data is invalid
if (data == null || !data.CanRead)
return null;
try
{
// Cache the initial offset
long initialOffset = data.Position;
// Try to parse the header
var package = ParseMatroshkaPackage(data);
if (package == null)
return null;
// Try to parse the entries
package.Entries = ParseEntries(data, package.EntryCount);
return package;
}
catch
{
// Ignore the actual error
return null;
}
}
/// <summary>
/// Parse a Stream into a MatroshkaPackage
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled MatroshkaPackage on success, null on error</returns>
public static MatroshkaPackage? ParseMatroshkaPackage(Stream data)
{
var obj = new MatroshkaPackage();
byte[] magic = data.ReadBytes(4);
obj.Signature = Encoding.ASCII.GetString(magic);
if (obj.Signature != MatroshkaMagicString)
return null;
obj.EntryCount = data.ReadUInt32LittleEndian();
if (obj.EntryCount == 0)
return null;
// Check if "matrosch" section is a longer header one or not based on whether the next uint is 0 or 1. Anything
// else will just already be starting the filename string, which is never going to start with this.
// Previously thought that the longer header was correlated with RC, but at least one executable
// (NecroVisioN.exe from the GamersGate patch NecroVisioN_Patch1.2_GG.exe) isn't RC and still has it.
long tempPosition = data.Position;
uint tempValue = data.ReadUInt32LittleEndian();
data.Seek(tempPosition, SeekOrigin.Begin);
// Only 0 or 1 have been observed for long sections
if (tempValue < 2)
{
obj.UnknownRCValue1 = data.ReadUInt32LittleEndian();
obj.UnknownRCValue2 = data.ReadUInt32LittleEndian();
obj.UnknownRCValue3 = data.ReadUInt32LittleEndian();
var keyHexBytes = data.ReadBytes(32);
obj.KeyHexString = Encoding.ASCII.GetString(keyHexBytes);
if (!data.ReadBytes(4).EqualsExactly([0x00, 0x00, 0x00, 0x00]))
return null;
}
return obj;
}
/// <summary>
/// Parse a Stream into a MatroshkaEntry array
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="entryCount">Number of entries in the array</param>
/// <returns>Filled MatroshkaEntry array on success, null on error</returns>
private static MatroshkaEntry[] ParseEntries(Stream data, uint entryCount)
{
var obj = new MatroshkaEntry[entryCount];
// Determine if file path size is 256 or 512 bytes
long tempPosition = data.Position;
data.Seek(data.Position + 256, SeekOrigin.Begin);
var tempValue = data.ReadUInt32LittleEndian();
data.Seek(tempPosition, SeekOrigin.Begin);
int gapSize = tempValue == 0 ? 512 : 256;
// Set default value for unknown value checking
bool? hasUnknown = null;
// Read entries
for (int i = 0; i < obj.Length; i++)
{
var entry = new MatroshkaEntry();
entry.Path = data.ReadBytes(gapSize);
entry.EntryType = (MatroshkaEntryType)data.ReadUInt32LittleEndian();
entry.Size = data.ReadUInt32LittleEndian();
entry.Offset = data.ReadUInt32LittleEndian();
// On the first entry, determine if the unknown value exists
if (hasUnknown == null)
{
tempPosition = data.Position;
tempValue = data.ReadUInt32LittleEndian();
data.Seek(tempPosition, SeekOrigin.Begin);
hasUnknown = tempValue == 0;
}
// TODO: Validate it's zero?
if (hasUnknown == true)
entry.Unknown = data.ReadUInt32LittleEndian();
entry.ModifiedTime = data.ReadUInt64LittleEndian();
entry.CreatedTime = data.ReadUInt64LittleEndian();
entry.AccessedTime = data.ReadUInt64LittleEndian();
entry.MD5 = data.ReadBytes(16);
obj[i] = entry;
}
return obj;
}
}
}

View File

@@ -18,18 +18,11 @@ namespace SabreTools.Serialization.Deserializers
#region IByteDeserializer
/// <inheritdoc cref="IByteDeserializer.Deserialize(byte[]?, int)"/>
public static MetadataFile? DeserializeBytes(byte[]? data, int offset, char delim)
{
var deserializer = new SeparatedValue();
return deserializer.Deserialize(data, offset, delim);
}
/// <inheritdoc/>
public override MetadataFile? Deserialize(byte[]? data, int offset)
=> Deserialize(data, offset, ',');
/// <inheritdoc/>
/// <inheritdoc cref="Deserialize(byte[], int)"/>
public MetadataFile? Deserialize(byte[]? data, int offset, char delim)
{
// If the data is invalid
@@ -42,42 +35,28 @@ namespace SabreTools.Serialization.Deserializers
// Create a memory stream and parse that
var dataStream = new MemoryStream(data, offset, data.Length - offset);
return DeserializeStream(dataStream, delim);
return Deserialize(dataStream, delim);
}
#endregion
#region IFileDeserializer
/// <inheritdoc cref="IFileDeserializer.Deserialize(string?)"/>
public static MetadataFile? DeserializeFile(string? path, char delim = ',')
{
var deserializer = new SeparatedValue();
return deserializer.Deserialize(path, delim);
}
/// <inheritdoc/>
public override MetadataFile? Deserialize(string? path)
=> Deserialize(path, ',');
/// <inheritdoc/>
/// <inheritdoc cref="Deserialize(string?)"/>
public MetadataFile? Deserialize(string? path, char delim)
{
using var stream = PathProcessor.OpenStream(path);
return DeserializeStream(stream, delim);
return Deserialize(stream, delim);
}
#endregion
#region IStreamDeserializer
/// <inheritdoc cref="IStreamDeserializer.Deserialize(Stream?)"/>
public static MetadataFile? DeserializeStream(Stream? data, char delim = ',')
{
var deserializer = new SeparatedValue();
return deserializer.Deserialize(data, delim);
}
/// <inheritdoc/>
public override MetadataFile? Deserialize(Stream? data)
=> Deserialize(data, ',');

View File

@@ -9,9 +9,6 @@ namespace SabreTools.Serialization.Deserializers
{
public class TapeArchive : BaseBinaryDeserializer<Archive>
{
/// <inheritdoc/>
protected override bool SkipCompression => true;
/// <inheritdoc/>
public override Archive? Deserialize(Stream? data)
{

View File

@@ -74,54 +74,54 @@ namespace SabreTools.Serialization.Deserializers
/// <returns>Filled OverlayHeader on success, null on error</returns>
private static OverlayHeader ParseOverlayHeader(Stream data)
{
var header = new OverlayHeader();
var obj = new OverlayHeader();
header.DllNameLen = data.ReadByteValue();
if (header.DllNameLen > 0)
obj.DllNameLen = data.ReadByteValue();
if (obj.DllNameLen > 0)
{
byte[] dllName = data.ReadBytes(header.DllNameLen);
header.DllName = Encoding.ASCII.GetString(dllName);
header.DllSize = data.ReadUInt32LittleEndian();
byte[] dllName = data.ReadBytes(obj.DllNameLen);
obj.DllName = Encoding.ASCII.GetString(dllName);
obj.DllSize = data.ReadUInt32LittleEndian();
}
// Read as a single block
header.Flags = (OverlayHeaderFlags)data.ReadUInt32LittleEndian();
obj.Flags = (OverlayHeaderFlags)data.ReadUInt32LittleEndian();
// Read as a single block
header.GraphicsData = data.ReadBytes(12);
obj.GraphicsData = data.ReadBytes(12);
// Read as a single block
header.WiseScriptExitEventOffset = data.ReadUInt32LittleEndian();
header.WiseScriptCancelEventOffset = data.ReadUInt32LittleEndian();
obj.WiseScriptExitEventOffset = data.ReadUInt32LittleEndian();
obj.WiseScriptCancelEventOffset = data.ReadUInt32LittleEndian();
// Read as a single block
header.WiseScriptInflatedSize = data.ReadUInt32LittleEndian();
header.WiseScriptDeflatedSize = data.ReadUInt32LittleEndian();
header.WiseDllDeflatedSize = data.ReadUInt32LittleEndian();
header.Ctl3d32DeflatedSize = data.ReadUInt32LittleEndian();
header.SomeData4DeflatedSize = data.ReadUInt32LittleEndian();
header.RegToolDeflatedSize = data.ReadUInt32LittleEndian();
header.ProgressDllDeflatedSize = data.ReadUInt32LittleEndian();
header.SomeData7DeflatedSize = data.ReadUInt32LittleEndian();
header.SomeData8DeflatedSize = data.ReadUInt32LittleEndian();
header.SomeData9DeflatedSize = data.ReadUInt32LittleEndian();
header.SomeData10DeflatedSize = data.ReadUInt32LittleEndian();
header.FinalFileDeflatedSize = data.ReadUInt32LittleEndian();
header.FinalFileInflatedSize = data.ReadUInt32LittleEndian();
header.EOF = data.ReadUInt32LittleEndian();
obj.WiseScriptInflatedSize = data.ReadUInt32LittleEndian();
obj.WiseScriptDeflatedSize = data.ReadUInt32LittleEndian();
obj.WiseDllDeflatedSize = data.ReadUInt32LittleEndian();
obj.Ctl3d32DeflatedSize = data.ReadUInt32LittleEndian();
obj.SomeData4DeflatedSize = data.ReadUInt32LittleEndian();
obj.RegToolDeflatedSize = data.ReadUInt32LittleEndian();
obj.ProgressDllDeflatedSize = data.ReadUInt32LittleEndian();
obj.SomeData7DeflatedSize = data.ReadUInt32LittleEndian();
obj.SomeData8DeflatedSize = data.ReadUInt32LittleEndian();
obj.SomeData9DeflatedSize = data.ReadUInt32LittleEndian();
obj.SomeData10DeflatedSize = data.ReadUInt32LittleEndian();
obj.FinalFileDeflatedSize = data.ReadUInt32LittleEndian();
obj.FinalFileInflatedSize = data.ReadUInt32LittleEndian();
obj.EOF = data.ReadUInt32LittleEndian();
// Newer installers read this and DibInflatedSize in the above block
header.DibDeflatedSize = data.ReadUInt32LittleEndian();
obj.DibDeflatedSize = data.ReadUInt32LittleEndian();
// Handle older overlay data
if (header.DibDeflatedSize > data.Length)
if (obj.DibDeflatedSize > data.Length)
{
header.DibDeflatedSize = 0;
obj.DibDeflatedSize = 0;
data.Seek(-4, SeekOrigin.Current);
return header;
return obj;
}
header.DibInflatedSize = data.ReadUInt32LittleEndian();
obj.DibInflatedSize = data.ReadUInt32LittleEndian();
// Peek at the next 2 bytes
ushort peek = data.ReadUInt16LittleEndian();
@@ -130,25 +130,25 @@ namespace SabreTools.Serialization.Deserializers
// If the next value is a known Endianness
if (Enum.IsDefined(typeof(Endianness), peek))
{
header.Endianness = (Endianness)data.ReadUInt16LittleEndian();
obj.Endianness = (Endianness)data.ReadUInt16LittleEndian();
}
else
{
// The first two values are part of the sizes block above
header.InstallScriptDeflatedSize = data.ReadUInt32LittleEndian();
header.CharacterSet = (CharacterSet)data.ReadUInt32LittleEndian();
header.Endianness = (Endianness)data.ReadUInt16LittleEndian();
obj.InstallScriptDeflatedSize = data.ReadUInt32LittleEndian();
obj.CharacterSet = (CharacterSet)data.ReadUInt32LittleEndian();
obj.Endianness = (Endianness)data.ReadUInt16LittleEndian();
}
// Endianness and init text len are read in a single block
header.InitTextLen = data.ReadByteValue();
if (header.InitTextLen > 0)
obj.InitTextLen = data.ReadByteValue();
if (obj.InitTextLen > 0)
{
byte[] initText = data.ReadBytes(header.InitTextLen);
header.InitText = Encoding.ASCII.GetString(initText);
byte[] initText = data.ReadBytes(obj.InitTextLen);
obj.InitText = Encoding.ASCII.GetString(initText);
}
return header;
return obj;
}
}
}

View File

@@ -33,6 +33,7 @@ namespace SabreTools.Serialization
return rva - matchingSection.VirtualAddress + matchingSection.PointerToRawData;
// Loop through all of the sections
uint maxVirtualAddress = 0, maxRawPointer = 0;
for (int i = 0; i < sections.Length; i++)
{
// If the section "starts" at 0, just skip it
@@ -44,6 +45,13 @@ namespace SabreTools.Serialization
if (rva < section.VirtualAddress)
continue;
// Cache the maximum matching section data, in case of a miss
if (rva >= section.VirtualAddress)
{
maxVirtualAddress = section.VirtualAddress;
maxRawPointer = section.PointerToRawData;
}
// Attempt to derive the physical address from the current section
if (section.VirtualSize != 0 && rva <= section.VirtualAddress + section.VirtualSize)
return rva - section.VirtualAddress + section.PointerToRawData;
@@ -51,7 +59,7 @@ namespace SabreTools.Serialization
return rva - section.VirtualAddress + section.PointerToRawData;
}
return 0;
return maxRawPointer != 0 ? rva - maxVirtualAddress + maxRawPointer : 0;
}
/// <summary>

View File

@@ -69,10 +69,7 @@ namespace SabreTools.Serialization.Printers
builder.AppendLine(localFileHeader.FileNameLength, " [Local File Header] File name length");
builder.AppendLine(localFileHeader.ExtraFieldLength, " [Local File Header] Extra field length");
builder.AppendLine(localFileHeader.FileName, " [Local File Header] File name");
// TODO: Reenable this when models are fixed
// var extraFields = Deserializers.PKZIP.ParseExtraFields(localFileHeader, localFileHeader.ExtraField);
// Print(builder, " [Local File Header] Extra Fields", extraFields);
Print(builder, " [Local File Header] Extra Fields", localFileHeader.ExtraFields);
}
#endregion
@@ -241,10 +238,7 @@ namespace SabreTools.Serialization.Printers
builder.AppendLine(entry.RelativeOffsetOfLocalHeader, " Relative offset of local header");
builder.AppendLine(entry.FileName, " File name");
builder.AppendLine(entry.FileComment, " File comment");
// TODO: Reenable this when models are fixed
// var extraFields = Deserializers.PKZIP.ParseExtraFields(entry, entry.ExtraField);
// Print(builder, " Extra Fields", extraFields);
Print(builder, " Extra Fields", entry.ExtraFields);
}
builder.AppendLine();

View File

@@ -193,8 +193,8 @@ namespace SabreTools.Serialization.Printers
if (header.CertificateTable != null)
{
builder.AppendLine(" Certificate Table (5)");
builder.AppendLine(header.CertificateTable.VirtualAddress, " Virtual address");
builder.AppendLine(header.CertificateTable.VirtualAddress.ConvertVirtualAddress(table), " Physical address");
builder.AppendLine(" Virtual address: N/A");
builder.AppendLine(header.CertificateTable.VirtualAddress, " Physical address");
builder.AppendLine(header.CertificateTable.Size, " Size");
}
if (header.BaseRelocationTable != null)
@@ -410,10 +410,10 @@ namespace SabreTools.Serialization.Printers
private static void Print(StringBuilder builder, CLRTokenDefinition entry, int i)
{
builder.AppendLine($" COFF Symbol Table Entry {i} (CLR Token Defintion)");
builder.AppendLine(entry.AuxFormat6AuxType, " Aux type");
builder.AppendLine(entry.AuxFormat6Reserved1, " Reserved");
builder.AppendLine(entry.AuxFormat6SymbolTableIndex, " Symbol table index");
builder.AppendLine(entry.AuxFormat6Reserved2, " Reserved");
builder.AppendLine(entry.AuxType, " Aux type");
builder.AppendLine(entry.Reserved1, " Reserved");
builder.AppendLine(entry.SymbolTableIndex, " Symbol table index");
builder.AppendLine(entry.Reserved2, " Reserved");
}
private static void Print(StringBuilder builder, COFFStringTable? stringTable)
@@ -543,12 +543,14 @@ namespace SabreTools.Serialization.Printers
builder.AppendLine(baseRelocationTableEntry.PageRVA, " Page RVA");
builder.AppendLine(baseRelocationTableEntry.PageRVA.ConvertVirtualAddress(table), " Page physical address");
builder.AppendLine(baseRelocationTableEntry.BlockSize, " Block size");
builder.AppendLine();
builder.AppendLine($" Base Relocation Table {i} Type and Offset Information:");
builder.AppendLine(" -------------------------");
if (baseRelocationTableEntry.TypeOffsetFieldEntries == null || baseRelocationTableEntry.TypeOffsetFieldEntries.Length == 0)
{
builder.AppendLine(" No base relocation table type and offset entries");
builder.AppendLine();
continue;
}

View File

@@ -12,7 +12,7 @@
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>1.9.0</Version>
<Version>1.9.2</Version>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>
@@ -63,10 +63,10 @@
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="SabreTools.ASN1" Version="1.6.0" />
<PackageReference Include="SabreTools.ASN1" Version="1.6.2" />
<PackageReference Include="SabreTools.Hashing" Version="1.5.0" />
<PackageReference Include="SabreTools.IO" Version="1.7.1" />
<PackageReference Include="SabreTools.Models" Version="1.7.0" />
<PackageReference Include="SabreTools.IO" Version="1.7.2" />
<PackageReference Include="SabreTools.Models" Version="1.7.1" />
<PackageReference Include="SabreTools.Matching" Version="1.6.0" />
<PackageReference Include="SharpCompress" Version="0.40.0" Condition="!$(TargetFramework.StartsWith(`net2`)) AND !$(TargetFramework.StartsWith(`net3`)) AND !$(TargetFramework.StartsWith(`net40`)) AND !$(TargetFramework.StartsWith(`net452`))" />
</ItemGroup>

View File

@@ -1,6 +1,4 @@
using System;
using System.IO;
using System.Reflection;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Serializers
@@ -9,7 +7,10 @@ namespace SabreTools.Serialization.Serializers
/// Base class for all binary serializers
/// </summary>
/// <typeparam name="TModel">Type of the model to serialize</typeparam>
/// <remarks>These methods assume there is a concrete implementation of the serializer for the model available</remarks>
/// <remarks>
/// This class allows all inheriting types to only implement <see cref="IStreamSerializer<>"/>
/// and still implicitly implement <see cref="IByteSerializer<>"/> and <see cref="IFileSerializer<>"/>
/// </remarks>
public abstract class BaseBinarySerializer<TModel> :
IByteSerializer<TModel>,
IFileSerializer<TModel>,
@@ -20,7 +21,7 @@ namespace SabreTools.Serialization.Serializers
/// <inheritdoc/>
public virtual byte[]? SerializeArray(TModel? obj)
{
using var stream = SerializeStream(obj);
using var stream = Serialize(obj);
if (stream == null)
return null;
@@ -39,7 +40,7 @@ namespace SabreTools.Serialization.Serializers
if (string.IsNullOrEmpty(path))
return false;
using var stream = SerializeStream(obj);
using var stream = Serialize(obj);
if (stream == null)
return false;
@@ -56,110 +57,5 @@ namespace SabreTools.Serialization.Serializers
public abstract Stream? Serialize(TModel? obj);
#endregion
#region Static Implementations
/// <inheritdoc cref="IByteSerializer.Deserialize(T?)"/>
public static byte[]? SerializeBytes(TModel? obj)
{
var serializer = GetType<IByteSerializer<TModel>>();
if (serializer == null)
return default;
return serializer.SerializeArray(obj);
}
/// <inheritdoc cref="IFileSerializer.Serialize(T?, string?)"/>
public static bool SerializeFile(TModel? obj, string? path)
{
var serializer = GetType<IFileSerializer<TModel>>();
if (serializer == null)
return default;
return serializer.Serialize(obj, path);
}
/// <inheritdoc cref="IStreamSerializer.Serialize(T?)"/>
public static Stream? SerializeStream(TModel? obj)
{
var serializer = GetType<IStreamSerializer<TModel>>();
if (serializer == null)
return default;
return serializer.Serialize(obj);
}
#endregion
#region Helpers
/// <summary>
/// Get a constructed instance of a type, if possible
/// </summary>
/// <typeparam name="TSerializer">Serializer type to construct</typeparam>
/// <returns>Serializer of the requested type, null on error</returns>
private static TSerializer? GetType<TSerializer>()
{
// If the serializer type is invalid
string? serializerName = typeof(TSerializer)?.Name;
if (serializerName == null)
return default;
// If the serializer has no generic arguments
var genericArgs = typeof(TSerializer).GetGenericArguments();
if (genericArgs == null || genericArgs.Length == 0)
return default;
// Loop through all loaded assemblies
Type modelType = genericArgs[0];
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
// If the assembly is invalid
if (assembly == null)
return default;
// If not all types can be loaded, use the ones that could be
Type?[] assemblyTypes = [];
try
{
assemblyTypes = assembly.GetTypes();
}
catch (ReflectionTypeLoadException rtle)
{
assemblyTypes = rtle.Types ?? [];
}
// Loop through all types
foreach (Type? type in assemblyTypes)
{
// If the type is invalid
if (type == null)
continue;
// If the type isn't a class
if (!type.IsClass)
continue;
// If the type doesn't implement the interface
var interfaceType = type.GetInterface(serializerName);
if (interfaceType == null)
continue;
// If the interface doesn't use the correct type parameter
var genericTypes = interfaceType.GetGenericArguments();
if (genericTypes.Length != 1 || genericTypes[0] != modelType)
continue;
// Try to create a concrete instance of the type
var instance = (TSerializer?)Activator.CreateInstance(type);
if (instance != null)
return instance;
}
}
return default;
}
#endregion
}
}

View File

@@ -24,18 +24,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public AACSMediaKeyBlock(MediaKeyBlock? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public AACSMediaKeyBlock(MediaKeyBlock model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public AACSMediaKeyBlock(MediaKeyBlock? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public AACSMediaKeyBlock(MediaKeyBlock model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public AACSMediaKeyBlock(MediaKeyBlock model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public AACSMediaKeyBlock(MediaKeyBlock model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public AACSMediaKeyBlock(MediaKeyBlock model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public AACSMediaKeyBlock(MediaKeyBlock model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create an AACS media key block from a byte array and offset
@@ -74,12 +82,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.AACS.DeserializeStream(data);
var model = new Deserializers.AACS().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new AACSMediaKeyBlock(model, data);
return new AACSMediaKeyBlock(model, data, currentOffset);
}
catch
{

View File

@@ -24,18 +24,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public BDPlusSVM(SVM? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public BDPlusSVM(SVM model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public BDPlusSVM(SVM? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public BDPlusSVM(SVM model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public BDPlusSVM(SVM model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public BDPlusSVM(SVM model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public BDPlusSVM(SVM model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public BDPlusSVM(SVM model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a BD+ SVM from a byte array and offset
@@ -74,12 +82,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.BDPlus.DeserializeStream(data);
var model = new Deserializers.BDPlus().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new BDPlusSVM(model, data);
return new BDPlusSVM(model, data, currentOffset);
}
catch
{

View File

@@ -0,0 +1,111 @@
using System;
using System.IO;
using SabreTools.IO.Compression.Deflate;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class BFPK : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no files
if (Files == null || Files.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < Files.Length; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the BFPK to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If we have no files
if (Files == null || Files.Length == 0)
return false;
// If we have an invalid index
if (index < 0 || index >= Files.Length)
return false;
// Get the file information
var file = Files[index];
if (file == null)
return false;
// Get the read index and length
int offset = file.Offset + 4;
int compressedSize = file.CompressedSize;
// Some files can lack the length prefix
if (compressedSize > Length)
{
offset -= 4;
compressedSize = file.UncompressedSize;
}
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
string filename = file.Name ?? $"file{index}";
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using FileStream fs = File.OpenWrite(filename);
// Read the data block
var data = ReadRangeFromSource(offset, compressedSize);
if (data.Length == 0)
return false;
// If we have uncompressed data
if (compressedSize == file.UncompressedSize)
{
fs.Write(data, 0, compressedSize);
fs.Flush();
}
else
{
using MemoryStream ms = new MemoryStream(data);
using ZlibStream zs = new ZlibStream(ms, CompressionMode.Decompress);
zs.CopyTo(fs);
fs.Flush();
}
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
}
}

View File

@@ -1,13 +1,9 @@
using System;
using System.IO;
using SabreTools.IO.Compression.Deflate;
using SabreTools.IO.Extensions;
using System.IO;
using SabreTools.Models.BFPK;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class BFPK : WrapperBase<Archive>, IExtractable
public partial class BFPK : WrapperBase<Archive>
{
#region Descriptive Properties
@@ -26,18 +22,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public BFPK(Archive? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public BFPK(Archive model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public BFPK(Archive? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public BFPK(Archive model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public BFPK(Archive model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public BFPK(Archive model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public BFPK(Archive model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public BFPK(Archive model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a BFPK archive from a byte array and offset
@@ -76,12 +80,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.BFPK.DeserializeStream(data);
var model = new Deserializers.BFPK().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new BFPK(model, data);
return new BFPK(model, data, currentOffset);
}
catch
{
@@ -90,110 +93,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no files
if (Files == null || Files.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < Files.Length; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the BFPK to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If we have no files
if (Files == null || Files.Length == 0)
return false;
// If we have an invalid index
if (index < 0 || index >= Files.Length)
return false;
// Get the file information
var file = Files[index];
if (file == null)
return false;
// Get the read index and length
int offset = file.Offset + 4;
int compressedSize = file.CompressedSize;
// Some files can lack the length prefix
if (compressedSize > Length)
{
offset -= 4;
compressedSize = file.UncompressedSize;
}
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
string filename = file.Name ?? $"file{index}";
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using FileStream fs = File.OpenWrite(filename);
// Read the data block
var data = _dataSource.ReadFrom(offset, compressedSize, retainPosition: true);
if (data == null)
return false;
// If we have uncompressed data
if (compressedSize == file.UncompressedSize)
{
fs.Write(data, 0, compressedSize);
fs.Flush();
}
else
{
using MemoryStream ms = new MemoryStream(data);
using ZlibStream zs = new ZlibStream(ms, CompressionMode.Decompress);
zs.CopyTo(fs);
fs.Flush();
}
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
#endregion
}
}

View File

@@ -0,0 +1,95 @@
using System;
using System.IO;
using SabreTools.Models.BSP;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class BSP : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no lumps
if (Lumps == null || Lumps.Length == 0)
return false;
// Loop through and extract all lumps to the output
bool allExtracted = true;
for (int i = 0; i < Lumps.Length; i++)
{
allExtracted &= ExtractLump(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a lump from the BSP to an output directory by index
/// </summary>
/// <param name="index">Lump index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the lump extracted, false otherwise</returns>
public bool ExtractLump(int index, string outputDirectory, bool includeDebug)
{
// If we have no lumps
if (Lumps == null || Lumps.Length == 0)
return false;
// If the lumps index is invalid
if (index < 0 || index >= Lumps.Length)
return false;
// Read the data
var lump = Lumps[index];
var data = ReadRangeFromSource(lump.Offset, lump.Length);
if (data.Length == 0)
return false;
// Create the filename
string filename = $"lump_{index}.bin";
switch ((LumpType)index)
{
case LumpType.LUMP_ENTITIES:
filename = "entities.ent";
break;
case LumpType.LUMP_TEXTURES:
filename = "texture_data.bin";
break;
}
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
}
}

View File

@@ -1,12 +1,9 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Models.BSP;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class BSP : WrapperBase<BspFile>, IExtractable
public partial class BSP : WrapperBase<BspFile>
{
#region Descriptive Properties
@@ -25,18 +22,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public BSP(BspFile? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public BSP(BspFile model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public BSP(BspFile? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public BSP(BspFile model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public BSP(BspFile model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public BSP(BspFile model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public BSP(BspFile model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public BSP(BspFile model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a BSP from a byte array and offset
@@ -75,12 +80,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.BSP.DeserializeStream(data);
var model = new Deserializers.BSP().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new BSP(model, data);
return new BSP(model, data, currentOffset);
}
catch
{
@@ -89,94 +93,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no lumps
if (Lumps == null || Lumps.Length == 0)
return false;
// Loop through and extract all lumps to the output
bool allExtracted = true;
for (int i = 0; i < Lumps.Length; i++)
{
allExtracted &= ExtractLump(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a lump from the BSP to an output directory by index
/// </summary>
/// <param name="index">Lump index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the lump extracted, false otherwise</returns>
public bool ExtractLump(int index, string outputDirectory, bool includeDebug)
{
// If we have no lumps
if (Lumps == null || Lumps.Length == 0)
return false;
// If the lumps index is invalid
if (index < 0 || index >= Lumps.Length)
return false;
// Read the data
var lump = Lumps[index];
var data = _dataSource.ReadFrom(lump.Offset, lump.Length, retainPosition: true);
if (data == null)
return false;
// Create the filename
string filename = $"lump_{index}.bin";
switch ((LumpType)index)
{
case LumpType.LUMP_ENTITIES:
filename = "entities.ent";
break;
case LumpType.LUMP_TEXTURES:
filename = "texture_data.bin";
break;
}
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
#endregion
}
}

View File

@@ -0,0 +1,53 @@
using System;
using System.IO;
using SabreTools.IO.Compression.BZip2;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
/// <summary>
/// This is a shell wrapper; one that does not contain
/// any actual parsing. It is used as a placeholder for
/// types that typically do not have models.
/// </summary>
public partial class BZip2 : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
if (_dataSource == null || !_dataSource.CanRead)
return false;
try
{
// Try opening the stream
using var bz2File = new BZip2InputStream(_dataSource, true);
// Ensure directory separators are consistent
string filename = Guid.NewGuid().ToString();
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Extract the file
using FileStream fs = File.OpenWrite(filename);
bz2File.CopyTo(fs);
fs.Flush();
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
}
}

View File

@@ -1,7 +1,4 @@
using System;
using System.IO;
using SabreTools.IO.Compression.BZip2;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
@@ -10,7 +7,7 @@ namespace SabreTools.Serialization.Wrappers
/// any actual parsing. It is used as a placeholder for
/// types that typically do not have models.
/// </summary>
public class BZip2 : WrapperBase, IExtractable
public partial class BZip2 : WrapperBase
{
#region Descriptive Properties
@@ -22,18 +19,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public BZip2(byte[]? data, int offset)
: base(data, offset)
{
// All logic is handled by the base class
}
public BZip2(byte[] data) : base(data) { }
/// <inheritdoc/>
public BZip2(Stream? data)
: base(data)
{
// All logic is handled by the base class
}
public BZip2(byte[] data, int offset) : base(data, offset) { }
/// <inheritdoc/>
public BZip2(byte[] data, int offset, int length) : base(data, offset, length) { }
/// <inheritdoc/>
public BZip2(Stream data) : base(data) { }
/// <inheritdoc/>
public BZip2(Stream data, long offset) : base(data, offset) { }
/// <inheritdoc/>
public BZip2(Stream data, long offset, long length) : base(data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a BZip2 archive from a byte array and offset
@@ -76,52 +81,10 @@ namespace SabreTools.Serialization.Wrappers
#if NETCOREAPP
/// <inheritdoc/>
public override string ExportJSON() => throw new NotImplementedException();
public override string ExportJSON() => throw new System.NotImplementedException();
#endif
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
if (_dataSource == null || !_dataSource.CanRead)
return false;
try
{
// Try opening the stream
using var bz2File = new BZip2InputStream(_dataSource, true);
// Ensure directory separators are consistent
string filename = Guid.NewGuid().ToString();
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Extract the file
using FileStream fs = File.OpenWrite(filename);
bz2File.CopyTo(fs);
fs.Flush();
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
#endregion
}
}

View File

@@ -0,0 +1,135 @@
using System;
using System.IO;
using System.Text;
using SabreTools.IO.Extensions;
using SabreTools.Models.CFB;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class CFB : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no files
if (DirectoryEntries == null || DirectoryEntries.Length == 0)
return false;
// Loop through and extract all directory entries to the output
bool allExtracted = true;
for (int i = 0; i < DirectoryEntries.Length; i++)
{
allExtracted &= ExtractEntry(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the CFB to an output directory by index
/// </summary>
/// <param name="index">Entry index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractEntry(int index, string outputDirectory, bool includeDebug)
{
// If we have no entries
if (DirectoryEntries == null || DirectoryEntries.Length == 0)
return false;
// If we have an invalid index
if (index < 0 || index >= DirectoryEntries.Length)
return false;
// Get the entry information
var entry = DirectoryEntries[index];
if (entry == null)
return false;
// Only try to extract stream objects
if (entry.ObjectType != ObjectType.StreamObject)
return true;
// Get the entry data
byte[]? data = GetDirectoryEntryData(entry);
if (data == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure the output filename is trimmed
string filename = entry.Name ?? $"entry{index}";
byte[] nameBytes = Encoding.UTF8.GetBytes(filename);
if (nameBytes[0] == 0xe4 && nameBytes[1] == 0xa1 && nameBytes[2] == 0x80)
filename = Encoding.UTF8.GetString(nameBytes, 3, nameBytes.Length - 3);
foreach (char c in Path.GetInvalidFileNameChars())
{
filename = filename.Replace(c, '_');
}
// Ensure directory separators are consistent
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using FileStream fs = File.OpenWrite(filename);
fs.Write(data);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
/// <summary>
/// Read the entry data for a single directory entry, if possible
/// </summary>
/// <param name="entry">Entry to try to retrieve data for</param>
/// <returns>Byte array representing the entry data on success, null otherwise</returns>
private byte[]? GetDirectoryEntryData(DirectoryEntry entry)
{
// If the CFB is invalid
if (Header == null)
return null;
// Only try to extract stream objects
if (entry.ObjectType != ObjectType.StreamObject)
return null;
// Determine which FAT is being used
bool miniFat = entry.StreamSize < Header.MiniStreamCutoffSize;
// Get the chain data
var chain = miniFat
? GetMiniFATSectorChainData((SectorNumber)entry.StartingSectorLocation)
: GetFATSectorChainData((SectorNumber)entry.StartingSectorLocation);
if (chain == null)
return null;
// Return only the proper amount of data
byte[] data = new byte[entry.StreamSize];
Array.Copy(chain, 0, data, 0, (int)Math.Min(chain.Length, (long)entry.StreamSize));
return data;
}
}
}

View File

@@ -1,14 +1,12 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.IO.Extensions;
using SabreTools.Models.CFB;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class CFB : WrapperBase<Binary>, IExtractable
public partial class CFB : WrapperBase<Binary>
{
#region Descriptive Properties
@@ -71,18 +69,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public CFB(Binary? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public CFB(Binary model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public CFB(Binary? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public CFB(Binary model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public CFB(Binary model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public CFB(Binary model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public CFB(Binary model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public CFB(Binary model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a Compound File Binary from a byte array and offset
@@ -121,12 +127,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.CFB.DeserializeStream(data);
var model = new Deserializers.CFB().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new CFB(model, data);
return new CFB(model, data, currentOffset);
}
catch
{
@@ -136,133 +141,6 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no files
if (DirectoryEntries == null || DirectoryEntries.Length == 0)
return false;
// Loop through and extract all directory entries to the output
bool allExtracted = true;
for (int i = 0; i < DirectoryEntries.Length; i++)
{
allExtracted &= ExtractEntry(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the CFB to an output directory by index
/// </summary>
/// <param name="index">Entry index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractEntry(int index, string outputDirectory, bool includeDebug)
{
// If we have no entries
if (DirectoryEntries == null || DirectoryEntries.Length == 0)
return false;
// If we have an invalid index
if (index < 0 || index >= DirectoryEntries.Length)
return false;
// Get the entry information
var entry = DirectoryEntries[index];
if (entry == null)
return false;
// Only try to extract stream objects
if (entry.ObjectType != ObjectType.StreamObject)
return true;
// Get the entry data
byte[]? data = GetDirectoryEntryData(entry);
if (data == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure the output filename is trimmed
string filename = entry.Name ?? $"entry{index}";
byte[] nameBytes = Encoding.UTF8.GetBytes(filename);
if (nameBytes[0] == 0xe4 && nameBytes[1] == 0xa1 && nameBytes[2] == 0x80)
filename = Encoding.UTF8.GetString(nameBytes, 3, nameBytes.Length - 3);
foreach (char c in Path.GetInvalidFileNameChars())
{
filename = filename.Replace(c, '_');
}
// Ensure directory separators are consistent
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using FileStream fs = File.OpenWrite(filename);
fs.Write(data);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
/// <summary>
/// Read the entry data for a single directory entry, if possible
/// </summary>
/// <param name="entry">Entry to try to retrieve data for</param>
/// <returns>Byte array representing the entry data on success, null otherwise</returns>
private byte[]? GetDirectoryEntryData(DirectoryEntry entry)
{
// If the CFB is invalid
if (Header == null)
return null;
// Only try to extract stream objects
if (entry.ObjectType != ObjectType.StreamObject)
return null;
// Determine which FAT is being used
bool miniFat = entry.StreamSize < Header.MiniStreamCutoffSize;
// Get the chain data
var chain = miniFat
? GetMiniFATSectorChainData((SectorNumber)entry.StartingSectorLocation)
: GetFATSectorChainData((SectorNumber)entry.StartingSectorLocation);
if (chain == null)
return null;
// Return only the proper amount of data
byte[] data = new byte[entry.StreamSize];
Array.Copy(chain, 0, data, 0, (int)Math.Min(chain.Length, (long)entry.StreamSize));
return data;
}
#endregion
#region FAT Sector Data
/// <summary>
@@ -322,8 +200,8 @@ namespace SabreTools.Serialization.Wrappers
return null;
// Try to read the sector data
var sectorData = _dataSource.ReadFrom(sectorDataOffset, (int)SectorSize, retainPosition: true);
if (sectorData == null)
var sectorData = ReadRangeFromSource(sectorDataOffset, (int)SectorSize);
if (sectorData.Length == 0)
return null;
// Add the sector data to the output

View File

@@ -60,18 +60,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public CHD(Header? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public CHD(Header model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public CHD(Header? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public CHD(Header model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public CHD(Header model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public CHD(Header model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public CHD(Header model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public CHD(Header model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a CHD header from a byte array and offset
@@ -110,12 +118,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.CHD.DeserializeStream(data);
var model = new Deserializers.CHD().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new CHD(model, data);
return new CHD(model, data, currentOffset);
}
catch
{

View File

@@ -15,18 +15,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public CIA(Models.N3DS.CIA? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public CIA(Models.N3DS.CIA model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public CIA(Models.N3DS.CIA? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public CIA(Models.N3DS.CIA model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public CIA(Models.N3DS.CIA model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public CIA(Models.N3DS.CIA model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public CIA(Models.N3DS.CIA model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public CIA(Models.N3DS.CIA model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a CIA archive from a byte array and offset
@@ -65,12 +73,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.CIA.DeserializeStream(data);
var model = new Deserializers.CIA().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new CIA(model, data);
return new CIA(model, data, currentOffset);
}
catch
{

View File

@@ -0,0 +1,114 @@
using System;
using System.Collections.Generic;
using System.IO;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class GCF : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no files
if (Files == null || Files.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < Files.Length; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the GCF to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If we have no files
if (Files == null || Files.Length == 0 || DataBlockOffsets == null)
return false;
// If the files index is invalid
if (index < 0 || index >= Files.Length)
return false;
// Get the file
var file = Files[index];
if (file?.BlockEntries == null || file.Size == 0)
return false;
// If the file is encrypted -- TODO: Revisit later
if (file.Encrypted)
return false;
// Get all data block offsets needed for extraction
var dataBlockOffsets = new List<long>();
for (int i = 0; i < file.BlockEntries.Length; i++)
{
var blockEntry = file.BlockEntries[i];
uint dataBlockIndex = blockEntry.FirstDataBlockIndex;
long blockEntrySize = blockEntry.FileDataSize;
while (blockEntrySize > 0)
{
long dataBlockOffset = DataBlockOffsets[dataBlockIndex++];
dataBlockOffsets.Add(dataBlockOffset);
blockEntrySize -= BlockSize;
}
}
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
string filename = file.Path ?? $"file{index}";
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
// Now read the data sequentially and write out while we have data left
long fileSize = file.Size;
for (int i = 0; i < dataBlockOffsets.Count; i++)
{
int readSize = (int)Math.Min(BlockSize, fileSize);
var data = ReadRangeFromSource((int)dataBlockOffsets[i], readSize);
if (data.Length == 0)
return false;
fs.Write(data, 0, data.Length);
fs.Flush();
}
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
}
}

View File

@@ -1,12 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class GCF : WrapperBase<Models.GCF.File>, IExtractable
public partial class GCF : WrapperBase<Models.GCF.File>
{
#region Descriptive Properties
@@ -167,18 +164,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public GCF(Models.GCF.File? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public GCF(Models.GCF.File model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public GCF(Models.GCF.File? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public GCF(Models.GCF.File model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public GCF(Models.GCF.File model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public GCF(Models.GCF.File model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public GCF(Models.GCF.File model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public GCF(Models.GCF.File model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create an GCF from a byte array and offset
@@ -217,12 +222,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.GCF.DeserializeStream(data);
var model = new Deserializers.GCF().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new GCF(model, data);
return new GCF(model, data, currentOffset);
}
catch
{
@@ -232,114 +236,6 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no files
if (Files == null || Files.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < Files.Length; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the GCF to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If we have no files
if (Files == null || Files.Length == 0 || DataBlockOffsets == null)
return false;
// If the files index is invalid
if (index < 0 || index >= Files.Length)
return false;
// Get the file
var file = Files[index];
if (file?.BlockEntries == null || file.Size == 0)
return false;
// If the file is encrypted -- TODO: Revisit later
if (file.Encrypted)
return false;
// Get all data block offsets needed for extraction
var dataBlockOffsets = new List<long>();
for (int i = 0; i < file.BlockEntries.Length; i++)
{
var blockEntry = file.BlockEntries[i];
uint dataBlockIndex = blockEntry.FirstDataBlockIndex;
long blockEntrySize = blockEntry.FileDataSize;
while (blockEntrySize > 0)
{
long dataBlockOffset = DataBlockOffsets[dataBlockIndex++];
dataBlockOffsets.Add(dataBlockOffset);
blockEntrySize -= BlockSize;
}
}
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
string filename = file.Path ?? $"file{index}";
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
// Now read the data sequentially and write out while we have data left
long fileSize = file.Size;
for (int i = 0; i < dataBlockOffsets.Count; i++)
{
int readSize = (int)Math.Min(BlockSize, fileSize);
var data = _dataSource.ReadFrom((int)dataBlockOffsets[i], readSize, retainPosition: true);
if (data == null)
return false;
fs.Write(data, 0, data.Length);
fs.Flush();
}
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
#endregion
#region Helper Classes
/// <summary>

View File

@@ -0,0 +1,71 @@
using System;
using System.IO;
using SabreTools.IO.Compression.Deflate;
using SabreTools.Models.GZIP;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class GZip : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Ensure there is data to extract
if (Header == null || DataOffset < 0)
{
if (includeDebug) Console.Error.WriteLine("Invalid archive detected, skipping...");
return false;
}
// Ensure that DEFLATE is being used
if (Header.CompressionMethod != CompressionMethod.Deflate)
{
if (includeDebug) Console.Error.WriteLine($"Invalid compression method {Header.CompressionMethod} detected, only DEFLATE is supported. Skipping...");
return false;
}
try
{
// Seek to the start of the compressed data
long offset = _dataSource.Seek(DataOffset, SeekOrigin.Begin);
if (offset != DataOffset)
{
if (includeDebug) Console.Error.WriteLine($"Could not seek to compressed data at {DataOffset}");
return false;
}
// Ensure directory separators are consistent
string filename = Header.OriginalFileName
?? (Filename != null ? Path.GetFileName(Filename).Replace(".gz", string.Empty) : null)
?? $"extracted_file";
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Open the source as a DEFLATE stream
var deflateStream = new DeflateStream(_dataSource, CompressionMode.Decompress, leaveOpen: true);
// Write the file
using var fs = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None);
deflateStream.CopyTo(fs);
fs.Flush();
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
}
}

View File

@@ -1,12 +1,9 @@
using System;
using System.IO;
using SabreTools.IO.Compression.Deflate;
using SabreTools.Models.GZIP;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class GZip : WrapperBase<Archive>, IExtractable
public partial class GZip : WrapperBase<Archive>
{
#region Descriptive Properties
@@ -74,18 +71,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public GZip(Archive? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public GZip(Archive model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public GZip(Archive? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public GZip(Archive model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public GZip(Archive model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public GZip(Archive model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public GZip(Archive model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public GZip(Archive model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a GZip archive from a byte array and offset
@@ -124,12 +129,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.GZip.DeserializeStream(data);
var model = new Deserializers.GZip().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new GZip(model, data);
return new GZip(model, data, currentOffset);
}
catch
{
@@ -138,69 +142,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Ensure there is data to extract
if (Header == null || DataOffset < 0)
{
if (includeDebug) Console.Error.WriteLine("Invalid archive detected, skipping...");
return false;
}
// Ensure that DEFLATE is being used
if (Header.CompressionMethod != CompressionMethod.Deflate)
{
if (includeDebug) Console.Error.WriteLine($"Invalid compression method {Header.CompressionMethod} detected, only DEFLATE is supported. Skipping...");
return false;
}
try
{
// Seek to the start of the compressed data
long offset = _dataSource.Seek(DataOffset, SeekOrigin.Begin);
if (offset != DataOffset)
{
if (includeDebug) Console.Error.WriteLine($"Could not seek to compressed data at {DataOffset}");
return false;
}
// Ensure directory separators are consistent
string filename = Header.OriginalFileName
?? (Filename != null ? Path.GetFileName(Filename).Replace(".gz", string.Empty) : null)
?? $"extracted_file";
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Open the source as a DEFLATE stream
var deflateStream = new DeflateStream(_dataSource, CompressionMode.Decompress, leaveOpen: true);
// Write the file
using var fs = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None);
deflateStream.CopyTo(fs);
fs.Flush();
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
#endregion
}
}

View File

@@ -14,18 +14,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public IRD(Models.IRD.File? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public IRD(Models.IRD.File model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public IRD(Models.IRD.File? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public IRD(Models.IRD.File model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public IRD(Models.IRD.File model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public IRD(Models.IRD.File model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public IRD(Models.IRD.File model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public IRD(Models.IRD.File model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create an IRD from a byte array and offset
@@ -64,12 +72,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.IRD.DeserializeStream(data);
var model = new Deserializers.IRD().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new IRD(model, data);
return new IRD(model, data, currentOffset);
}
catch
{

View File

@@ -0,0 +1,127 @@
using System;
using System.IO;
using SabreTools.IO.Compression.Blast;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
/// <remarks>
/// Reference (de)compressor: https://www.sac.sk/download/pack/icomp95.zip
/// </remarks>
/// <see href="https://github.com/wfr/unshieldv3"/>
public partial class InstallShieldArchiveV3 : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Get the file count
int fileCount = Files.Length;
if (fileCount == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < fileCount; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the ISAv3 to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If the files index is invalid
if (index < 0 || index >= FileCount)
return false;
// Get the file
var file = Files[index];
if (file == null)
return false;
// Create the filename
var filename = file.Name;
if (filename == null)
return false;
// Get the directory index
int dirIndex = FileDirMap[index];
if (dirIndex < 0 || dirIndex > DirCount)
return false;
// Get the directory name
var dirName = Directories[dirIndex].Name;
if (dirName != null)
filename = Path.Combine(dirName, filename);
// Get and adjust the file offset
long fileOffset = file.Offset + DataStart;
if (fileOffset < 0 || fileOffset >= Length)
return false;
// Get the file sizes
long fileSize = file.CompressedSize;
long outputFileSize = file.UncompressedSize;
// Read the compressed data directly
var compressedData = ReadRangeFromSource((int)fileOffset, (int)fileSize);
if (compressedData.Length == 0)
return false;
// If the compressed and uncompressed sizes match
byte[] data;
if (fileSize == outputFileSize)
{
data = compressedData;
}
else
{
// Decompress the data
var decomp = Decompressor.Create();
using var outData = new MemoryStream();
decomp.CopyTo(compressedData, outData);
data = outData.ToArray();
}
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !System.IO.Directory.Exists(directoryName))
System.IO.Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = System.IO.File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return false;
}
}
}

View File

@@ -1,10 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using SabreTools.IO.Compression.Blast;
using SabreTools.IO.Extensions;
using SabreTools.Models.InstallShieldArchiveV3;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
@@ -12,7 +8,7 @@ namespace SabreTools.Serialization.Wrappers
/// Reference (de)compressor: https://www.sac.sk/download/pack/icomp95.zip
/// </remarks>
/// <see href="https://github.com/wfr/unshieldv3"/>
public partial class InstallShieldArchiveV3 : WrapperBase<Archive>, IExtractable
public partial class InstallShieldArchiveV3 : WrapperBase<Archive>
{
#region Descriptive Properties
@@ -112,18 +108,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public InstallShieldArchiveV3(Archive? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public InstallShieldArchiveV3(Archive model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public InstallShieldArchiveV3(Archive? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public InstallShieldArchiveV3(Archive model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public InstallShieldArchiveV3(Archive model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public InstallShieldArchiveV3(Archive model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public InstallShieldArchiveV3(Archive model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public InstallShieldArchiveV3(Archive model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create an InstallShield Archive V3 from a byte array and offset
@@ -162,12 +166,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.InstallShieldArchiveV3.DeserializeStream(data);
var model = new Deserializers.InstallShieldArchiveV3().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new InstallShieldArchiveV3(model, data);
return new InstallShieldArchiveV3(model, data, currentOffset);
}
catch
{
@@ -176,122 +179,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Get the file count
int fileCount = Files.Length;
if (fileCount == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < fileCount; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the ISAv3 to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If the files index is invalid
if (index < 0 || index >= FileCount)
return false;
// Get the file
var file = Files[index];
if (file == null)
return false;
// Create the filename
var filename = file.Name;
if (filename == null)
return false;
// Get the directory index
int dirIndex = FileDirMap[index];
if (dirIndex < 0 || dirIndex > DirCount)
return false;
// Get the directory name
var dirName = Directories[dirIndex].Name;
if (dirName != null)
filename = Path.Combine(dirName, filename);
// Get and adjust the file offset
long fileOffset = file.Offset + DataStart;
if (fileOffset < 0 || fileOffset >= Length)
return false;
// Get the file sizes
long fileSize = file.CompressedSize;
long outputFileSize = file.UncompressedSize;
// Read the compressed data directly
var compressedData = _dataSource.ReadFrom((int)fileOffset, (int)fileSize, retainPosition: true);
if (compressedData == null)
return false;
// If the compressed and uncompressed sizes match
byte[] data;
if (fileSize == outputFileSize)
{
data = compressedData;
}
else
{
// Decompress the data
var decomp = Decompressor.Create();
using var outData = new MemoryStream();
decomp.CopyTo(compressedData, outData);
data = outData.ToArray();
}
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !System.IO.Directory.Exists(directoryName))
System.IO.Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = System.IO.File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return false;
}
#endregion
}
}

View File

@@ -0,0 +1,838 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
using SabreTools.Hashing;
using SabreTools.IO.Compression.zlib;
using SabreTools.Models.InstallShieldCabinet;
using SabreTools.Serialization.Interfaces;
using static SabreTools.Models.InstallShieldCabinet.Constants;
namespace SabreTools.Serialization.Wrappers
{
public partial class InstallShieldCabinet : WrapperBase<Cabinet>, IExtractable
{
#region Extension Properties
/// <summary>
/// Reference to the next cabinet header
/// </summary>
/// <remarks>Only used in multi-file</remarks>
public InstallShieldCabinet? Next { get; set; }
/// <summary>
/// Reference to the next previous header
/// </summary>
/// <remarks>Only used in multi-file</remarks>
public InstallShieldCabinet? Prev { get; set; }
/// <summary>
/// Volume index ID, 0 for headers
/// </summary>
/// <remarks>Only used in multi-file</remarks>
public ushort VolumeID { get; set; }
#endregion
#region Extraction State
/// <summary>
/// Base filename path for related CAB files
/// </summary>
internal string? FilenamePattern { get; set; }
#endregion
#region Constants
/// <summary>
/// Default buffer size
/// </summary>
private const int BUFFER_SIZE = 64 * 1024;
/// <summary>
/// Maximum size of the window in bits
/// </summary>
private const int MAX_WBITS = 15;
#endregion
#region Cabinet Set
/// <summary>
/// Open a cabinet set for reading, if possible
/// </summary>
/// <param name="pattern">Filename pattern for matching cabinet files</param>
/// <returns>Wrapper representing the set, null on error</returns>
public static InstallShieldCabinet? OpenSet(string? pattern)
{
// An invalid pattern means no cabinet files
if (string.IsNullOrEmpty(pattern))
return null;
// Create a placeholder wrapper for output
InstallShieldCabinet? set = null;
// Loop until there are no parts left
bool iterate = true;
InstallShieldCabinet? previous = null;
for (ushort i = 1; iterate; i++)
{
var file = OpenFileForReading(pattern, i, HEADER_SUFFIX);
if (file != null)
iterate = false;
else
file = OpenFileForReading(pattern, i, CABINET_SUFFIX);
if (file == null)
break;
var current = Create(file);
if (current == null)
break;
current.VolumeID = i;
if (previous != null)
{
previous.Next = current;
current.Prev = previous;
}
else
{
set = current;
previous = current;
}
}
// Set the pattern, if possible
if (set != null)
set.FilenamePattern = pattern;
return set;
}
/// <summary>
/// Open the numbered cabinet set volume
/// </summary>
/// <param name="volumeId">Volume ID, 1-indexed</param>
/// <returns>Wrapper representing the volume on success, null otherwise</returns>
public InstallShieldCabinet? OpenVolume(ushort volumeId, out Stream? volumeStream)
{
// Normalize the volume ID for odd cases
if (volumeId == ushort.MinValue || volumeId == ushort.MaxValue)
volumeId = 1;
// Try to open the file as a stream
volumeStream = OpenFileForReading(FilenamePattern, volumeId, CABINET_SUFFIX);
if (volumeStream == null)
{
Console.Error.WriteLine($"Failed to open input cabinet file {volumeId}");
return null;
}
// Try to parse the stream into a cabinet
var volume = Create(volumeStream);
if (volume == null)
{
Console.Error.WriteLine($"Failed to open input cabinet file {volumeId}");
return null;
}
// Set the volume ID and return
volume.VolumeID = volumeId;
return volume;
}
/// <summary>
/// Open a cabinet file for reading
/// </summary>
/// <param name="index">Cabinet part index to be opened</param>
/// <param name="suffix">Cabinet files suffix (e.g. `.cab`)</param>
/// <returns>A Stream representing the cabinet part, null on error</returns>
public Stream? OpenFileForReading(int index, string suffix)
=> OpenFileForReading(FilenamePattern, index, suffix);
/// <summary>
/// Create the generic filename pattern to look for from the input filename
/// </summary>
/// <returns>String representing the filename pattern for a cabinet set, null on error</returns>
private static string? CreateFilenamePattern(string filename)
{
string? pattern = null;
if (string.IsNullOrEmpty(filename))
return pattern;
string? directory = Path.GetDirectoryName(Path.GetFullPath(filename));
if (directory != null)
pattern = Path.Combine(directory, Path.GetFileNameWithoutExtension(filename));
else
pattern = Path.GetFileNameWithoutExtension(filename);
return new Regex(@"\d+$").Replace(pattern, string.Empty);
}
/// <summary>
/// Open a cabinet file for reading
/// </summary>
/// <param name="pattern">Filename pattern for matching cabinet files</param>
/// <param name="index">Cabinet part index to be opened</param>
/// <param name="suffix">Cabinet files suffix (e.g. `.cab`)</param>
/// <returns>A Stream representing the cabinet part, null on error</returns>
private static Stream? OpenFileForReading(string? pattern, int index, string suffix)
{
// An invalid pattern means no cabinet files
if (string.IsNullOrEmpty(pattern))
return null;
// Attempt lower-case extension
string filename = $"{pattern}{index}.{suffix}";
if (File.Exists(filename))
return File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// Attempt upper-case extension
filename = $"{pattern}{index}.{suffix.ToUpperInvariant()}";
if (File.Exists(filename))
return File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
return null;
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Open the full set if possible
var cabinet = this;
if (Filename != null)
{
// Get the name of the first cabinet file or header
string pattern = CreateFilenamePattern(Filename)!;
bool cabinetHeaderExists = File.Exists(pattern + "1.hdr");
bool shouldScanCabinet = cabinetHeaderExists
? Filename.Equals(pattern + "1.hdr", StringComparison.OrdinalIgnoreCase)
: Filename.Equals(pattern + "1.cab", StringComparison.OrdinalIgnoreCase);
// If we have anything but the first file
if (!shouldScanCabinet)
return false;
// Open the set from the pattern
cabinet = OpenSet(pattern);
}
// If the cabinet set could not be opened
if (cabinet == null)
return false;
try
{
for (int i = 0; i < cabinet.FileCount; i++)
{
try
{
// Check if the file is valid first
if (!cabinet.FileIsValid(i))
continue;
// Ensure directory separators are consistent
string filename = cabinet.GetFileName(i) ?? $"BAD_FILENAME{i}";
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
cabinet.FileSave(i, filename);
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
}
}
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Save the file at the given index to the filename specified
/// </summary>
public bool FileSave(int index, string filename, bool useOld = false)
{
// Get the file descriptor
if (!TryGetFileDescriptor(index, out var fileDescriptor) || fileDescriptor == null)
return false;
// If the file is split
if (fileDescriptor.LinkFlags == LinkFlags.LINK_PREV)
return FileSave((int)fileDescriptor.LinkPrevious, filename, useOld);
// Get the reader at the index
var reader = Reader.Create(this, index, fileDescriptor);
if (reader == null)
return false;
// Create the output file and hasher
FileStream output = File.OpenWrite(filename);
var md5 = new HashWrapper(HashType.MD5);
long readBytesLeft = (long)GetReadableBytes(fileDescriptor);
long writeBytesLeft = (long)GetWritableBytes(fileDescriptor);
byte[] inputBuffer;
byte[] outputBuffer = new byte[BUFFER_SIZE];
long totalWritten = 0;
// Read while there are bytes remaining
while (readBytesLeft > 0 && writeBytesLeft > 0)
{
long bytesToWrite = BUFFER_SIZE;
int result;
// Handle compressed files
#if NET20 || NET35
if ((fileDescriptor.Flags & FileFlags.FILE_COMPRESSED) != 0)
#else
if (fileDescriptor.Flags.HasFlag(FileFlags.FILE_COMPRESSED))
#endif
{
// Attempt to read the length value
byte[] lengthArr = new byte[sizeof(ushort)];
if (!reader.Read(lengthArr, 0, lengthArr.Length))
{
Console.Error.WriteLine($"Failed to read {lengthArr.Length} bytes of file {index} ({GetFileName(index)}) from input cabinet file {fileDescriptor.Volume}");
reader.Dispose();
output?.Close();
return false;
}
// Attempt to read the specified number of bytes
ushort bytesToRead = BitConverter.ToUInt16(lengthArr, 0);
inputBuffer = new byte[BUFFER_SIZE + 1];
if (!reader.Read(inputBuffer, 0, bytesToRead))
{
Console.Error.WriteLine($"Failed to read {lengthArr.Length} bytes of file {index} ({GetFileName(index)}) from input cabinet file {fileDescriptor.Volume}");
reader.Dispose();
output?.Close();
return false;
}
// Add a null byte to make inflate happy
inputBuffer[bytesToRead] = 0;
ulong readBytes = (ulong)(bytesToRead + 1);
// Uncompress into a buffer
if (useOld)
result = UncompressOld(outputBuffer, ref bytesToWrite, inputBuffer, ref readBytes);
else
result = Uncompress(outputBuffer, ref bytesToWrite, inputBuffer, ref readBytes);
// If we didn't get a positive result that's not a data error (false positives)
if (result != zlibConst.Z_OK && result != zlibConst.Z_DATA_ERROR)
{
Console.Error.WriteLine($"Decompression failed with code {result.ToZlibConstName()}. bytes_to_read={bytesToRead}, volume={fileDescriptor.Volume}, read_bytes={readBytes}");
reader.Dispose();
output?.Close();
return false;
}
// Set remaining bytes
readBytesLeft -= 2;
readBytesLeft -= bytesToRead;
}
// Handle uncompressed files
else
{
bytesToWrite = Math.Min(readBytesLeft, BUFFER_SIZE);
if (!reader.Read(outputBuffer, 0, (int)bytesToWrite))
{
Console.Error.WriteLine($"Failed to write {bytesToWrite} bytes from input cabinet file {fileDescriptor.Volume}");
reader.Dispose();
output?.Close();
return false;
}
// Set remaining bytes
readBytesLeft -= (uint)bytesToWrite;
}
// Hash and write the next block
bytesToWrite = Math.Min(bytesToWrite, writeBytesLeft);
md5.Process(outputBuffer, 0, (int)bytesToWrite);
output?.Write(outputBuffer, 0, (int)bytesToWrite);
totalWritten += bytesToWrite;
writeBytesLeft -= bytesToWrite;
}
// Validate the number of bytes written
if ((long)fileDescriptor.ExpandedSize != totalWritten)
Console.WriteLine($"Expanded size of file {index} ({GetFileName(index)}) expected to be {fileDescriptor.ExpandedSize}, but was {totalWritten}");
// Finalize output values
md5.Terminate();
reader?.Dispose();
output?.Close();
// Validate the data written, if required
if (MajorVersion >= 6)
{
string expectedMd5 = BitConverter.ToString(fileDescriptor.MD5!);
expectedMd5 = expectedMd5.ToLowerInvariant().Replace("-", string.Empty);
string? actualMd5 = md5.CurrentHashString;
if (actualMd5 == null || actualMd5 != expectedMd5)
{
Console.Error.WriteLine($"MD5 checksum failure for file {index} ({GetFileName(index)})");
return false;
}
}
return true;
}
/// <summary>
/// Save the file at the given index to the filename specified as raw
/// </summary>
public bool FileSaveRaw(int index, string filename)
{
// Get the file descriptor
if (!TryGetFileDescriptor(index, out var fileDescriptor) || fileDescriptor == null)
return false;
// If the file is split
if (fileDescriptor.LinkFlags == LinkFlags.LINK_PREV)
return FileSaveRaw((int)fileDescriptor.LinkPrevious, filename);
// Get the reader at the index
var reader = Reader.Create(this, index, fileDescriptor);
if (reader == null)
return false;
// Create the output file
FileStream output = File.OpenWrite(filename);
ulong bytesLeft = GetReadableBytes(fileDescriptor);
byte[] outputBuffer = new byte[BUFFER_SIZE];
// Read while there are bytes remaining
while (bytesLeft > 0)
{
ulong bytesToWrite = Math.Min(bytesLeft, BUFFER_SIZE);
if (!reader.Read(outputBuffer, 0, (int)bytesToWrite))
{
Console.Error.WriteLine($"Failed to read {bytesToWrite} bytes from input cabinet file {fileDescriptor.Volume}");
reader.Dispose();
output?.Close();
return false;
}
// Set remaining bytes
bytesLeft -= (uint)bytesToWrite;
// Write the next block
output.Write(outputBuffer, 0, (int)bytesToWrite);
}
// Finalize output values
reader.Dispose();
output?.Close();
return true;
}
/// <summary>
/// Uncompress a source byte array to a destination
/// </summary>
private unsafe static int Uncompress(byte[] dest, ref long destLen, byte[] source, ref ulong sourceLen)
{
fixed (byte* sourcePtr = source)
fixed (byte* destPtr = dest)
{
var stream = new ZLib.z_stream_s
{
next_in = sourcePtr,
avail_in = (uint)sourceLen,
next_out = destPtr,
avail_out = (uint)destLen,
};
// make second parameter negative to disable checksum verification
int err = ZLib.inflateInit2_(stream, -MAX_WBITS, ZLib.zlibVersion(), source.Length);
if (err != zlibConst.Z_OK)
return err;
err = ZLib.inflate(stream, 1);
if (err != zlibConst.Z_OK && err != zlibConst.Z_STREAM_END)
{
ZLib.inflateEnd(stream);
return err;
}
destLen = stream.total_out;
sourceLen = stream.total_in;
return ZLib.inflateEnd(stream);
}
}
/// <summary>
/// Uncompress a source byte array to a destination (old version)
/// </summary>
private unsafe static int UncompressOld(byte[] dest, ref long destLen, byte[] source, ref ulong sourceLen)
{
fixed (byte* sourcePtr = source)
fixed (byte* destPtr = dest)
{
var stream = new ZLib.z_stream_s
{
next_in = sourcePtr,
avail_in = (uint)sourceLen,
next_out = destPtr,
avail_out = (uint)destLen,
};
destLen = 0;
sourceLen = 0;
// make second parameter negative to disable checksum verification
int err = ZLib.inflateInit2_(stream, -MAX_WBITS, ZLib.zlibVersion(), source.Length);
if (err != zlibConst.Z_OK)
return err;
while (stream.avail_in > 1)
{
err = ZLib.inflate(stream, 1);
if (err != zlibConst.Z_OK)
{
ZLib.inflateEnd(stream);
return err;
}
}
destLen = stream.total_out;
sourceLen = stream.total_in;
return ZLib.inflateEnd(stream);
}
}
#endregion
#region Obfuscation
/// <summary>
/// Deobfuscate a buffer
/// </summary>
public static void Deobfuscate(byte[] buffer, long size, ref uint offset)
{
offset = Deobfuscate(buffer, size, offset);
}
/// <summary>
/// Deobfuscate a buffer with a seed value
/// </summary>
/// <remarks>Seed is 0 at file start</remarks>
public static uint Deobfuscate(byte[] buffer, long size, uint seed)
{
for (int i = 0; size > 0; size--, i++, seed++)
{
buffer[i] = (byte)(ROR8(buffer[i] ^ 0xd5, 2) - (seed % 0x47));
}
return seed;
}
/// <summary>
/// Obfuscate a buffer
/// </summary>
public static void Obfuscate(byte[] buffer, long size, ref uint offset)
{
offset = Obfuscate(buffer, size, offset);
}
/// <summary>
/// Obfuscate a buffer with a seed value
/// </summary>
/// <remarks>Seed is 0 at file start</remarks>
public static uint Obfuscate(byte[] buffer, long size, uint seed)
{
for (int i = 0; size > 0; size--, i++, seed++)
{
buffer[i] = (byte)(ROL8(buffer[i] ^ 0xd5, 2) + (seed % 0x47));
}
return seed;
}
/// <summary>
/// Rotate Right 8
/// </summary>
private static int ROR8(int x, byte n) => (x >> n) | (x << (8 - n));
/// <summary>
/// Rotate Left 8
/// </summary>
private static int ROL8(int x, byte n) => (x << n) | (x >> (8 - n));
#endregion
#region Helper Classes
/// <summary>
/// Helper to read a single file from a cabinet set
/// </summary>
private class Reader : IDisposable
{
#region Private Instance Variables
/// <summary>
/// Cabinet file to read from
/// </summary>
private readonly InstallShieldCabinet _cabinet;
/// <summary>
/// Currently selected index
/// </summary>
private readonly uint _index;
/// <summary>
/// File descriptor defining the currently selected index
/// </summary>
private readonly FileDescriptor _fileDescriptor;
/// <summary>
/// Offset in the data where the file exists
/// </summary>
private ulong _dataOffset;
/// <summary>
/// Number of bytes left in the current volume
/// </summary>
private ulong _volumeBytesLeft;
/// <summary>
/// Handle to the current volume stream
/// </summary>
private Stream? _volumeFile;
/// <summary>
/// Current volume header
/// </summary>
private VolumeHeader? _volumeHeader;
/// <summary>
/// Current volume ID
/// </summary>
private ushort _volumeId;
/// <summary>
/// Offset for obfuscation seed
/// </summary>
private uint _obfuscationOffset;
#endregion
#region Constructors
private Reader(InstallShieldCabinet cabinet, uint index, FileDescriptor fileDescriptor)
{
_cabinet = cabinet;
_index = index;
_fileDescriptor = fileDescriptor;
}
#endregion
/// <summary>
/// Create a new <see cref="Reader"> from an existing cabinet, index, and file descriptor
/// </summary>
public static Reader? Create(InstallShieldCabinet cabinet, int index, FileDescriptor fileDescriptor)
{
var reader = new Reader(cabinet, (uint)index, fileDescriptor);
for (; ; )
{
// If the volume is invalid
if (!reader.OpenVolume(fileDescriptor.Volume))
{
Console.Error.WriteLine($"Failed to open volume {fileDescriptor.Volume}");
return null;
}
else if (reader._volumeFile == null || reader._volumeHeader == null)
{
Console.Error.WriteLine($"Volume {fileDescriptor.Volume} is invalid");
return null;
}
// Start with the correct volume for IS5 cabinets
if (reader._cabinet.MajorVersion <= 5 && index > (int)reader._volumeHeader.LastFileIndex)
{
// Normalize the volume ID for odd cases
if (fileDescriptor.Volume == ushort.MinValue || fileDescriptor.Volume == ushort.MaxValue)
fileDescriptor.Volume = 1;
fileDescriptor.Volume++;
continue;
}
break;
}
return reader;
}
/// <summary>
/// Dispose of the current object
/// </summary>
public void Dispose()
{
_volumeFile?.Close();
}
#region Reading
/// <summary>
/// Read a certain number of bytes from the current volume
/// </summary>
public bool Read(byte[] buffer, int start, long size)
{
long bytesLeft = size;
while (bytesLeft > 0)
{
// Open the next volume, if necessary
if (_volumeBytesLeft == 0)
{
if (!OpenNextVolume(out _))
return false;
}
// Get the number of bytes to read from this volume
int bytesToRead = (int)Math.Min(bytesLeft, (long)_volumeBytesLeft);
if (bytesToRead == 0)
break;
// Read as much as possible from this volume
if (bytesToRead != _volumeFile!.Read(buffer, start, bytesToRead))
return false;
// Set the number of bytes left
bytesLeft -= bytesToRead;
_volumeBytesLeft -= (uint)bytesToRead;
}
#if NET20 || NET35
if ((_fileDescriptor.Flags & FileFlags.FILE_OBFUSCATED) != 0)
#else
if (_fileDescriptor.Flags.HasFlag(FileFlags.FILE_OBFUSCATED))
#endif
Deobfuscate(buffer, size, ref _obfuscationOffset);
return true;
}
/// <summary>
/// Open the next volume based on the current index
/// </summary>
private bool OpenNextVolume(out ushort nextVolume)
{
nextVolume = (ushort)(_volumeId + 1);
return OpenVolume(nextVolume);
}
/// <summary>
/// Open the volume at the inputted index
/// </summary>
private bool OpenVolume(ushort volume)
{
// Read the volume from the cabinet set
var next = _cabinet.OpenVolume(volume, out var volumeStream);
if (next?.VolumeHeader == null || volumeStream == null)
{
Console.Error.WriteLine($"Failed to open input cabinet file {volume}");
return false;
}
// Assign the next items
_volumeFile?.Close();
_volumeFile = volumeStream;
_volumeHeader = next.VolumeHeader;
// Enable support for split archives for IS5
if (_cabinet.MajorVersion == 5)
{
if (_index < (_cabinet.FileCount - 1)
&& _index == _volumeHeader.LastFileIndex
&& _volumeHeader.LastFileSizeCompressed != _fileDescriptor.CompressedSize)
{
_fileDescriptor.Flags |= FileFlags.FILE_SPLIT;
}
else if (_index > 0
&& _index == _volumeHeader.FirstFileIndex
&& _volumeHeader.FirstFileSizeCompressed != _fileDescriptor.CompressedSize)
{
_fileDescriptor.Flags |= FileFlags.FILE_SPLIT;
}
}
ulong volumeBytesLeftCompressed, volumeBytesLeftExpanded;
#if NET20 || NET35
if ((_fileDescriptor.Flags & FileFlags.FILE_SPLIT) != 0)
#else
if (_fileDescriptor.Flags.HasFlag(FileFlags.FILE_SPLIT))
#endif
{
if (_index == _volumeHeader.LastFileIndex && _volumeHeader.LastFileOffset != 0x7FFFFFFF)
{
// can be first file too
_dataOffset = _volumeHeader.LastFileOffset;
volumeBytesLeftExpanded = _volumeHeader.LastFileSizeExpanded;
volumeBytesLeftCompressed = _volumeHeader.LastFileSizeCompressed;
}
else if (_index == _volumeHeader.FirstFileIndex)
{
_dataOffset = _volumeHeader.FirstFileOffset;
volumeBytesLeftExpanded = _volumeHeader.FirstFileSizeExpanded;
volumeBytesLeftCompressed = _volumeHeader.FirstFileSizeCompressed;
}
else
{
return true;
}
}
else
{
_dataOffset = _fileDescriptor.DataOffset;
volumeBytesLeftExpanded = _fileDescriptor.ExpandedSize;
volumeBytesLeftCompressed = _fileDescriptor.CompressedSize;
}
#if NET20 || NET35
if ((_fileDescriptor.Flags & FileFlags.FILE_COMPRESSED) != 0)
#else
if (_fileDescriptor.Flags.HasFlag(FileFlags.FILE_COMPRESSED))
#endif
_volumeBytesLeft = volumeBytesLeftCompressed;
else
_volumeBytesLeft = volumeBytesLeftExpanded;
_volumeFile.Seek((long)_dataOffset, SeekOrigin.Begin);
_volumeId = volume;
return true;
}
#endregion
}
#endregion
}
}

View File

@@ -1,15 +1,10 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
using SabreTools.Hashing;
using SabreTools.IO.Compression.zlib;
using SabreTools.Models.InstallShieldCabinet;
using SabreTools.Serialization.Interfaces;
using static SabreTools.Models.InstallShieldCabinet.Constants;
namespace SabreTools.Serialization.Wrappers
{
public partial class InstallShieldCabinet : WrapperBase<Cabinet>, IExtractable
public partial class InstallShieldCabinet : WrapperBase<Cabinet>
{
#region Descriptive Properties
@@ -68,64 +63,31 @@ namespace SabreTools.Serialization.Wrappers
/// <inheritdoc cref="Cabinet.VolumeHeader"/>
public VolumeHeader? VolumeHeader => Model.VolumeHeader;
/// <summary>
/// Reference to the next cabinet header
/// </summary>
/// <remarks>Only used in multi-file</remarks>
public InstallShieldCabinet? Next { get; set; }
/// <summary>
/// Reference to the next previous header
/// </summary>
/// <remarks>Only used in multi-file</remarks>
public InstallShieldCabinet? Prev { get; set; }
/// <summary>
/// Volume index ID, 0 for headers
/// </summary>
/// <remarks>Only used in multi-file</remarks>
public ushort VolumeID { get; set; }
#endregion
#region Extraction State
/// <summary>
/// Base filename path for related CAB files
/// </summary>
internal string? FilenamePattern { get; set; }
#endregion
#region Constants
/// <summary>
/// Default buffer size
/// </summary>
private const int BUFFER_SIZE = 64 * 1024;
/// <summary>
/// Maximum size of the window in bits
/// </summary>
private const int MAX_WBITS = 15;
#endregion
#region Constructors
/// <inheritdoc/>
public InstallShieldCabinet(Cabinet? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public InstallShieldCabinet(Cabinet model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public InstallShieldCabinet(Cabinet? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public InstallShieldCabinet(Cabinet model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public InstallShieldCabinet(Cabinet model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public InstallShieldCabinet(Cabinet model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public InstallShieldCabinet(Cabinet model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public InstallShieldCabinet(Cabinet model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create an InstallShield Cabinet from a byte array and offset
@@ -164,12 +126,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.InstallShieldCabinet.DeserializeStream(data);
var model = new Deserializers.InstallShieldCabinet().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new InstallShieldCabinet(model, data);
return new InstallShieldCabinet(model, data, currentOffset);
}
catch
{
@@ -179,148 +140,6 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region Cabinet Set
/// <summary>
/// Open a cabinet set for reading, if possible
/// </summary>
/// <param name="pattern">Filename pattern for matching cabinet files</param>
/// <returns>Wrapper representing the set, null on error</returns>
public static InstallShieldCabinet? OpenSet(string? pattern)
{
// An invalid pattern means no cabinet files
if (string.IsNullOrEmpty(pattern))
return null;
// Create a placeholder wrapper for output
InstallShieldCabinet? set = null;
// Loop until there are no parts left
bool iterate = true;
InstallShieldCabinet? previous = null;
for (ushort i = 1; iterate; i++)
{
var file = OpenFileForReading(pattern, i, HEADER_SUFFIX);
if (file != null)
iterate = false;
else
file = OpenFileForReading(pattern, i, CABINET_SUFFIX);
if (file == null)
break;
var current = Create(file);
if (current == null)
break;
current.VolumeID = i;
if (previous != null)
{
previous.Next = current;
current.Prev = previous;
}
else
{
set = current;
previous = current;
}
}
// Set the pattern, if possible
if (set != null)
set.FilenamePattern = pattern;
return set;
}
/// <summary>
/// Open the numbered cabinet set volume
/// </summary>
/// <param name="volumeId">Volume ID, 1-indexed</param>
/// <returns>Wrapper representing the volume on success, null otherwise</returns>
public InstallShieldCabinet? OpenVolume(ushort volumeId, out Stream? volumeStream)
{
// Normalize the volume ID for odd cases
if (volumeId == ushort.MinValue || volumeId == ushort.MaxValue)
volumeId = 1;
// Try to open the file as a stream
volumeStream = OpenFileForReading(FilenamePattern, volumeId, CABINET_SUFFIX);
if (volumeStream == null)
{
Console.Error.WriteLine($"Failed to open input cabinet file {volumeId}");
return null;
}
// Try to parse the stream into a cabinet
var volume = Create(volumeStream);
if (volume == null)
{
Console.Error.WriteLine($"Failed to open input cabinet file {volumeId}");
return null;
}
// Set the volume ID and return
volume.VolumeID = volumeId;
return volume;
}
/// <summary>
/// Open a cabinet file for reading
/// </summary>
/// <param name="index">Cabinet part index to be opened</param>
/// <param name="suffix">Cabinet files suffix (e.g. `.cab`)</param>
/// <returns>A Stream representing the cabinet part, null on error</returns>
public Stream? OpenFileForReading(int index, string suffix)
=> OpenFileForReading(FilenamePattern, index, suffix);
/// <summary>
/// Create the generic filename pattern to look for from the input filename
/// </summary>
/// <returns>String representing the filename pattern for a cabinet set, null on error</returns>
private static string? CreateFilenamePattern(string filename)
{
string? pattern = null;
if (string.IsNullOrEmpty(filename))
return pattern;
string? directory = Path.GetDirectoryName(Path.GetFullPath(filename));
if (directory != null)
pattern = Path.Combine(directory, Path.GetFileNameWithoutExtension(filename));
else
pattern = Path.GetFileNameWithoutExtension(filename);
return new Regex(@"\d+$").Replace(pattern, string.Empty);
}
/// <summary>
/// Open a cabinet file for reading
/// </summary>
/// <param name="pattern">Filename pattern for matching cabinet files</param>
/// <param name="index">Cabinet part index to be opened</param>
/// <param name="suffix">Cabinet files suffix (e.g. `.cab`)</param>
/// <returns>A Stream representing the cabinet part, null on error</returns>
private static Stream? OpenFileForReading(string? pattern, int index, string suffix)
{
// An invalid pattern means no cabinet files
if (string.IsNullOrEmpty(pattern))
return null;
// Attempt lower-case extension
string filename = $"{pattern}{index}.{suffix}";
if (File.Exists(filename))
return File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// Attempt upper-case extension
filename = $"{pattern}{index}.{suffix.ToUpperInvariant()}";
if (File.Exists(filename))
return File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
return null;
}
#endregion
#region Component
/// <summary>
@@ -374,336 +193,6 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Open the full set if possible
var cabinet = this;
if (Filename != null)
{
// Get the name of the first cabinet file or header
string pattern = CreateFilenamePattern(Filename)!;
bool cabinetHeaderExists = File.Exists(pattern + "1.hdr");
bool shouldScanCabinet = cabinetHeaderExists
? Filename.Equals(pattern + "1.hdr", StringComparison.OrdinalIgnoreCase)
: Filename.Equals(pattern + "1.cab", StringComparison.OrdinalIgnoreCase);
// If we have anything but the first file
if (!shouldScanCabinet)
return false;
// Open the set from the pattern
cabinet = OpenSet(pattern);
}
// If the cabinet set could not be opened
if (cabinet == null)
return false;
try
{
for (int i = 0; i < cabinet.FileCount; i++)
{
try
{
// Check if the file is valid first
if (!cabinet.FileIsValid(i))
continue;
// Ensure directory separators are consistent
string filename = cabinet.GetFileName(i) ?? $"BAD_FILENAME{i}";
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
cabinet.FileSave(i, filename);
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
}
}
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Save the file at the given index to the filename specified
/// </summary>
public bool FileSave(int index, string filename, bool useOld = false)
{
// Get the file descriptor
if (!TryGetFileDescriptor(index, out var fileDescriptor) || fileDescriptor == null)
return false;
// If the file is split
if (fileDescriptor.LinkFlags == LinkFlags.LINK_PREV)
return FileSave((int)fileDescriptor.LinkPrevious, filename, useOld);
// Get the reader at the index
var reader = Reader.Create(this, index, fileDescriptor);
if (reader == null)
return false;
// Create the output file and hasher
FileStream output = File.OpenWrite(filename);
var md5 = new HashWrapper(HashType.MD5);
long readBytesLeft = (long)GetReadableBytes(fileDescriptor);
long writeBytesLeft = (long)GetWritableBytes(fileDescriptor);
byte[] inputBuffer;
byte[] outputBuffer = new byte[BUFFER_SIZE];
long totalWritten = 0;
// Read while there are bytes remaining
while (readBytesLeft > 0 && writeBytesLeft > 0)
{
long bytesToWrite = BUFFER_SIZE;
int result;
// Handle compressed files
#if NET20 || NET35
if ((fileDescriptor.Flags & FileFlags.FILE_COMPRESSED) != 0)
#else
if (fileDescriptor.Flags.HasFlag(FileFlags.FILE_COMPRESSED))
#endif
{
// Attempt to read the length value
byte[] lengthArr = new byte[sizeof(ushort)];
if (!reader.Read(lengthArr, 0, lengthArr.Length))
{
Console.Error.WriteLine($"Failed to read {lengthArr.Length} bytes of file {index} ({GetFileName(index)}) from input cabinet file {fileDescriptor.Volume}");
reader.Dispose();
output?.Close();
return false;
}
// Attempt to read the specified number of bytes
ushort bytesToRead = BitConverter.ToUInt16(lengthArr, 0);
inputBuffer = new byte[BUFFER_SIZE + 1];
if (!reader.Read(inputBuffer, 0, bytesToRead))
{
Console.Error.WriteLine($"Failed to read {lengthArr.Length} bytes of file {index} ({GetFileName(index)}) from input cabinet file {fileDescriptor.Volume}");
reader.Dispose();
output?.Close();
return false;
}
// Add a null byte to make inflate happy
inputBuffer[bytesToRead] = 0;
ulong readBytes = (ulong)(bytesToRead + 1);
// Uncompress into a buffer
if (useOld)
result = UncompressOld(outputBuffer, ref bytesToWrite, inputBuffer, ref readBytes);
else
result = Uncompress(outputBuffer, ref bytesToWrite, inputBuffer, ref readBytes);
// If we didn't get a positive result that's not a data error (false positives)
if (result != zlibConst.Z_OK && result != zlibConst.Z_DATA_ERROR)
{
Console.Error.WriteLine($"Decompression failed with code {result.ToZlibConstName()}. bytes_to_read={bytesToRead}, volume={fileDescriptor.Volume}, read_bytes={readBytes}");
reader.Dispose();
output?.Close();
return false;
}
// Set remaining bytes
readBytesLeft -= 2;
readBytesLeft -= bytesToRead;
}
// Handle uncompressed files
else
{
bytesToWrite = Math.Min(readBytesLeft, BUFFER_SIZE);
if (!reader.Read(outputBuffer, 0, (int)bytesToWrite))
{
Console.Error.WriteLine($"Failed to write {bytesToWrite} bytes from input cabinet file {fileDescriptor.Volume}");
reader.Dispose();
output?.Close();
return false;
}
// Set remaining bytes
readBytesLeft -= (uint)bytesToWrite;
}
// Hash and write the next block
bytesToWrite = Math.Min(bytesToWrite, writeBytesLeft);
md5.Process(outputBuffer, 0, (int)bytesToWrite);
output?.Write(outputBuffer, 0, (int)bytesToWrite);
totalWritten += bytesToWrite;
writeBytesLeft -= bytesToWrite;
}
// Validate the number of bytes written
if ((long)fileDescriptor.ExpandedSize != totalWritten)
Console.WriteLine($"Expanded size of file {index} ({GetFileName(index)}) expected to be {fileDescriptor.ExpandedSize}, but was {totalWritten}");
// Finalize output values
md5.Terminate();
reader?.Dispose();
output?.Close();
// Validate the data written, if required
if (MajorVersion >= 6)
{
string expectedMd5 = BitConverter.ToString(fileDescriptor.MD5!);
expectedMd5 = expectedMd5.ToLowerInvariant().Replace("-", string.Empty);
string? actualMd5 = md5.CurrentHashString;
if (actualMd5 == null || actualMd5 != expectedMd5)
{
Console.Error.WriteLine($"MD5 checksum failure for file {index} ({GetFileName(index)})");
return false;
}
}
return true;
}
/// <summary>
/// Save the file at the given index to the filename specified as raw
/// </summary>
public bool FileSaveRaw(int index, string filename)
{
// Get the file descriptor
if (!TryGetFileDescriptor(index, out var fileDescriptor) || fileDescriptor == null)
return false;
// If the file is split
if (fileDescriptor.LinkFlags == LinkFlags.LINK_PREV)
return FileSaveRaw((int)fileDescriptor.LinkPrevious, filename);
// Get the reader at the index
var reader = Reader.Create(this, index, fileDescriptor);
if (reader == null)
return false;
// Create the output file
FileStream output = File.OpenWrite(filename);
ulong bytesLeft = GetReadableBytes(fileDescriptor);
byte[] outputBuffer = new byte[BUFFER_SIZE];
// Read while there are bytes remaining
while (bytesLeft > 0)
{
ulong bytesToWrite = Math.Min(bytesLeft, BUFFER_SIZE);
if (!reader.Read(outputBuffer, 0, (int)bytesToWrite))
{
Console.Error.WriteLine($"Failed to read {bytesToWrite} bytes from input cabinet file {fileDescriptor.Volume}");
reader.Dispose();
output?.Close();
return false;
}
// Set remaining bytes
bytesLeft -= (uint)bytesToWrite;
// Write the next block
output.Write(outputBuffer, 0, (int)bytesToWrite);
}
// Finalize output values
reader.Dispose();
output?.Close();
return true;
}
/// <summary>
/// Uncompress a source byte array to a destination
/// </summary>
private unsafe static int Uncompress(byte[] dest, ref long destLen, byte[] source, ref ulong sourceLen)
{
fixed (byte* sourcePtr = source)
fixed (byte* destPtr = dest)
{
var stream = new ZLib.z_stream_s
{
next_in = sourcePtr,
avail_in = (uint)sourceLen,
next_out = destPtr,
avail_out = (uint)destLen,
};
// make second parameter negative to disable checksum verification
int err = ZLib.inflateInit2_(stream, -MAX_WBITS, ZLib.zlibVersion(), source.Length);
if (err != zlibConst.Z_OK)
return err;
err = ZLib.inflate(stream, 1);
if (err != zlibConst.Z_OK && err != zlibConst.Z_STREAM_END)
{
ZLib.inflateEnd(stream);
return err;
}
destLen = stream.total_out;
sourceLen = stream.total_in;
return ZLib.inflateEnd(stream);
}
}
/// <summary>
/// Uncompress a source byte array to a destination (old version)
/// </summary>
private unsafe static int UncompressOld(byte[] dest, ref long destLen, byte[] source, ref ulong sourceLen)
{
fixed (byte* sourcePtr = source)
fixed (byte* destPtr = dest)
{
var stream = new ZLib.z_stream_s
{
next_in = sourcePtr,
avail_in = (uint)sourceLen,
next_out = destPtr,
avail_out = (uint)destLen,
};
destLen = 0;
sourceLen = 0;
// make second parameter negative to disable checksum verification
int err = ZLib.inflateInit2_(stream, -MAX_WBITS, ZLib.zlibVersion(), source.Length);
if (err != zlibConst.Z_OK)
return err;
while (stream.avail_in > 1)
{
err = ZLib.inflate(stream, 1);
if (err != zlibConst.Z_OK)
{
ZLib.inflateEnd(stream);
return err;
}
}
destLen = stream.total_out;
sourceLen = stream.total_in;
return ZLib.inflateEnd(stream);
}
}
#endregion
#region File
/// <summary>
@@ -882,312 +371,5 @@ namespace SabreTools.Serialization.Wrappers
=> GetFileGroupFromFile(index)?.Name;
#endregion
#region Obfuscation
/// <summary>
/// Deobfuscate a buffer
/// </summary>
public static void Deobfuscate(byte[] buffer, long size, ref uint offset)
{
offset = Deobfuscate(buffer, size, offset);
}
/// <summary>
/// Deobfuscate a buffer with a seed value
/// </summary>
/// <remarks>Seed is 0 at file start</remarks>
public static uint Deobfuscate(byte[] buffer, long size, uint seed)
{
for (int i = 0; size > 0; size--, i++, seed++)
{
buffer[i] = (byte)(ROR8(buffer[i] ^ 0xd5, 2) - (seed % 0x47));
}
return seed;
}
/// <summary>
/// Obfuscate a buffer
/// </summary>
public static void Obfuscate(byte[] buffer, long size, ref uint offset)
{
offset = Obfuscate(buffer, size, offset);
}
/// <summary>
/// Obfuscate a buffer with a seed value
/// </summary>
/// <remarks>Seed is 0 at file start</remarks>
public static uint Obfuscate(byte[] buffer, long size, uint seed)
{
for (int i = 0; size > 0; size--, i++, seed++)
{
buffer[i] = (byte)(ROL8(buffer[i] ^ 0xd5, 2) + (seed % 0x47));
}
return seed;
}
/// <summary>
/// Rotate Right 8
/// </summary>
private static int ROR8(int x, byte n) => (x >> n) | (x << (8 - n));
/// <summary>
/// Rotate Left 8
/// </summary>
private static int ROL8(int x, byte n) => (x << n) | (x >> (8 - n));
#endregion
#region Helper Classes
/// <summary>
/// Helper to read a single file from a cabinet set
/// </summary>
private class Reader : IDisposable
{
#region Private Instance Variables
/// <summary>
/// Cabinet file to read from
/// </summary>
private readonly InstallShieldCabinet _cabinet;
/// <summary>
/// Currently selected index
/// </summary>
private readonly uint _index;
/// <summary>
/// File descriptor defining the currently selected index
/// </summary>
private readonly FileDescriptor _fileDescriptor;
/// <summary>
/// Offset in the data where the file exists
/// </summary>
private ulong _dataOffset;
/// <summary>
/// Number of bytes left in the current volume
/// </summary>
private ulong _volumeBytesLeft;
/// <summary>
/// Handle to the current volume stream
/// </summary>
private Stream? _volumeFile;
/// <summary>
/// Current volume header
/// </summary>
private VolumeHeader? _volumeHeader;
/// <summary>
/// Current volume ID
/// </summary>
private ushort _volumeId;
/// <summary>
/// Offset for obfuscation seed
/// </summary>
private uint _obfuscationOffset;
#endregion
#region Constructors
private Reader(InstallShieldCabinet cabinet, uint index, FileDescriptor fileDescriptor)
{
_cabinet = cabinet;
_index = index;
_fileDescriptor = fileDescriptor;
}
#endregion
/// <summary>
/// Create a new <see cref="Reader"> from an existing cabinet, index, and file descriptor
/// </summary>
public static Reader? Create(InstallShieldCabinet cabinet, int index, FileDescriptor fileDescriptor)
{
var reader = new Reader(cabinet, (uint)index, fileDescriptor);
for (; ; )
{
// If the volume is invalid
if (!reader.OpenVolume(fileDescriptor.Volume))
{
Console.Error.WriteLine($"Failed to open volume {fileDescriptor.Volume}");
return null;
}
else if (reader._volumeFile == null || reader._volumeHeader == null)
{
Console.Error.WriteLine($"Volume {fileDescriptor.Volume} is invalid");
return null;
}
// Start with the correct volume for IS5 cabinets
if (reader._cabinet.MajorVersion <= 5 && index > (int)reader._volumeHeader.LastFileIndex)
{
// Normalize the volume ID for odd cases
if (fileDescriptor.Volume == ushort.MinValue || fileDescriptor.Volume == ushort.MaxValue)
fileDescriptor.Volume = 1;
fileDescriptor.Volume++;
continue;
}
break;
}
return reader;
}
/// <summary>
/// Dispose of the current object
/// </summary>
public void Dispose()
{
_volumeFile?.Close();
}
#region Reading
/// <summary>
/// Read a certain number of bytes from the current volume
/// </summary>
public bool Read(byte[] buffer, int start, long size)
{
long bytesLeft = size;
while (bytesLeft > 0)
{
// Open the next volume, if necessary
if (_volumeBytesLeft == 0)
{
if (!OpenNextVolume(out _))
return false;
}
// Get the number of bytes to read from this volume
int bytesToRead = (int)Math.Min(bytesLeft, (long)_volumeBytesLeft);
if (bytesToRead == 0)
break;
// Read as much as possible from this volume
if (bytesToRead != _volumeFile!.Read(buffer, start, bytesToRead))
return false;
// Set the number of bytes left
bytesLeft -= bytesToRead;
_volumeBytesLeft -= (uint)bytesToRead;
}
#if NET20 || NET35
if ((_fileDescriptor.Flags & FileFlags.FILE_OBFUSCATED) != 0)
#else
if (_fileDescriptor.Flags.HasFlag(FileFlags.FILE_OBFUSCATED))
#endif
Deobfuscate(buffer, size, ref _obfuscationOffset);
return true;
}
/// <summary>
/// Open the next volume based on the current index
/// </summary>
private bool OpenNextVolume(out ushort nextVolume)
{
nextVolume = (ushort)(_volumeId + 1);
return OpenVolume(nextVolume);
}
/// <summary>
/// Open the volume at the inputted index
/// </summary>
private bool OpenVolume(ushort volume)
{
// Read the volume from the cabinet set
var next = _cabinet.OpenVolume(volume, out var volumeStream);
if (next?.VolumeHeader == null || volumeStream == null)
{
Console.Error.WriteLine($"Failed to open input cabinet file {volume}");
return false;
}
// Assign the next items
_volumeFile?.Close();
_volumeFile = volumeStream;
_volumeHeader = next.VolumeHeader;
// Enable support for split archives for IS5
if (_cabinet.MajorVersion == 5)
{
if (_index < (_cabinet.FileCount - 1)
&& _index == _volumeHeader.LastFileIndex
&& _volumeHeader.LastFileSizeCompressed != _fileDescriptor.CompressedSize)
{
_fileDescriptor.Flags |= FileFlags.FILE_SPLIT;
}
else if (_index > 0
&& _index == _volumeHeader.FirstFileIndex
&& _volumeHeader.FirstFileSizeCompressed != _fileDescriptor.CompressedSize)
{
_fileDescriptor.Flags |= FileFlags.FILE_SPLIT;
}
}
ulong volumeBytesLeftCompressed, volumeBytesLeftExpanded;
#if NET20 || NET35
if ((_fileDescriptor.Flags & FileFlags.FILE_SPLIT) != 0)
#else
if (_fileDescriptor.Flags.HasFlag(FileFlags.FILE_SPLIT))
#endif
{
if (_index == _volumeHeader.LastFileIndex && _volumeHeader.LastFileOffset != 0x7FFFFFFF)
{
// can be first file too
_dataOffset = _volumeHeader.LastFileOffset;
volumeBytesLeftExpanded = _volumeHeader.LastFileSizeExpanded;
volumeBytesLeftCompressed = _volumeHeader.LastFileSizeCompressed;
}
else if (_index == _volumeHeader.FirstFileIndex)
{
_dataOffset = _volumeHeader.FirstFileOffset;
volumeBytesLeftExpanded = _volumeHeader.FirstFileSizeExpanded;
volumeBytesLeftCompressed = _volumeHeader.FirstFileSizeCompressed;
}
else
{
return true;
}
}
else
{
_dataOffset = _fileDescriptor.DataOffset;
volumeBytesLeftExpanded = _fileDescriptor.ExpandedSize;
volumeBytesLeftCompressed = _fileDescriptor.CompressedSize;
}
#if NET20 || NET35
if ((_fileDescriptor.Flags & FileFlags.FILE_COMPRESSED) != 0)
#else
if (_fileDescriptor.Flags.HasFlag(FileFlags.FILE_COMPRESSED))
#endif
_volumeBytesLeft = volumeBytesLeftCompressed;
else
_volumeBytesLeft = volumeBytesLeftExpanded;
_volumeFile.Seek((long)_dataOffset, SeekOrigin.Begin);
_volumeId = volume;
return true;
}
#endregion
}
#endregion
}
}

View File

@@ -0,0 +1,66 @@
using System;
using System.IO;
using SabreTools.IO.Compression.SZDD;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class LZKWAJ : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Get the length of the compressed data
long compressedSize = Length - DataOffset;
if (compressedSize < DataOffset)
return false;
// Read in the data as an array
byte[]? contents = ReadRangeFromSource(DataOffset, (int)compressedSize);
if (contents.Length == 0)
return false;
// Get the decompressor
var decompressor = Decompressor.CreateKWAJ(contents, CompressionType);
if (decompressor == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Create the full output path
string filename = FileName ?? "tempfile";
if (FileExtension != null)
filename += $".{FileExtension}";
// Ensure directory separators are consistent
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
decompressor.CopyTo(fs);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
}
}

View File

@@ -1,13 +1,9 @@
using System;
using System.IO;
using SabreTools.IO.Compression.SZDD;
using SabreTools.IO.Extensions;
using SabreTools.Models.LZ;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class LZKWAJ : WrapperBase<KWAJFile>, IExtractable
public partial class LZKWAJ : WrapperBase<KWAJFile>
{
#region Descriptive Properties
@@ -35,18 +31,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public LZKWAJ(KWAJFile? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public LZKWAJ(KWAJFile model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public LZKWAJ(KWAJFile? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public LZKWAJ(KWAJFile model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public LZKWAJ(KWAJFile model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public LZKWAJ(KWAJFile model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public LZKWAJ(KWAJFile model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public LZKWAJ(KWAJFile model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create an LZ (KWAJ variant) from a byte array and offset
@@ -85,12 +89,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.LZKWAJ.DeserializeStream(data);
var model = new Deserializers.LZKWAJ().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new LZKWAJ(model, data);
return new LZKWAJ(model, data, currentOffset);
}
catch
{
@@ -99,65 +102,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Get the length of the compressed data
long compressedSize = Length - DataOffset;
if (compressedSize < DataOffset)
return false;
// Read in the data as an array
byte[]? contents = _dataSource.ReadFrom(DataOffset, (int)compressedSize, retainPosition: true);
if (contents == null)
return false;
// Get the decompressor
var decompressor = Decompressor.CreateKWAJ(contents, CompressionType);
if (decompressor == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Create the full output path
string filename = FileName ?? "tempfile";
if (FileExtension != null)
filename += $".{FileExtension}";
// Ensure directory separators are consistent
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
decompressor.CopyTo(fs);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
#endregion
}
}

View File

@@ -0,0 +1,62 @@
using System;
using System.IO;
using SabreTools.IO.Compression.SZDD;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class LZQBasic : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Get the length of the compressed data
long compressedSize = Length - 12;
if (compressedSize < 12)
return false;
// Read in the data as an array
byte[]? contents = ReadRangeFromSource(12, (int)compressedSize);
if (contents.Length == 0)
return false;
// Get the decompressor
var decompressor = Decompressor.CreateQBasic(contents);
if (decompressor == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
string filename = "tempfile.bin";
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
decompressor.CopyTo(fs);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
}
}

View File

@@ -1,13 +1,9 @@
using System;
using System.IO;
using SabreTools.IO.Compression.SZDD;
using SabreTools.IO.Extensions;
using SabreTools.Models.LZ;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class LZQBasic : WrapperBase<QBasicFile>, IExtractable
public partial class LZQBasic : WrapperBase<QBasicFile>
{
#region Descriptive Properties
@@ -19,18 +15,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public LZQBasic(QBasicFile? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public LZQBasic(QBasicFile model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public LZQBasic(QBasicFile? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public LZQBasic(QBasicFile model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public LZQBasic(QBasicFile model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public LZQBasic(QBasicFile model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public LZQBasic(QBasicFile model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public LZQBasic(QBasicFile model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create an LZ (QBasic variant) from a byte array and offset
@@ -69,12 +73,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.LZQBasic.DeserializeStream(data);
var model = new Deserializers.LZQBasic().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new LZQBasic(model, data);
return new LZQBasic(model, data, currentOffset);
}
catch
{
@@ -83,61 +86,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Get the length of the compressed data
long compressedSize = Length - 12;
if (compressedSize < 12)
return false;
// Read in the data as an array
byte[]? contents = _dataSource.ReadFrom(12, (int)compressedSize, retainPosition: true);
if (contents == null)
return false;
// Get the decompressor
var decompressor = Decompressor.CreateQBasic(contents);
if (decompressor == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
string filename = "tempfile.bin";
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
decompressor.CopyTo(fs);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
#endregion
}
}

View File

@@ -0,0 +1,102 @@
using System;
using System.IO;
using SabreTools.IO.Compression.SZDD;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class LZSZDD : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
=> Extract(string.Empty, outputDirectory, includeDebug);
/// <inheritdoc cref="Extract(string, bool)"/>
/// <param name="filename">Original name of the file to convert to the output name</param>
public bool Extract(string filename, string outputDirectory, bool includeDebug)
{
// Ensure the filename
if (filename.Length == 0 && Filename != null)
filename = Filename;
// Get the length of the compressed data
long compressedSize = Length - 14;
if (compressedSize < 14)
return false;
// Read in the data as an array
byte[]? contents = ReadRangeFromSource(14, (int)compressedSize);
if (contents.Length == 0)
return false;
// Get the decompressor
var decompressor = Decompressor.CreateSZDD(contents);
if (decompressor == null)
return false;
// Create the output file
filename = GetExpandedName(filename).TrimEnd('\0');
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
decompressor.CopyTo(fs);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
/// <summary>
/// Get the full name of the input file
/// </summary>
private string GetExpandedName(string input)
{
// If the extension is missing
string extension = Path.GetExtension(input).TrimStart('.');
if (string.IsNullOrEmpty(extension))
return Path.GetFileNameWithoutExtension(input);
// If the extension is a single character
if (extension.Length == 1)
{
if (extension == "_" || extension == "$")
return $"{Path.GetFileNameWithoutExtension(input)}.{char.ToLower(LastChar)}";
return Path.GetFileNameWithoutExtension(input);
}
// If the extension isn't formatted
if (!extension.EndsWith("_"))
return Path.GetFileNameWithoutExtension(input);
// Handle replacing characters
char c = (char.IsUpper(input[0]) ? char.ToLower(LastChar) : char.ToUpper(LastChar));
string text2 = extension.Substring(0, extension.Length - 1) + c;
return Path.GetFileNameWithoutExtension(input) + "." + text2;
}
}
}

View File

@@ -1,13 +1,9 @@
using System;
using System.IO;
using SabreTools.IO.Compression.SZDD;
using SabreTools.IO.Extensions;
using SabreTools.Models.LZ;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class LZSZDD : WrapperBase<SZDDFile>, IExtractable
public partial class LZSZDD : WrapperBase<SZDDFile>
{
#region Descriptive Properties
@@ -26,18 +22,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public LZSZDD(SZDDFile? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public LZSZDD(SZDDFile model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public LZSZDD(SZDDFile? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public LZSZDD(SZDDFile model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public LZSZDD(SZDDFile model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public LZSZDD(SZDDFile model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public LZSZDD(SZDDFile model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public LZSZDD(SZDDFile model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create an LZ (SZDD variant) from a byte array and offset
@@ -76,12 +80,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.LZSZDD.DeserializeStream(data);
var model = new Deserializers.LZSZDD().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new LZSZDD(model, data);
return new LZSZDD(model, data, currentOffset);
}
catch
{
@@ -90,101 +93,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
=> Extract(string.Empty, outputDirectory, includeDebug);
/// <inheritdoc cref="Extract(string, bool)"/>
/// <param name="filename">Original name of the file to convert to the output name</param>
public bool Extract(string filename, string outputDirectory, bool includeDebug)
{
// Ensure the filename
if (filename.Length == 0 && Filename != null)
filename = Filename;
// Get the length of the compressed data
long compressedSize = Length - 14;
if (compressedSize < 14)
return false;
// Read in the data as an array
byte[]? contents = _dataSource.ReadFrom(14, (int)compressedSize, retainPosition: true);
if (contents == null)
return false;
// Get the decompressor
var decompressor = Decompressor.CreateSZDD(contents);
if (decompressor == null)
return false;
// Create the output file
filename = GetExpandedName(filename).TrimEnd('\0');
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
decompressor.CopyTo(fs);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
/// <summary>
/// Get the full name of the input file
/// </summary>
private string GetExpandedName(string input)
{
// If the extension is missing
string extension = Path.GetExtension(input).TrimStart('.');
if (string.IsNullOrEmpty(extension))
return Path.GetFileNameWithoutExtension(input);
// If the extension is a single character
if (extension.Length == 1)
{
if (extension == "_" || extension == "$")
return $"{Path.GetFileNameWithoutExtension(input)}.{char.ToLower(LastChar)}";
return Path.GetFileNameWithoutExtension(input);
}
// If the extension isn't formatted
if (!extension.EndsWith("_"))
return Path.GetFileNameWithoutExtension(input);
// Handle replacing characters
char c = (char.IsUpper(input[0]) ? char.ToLower(LastChar) : char.ToUpper(LastChar));
string text2 = extension.Substring(0, extension.Length - 1) + c;
return Path.GetFileNameWithoutExtension(input) + "." + text2;
}
#endregion
}
}

View File

@@ -33,18 +33,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public LinearExecutable(Executable? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public LinearExecutable(Executable model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public LinearExecutable(Executable? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public LinearExecutable(Executable model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public LinearExecutable(Executable model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public LinearExecutable(Executable model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public LinearExecutable(Executable model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public LinearExecutable(Executable model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create an LE/LX executable from a byte array and offset
@@ -83,12 +91,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.LinearExecutable.DeserializeStream(data);
var model = new Deserializers.LinearExecutable().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new LinearExecutable(model, data);
return new LinearExecutable(model, data, currentOffset);
}
catch
{
@@ -139,7 +146,7 @@ namespace SabreTools.Serialization.Wrappers
return [];
// Read the entry data and return
return _dataSource.ReadFrom(offset, length, retainPosition: true);
return ReadRangeFromSource(offset, length);
}
/// <summary>
@@ -315,7 +322,7 @@ namespace SabreTools.Serialization.Wrappers
if (length == -1)
length = Length;
return _dataSource.ReadFrom(rangeStart, (int)length, retainPosition: true);
return ReadRangeFromSource(rangeStart, (int)length);
}
#endregion

View File

@@ -1,8 +1,9 @@
using System.IO;
using SabreTools.Models.MSDOS;
namespace SabreTools.Serialization.Wrappers
{
public class MSDOS : WrapperBase<Models.MSDOS.Executable>
public class MSDOS : WrapperBase<Executable>
{
#region Descriptive Properties
@@ -14,18 +15,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public MSDOS(Models.MSDOS.Executable? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public MSDOS(Executable model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public MSDOS(Models.MSDOS.Executable? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public MSDOS(Executable model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public MSDOS(Executable model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public MSDOS(Executable model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public MSDOS(Executable model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public MSDOS(Executable model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create an MS-DOS executable from a byte array and offset
@@ -64,12 +73,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.MSDOS.DeserializeStream(data);
var model = new Deserializers.MSDOS().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new MSDOS(model, data);
return new MSDOS(model, data, currentOffset);
}
catch
{

View File

@@ -0,0 +1,292 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Models.MicrosoftCabinet;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class MicrosoftCabinet : IExtractable
{
#region Extension Properties
/// <summary>
/// Reference to the next cabinet header
/// </summary>
/// <remarks>Only used in multi-file</remarks>
public MicrosoftCabinet? Next { get; set; }
/// <summary>
/// Reference to the next previous header
/// </summary>
/// <remarks>Only used in multi-file</remarks>
public MicrosoftCabinet? Prev { get; set; }
#endregion
#region Cabinet Set
/// <summary>
/// Open a cabinet set for reading, if possible
/// </summary>
/// <param name="filename">Filename for one cabinet in the set</param>
/// <returns>Wrapper representing the set, null on error</returns>
private static MicrosoftCabinet? OpenSet(string? filename)
{
// If the file is invalid
if (string.IsNullOrEmpty(filename))
return null;
else if (!File.Exists(filename!))
return null;
// Get the full file path and directory
filename = Path.GetFullPath(filename);
// Read in the current file and try to parse
var stream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
var current = Create(stream);
if (current?.Header == null)
return null;
// Seek to the first part of the cabinet set
while (current.CabinetPrev != null)
{
// Attempt to open the previous cabinet
var prev = current.OpenPrevious(filename);
if (prev?.Header == null)
break;
// Assign previous as new current
current = prev;
}
// Cache the current start of the cabinet set
var start = current;
// Read in the cabinet parts sequentially
while (current.CabinetNext != null)
{
// Open the next cabinet and try to parse
var next = current.OpenNext(filename);
if (next?.Header == null)
break;
// Add the next and previous links, resetting current
next.Prev = current;
current.Next = next;
current = next;
}
// Return the start of the set
return start;
}
/// <summary>
/// Open the next archive, if possible
/// </summary>
/// <param name="filename">Filename for one cabinet in the set</param>
private MicrosoftCabinet? OpenNext(string? filename)
{
// Ignore invalid archives
if (Header == null || string.IsNullOrEmpty(filename))
return null;
// Normalize the filename
filename = Path.GetFullPath(filename);
// Get if the cabinet has a next part
string? next = CabinetNext;
if (string.IsNullOrEmpty(next))
return null;
// Get the full next path
string? folder = Path.GetDirectoryName(filename);
if (folder != null)
next = Path.Combine(folder, next);
// Open and return the next cabinet
var fs = File.Open(next, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
return Create(fs);
}
/// <summary>
/// Open the previous archive, if possible
/// </summary>
/// <param name="filename">Filename for one cabinet in the set</param>
private MicrosoftCabinet? OpenPrevious(string? filename)
{
// Ignore invalid archives
if (Header == null || string.IsNullOrEmpty(filename))
return null;
// Normalize the filename
filename = Path.GetFullPath(filename);
// Get if the cabinet has a previous part
string? prev = CabinetPrev;
if (string.IsNullOrEmpty(prev))
return null;
// Get the full next path
string? folder = Path.GetDirectoryName(filename);
if (folder != null)
prev = Path.Combine(folder, prev);
// Open and return the previous cabinet
var fs = File.Open(prev, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
return Create(fs);
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Display warning
Console.WriteLine("WARNING: LZX and Quantum compression schemes are not supported so some files may be skipped!");
// Open the full set if possible
var cabinet = this;
if (Filename != null)
cabinet = OpenSet(Filename);
// If the archive is invalid
if (cabinet?.Folders == null || cabinet.Folders.Length == 0)
return false;
try
{
// Loop through the folders
bool allExtracted = true;
for (int f = 0; f < cabinet.Folders.Length; f++)
{
var folder = cabinet.Folders[f];
allExtracted &= cabinet.ExtractFolder(Filename, outputDirectory, folder, f, includeDebug);
}
return allExtracted;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Extract the contents of a single folder
/// </summary>
/// <param name="filename">Filename for one cabinet in the set, if available</param>
/// <param name="outputDirectory">Path to the output directory</param>
/// <param name="folder">Folder containing the blocks to decompress</param>
/// <param name="folderIndex">Index of the folder in the cabinet</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if all files extracted, false otherwise</returns>
private bool ExtractFolder(string? filename,
string outputDirectory,
CFFOLDER? folder,
int folderIndex,
bool includeDebug)
{
// Decompress the blocks, if possible
using var blockStream = DecompressBlocks(filename, folder, folderIndex, includeDebug);
if (blockStream == null || blockStream.Length == 0)
return false;
// Loop through the files
bool allExtracted = true;
var files = GetFiles(folderIndex);
for (int i = 0; i < files.Length; i++)
{
var file = files[i];
allExtracted &= ExtractFile(outputDirectory, blockStream, file, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract the contents of a single file
/// </summary>
/// <param name="outputDirectory">Path to the output directory</param>
/// <param name="blockStream">Stream representing the uncompressed block data</param>
/// <param name="file">File information</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
private static bool ExtractFile(string outputDirectory, Stream blockStream, CFFILE file, bool includeDebug)
{
try
{
blockStream.Seek(file.FolderStartOffset, SeekOrigin.Begin);
byte[] fileData = blockStream.ReadBytes((int)file.FileSize);
// Ensure directory separators are consistent
string filename = file.Name!;
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Open the output file for writing
using var fs = File.OpenWrite(filename);
fs.Write(fileData, 0, fileData.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
#endregion
#region Checksumming
/// <summary>
/// The computation and verification of checksums found in CFDATA structure entries cabinet files is
/// done by using a function described by the following mathematical notation. When checksums are
/// not supplied by the cabinet file creating application, the checksum field is set to 0 (zero). Cabinet
/// extracting applications do not compute or verify the checksum if the field is set to 0 (zero).
/// </summary>
private static uint ChecksumData(byte[] data)
{
uint[] C =
[
S(data, 1, data.Length),
S(data, 2, data.Length),
S(data, 3, data.Length),
S(data, 4, data.Length),
];
return C[0] ^ C[1] ^ C[2] ^ C[3];
}
/// <summary>
/// Individual algorithmic step
/// </summary>
private static uint S(byte[] a, int b, int x)
{
int n = a.Length;
if (x < 4 && b > n % 4)
return 0;
else if (x < 4 && b <= n % 4)
return a[n - b + 1];
else // if (x >= 4)
return a[n - x + b] ^ S(a, b, x - 4);
}
#endregion
}
}

View File

@@ -1,13 +1,11 @@
using System;
using System.IO;
using SabreTools.IO.Compression.MSZIP;
using SabreTools.IO.Extensions;
using SabreTools.Models.MicrosoftCabinet;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class MicrosoftCabinet : WrapperBase<Cabinet>, IExtractable
public partial class MicrosoftCabinet : WrapperBase<Cabinet>
{
#region Descriptive Properties
@@ -36,38 +34,34 @@ namespace SabreTools.Serialization.Wrappers
/// <inheritdoc cref="CFHEADER.CabinetNext"/>
public string? CabinetNext => Header?.CabinetNext;
/// <summary>
/// Reference to the next cabinet header
/// </summary>
/// <remarks>Only used in multi-file</remarks>
public MicrosoftCabinet? Next { get; set; }
/// <inheritdoc cref="CFHEADER.CabinetPrev"/>
public string? CabinetPrev => Header?.CabinetPrev;
/// <summary>
/// Reference to the next previous header
/// </summary>
/// <remarks>Only used in multi-file</remarks>
public MicrosoftCabinet? Prev { get; set; }
#endregion
#region Constructors
/// <inheritdoc/>
public MicrosoftCabinet(Cabinet? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public MicrosoftCabinet(Cabinet model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public MicrosoftCabinet(Cabinet? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public MicrosoftCabinet(Cabinet model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public MicrosoftCabinet(Cabinet model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public MicrosoftCabinet(Cabinet model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public MicrosoftCabinet(Cabinet model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public MicrosoftCabinet(Cabinet model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a Microsoft Cabinet from a byte array and offset
@@ -106,12 +100,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.MicrosoftCabinet.DeserializeStream(data);
var model = new Deserializers.MicrosoftCabinet().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new MicrosoftCabinet(model, data);
return new MicrosoftCabinet(model, data, currentOffset);
}
catch
{
@@ -121,271 +114,6 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Display warning
Console.WriteLine("WARNING: LZX and Quantum compression schemes are not supported so some files may be skipped!");
// Open the full set if possible
var cabinet = this;
if (Filename != null)
cabinet = OpenSet(Filename);
// If the archive is invalid
if (cabinet?.Folders == null || cabinet.Folders.Length == 0)
return false;
try
{
// Loop through the folders
bool allExtracted = true;
for (int f = 0; f < cabinet.Folders.Length; f++)
{
var folder = cabinet.Folders[f];
allExtracted &= cabinet.ExtractFolder(Filename, outputDirectory, folder, f, includeDebug);
}
return allExtracted;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Extract the contents of a single folder
/// </summary>
/// <param name="filename">Filename for one cabinet in the set, if available</param>
/// <param name="outputDirectory">Path to the output directory</param>
/// <param name="folder">Folder containing the blocks to decompress</param>
/// <param name="folderIndex">Index of the folder in the cabinet</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if all files extracted, false otherwise</returns>
private bool ExtractFolder(string? filename,
string outputDirectory,
CFFOLDER? folder,
int folderIndex,
bool includeDebug)
{
// Decompress the blocks, if possible
using var blockStream = DecompressBlocks(filename, folder, folderIndex, includeDebug);
if (blockStream == null || blockStream.Length == 0)
return false;
// Loop through the files
bool allExtracted = true;
var files = GetFiles(folderIndex);
for (int i = 0; i < files.Length; i++)
{
var file = files[i];
allExtracted &= ExtractFile(outputDirectory, blockStream, file, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract the contents of a single file
/// </summary>
/// <param name="outputDirectory">Path to the output directory</param>
/// <param name="blockStream">Stream representing the uncompressed block data</param>
/// <param name="file">File information</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
private static bool ExtractFile(string outputDirectory, Stream blockStream, CFFILE file, bool includeDebug)
{
try
{
blockStream.Seek(file.FolderStartOffset, SeekOrigin.Begin);
byte[] fileData = blockStream.ReadBytes((int)file.FileSize);
// Ensure directory separators are consistent
string filename = file.Name!;
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Open the output file for writing
using var fs = File.OpenWrite(filename);
fs.Write(fileData, 0, fileData.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
#endregion
#region Cabinet Set
/// <summary>
/// Open a cabinet set for reading, if possible
/// </summary>
/// <param name="filename">Filename for one cabinet in the set</param>
/// <returns>Wrapper representing the set, null on error</returns>
private static MicrosoftCabinet? OpenSet(string? filename)
{
// If the file is invalid
if (string.IsNullOrEmpty(filename))
return null;
else if (!File.Exists(filename!))
return null;
// Get the full file path and directory
filename = Path.GetFullPath(filename);
// Read in the current file and try to parse
var stream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
var current = Create(stream);
if (current?.Header == null)
return null;
// Seek to the first part of the cabinet set
while (current.CabinetPrev != null)
{
// Attempt to open the previous cabinet
var prev = current.OpenPrevious(filename);
if (prev?.Header == null)
break;
// Assign previous as new current
current = prev;
}
// Cache the current start of the cabinet set
var start = current;
// Read in the cabinet parts sequentially
while (current.CabinetNext != null)
{
// Open the next cabinet and try to parse
var next = current.OpenNext(filename);
if (next?.Header == null)
break;
// Add the next and previous links, resetting current
next.Prev = current;
current.Next = next;
current = next;
}
// Return the start of the set
return start;
}
/// <summary>
/// Open the next archive, if possible
/// </summary>
/// <param name="filename">Filename for one cabinet in the set</param>
private MicrosoftCabinet? OpenNext(string? filename)
{
// Ignore invalid archives
if (Header == null || string.IsNullOrEmpty(filename))
return null;
// Normalize the filename
filename = Path.GetFullPath(filename);
// Get if the cabinet has a next part
string? next = CabinetNext;
if (string.IsNullOrEmpty(next))
return null;
// Get the full next path
string? folder = Path.GetDirectoryName(filename);
if (folder != null)
next = Path.Combine(folder, next);
// Open and return the next cabinet
var fs = File.Open(next, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
return Create(fs);
}
/// <summary>
/// Open the previous archive, if possible
/// </summary>
/// <param name="filename">Filename for one cabinet in the set</param>
private MicrosoftCabinet? OpenPrevious(string? filename)
{
// Ignore invalid archives
if (Header == null || string.IsNullOrEmpty(filename))
return null;
// Normalize the filename
filename = Path.GetFullPath(filename);
// Get if the cabinet has a previous part
string? prev = CabinetPrev;
if (string.IsNullOrEmpty(prev))
return null;
// Get the full next path
string? folder = Path.GetDirectoryName(filename);
if (folder != null)
prev = Path.Combine(folder, prev);
// Open and return the previous cabinet
var fs = File.Open(prev, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
return Create(fs);
}
#endregion
#region Checksumming
/// <summary>
/// The computation and verification of checksums found in CFDATA structure entries cabinet files is
/// done by using a function described by the following mathematical notation. When checksums are
/// not supplied by the cabinet file creating application, the checksum field is set to 0 (zero). Cabinet
/// extracting applications do not compute or verify the checksum if the field is set to 0 (zero).
/// </summary>
private static uint ChecksumData(byte[] data)
{
uint[] C =
[
S(data, 1, data.Length),
S(data, 2, data.Length),
S(data, 3, data.Length),
S(data, 4, data.Length),
];
return C[0] ^ C[1] ^ C[2] ^ C[3];
}
/// <summary>
/// Individual algorithmic step
/// </summary>
private static uint S(byte[] a, int b, int x)
{
int n = a.Length;
if (x < 4 && b > n % 4)
return 0;
else if (x < 4 && b <= n % 4)
return a[n - b + 1];
else // if (x >= 4)
return a[n - x + b] ^ S(a, b, x - 4);
}
#endregion
#region Files
/// <summary>

View File

@@ -0,0 +1,79 @@
using System;
using SabreTools.Serialization.Interfaces;
#if (NET452_OR_GREATER || NETCOREAPP) && (WINX86 || WINX64)
using StormLibSharp;
#endif
namespace SabreTools.Serialization.Wrappers
{
public partial class MoPaQ : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
#if NET20 || NET35 || !(WINX86 || WINX64)
Console.WriteLine("Extraction is not supported for this framework!");
Console.WriteLine();
return false;
#else
try
{
if (Filename == null || !File.Exists(Filename))
return false;
// Try to open the archive and listfile
var mpqArchive = new MpqArchive(Filename, FileAccess.Read);
string? listfile = null;
MpqFileStream listStream = mpqArchive.OpenFile("(listfile)");
// If we can't read the listfile, we just return
if (!listStream.CanRead)
return false;
// Read the listfile in for processing
using (var sr = new StreamReader(listStream))
{
listfile = sr.ReadToEnd();
}
// Split the listfile by newlines
string[] listfileLines = listfile.Replace("\r\n", "\n").Split('\n');
// Loop over each entry
foreach (string sub in listfileLines)
{
// Ensure directory separators are consistent
string filename = sub;
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outDir, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
mpqArchive.ExtractFile(sub, filename);
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.WriteLine(ex);
}
}
return true;
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.WriteLine(ex);
return false;
}
#endif
}
}
}

View File

@@ -1,14 +1,9 @@
using System;
using System.IO;
using SabreTools.Serialization.Interfaces;
using System.IO;
using SabreTools.Models.MoPaQ;
#if (NET452_OR_GREATER || NETCOREAPP) && (WINX86 || WINX64)
using StormLibSharp;
#endif
namespace SabreTools.Serialization.Wrappers
{
public partial class MoPaQ : WrapperBase<Archive>, IExtractable
public partial class MoPaQ : WrapperBase<Archive>
{
#region Descriptive Properties
@@ -17,37 +12,57 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region No-Model Constructors
/// <inheritdoc/>
/// <remarks>This should only be used for until MPQ parsing is fixed</remarks>
public MoPaQ(byte[] data) : base(new Archive(), data) { }
/// <inheritdoc/>
/// <remarks>This should only be used for until MPQ parsing is fixed</remarks>
public MoPaQ(byte[] data, int offset) : base(new Archive(), data, offset) { }
/// <inheritdoc/>
/// <remarks>This should only be used for until MPQ parsing is fixed</remarks>
public MoPaQ(byte[] data, int offset, int length) : base(new Archive(), data, offset, length) { }
/// <inheritdoc/>
/// <remarks>This should only be used for until MPQ parsing is fixed</remarks>
public MoPaQ(Stream data) : base(new Archive(), data) { }
/// <inheritdoc/>
/// <remarks>This should only be used for until MPQ parsing is fixed</remarks>
public MoPaQ(Stream data, long offset) : base(new Archive(), data, offset) { }
/// <inheritdoc/>
/// <remarks>This should only be used for until MPQ parsing is fixed</remarks>
public MoPaQ(Stream data, long offset, long length) : base(new Archive(), data, offset, length) { }
#endregion
#region Constructors
/// <inheritdoc/>
/// <remarks>This should only be used for until MPQ parsing is fixed</remarks>
public MoPaQ(byte[]? data, int offset)
: base(new Archive(), data, offset)
{
// All logic is handled by the base class
}
public MoPaQ(Archive model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
/// <remarks>This should only be used for until MPQ parsing is fixed</remarks>
public MoPaQ(Stream? data)
: base(new Archive(), data)
{
// All logic is handled by the base class
}
public MoPaQ(Archive model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public MoPaQ(Archive? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public MoPaQ(Archive model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public MoPaQ(Archive? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public MoPaQ(Archive model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public MoPaQ(Archive model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public MoPaQ(Archive model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a MoPaQ archive from a byte array and offset
@@ -86,12 +101,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.MoPaQ.DeserializeStream(data);
var model = new Deserializers.MoPaQ().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new MoPaQ(model, data);
return new MoPaQ(model, data, currentOffset);
}
catch
{
@@ -100,77 +114,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
#if NET20 || NET35 || !(WINX86 || WINX64)
Console.WriteLine("Extraction is not supported for this framework!");
Console.WriteLine();
return false;
#else
try
{
if (Filename == null || !File.Exists(Filename))
return false;
// Try to open the archive and listfile
var mpqArchive = new MpqArchive(Filename, FileAccess.Read);
string? listfile = null;
MpqFileStream listStream = mpqArchive.OpenFile("(listfile)");
// If we can't read the listfile, we just return
if (!listStream.CanRead)
return false;
// Read the listfile in for processing
using (var sr = new StreamReader(listStream))
{
listfile = sr.ReadToEnd();
}
// Split the listfile by newlines
string[] listfileLines = listfile.Replace("\r\n", "\n").Split('\n');
// Loop over each entry
foreach (string sub in listfileLines)
{
// Ensure directory separators are consistent
string filename = sub;
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outDir, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
mpqArchive.ExtractFile(sub, filename);
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.WriteLine(ex);
}
}
return true;
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.WriteLine(ex);
return false;
}
#endif
}
#endregion
}
}

View File

@@ -0,0 +1,65 @@
using System;
using static SabreTools.Models.N3DS.Constants;
namespace SabreTools.Serialization.Wrappers
{
public partial class N3DS
{
/// <summary>
/// Get the initial value for the ExeFS counter
/// </summary>
public byte[] ExeFSIV(int index)
{
if (Partitions == null)
return [];
if (index < 0 || index >= Partitions.Length)
return [];
var header = Partitions[index];
if (header == null || header.MagicID != NCCHMagicNumber)
return [];
byte[] partitionIdBytes = BitConverter.GetBytes(header.PartitionId);
Array.Reverse(partitionIdBytes);
return [.. partitionIdBytes, .. ExefsCounter];
}
/// <summary>
/// Get the initial value for the plain counter
/// </summary>
public byte[] PlainIV(int index)
{
if (Partitions == null)
return [];
if (index < 0 || index >= Partitions.Length)
return [];
var header = Partitions[index];
if (header == null || header.MagicID != NCCHMagicNumber)
return [];
byte[] partitionIdBytes = BitConverter.GetBytes(header.PartitionId);
Array.Reverse(partitionIdBytes);
return [.. partitionIdBytes, .. PlainCounter];
}
/// <summary>
/// Get the initial value for the RomFS counter
/// </summary>
public byte[] RomFSIV(int index)
{
if (Partitions == null)
return [];
if (index < 0 || index >= Partitions.Length)
return [];
var header = Partitions[index];
if (header == null || header.MagicID != NCCHMagicNumber)
return [];
byte[] partitionIdBytes = BitConverter.GetBytes(header.PartitionId);
Array.Reverse(partitionIdBytes);
return [.. partitionIdBytes, .. RomfsCounter];
}
}
}

View File

@@ -5,7 +5,7 @@ using static SabreTools.Models.N3DS.Constants;
namespace SabreTools.Serialization.Wrappers
{
public class N3DS : WrapperBase<Cart>
public partial class N3DS : WrapperBase<Cart>
{
#region Descriptive Properties
@@ -209,18 +209,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public N3DS(Cart? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public N3DS(Cart model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public N3DS(Cart? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public N3DS(Cart model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public N3DS(Cart model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public N3DS(Cart model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public N3DS(Cart model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public N3DS(Cart model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a 3DS cart image from a byte array and offset
@@ -259,12 +267,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.N3DS.DeserializeStream(data);
var model = new Deserializers.N3DS().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new N3DS(model, data);
return new N3DS(model, data, currentOffset);
}
catch
{
@@ -349,67 +356,6 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region Encryption
/// <summary>
/// Get the initial value for the ExeFS counter
/// </summary>
public byte[] ExeFSIV(int index)
{
if (Partitions == null)
return [];
if (index < 0 || index >= Partitions.Length)
return [];
var header = Partitions[index];
if (header == null || header.MagicID != NCCHMagicNumber)
return [];
byte[] partitionIdBytes = BitConverter.GetBytes(header.PartitionId);
Array.Reverse(partitionIdBytes);
return [.. partitionIdBytes, .. ExefsCounter];
}
/// <summary>
/// Get the initial value for the plain counter
/// </summary>
public byte[] PlainIV(int index)
{
if (Partitions == null)
return [];
if (index < 0 || index >= Partitions.Length)
return [];
var header = Partitions[index];
if (header == null || header.MagicID != NCCHMagicNumber)
return [];
byte[] partitionIdBytes = BitConverter.GetBytes(header.PartitionId);
Array.Reverse(partitionIdBytes);
return [.. partitionIdBytes, .. PlainCounter];
}
/// <summary>
/// Get the initial value for the RomFS counter
/// </summary>
public byte[] RomFSIV(int index)
{
if (Partitions == null)
return [];
if (index < 0 || index >= Partitions.Length)
return [];
var header = Partitions[index];
if (header == null || header.MagicID != NCCHMagicNumber)
return [];
byte[] partitionIdBytes = BitConverter.GetBytes(header.PartitionId);
Array.Reverse(partitionIdBytes);
return [.. partitionIdBytes, .. RomfsCounter];
}
#endregion
#region Offsets
/// <summary>

View File

@@ -14,18 +14,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public NCF(Models.NCF.File? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public NCF(Models.NCF.File model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public NCF(Models.NCF.File? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public NCF(Models.NCF.File model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public NCF(Models.NCF.File model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public NCF(Models.NCF.File model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public NCF(Models.NCF.File model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public NCF(Models.NCF.File model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create an NCF from a byte array and offset
@@ -64,12 +72,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.NCF.DeserializeStream(data);
var model = new Deserializers.NCF().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new NCF(model, data);
return new NCF(model, data, currentOffset);
}
catch
{

View File

@@ -0,0 +1,226 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Matching;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class NewExecutable : IExtractable
{
/// <inheritdoc/>
/// <remarks>
/// This extracts the following data:
/// - Archives and executables in the overlay
/// - Wise installers
/// </remarks>
public bool Extract(string outputDirectory, bool includeDebug)
{
bool overlay = ExtractFromOverlay(outputDirectory, includeDebug);
bool wise = ExtractWise(outputDirectory, includeDebug);
return overlay | wise;
}
/// <summary>
/// Extract data from the overlay
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractFromOverlay(string outputDirectory, bool includeDebug)
{
try
{
// Cache the overlay data for easier reading
var overlayData = OverlayData;
if (overlayData.Length == 0)
return false;
// Set the output variables
int overlayOffset = 0;
string extension = string.Empty;
// Only process the overlay if it is recognized
for (; overlayOffset < 0x400 && overlayOffset < overlayData.Length; overlayOffset++)
{
int temp = overlayOffset;
byte[] overlaySample = overlayData.ReadBytes(ref temp, 0x10);
if (overlaySample.StartsWith([0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]))
{
extension = "7z";
break;
}
else if (overlaySample.StartsWith([0x3B, 0x21, 0x40, 0x49, 0x6E, 0x73, 0x74, 0x61, 0x6C, 0x6C]))
{
// 7-zip SFX script -- ";!@Install" to ";!@InstallEnd@!"
overlayOffset = overlayData.FirstPosition([0x3B, 0x21, 0x40, 0x49, 0x6E, 0x73, 0x74, 0x61, 0x6C, 0x6C, 0x45, 0x6E, 0x64, 0x40, 0x21]);
if (overlayOffset == -1)
return false;
overlayOffset += 15;
extension = "7z";
break;
}
else if (overlaySample.StartsWith([0x42, 0x5A, 0x68]))
{
extension = "bz2";
break;
}
else if (overlaySample.StartsWith([0x1F, 0x8B]))
{
extension = "gz";
break;
}
else if (overlaySample.StartsWith(Models.MicrosoftCabinet.Constants.SignatureBytes))
{
extension = "cab";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.LocalFileHeaderSignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.EndOfCentralDirectoryRecordSignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.EndOfCentralDirectoryRecord64SignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.DataDescriptorSignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00]))
{
extension = "rar";
break;
}
else if (overlaySample.StartsWith([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01, 0x00]))
{
extension = "rar";
break;
}
else if (overlaySample.StartsWith([0x55, 0x48, 0x41, 0x06]))
{
extension = "uha";
break;
}
else if (overlaySample.StartsWith([0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]))
{
extension = "xz";
break;
}
else if (overlaySample.StartsWith(Models.MSDOS.Constants.SignatureBytes))
{
extension = "bin"; // exe/dll
break;
}
}
// If the extension is unset
if (extension.Length == 0)
return false;
// Create the temp filename
string tempFile = $"embedded_overlay.{extension}";
if (Filename != null)
tempFile = $"{Path.GetFileName(Filename)}-{tempFile}";
tempFile = Path.Combine(outputDirectory, tempFile);
var directoryName = Path.GetDirectoryName(tempFile);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Write the resource data to a temp file
using var tempStream = File.Open(tempFile, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
tempStream?.Write(overlayData, overlayOffset, overlayData.Length - overlayOffset);
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Extract data from a Wise installer
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractWise(string outputDirectory, bool includeDebug)
{
// Get the source data for reading
Stream source = _dataSource;
if (Filename != null)
{
// Try to open a multipart file
if (WiseOverlayHeader.OpenFile(Filename, includeDebug, out var temp) && temp != null)
source = temp;
}
// Try to find the overlay header
long offset = FindWiseOverlayHeader();
if (offset > 0 && offset < Length)
return ExtractWiseOverlay(outputDirectory, includeDebug, source, offset);
// Everything else could not extract
return false;
}
/// <summary>
/// Extract using Wise overlay
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <param name="source">Potentially multi-part stream to read</param>
/// <param name="offset">Offset to the start of the overlay header</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
private bool ExtractWiseOverlay(string outputDirectory, bool includeDebug, Stream source, long offset)
{
// Seek to the overlay and parse
source.Seek(offset, SeekOrigin.Begin);
var header = WiseOverlayHeader.Create(source);
if (header == null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse the overlay header");
return false;
}
// Extract the header-defined files
bool extracted = header.ExtractHeaderDefinedFiles(outputDirectory, includeDebug);
if (!extracted)
{
if (includeDebug) Console.Error.WriteLine("Could not extract header-defined files");
return false;
}
// Open the script file from the output directory
var scriptStream = File.OpenRead(Path.Combine(outputDirectory, "WiseScript.bin"));
var script = WiseScript.Create(scriptStream);
if (script == null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse WiseScript.bin");
return false;
}
// Get the source directory
string? sourceDirectory = null;
if (Filename != null)
sourceDirectory = Path.GetDirectoryName(Path.GetFullPath(Filename));
// Process the state machine
return script.ProcessStateMachine(header, sourceDirectory, outputDirectory, includeDebug);
}
}
}

View File

@@ -2,13 +2,11 @@
using System.Collections.Generic;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Matching;
using SabreTools.Models.NewExecutable;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class NewExecutable : WrapperBase<Executable>, IExtractable
public partial class NewExecutable : WrapperBase<Executable>
{
#region Descriptive Properties
@@ -36,7 +34,7 @@ namespace SabreTools.Serialization.Wrappers
{
get
{
lock (_sourceDataLock)
lock (_overlayAddressLock)
{
// Use the cached data if possible
if (_overlayAddress != null)
@@ -65,10 +63,13 @@ namespace SabreTools.Serialization.Wrappers
if (entry.FlagWord.HasFlag(SegmentTableEntryFlag.RELOCINFO))
#endif
{
_dataSource.Seek(offset, SeekOrigin.Begin);
var relocationData = Deserializers.NewExecutable.ParsePerSegmentData(_dataSource);
lock (_dataSourceLock)
{
_dataSource.Seek(offset, SeekOrigin.Begin);
var relocationData = Deserializers.NewExecutable.ParsePerSegmentData(_dataSource);
offset = _dataSource.Position;
offset = _dataSource.Position;
}
}
if (offset > endOfSectionData)
@@ -94,6 +95,10 @@ namespace SabreTools.Serialization.Wrappers
if (endOfSectionData <= 0)
endOfSectionData = -1;
// Adjust the position of the data by 705 bytes
// TODO: Investigate what the byte data is
endOfSectionData += 705;
// Cache and return the position
_overlayAddress = endOfSectionData;
return _overlayAddress.Value;
@@ -105,11 +110,11 @@ namespace SabreTools.Serialization.Wrappers
/// Overlay data, if it exists
/// </summary>
/// <see href="https://codeberg.org/CYBERDEV/REWise/src/branch/master/src/exefile.c"/>
public byte[]? OverlayData
public byte[] OverlayData
{
get
{
lock (_sourceDataLock)
lock (_overlayDataLock)
{
// Use the cached data if possible
if (_overlayData != null)
@@ -118,54 +123,27 @@ namespace SabreTools.Serialization.Wrappers
// Get the available source length, if possible
long dataLength = Length;
if (dataLength == -1)
return null;
{
_overlayData = [];
return _overlayData;
}
// If a required property is missing
if (Header == null || SegmentTable == null || ResourceTable?.ResourceTypes == null)
return null;
// Search through the segments table to find the furthest
long endOfSectionData = -1;
foreach (var entry in SegmentTable)
{
// Get end of segment data
long offset = (entry.Offset * (1 << Header.SegmentAlignmentShiftCount)) + entry.Length;
// Read and find the end of the relocation data
#if NET20 || NET35
if ((entry.FlagWord & SegmentTableEntryFlag.RELOCINFO) != 0)
#else
if (entry.FlagWord.HasFlag(SegmentTableEntryFlag.RELOCINFO))
#endif
{
_dataSource.Seek(offset, SeekOrigin.Begin);
var relocationData = Deserializers.NewExecutable.ParsePerSegmentData(_dataSource);
offset = _dataSource.Position;
}
if (offset > endOfSectionData)
endOfSectionData = offset;
_overlayData = [];
return _overlayData;
}
// Search through the resources table to find the furthest
foreach (var entry in ResourceTable.ResourceTypes)
{
// Skip invalid entries
if (entry.ResourceCount == 0 || entry.Resources == null || entry.Resources.Length == 0)
continue;
foreach (var resource in entry.Resources)
{
int offset = (resource.Offset << ResourceTable.AlignmentShiftCount) + resource.Length;
if (offset > endOfSectionData)
endOfSectionData = offset;
}
}
// Get the overlay address if possible
long endOfSectionData = OverlayAddress;
// If we didn't find the end of section data
if (endOfSectionData <= 0)
return null;
{
_overlayData = [];
return _overlayData;
}
// If we're at the end of the file, cache an empty byte array
if (endOfSectionData >= dataLength)
@@ -176,7 +154,7 @@ namespace SabreTools.Serialization.Wrappers
// Otherwise, cache and return the data
long overlayLength = dataLength - endOfSectionData;
_overlayData = _dataSource.ReadFrom((int)endOfSectionData, (int)overlayLength, retainPosition: true);
_overlayData = ReadRangeFromSource((int)endOfSectionData, (int)overlayLength);
return _overlayData;
}
}
@@ -185,11 +163,11 @@ namespace SabreTools.Serialization.Wrappers
/// <summary>
/// Overlay strings, if they exist
/// </summary>
public List<string>? OverlayStrings
public List<string> OverlayStrings
{
get
{
lock (_sourceDataLock)
lock (_overlayStringsLock)
{
// Use the cached data if possible
if (_overlayStrings != null)
@@ -198,57 +176,21 @@ namespace SabreTools.Serialization.Wrappers
// Get the available source length, if possible
long dataLength = Length;
if (dataLength == -1)
return null;
// If a required property is missing
if (Header == null || SegmentTable == null || ResourceTable?.ResourceTypes == null)
return null;
// Search through the segments table to find the furthest
int endOfSectionData = -1;
foreach (var entry in SegmentTable)
{
int offset = (entry.Offset << Header.SegmentAlignmentShiftCount) + entry.Length;
if (offset > endOfSectionData)
endOfSectionData = offset;
}
// Search through the resources table to find the furthest
foreach (var entry in ResourceTable.ResourceTypes)
{
// Skip invalid entries
if (entry.ResourceCount == 0 || entry.Resources == null || entry.Resources.Length == 0)
continue;
foreach (var resource in entry.Resources)
{
int offset = (resource.Offset << ResourceTable.AlignmentShiftCount) + resource.Length;
if (offset > endOfSectionData)
endOfSectionData = offset;
}
}
// If we didn't find the end of section data
if (endOfSectionData <= 0)
return null;
// Adjust the position of the data by 705 bytes
// TODO: Investigate what the byte data is
endOfSectionData += 705;
// If we're at the end of the file, cache an empty list
if (endOfSectionData >= dataLength)
{
_overlayStrings = [];
return _overlayStrings;
}
// TODO: Revisit the 16 MiB limit
// Cap the check for overlay strings to 16 MiB (arbitrary)
long overlayLength = Math.Min(dataLength - endOfSectionData, 16 * 1024 * 1024);
// Get the overlay data, if possible
var overlayData = OverlayData;
if (overlayData.Length == 0)
{
_overlayStrings = [];
return _overlayStrings;
}
// Otherwise, cache and return the strings
_overlayStrings = _dataSource.ReadStringsFrom(endOfSectionData, (int)overlayLength, charLimit: 3);
_overlayStrings = overlayData.ReadStringsFrom(charLimit: 3) ?? [];
return _overlayStrings;
}
}
@@ -269,23 +211,26 @@ namespace SabreTools.Serialization.Wrappers
/// <summary>
/// Stub executable data, if it exists
/// </summary>
public byte[]? StubExecutableData
public byte[] StubExecutableData
{
get
{
lock (_sourceDataLock)
lock (_stubExecutableDataLock)
{
// If we already have cached data, just use that immediately
if (_stubExecutableData != null)
return _stubExecutableData;
if (Stub?.Header?.NewExeHeaderAddr == null)
return null;
{
_stubExecutableData = [];
return _stubExecutableData;
}
// Populate the raw stub executable data based on the source
int endOfStubHeader = 0x40;
int lengthOfStubExecutableData = (int)Stub.Header.NewExeHeaderAddr - endOfStubHeader;
_stubExecutableData = _dataSource.ReadFrom(endOfStubHeader, lengthOfStubExecutableData, retainPosition: true);
_stubExecutableData = ReadRangeFromSource(endOfStubHeader, lengthOfStubExecutableData);
// Cache and return the stub executable data, even if null
return _stubExecutableData;
@@ -302,43 +247,66 @@ namespace SabreTools.Serialization.Wrappers
/// </summary>
private long? _overlayAddress = null;
/// <summary>
/// Lock object for <see cref="_overlayAddress"/>
/// </summary>
private readonly object _overlayAddressLock = new();
/// <summary>
/// Overlay data, if it exists
/// </summary>
private byte[]? _overlayData = null;
/// <summary>
/// Lock object for <see cref="_overlayData"/>
/// </summary>
private readonly object _overlayDataLock = new();
/// <summary>
/// Overlay strings, if they exist
/// </summary>
private List<string>? _overlayStrings = null;
/// <summary>
/// Lock object for <see cref="_overlayStrings"/>
/// </summary>
private readonly object _overlayStringsLock = new();
/// <summary>
/// Stub executable data, if it exists
/// </summary>
private byte[]? _stubExecutableData = null;
/// <summary>
/// Lock object for reading from the source
/// Lock object for <see cref="_stubExecutableData"/>
/// </summary>
private readonly object _sourceDataLock = new();
private readonly object _stubExecutableDataLock = new();
#endregion
#region Constructors
/// <inheritdoc/>
public NewExecutable(Executable? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public NewExecutable(Executable model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public NewExecutable(Executable? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public NewExecutable(Executable model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public NewExecutable(Executable model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public NewExecutable(Executable model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public NewExecutable(Executable model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public NewExecutable(Executable model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create an NE executable from a byte array and offset
@@ -377,12 +345,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.NewExecutable.DeserializeStream(data);
var model = new Deserializers.NewExecutable().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new NewExecutable(model, data);
return new NewExecutable(model, data, currentOffset);
}
catch
{
@@ -392,205 +359,6 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region Extraction
/// <inheritdoc/>
/// <remarks>
/// This extracts the following data:
/// - Archives and executables in the overlay
/// - Wise installers
/// </remarks>
public bool Extract(string outputDirectory, bool includeDebug)
{
bool overlay = ExtractFromOverlay(outputDirectory, includeDebug);
bool wise = ExtractWise(outputDirectory, includeDebug);
return overlay | wise;
}
/// <summary>
/// Extract data from the overlay
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractFromOverlay(string outputDirectory, bool includeDebug)
{
try
{
// Cache the overlay data for easier reading
var overlayData = OverlayData;
if (overlayData == null || overlayData.Length == 0)
return false;
// Set the output variables
int overlayOffset = 0;
string extension = string.Empty;
// Only process the overlay if it is recognized
for (; overlayOffset < 0x100 && overlayOffset < overlayData.Length; overlayOffset++)
{
int temp = overlayOffset;
byte[] overlaySample = overlayData.ReadBytes(ref temp, 0x10);
if (overlaySample.StartsWith([0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]))
{
extension = "7z";
break;
}
else if (overlaySample.StartsWith([0x3B, 0x21, 0x40, 0x49, 0x6E, 0x73, 0x74, 0x61, 0x6C, 0x6C]))
{
// 7-zip SFX script -- ";!@Install" to ";!@InstallEnd@!"
overlayOffset = overlayData.FirstPosition([0x3B, 0x21, 0x40, 0x49, 0x6E, 0x73, 0x74, 0x61, 0x6C, 0x6C, 0x45, 0x6E, 0x64, 0x40, 0x21]);
if (overlayOffset == -1)
return false;
overlayOffset += 15;
extension = "7z";
break;
}
else if (overlaySample.StartsWith(Models.MicrosoftCabinet.Constants.SignatureBytes))
{
extension = "cab";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.LocalFileHeaderSignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.EndOfCentralDirectoryRecordSignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.EndOfCentralDirectoryRecord64SignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.DataDescriptorSignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00]))
{
extension = "rar";
break;
}
else if (overlaySample.StartsWith([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01, 0x00]))
{
extension = "rar";
break;
}
else if (overlaySample.StartsWith(Models.MSDOS.Constants.SignatureBytes))
{
extension = "bin"; // exe/dll
break;
}
}
// If the extension is unset
if (extension.Length == 0)
return false;
// Create the temp filename
string tempFile = $"embedded_overlay.{extension}";
if (Filename != null)
tempFile = $"{Path.GetFileName(Filename)}-{tempFile}";
tempFile = Path.Combine(outputDirectory, tempFile);
var directoryName = Path.GetDirectoryName(tempFile);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Write the resource data to a temp file
using var tempStream = File.Open(tempFile, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
tempStream?.Write(overlayData, overlayOffset, overlayData.Length - overlayOffset);
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Extract data from a Wise installer
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractWise(string outputDirectory, bool includeDebug)
{
// Get the source data for reading
Stream source = _dataSource;
if (Filename != null)
{
// Try to open a multipart file
if (WiseOverlayHeader.OpenFile(Filename, includeDebug, out var temp) && temp != null)
source = temp;
}
// Try to find the overlay header
long offset = FindWiseOverlayHeader();
if (offset > 0 && offset < Length)
return ExtractWiseOverlay(outputDirectory, includeDebug, source, offset);
// Everything else could not extract
return false;
}
/// <summary>
/// Extract using Wise overlay
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <param name="source">Potentially multi-part stream to read</param>
/// <param name="offset">Offset to the start of the overlay header</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
private bool ExtractWiseOverlay(string outputDirectory, bool includeDebug, Stream source, long offset)
{
// Seek to the overlay and parse
source.Seek(offset, SeekOrigin.Begin);
var header = WiseOverlayHeader.Create(source);
if (header == null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse the overlay header");
return false;
}
// Extract the header-defined files
bool extracted = header.ExtractHeaderDefinedFiles(outputDirectory, includeDebug);
if (!extracted)
{
if (includeDebug) Console.Error.WriteLine("Could not extract header-defined files");
return false;
}
// Open the script file from the output directory
var scriptStream = File.OpenRead(Path.Combine(outputDirectory, "WiseScript.bin"));
var script = WiseScript.Create(scriptStream);
if (script == null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse WiseScript.bin");
return false;
}
// Get the source directory
string? sourceDirectory = null;
if (Filename != null)
sourceDirectory = Path.GetDirectoryName(Path.GetFullPath(Filename));
// Process the state machine
return script.ProcessStateMachine(header, sourceDirectory, outputDirectory, includeDebug);
}
#endregion
#region Resources
/// <summary>
@@ -605,28 +373,31 @@ namespace SabreTools.Serialization.Wrappers
if (overlayOffset < 0 || overlayOffset >= Length)
return -1;
// Attempt to get the overlay header
_dataSource.Seek(overlayOffset, SeekOrigin.Begin);
var header = WiseOverlayHeader.Create(_dataSource);
if (header != null)
return overlayOffset;
// Align and loop to see if it can be found
_dataSource.Seek(overlayOffset, SeekOrigin.Begin);
_dataSource.AlignToBoundary(0x10);
overlayOffset = _dataSource.Position;
while (_dataSource.Position < Length)
lock (_dataSourceLock)
{
// Attempt to get the overlay header
_dataSource.Seek(overlayOffset, SeekOrigin.Begin);
header = WiseOverlayHeader.Create(_dataSource);
var header = WiseOverlayHeader.Create(_dataSource);
if (header != null)
return overlayOffset;
overlayOffset += 0x10;
}
// Align and loop to see if it can be found
_dataSource.Seek(overlayOffset, SeekOrigin.Begin);
_dataSource.AlignToBoundary(0x10);
overlayOffset = _dataSource.Position;
while (_dataSource.Position < Length)
{
_dataSource.Seek(overlayOffset, SeekOrigin.Begin);
header = WiseOverlayHeader.Create(_dataSource);
if (header != null)
return overlayOffset;
header = null;
return -1;
overlayOffset += 0x10;
}
header = null;
return -1;
}
}
/// <summary>
@@ -692,7 +463,7 @@ namespace SabreTools.Serialization.Wrappers
return [];
// Read the resource data and return
return _dataSource.ReadFrom(offset, length, retainPosition: true);
return ReadRangeFromSource(offset, length);
}
/// <summary>
@@ -784,7 +555,7 @@ namespace SabreTools.Serialization.Wrappers
return [];
// Read the segment data and return
return _dataSource.ReadFrom(offset, length, retainPosition: true);
return ReadRangeFromSource(offset, length);
}
/// <summary>
@@ -854,7 +625,7 @@ namespace SabreTools.Serialization.Wrappers
if (length == -1)
length = Length;
return _dataSource.ReadFrom(rangeStart, (int)length, retainPosition: true);
return ReadRangeFromSource(rangeStart, (int)length);
}
#endregion

View File

@@ -0,0 +1,437 @@
using System;
using SabreTools.IO.Extensions;
using SabreTools.Models.Nitro;
namespace SabreTools.Serialization.Wrappers
{
public partial class Nitro
{
#region Encryption process variables
private uint[] _cardHash = new uint[0x412];
private uint[] _arg2 = new uint[3];
#endregion
#region Encrypt
/// <summary>
/// Encrypt secure area in the DS/DSi file
/// </summary>s
/// <param name="tableData">Blowfish table data as a byte array</param>
/// <param name="force">Indicates if the operation should be forced</param>
public void EncryptSecureArea(byte[] tableData, bool force)
{
// If we're forcing the operation, tell the user
if (force)
{
Console.WriteLine("File is not verified due to force flag being set.");
}
// If we're not forcing the operation, check to see if we should be proceeding
else
{
bool? isDecrypted = CheckIfDecrypted(out string? message);
if (message != null)
Console.WriteLine(message);
if (isDecrypted == null)
{
Console.WriteLine("File has an empty secure area, cannot proceed");
return;
}
else if (!isDecrypted.Value)
{
Console.WriteLine("File is already encrypted");
return;
}
}
EncryptARM9(tableData);
Console.WriteLine("File has been encrypted");
}
/// <summary>
/// Encrypt the secure ARM9 region of the file, if possible
/// </summary>
/// <param name="tableData">Blowfish table data as a byte array</param>
private void EncryptARM9(byte[] tableData)
{
// If the secure area is invalid, nothing can be done
if (SecureArea == null)
return;
// Point to the beginning of the secure area
int readOffset = 0;
// Grab the first two blocks
uint p0 = SecureArea.ReadUInt32LittleEndian(ref readOffset);
uint p1 = SecureArea.ReadUInt32LittleEndian(ref readOffset);
// Perform the initialization steps
Init1(tableData);
_arg2[1] <<= 1;
_arg2[2] >>= 1;
Init2();
// Ensure alignment
readOffset = 0x08;
int writeOffset = 0x08;
// Loop throgh the main encryption step
uint size = 0x800 - 8;
while (size > 0)
{
p0 = SecureArea.ReadUInt32LittleEndian(ref readOffset);
p1 = SecureArea.ReadUInt32LittleEndian(ref readOffset);
Encrypt(ref p1, ref p0);
SecureArea.Write(ref writeOffset, p0);
SecureArea.Write(ref writeOffset, p1);
size -= 8;
}
// Replace the header explicitly
readOffset = 0;
writeOffset = 0;
p0 = SecureArea.ReadUInt32LittleEndian(ref readOffset);
p1 = SecureArea.ReadUInt32LittleEndian(ref readOffset);
if (p0 == 0xE7FFDEFF && p1 == 0xE7FFDEFF)
{
p0 = Constants.MAGIC30;
p1 = Constants.MAGIC34;
}
Encrypt(ref p1, ref p0);
Init1(tableData);
Encrypt(ref p1, ref p0);
SecureArea.Write(ref writeOffset, p0);
SecureArea.Write(ref writeOffset, p1);
}
/// <summary>
/// Perform an encryption step
/// </summary>
/// <param name="arg1">First unsigned value to use in encryption</param>
/// <param name="arg2">Second unsigned value to use in encryption</param>
private void Encrypt(ref uint arg1, ref uint arg2)
{
uint a = arg1;
uint b = arg2;
for (int i = 0; i < 16; i++)
{
uint c = _cardHash[i] ^ a;
a = b ^ Lookup(c);
b = c;
}
arg2 = a ^ _cardHash[16];
arg1 = b ^ _cardHash[17];
}
#endregion
#region Decrypt
/// <summary>
/// Decrypt secure area in the DS/DSi file
/// </summary>s
/// <param name="tableData">Blowfish table data as a byte array</param>
/// <param name="force">Indicates if the operation should be forced</param>
public void DecryptSecureArea(byte[] tableData, bool force)
{
// If we're forcing the operation, tell the user
if (force)
{
Console.WriteLine("File is not verified due to force flag being set.");
}
// If we're not forcing the operation, check to see if we should be proceeding
else
{
bool? isDecrypted = CheckIfDecrypted(out string? message);
if (message != null)
Console.WriteLine(message);
if (isDecrypted == null)
{
Console.WriteLine("File has an empty secure area, cannot proceed");
return;
}
else if (isDecrypted.Value)
{
Console.WriteLine("File is already decrypted");
return;
}
}
DecryptARM9(tableData);
Console.WriteLine("File has been decrypted");
}
/// <summary>
/// Decrypt the secure ARM9 region of the file, if possible
/// </summary>
/// <param name="tableData">Blowfish table data as a byte array</param>
private void DecryptARM9(byte[] tableData)
{
// If the secure area is invalid, nothing can be done
if (SecureArea == null)
return;
// Point to the beginning of the secure area
int readOffset = 0;
int writeOffset = 0;
// Grab the first two blocks
uint p0 = SecureArea.ReadUInt32LittleEndian(ref readOffset);
uint p1 = SecureArea.ReadUInt32LittleEndian(ref readOffset);
// Perform the initialization steps
Init1(tableData);
Decrypt(ref p1, ref p0);
_arg2[1] <<= 1;
_arg2[2] >>= 1;
Init2();
// Set the proper flags
Decrypt(ref p1, ref p0);
if (p0 == Constants.MAGIC30 && p1 == Constants.MAGIC34)
{
p0 = 0xE7FFDEFF;
p1 = 0xE7FFDEFF;
}
SecureArea.Write(ref writeOffset, p0);
SecureArea.Write(ref writeOffset, p1);
// Ensure alignment
readOffset = 0x08;
writeOffset = 0x08;
// Loop throgh the main encryption step
uint size = 0x800 - 8;
while (size > 0)
{
p0 = SecureArea.ReadUInt32LittleEndian(ref readOffset);
p1 = SecureArea.ReadUInt32LittleEndian(ref readOffset);
Decrypt(ref p1, ref p0);
SecureArea.Write(ref writeOffset, p0);
SecureArea.Write(ref writeOffset, p1);
size -= 8;
}
}
/// <summary>
/// Perform a decryption step
/// </summary>
/// <param name="arg1">First unsigned value to use in decryption</param>
/// <param name="arg2">Second unsigned value to use in decryption</param>
private void Decrypt(ref uint arg1, ref uint arg2)
{
uint a = arg1;
uint b = arg2;
for (int i = 17; i > 1; i--)
{
uint c = _cardHash[i] ^ a;
a = b ^ Lookup(c);
b = c;
}
arg1 = b ^ _cardHash[0];
arg2 = a ^ _cardHash[1];
}
#endregion
#region Common
/// <summary>
/// Determine if the current file is already decrypted or not (or has an empty secure area)
/// </summary>
/// <param name="message">Optional message with more information on the result</param>
/// <returns>True if the file has known values for a decrypted file, null if it's empty, false otherwise</returns>
public bool? CheckIfDecrypted(out string? message)
{
// Return empty if the secure area is undefined
if (SecureArea == null)
{
message = "Secure area is undefined. Cannot be encrypted or decrypted.";
return null;
}
int offset = 0;
uint firstValue = SecureArea.ReadUInt32LittleEndian(ref offset);
uint secondValue = SecureArea.ReadUInt32LittleEndian(ref offset);
// Empty secure area standard
if (firstValue == 0x00000000 && secondValue == 0x00000000)
{
message = "Empty secure area found. Cannot be encrypted or decrypted.";
return null;
}
// Improperly decrypted empty secure area (decrypt empty with woodsec)
else if ((firstValue == 0xE386C397 && secondValue == 0x82775B7E)
|| (firstValue == 0xF98415B8 && secondValue == 0x698068FC)
|| (firstValue == 0xA71329EE && secondValue == 0x2A1D4C38)
|| (firstValue == 0xC44DCC48 && secondValue == 0x38B6F8CB)
|| (firstValue == 0x3A9323B5 && secondValue == 0xC0387241))
{
message = "Improperly decrypted empty secure area found. Should be encrypted to get proper value.";
return true;
}
// Improperly encrypted empty secure area (encrypt empty with woodsec)
else if ((firstValue == 0x4BCE88BE && secondValue == 0xD3662DD1)
|| (firstValue == 0x2543C534 && secondValue == 0xCC4BE38E))
{
message = "Improperly encrypted empty secure area found. Should be decrypted to get proper value.";
return false;
}
// Properly decrypted nonstandard value (mastering issue)
else if ((firstValue == 0xD0D48B67 && secondValue == 0x39392F23) // Dragon Quest 5 (EU)
|| (firstValue == 0x014A191A && secondValue == 0xA5C470B9) // Dragon Quest 5 (USA)
|| (firstValue == 0x7829BC8D && secondValue == 0x9968EF44) // Dragon Quest 5 (JP)
|| (firstValue == 0xC4A15AB8 && secondValue == 0xD2E667C8) // Prince of Persia (EU)
|| (firstValue == 0xD5E97D20 && secondValue == 0x21B2A159)) // Prince of Persia (USA)
{
message = "Decrypted secure area for known, nonstandard value found.";
return true;
}
// Properly decrypted prototype value
else if (firstValue == 0xBA35F813 && secondValue == 0xB691AAE8)
{
message = "Decrypted secure area for prototype found.";
return true;
}
// Strange, unlicenced values that can't determine decryption state
else if ((firstValue == 0xE1D830D8 && secondValue == 0xE3530000) // Aquela Ball (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xDC002A02 && secondValue == 0x2900E612) // Bahlz (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE1A03BA3 && secondValue == 0xE2011CFF) // Battle Ship (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE3A01001 && secondValue == 0xE1A02001) // Breakout!! DS (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE793200C && secondValue == 0xE4812004) // Bubble Fusion (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE583C0DC && secondValue == 0x0A00000B) // Carre Rouge (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0x0202453C && secondValue == 0x02060164) // ChainReaction (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xEBFFF218 && secondValue == 0xE31000FF) // Collection (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0x4A6CD003 && secondValue == 0x425B2301) // DiggerDS (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE3A00001 && secondValue == 0xEBFFFF8C) // Double Skill (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0x21043701 && secondValue == 0x45BA448C) // DSChess (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE59D0010 && secondValue == 0xE0833000) // Hexa-Virus (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE5C3A006 && secondValue == 0xE5C39007) // Invasion (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE1D920F4 && secondValue == 0xE06A3000) // JoggleDS (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE59F32EC && secondValue == 0xE5DD7011) // London Underground (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE08A3503 && secondValue == 0xE1D3C4B8) // NumberMinds (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE1A0C001 && secondValue == 0xE0031001) // Paddle Battle (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE1A03005 && secondValue == 0xE88D0180) // Pop the Balls (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE8BD4030 && secondValue == 0xE12FFF1E) // Solitaire DS (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE0A88006 && secondValue == 0xE1A00003) // Squash DS (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE51F3478 && secondValue == 0xEB004A02) // Super Snake DS (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0x1C200052 && secondValue == 0xFD12F013) // Tales of Dagur (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0x601F491E && secondValue == 0x041B880B) // Tetris & Touch (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE1A03843 && secondValue == 0xE0000293) // Tic Tac Toe (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE3530000 && secondValue == 0x13A03003) // Warrior Training (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0x02054A80 && secondValue == 0x02054B80)) // Zi (World) (Unl) (Datel Games n' Music)
{
message = "Unlicensed invalid value found. Unknown if encrypted or decrypted.";
return null;
}
// Standard decryption values
message = null;
return firstValue == 0xE7FFDEFF && secondValue == 0xE7FFDEFF;
}
/// <summary>
/// First common initialization step
/// </summary>
/// <param name="tableData">Blowfish table data as a byte array</param>
private void Init1(byte[] tableData)
{
Buffer.BlockCopy(tableData, 0, _cardHash, 0, 4 * (1024 + 18));
_arg2 = [GameCode, GameCode >> 1, GameCode << 1];
Init2();
Init2();
}
/// <summary>
/// Second common initialization step
/// </summary>
private void Init2()
{
Encrypt(ref _arg2[2], ref _arg2[1]);
Encrypt(ref _arg2[1], ref _arg2[0]);
byte[] allBytes = [.. BitConverter.GetBytes(_arg2[0]),
.. BitConverter.GetBytes(_arg2[1]),
.. BitConverter.GetBytes(_arg2[2])];
UpdateHashtable(allBytes);
}
/// <summary>
/// Lookup the value from the hashtable
/// </summary>
/// <param name="v">Value to lookup in the hashtable</param>
/// <returns>Processed value through the hashtable</returns>
private uint Lookup(uint v)
{
uint a = (v >> 24) & 0xFF;
uint b = (v >> 16) & 0xFF;
uint c = (v >> 8) & 0xFF;
uint d = (v >> 0) & 0xFF;
a = _cardHash[a + 18 + 0];
b = _cardHash[b + 18 + 256];
c = _cardHash[c + 18 + 512];
d = _cardHash[d + 18 + 768];
return d + (c ^ (b + a));
}
/// <summary>
/// Update the hashtable
/// </summary>
/// <param name="arg1">Value to update the hashtable with</param>
private void UpdateHashtable(byte[] arg1)
{
for (int j = 0; j < 18; j++)
{
uint r3 = 0;
for (int i = 0; i < 4; i++)
{
r3 <<= 8;
r3 |= arg1[(j * 4 + i) & 7];
}
_cardHash[j] ^= r3;
}
uint tmp1 = 0;
uint tmp2 = 0;
for (int i = 0; i < 18; i += 2)
{
Encrypt(ref tmp1, ref tmp2);
_cardHash[i + 0] = tmp1;
_cardHash[i + 1] = tmp2;
}
for (int i = 0; i < 0x400; i += 2)
{
Encrypt(ref tmp1, ref tmp2);
_cardHash[i + 18 + 0] = tmp1;
_cardHash[i + 18 + 1] = tmp2;
}
}
#endregion
}
}

View File

@@ -1,11 +1,9 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using System.IO;
using SabreTools.Models.Nitro;
namespace SabreTools.Serialization.Wrappers
{
public class Nitro : WrapperBase<Cart>
public partial class Nitro : WrapperBase<Cart>
{
#region Descriptive Properties
@@ -27,28 +25,29 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region Encryption process variables
private uint[] _cardHash = new uint[0x412];
private uint[] _arg2 = new uint[3];
#endregion
#region Constructors
/// <inheritdoc/>
public Nitro(Cart? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public Nitro(Cart model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public Nitro(Cart? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public Nitro(Cart model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public Nitro(Cart model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public Nitro(Cart model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public Nitro(Cart model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public Nitro(Cart model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a NDS cart image from a byte array and offset
@@ -87,12 +86,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.Nitro.DeserializeStream(data);
var model = new Deserializers.Nitro().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new Nitro(model, data);
return new Nitro(model, data, currentOffset);
}
catch
{
@@ -101,430 +99,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Encryption
#region Encrypt
/// <summary>
/// Encrypt secure area in the DS/DSi file
/// </summary>s
/// <param name="tableData">Blowfish table data as a byte array</param>
/// <param name="force">Indicates if the operation should be forced</param>
public void EncryptSecureArea(byte[] tableData, bool force)
{
// If we're forcing the operation, tell the user
if (force)
{
Console.WriteLine("File is not verified due to force flag being set.");
}
// If we're not forcing the operation, check to see if we should be proceeding
else
{
bool? isDecrypted = CheckIfDecrypted(out string? message);
if (message != null)
Console.WriteLine(message);
if (isDecrypted == null)
{
Console.WriteLine("File has an empty secure area, cannot proceed");
return;
}
else if (!isDecrypted.Value)
{
Console.WriteLine("File is already encrypted");
return;
}
}
EncryptARM9(tableData);
Console.WriteLine("File has been encrypted");
}
/// <summary>
/// Encrypt the secure ARM9 region of the file, if possible
/// </summary>
/// <param name="tableData">Blowfish table data as a byte array</param>
private void EncryptARM9(byte[] tableData)
{
// If the secure area is invalid, nothing can be done
if (SecureArea == null)
return;
// Point to the beginning of the secure area
int readOffset = 0;
// Grab the first two blocks
uint p0 = SecureArea.ReadUInt32LittleEndian(ref readOffset);
uint p1 = SecureArea.ReadUInt32LittleEndian(ref readOffset);
// Perform the initialization steps
Init1(tableData);
_arg2[1] <<= 1;
_arg2[2] >>= 1;
Init2();
// Ensure alignment
readOffset = 0x08;
int writeOffset = 0x08;
// Loop throgh the main encryption step
uint size = 0x800 - 8;
while (size > 0)
{
p0 = SecureArea.ReadUInt32LittleEndian(ref readOffset);
p1 = SecureArea.ReadUInt32LittleEndian(ref readOffset);
Encrypt(ref p1, ref p0);
SecureArea.Write(ref writeOffset, p0);
SecureArea.Write(ref writeOffset, p1);
size -= 8;
}
// Replace the header explicitly
readOffset = 0;
writeOffset = 0;
p0 = SecureArea.ReadUInt32LittleEndian(ref readOffset);
p1 = SecureArea.ReadUInt32LittleEndian(ref readOffset);
if (p0 == 0xE7FFDEFF && p1 == 0xE7FFDEFF)
{
p0 = Constants.MAGIC30;
p1 = Constants.MAGIC34;
}
Encrypt(ref p1, ref p0);
Init1(tableData);
Encrypt(ref p1, ref p0);
SecureArea.Write(ref writeOffset, p0);
SecureArea.Write(ref writeOffset, p1);
}
/// <summary>
/// Perform an encryption step
/// </summary>
/// <param name="arg1">First unsigned value to use in encryption</param>
/// <param name="arg2">Second unsigned value to use in encryption</param>
private void Encrypt(ref uint arg1, ref uint arg2)
{
uint a = arg1;
uint b = arg2;
for (int i = 0; i < 16; i++)
{
uint c = _cardHash[i] ^ a;
a = b ^ Lookup(c);
b = c;
}
arg2 = a ^ _cardHash[16];
arg1 = b ^ _cardHash[17];
}
#endregion
#region Decrypt
/// <summary>
/// Decrypt secure area in the DS/DSi file
/// </summary>s
/// <param name="tableData">Blowfish table data as a byte array</param>
/// <param name="force">Indicates if the operation should be forced</param>
public void DecryptSecureArea(byte[] tableData, bool force)
{
// If we're forcing the operation, tell the user
if (force)
{
Console.WriteLine("File is not verified due to force flag being set.");
}
// If we're not forcing the operation, check to see if we should be proceeding
else
{
bool? isDecrypted = CheckIfDecrypted(out string? message);
if (message != null)
Console.WriteLine(message);
if (isDecrypted == null)
{
Console.WriteLine("File has an empty secure area, cannot proceed");
return;
}
else if (isDecrypted.Value)
{
Console.WriteLine("File is already decrypted");
return;
}
}
DecryptARM9(tableData);
Console.WriteLine("File has been decrypted");
}
/// <summary>
/// Decrypt the secure ARM9 region of the file, if possible
/// </summary>
/// <param name="tableData">Blowfish table data as a byte array</param>
private void DecryptARM9(byte[] tableData)
{
// If the secure area is invalid, nothing can be done
if (SecureArea == null)
return;
// Point to the beginning of the secure area
int readOffset = 0;
int writeOffset = 0;
// Grab the first two blocks
uint p0 = SecureArea.ReadUInt32LittleEndian(ref readOffset);
uint p1 = SecureArea.ReadUInt32LittleEndian(ref readOffset);
// Perform the initialization steps
Init1(tableData);
Decrypt(ref p1, ref p0);
_arg2[1] <<= 1;
_arg2[2] >>= 1;
Init2();
// Set the proper flags
Decrypt(ref p1, ref p0);
if (p0 == Constants.MAGIC30 && p1 == Constants.MAGIC34)
{
p0 = 0xE7FFDEFF;
p1 = 0xE7FFDEFF;
}
SecureArea.Write(ref writeOffset, p0);
SecureArea.Write(ref writeOffset, p1);
// Ensure alignment
readOffset = 0x08;
writeOffset = 0x08;
// Loop throgh the main encryption step
uint size = 0x800 - 8;
while (size > 0)
{
p0 = SecureArea.ReadUInt32LittleEndian(ref readOffset);
p1 = SecureArea.ReadUInt32LittleEndian(ref readOffset);
Decrypt(ref p1, ref p0);
SecureArea.Write(ref writeOffset, p0);
SecureArea.Write(ref writeOffset, p1);
size -= 8;
}
}
/// <summary>
/// Perform a decryption step
/// </summary>
/// <param name="arg1">First unsigned value to use in decryption</param>
/// <param name="arg2">Second unsigned value to use in decryption</param>
private void Decrypt(ref uint arg1, ref uint arg2)
{
uint a = arg1;
uint b = arg2;
for (int i = 17; i > 1; i--)
{
uint c = _cardHash[i] ^ a;
a = b ^ Lookup(c);
b = c;
}
arg1 = b ^ _cardHash[0];
arg2 = a ^ _cardHash[1];
}
#endregion
#region Common
/// <summary>
/// Determine if the current file is already decrypted or not (or has an empty secure area)
/// </summary>
/// <param name="message">Optional message with more information on the result</param>
/// <returns>True if the file has known values for a decrypted file, null if it's empty, false otherwise</returns>
public bool? CheckIfDecrypted(out string? message)
{
// Return empty if the secure area is undefined
if (SecureArea == null)
{
message = "Secure area is undefined. Cannot be encrypted or decrypted.";
return null;
}
int offset = 0;
uint firstValue = SecureArea.ReadUInt32LittleEndian(ref offset);
uint secondValue = SecureArea.ReadUInt32LittleEndian(ref offset);
// Empty secure area standard
if (firstValue == 0x00000000 && secondValue == 0x00000000)
{
message = "Empty secure area found. Cannot be encrypted or decrypted.";
return null;
}
// Improperly decrypted empty secure area (decrypt empty with woodsec)
else if ((firstValue == 0xE386C397 && secondValue == 0x82775B7E)
|| (firstValue == 0xF98415B8 && secondValue == 0x698068FC)
|| (firstValue == 0xA71329EE && secondValue == 0x2A1D4C38)
|| (firstValue == 0xC44DCC48 && secondValue == 0x38B6F8CB)
|| (firstValue == 0x3A9323B5 && secondValue == 0xC0387241))
{
message = "Improperly decrypted empty secure area found. Should be encrypted to get proper value.";
return true;
}
// Improperly encrypted empty secure area (encrypt empty with woodsec)
else if ((firstValue == 0x4BCE88BE && secondValue == 0xD3662DD1)
|| (firstValue == 0x2543C534 && secondValue == 0xCC4BE38E))
{
message = "Improperly encrypted empty secure area found. Should be decrypted to get proper value.";
return false;
}
// Properly decrypted nonstandard value (mastering issue)
else if ((firstValue == 0xD0D48B67 && secondValue == 0x39392F23) // Dragon Quest 5 (EU)
|| (firstValue == 0x014A191A && secondValue == 0xA5C470B9) // Dragon Quest 5 (USA)
|| (firstValue == 0x7829BC8D && secondValue == 0x9968EF44) // Dragon Quest 5 (JP)
|| (firstValue == 0xC4A15AB8 && secondValue == 0xD2E667C8) // Prince of Persia (EU)
|| (firstValue == 0xD5E97D20 && secondValue == 0x21B2A159)) // Prince of Persia (USA)
{
message = "Decrypted secure area for known, nonstandard value found.";
return true;
}
// Properly decrypted prototype value
else if (firstValue == 0xBA35F813 && secondValue == 0xB691AAE8)
{
message = "Decrypted secure area for prototype found.";
return true;
}
// Strange, unlicenced values that can't determine decryption state
else if ((firstValue == 0xE1D830D8 && secondValue == 0xE3530000) // Aquela Ball (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xDC002A02 && secondValue == 0x2900E612) // Bahlz (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE1A03BA3 && secondValue == 0xE2011CFF) // Battle Ship (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE3A01001 && secondValue == 0xE1A02001) // Breakout!! DS (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE793200C && secondValue == 0xE4812004) // Bubble Fusion (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE583C0DC && secondValue == 0x0A00000B) // Carre Rouge (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0x0202453C && secondValue == 0x02060164) // ChainReaction (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xEBFFF218 && secondValue == 0xE31000FF) // Collection (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0x4A6CD003 && secondValue == 0x425B2301) // DiggerDS (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE3A00001 && secondValue == 0xEBFFFF8C) // Double Skill (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0x21043701 && secondValue == 0x45BA448C) // DSChess (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE59D0010 && secondValue == 0xE0833000) // Hexa-Virus (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE5C3A006 && secondValue == 0xE5C39007) // Invasion (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE1D920F4 && secondValue == 0xE06A3000) // JoggleDS (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE59F32EC && secondValue == 0xE5DD7011) // London Underground (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE08A3503 && secondValue == 0xE1D3C4B8) // NumberMinds (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE1A0C001 && secondValue == 0xE0031001) // Paddle Battle (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE1A03005 && secondValue == 0xE88D0180) // Pop the Balls (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE8BD4030 && secondValue == 0xE12FFF1E) // Solitaire DS (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE0A88006 && secondValue == 0xE1A00003) // Squash DS (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE51F3478 && secondValue == 0xEB004A02) // Super Snake DS (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0x1C200052 && secondValue == 0xFD12F013) // Tales of Dagur (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0x601F491E && secondValue == 0x041B880B) // Tetris & Touch (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE1A03843 && secondValue == 0xE0000293) // Tic Tac Toe (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0xE3530000 && secondValue == 0x13A03003) // Warrior Training (World) (Unl) (Datel Games n' Music)
|| (firstValue == 0x02054A80 && secondValue == 0x02054B80)) // Zi (World) (Unl) (Datel Games n' Music)
{
message = "Unlicensed invalid value found. Unknown if encrypted or decrypted.";
return null;
}
// Standard decryption values
message = null;
return firstValue == 0xE7FFDEFF && secondValue == 0xE7FFDEFF;
}
/// <summary>
/// First common initialization step
/// </summary>
/// <param name="tableData">Blowfish table data as a byte array</param>
private void Init1(byte[] tableData)
{
Buffer.BlockCopy(tableData, 0, _cardHash, 0, 4 * (1024 + 18));
_arg2 = [GameCode, GameCode >> 1, GameCode << 1];
Init2();
Init2();
}
/// <summary>
/// Second common initialization step
/// </summary>
private void Init2()
{
Encrypt(ref _arg2[2], ref _arg2[1]);
Encrypt(ref _arg2[1], ref _arg2[0]);
byte[] allBytes = [.. BitConverter.GetBytes(_arg2[0]),
.. BitConverter.GetBytes(_arg2[1]),
.. BitConverter.GetBytes(_arg2[2])];
UpdateHashtable(allBytes);
}
/// <summary>
/// Lookup the value from the hashtable
/// </summary>
/// <param name="v">Value to lookup in the hashtable</param>
/// <returns>Processed value through the hashtable</returns>
private uint Lookup(uint v)
{
uint a = (v >> 24) & 0xFF;
uint b = (v >> 16) & 0xFF;
uint c = (v >> 8) & 0xFF;
uint d = (v >> 0) & 0xFF;
a = _cardHash[a + 18 + 0];
b = _cardHash[b + 18 + 256];
c = _cardHash[c + 18 + 512];
d = _cardHash[d + 18 + 768];
return d + (c ^ (b + a));
}
/// <summary>
/// Update the hashtable
/// </summary>
/// <param name="arg1">Value to update the hashtable with</param>
private void UpdateHashtable(byte[] arg1)
{
for (int j = 0; j < 18; j++)
{
uint r3 = 0;
for (int i = 0; i < 4; i++)
{
r3 <<= 8;
r3 |= arg1[(j * 4 + i) & 7];
}
_cardHash[j] ^= r3;
}
uint tmp1 = 0;
uint tmp2 = 0;
for (int i = 0; i < 18; i += 2)
{
Encrypt(ref tmp1, ref tmp2);
_cardHash[i + 0] = tmp1;
_cardHash[i + 1] = tmp2;
}
for (int i = 0; i < 0x400; i += 2)
{
Encrypt(ref tmp1, ref tmp2);
_cardHash[i + 18 + 0] = tmp1;
_cardHash[i + 18 + 1] = tmp2;
}
}
#endregion
#endregion
}
}

View File

@@ -0,0 +1,83 @@
using System;
using System.IO;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class PAK : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no directory items
if (DirectoryItems == null || DirectoryItems.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < DirectoryItems.Length; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the PAK to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If we have no directory items
if (DirectoryItems == null || DirectoryItems.Length == 0)
return false;
// If the directory item index is invalid
if (index < 0 || index >= DirectoryItems.Length)
return false;
// Read the item data
var directoryItem = DirectoryItems[index];
var data = ReadRangeFromSource((int)directoryItem.ItemOffset, (int)directoryItem.ItemLength);
if (data.Length == 0)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
string filename = directoryItem.ItemName ?? $"file{index}";
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = System.IO.File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
}
}

View File

@@ -1,12 +1,9 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Models.PAK;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class PAK : WrapperBase<Models.PAK.File>, IExtractable
public partial class PAK : WrapperBase<Models.PAK.File>
{
#region Descriptive Properties
@@ -25,18 +22,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public PAK(Models.PAK.File? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public PAK(Models.PAK.File model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public PAK(Models.PAK.File? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public PAK(Models.PAK.File model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public PAK(Models.PAK.File model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public PAK(Models.PAK.File model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public PAK(Models.PAK.File model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public PAK(Models.PAK.File model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a PAK from a byte array and offset
@@ -75,12 +80,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.PAK.DeserializeStream(data);
var model = new Deserializers.PAK().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new PAK(model, data);
return new PAK(model, data, currentOffset);
}
catch
{
@@ -89,83 +93,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no directory items
if (DirectoryItems == null || DirectoryItems.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < DirectoryItems.Length; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the PAK to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If we have no directory items
if (DirectoryItems == null || DirectoryItems.Length == 0)
return false;
// If the directory item index is invalid
if (index < 0 || index >= DirectoryItems.Length)
return false;
// Read the item data
var directoryItem = DirectoryItems[index];
var data = _dataSource.ReadFrom((int)directoryItem.ItemOffset, (int)directoryItem.ItemLength, retainPosition: true);
if (data == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
string filename = directoryItem.ItemName ?? $"file{index}";
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = System.IO.File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
#endregion
}
}

View File

@@ -0,0 +1,88 @@
using System;
using System.IO;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class PFF : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no segments
if (Segments == null || Segments.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < Segments.Length; i++)
{
allExtracted &= ExtractSegment(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a segment from the PFF to an output directory by index
/// </summary>
/// <param name="index">Segment index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the segment extracted, false otherwise</returns>
public bool ExtractSegment(int index, string outputDirectory, bool includeDebug)
{
// If we have no files
if (FileCount == 0)
return false;
// If we have no segments
if (Segments == null || Segments.Length == 0)
return false;
// If we have an invalid index
if (index < 0 || index >= Segments.Length)
return false;
// Get the read index and length
var segment = Segments[index];
int offset = (int)segment.FileLocation;
int size = (int)segment.FileSize;
try
{
// Ensure directory separators are consistent
string filename = segment.FileName ?? $"file{index}";
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Create the output file
using FileStream fs = File.OpenWrite(filename);
// Read the data block
var data = ReadRangeFromSource(offset, size);
if (data.Length == 0)
return false;
// Write the data -- TODO: Compressed data?
fs.Write(data, 0, size);
fs.Flush();
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
}
}

View File

@@ -1,12 +1,9 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Models.PFF;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class PFF : WrapperBase<Archive>, IExtractable
public partial class PFF : WrapperBase<Archive>
{
#region Descriptive Properties
@@ -30,18 +27,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public PFF(Archive? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public PFF(Archive model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public PFF(Archive? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public PFF(Archive model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public PFF(Archive model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public PFF(Archive model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public PFF(Archive model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public PFF(Archive model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a PFF archive from a byte array and offset
@@ -80,12 +85,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.PFF.DeserializeStream(data);
var model = new Deserializers.PFF().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new PFF(model, data);
return new PFF(model, data, currentOffset);
}
catch
{
@@ -94,88 +98,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no segments
if (Segments == null || Segments.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < Segments.Length; i++)
{
allExtracted &= ExtractSegment(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a segment from the PFF to an output directory by index
/// </summary>
/// <param name="index">Segment index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the segment extracted, false otherwise</returns>
public bool ExtractSegment(int index, string outputDirectory, bool includeDebug)
{
// If we have no files
if (FileCount == 0)
return false;
// If we have no segments
if (Segments == null || Segments.Length == 0)
return false;
// If we have an invalid index
if (index < 0 || index >= Segments.Length)
return false;
// Get the read index and length
var segment = Segments[index];
int offset = (int)segment.FileLocation;
int size = (int)segment.FileSize;
try
{
// Ensure directory separators are consistent
string filename = segment.FileName ?? $"file{index}";
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Create the output file
using FileStream fs = File.OpenWrite(filename);
// Read the data block
var data = _dataSource.ReadFrom(offset, size, retainPosition: true);
if (data == null)
return false;
// Write the data -- TODO: Compressed data?
fs.Write(data, 0, size);
fs.Flush();
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
#endregion
}
}

View File

@@ -22,18 +22,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public PIC(DiscInformation? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public PIC(DiscInformation model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public PIC(DiscInformation? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public PIC(DiscInformation model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public PIC(DiscInformation model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public PIC(DiscInformation model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public PIC(DiscInformation model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public PIC(DiscInformation model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a PIC disc information object from a byte array and offset
@@ -73,12 +81,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.PIC.DeserializeStream(data);
var model = new Deserializers.PIC().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new PIC(model, data);
return new PIC(model, data, currentOffset);
}
catch
{

View File

@@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using SabreTools.Serialization.Interfaces;
#if NET462_OR_GREATER || NETCOREAPP
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
using SharpCompress.Readers;
#endif
namespace SabreTools.Serialization.Wrappers
{
public partial class PKZIP : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
=> Extract(outputDirectory, lookForHeader: false, includeDebug);
/// <inheritdoc cref="Extract(string, bool)"/>
public bool Extract(string outputDirectory, bool lookForHeader, bool includeDebug)
{
if (_dataSource == null || !_dataSource.CanRead)
return false;
#if NET462_OR_GREATER || NETCOREAPP
try
{
var readerOptions = new ReaderOptions() { LookForHeader = lookForHeader };
var zipFile = ZipArchive.Open(_dataSource, readerOptions);
// If the file exists
if (!string.IsNullOrEmpty(Filename) && File.Exists(Filename!))
{
// Find all file parts
FileInfo[] parts = [.. ArchiveFactory.GetFileParts(new FileInfo(Filename))];
// If there are multiple parts
if (parts.Length > 1)
zipFile = ZipArchive.Open(parts, readerOptions);
// Try to read the file path if no entries are found
else if (zipFile.Entries.Count == 0)
zipFile = ZipArchive.Open(parts, readerOptions);
}
foreach (var entry in zipFile.Entries)
{
try
{
// If the entry is a directory
if (entry.IsDirectory)
continue;
// If the entry has an invalid key
if (entry.Key == null)
continue;
// If the entry is partial due to an incomplete multi-part archive, skip it
if (!entry.IsComplete)
continue;
// Ensure directory separators are consistent
string filename = entry.Key;
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
entry.WriteToFile(filename);
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
}
}
return true;
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
return false;
}
#else
Console.WriteLine("Extraction is not supported for this framework!");
Console.WriteLine();
return false;
#endif
}
/// <summary>
/// Try to find all parts of the archive, if possible
/// </summary>
/// <param name="firstPart">Path of the first archive part</param>
/// <returns>List of all found parts, if possible</returns>
public static List<string> FindParts(string firstPart)
{
// Define the regex patterns
const string zipPattern = @"^(.*\.)(zipx?|zx?[0-9]+)$";
const string genericPattern = @"^(.*\.)([0-9]+)$";
// Ensure the full path is available
firstPart = Path.GetFullPath(firstPart);
string filename = Path.GetFileName(firstPart);
string? directory = Path.GetDirectoryName(firstPart);
// Make the output list
List<string> parts = [];
// Determine which pattern is being used
Match match;
Func<int, string> nextPartFunc;
if (Regex.IsMatch(filename, zipPattern, RegexOptions.IgnoreCase))
{
match = Regex.Match(filename, zipPattern, RegexOptions.IgnoreCase);
nextPartFunc = (i) =>
{
return string.Concat(
match.Groups[1].Value,
Regex.Replace(match.Groups[2].Value, @"[^xz]", ""),
$"{i:D2}");
};
}
else if (Regex.IsMatch(filename, genericPattern, RegexOptions.IgnoreCase))
{
match = Regex.Match(filename, genericPattern, RegexOptions.IgnoreCase);
nextPartFunc = (i) =>
{
return string.Concat(
match.Groups[1].Value,
$"{i + 1}".PadLeft(match.Groups[2].Value.Length, '0')
);
};
}
else
{
return [firstPart];
}
// Loop and add the files
parts.Add(firstPart);
for (int i = 1; ; i++)
{
string nextPart = nextPartFunc(i);
if (directory != null)
nextPart = Path.Combine(directory, nextPart);
if (!File.Exists(nextPart))
break;
parts.Add(nextPart);
}
return parts;
}
}
}

View File

@@ -1,18 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using SabreTools.Models.PKZIP;
using SabreTools.Serialization.Interfaces;
#if NET462_OR_GREATER || NETCOREAPP
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
using SharpCompress.Readers;
#endif
namespace SabreTools.Serialization.Wrappers
{
public class PKZIP : WrapperBase<Archive>, IExtractable
public partial class PKZIP : WrapperBase<Archive>
{
#region Descriptive Properties
@@ -43,34 +34,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
/// <remarks>This should only be used for WinZipSFX</remarks>
public PKZIP(byte[]? data, int offset)
: base(new Archive(), data, offset)
{
// All logic is handled by the base class
}
public PKZIP(Archive model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
/// <remarks>This should only be used for WinZipSFX</remarks>
public PKZIP(Stream? data)
: base(new Archive(), data)
{
// All logic is handled by the base class
}
public PKZIP(Archive model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public PKZIP(Archive? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public PKZIP(Archive model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public PKZIP(Archive? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public PKZIP(Archive model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public PKZIP(Archive model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public PKZIP(Archive model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a PKZIP archive (or derived format) from a byte array and offset
@@ -109,12 +92,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.PKZIP.DeserializeStream(data);
var model = new Deserializers.PKZIP().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new PKZIP(model, data);
return new PKZIP(model, data, currentOffset);
}
catch
{
@@ -123,157 +105,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
=> Extract(outputDirectory, lookForHeader: false, includeDebug);
/// <inheritdoc cref="Extract(string, bool)"/>
public bool Extract(string outputDirectory, bool lookForHeader, bool includeDebug)
{
if (_dataSource == null || !_dataSource.CanRead)
return false;
#if NET462_OR_GREATER || NETCOREAPP
try
{
var readerOptions = new ReaderOptions() { LookForHeader = lookForHeader };
var zipFile = ZipArchive.Open(_dataSource, readerOptions);
// If the file exists
if (!string.IsNullOrEmpty(Filename) && File.Exists(Filename!))
{
// Find all file parts
FileInfo[] parts = [.. ArchiveFactory.GetFileParts(new FileInfo(Filename))];
// If there are multiple parts
if (parts.Length > 1)
zipFile = ZipArchive.Open(parts, readerOptions);
// Try to read the file path if no entries are found
else if (zipFile.Entries.Count == 0)
zipFile = ZipArchive.Open(parts, readerOptions);
}
foreach (var entry in zipFile.Entries)
{
try
{
// If the entry is a directory
if (entry.IsDirectory)
continue;
// If the entry has an invalid key
if (entry.Key == null)
continue;
// If the entry is partial due to an incomplete multi-part archive, skip it
if (!entry.IsComplete)
continue;
// Ensure directory separators are consistent
string filename = entry.Key;
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
entry.WriteToFile(filename);
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
}
}
return true;
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
return false;
}
#else
Console.WriteLine("Extraction is not supported for this framework!");
Console.WriteLine();
return false;
#endif
}
/// <summary>
/// Try to find all parts of the archive, if possible
/// </summary>
/// <param name="firstPart">Path of the first archive part</param>
/// <returns>List of all found parts, if possible</returns>
public static List<string> FindParts(string firstPart)
{
// Define the regex patterns
const string zipPattern = @"^(.*\.)(zipx?|zx?[0-9]+)$";
const string genericPattern = @"^(.*\.)([0-9]+)$";
// Ensure the full path is available
firstPart = Path.GetFullPath(firstPart);
string filename = Path.GetFileName(firstPart);
string? directory = Path.GetDirectoryName(firstPart);
// Make the output list
List<string> parts = [];
// Determine which pattern is being used
Match match;
Func<int, string> nextPartFunc;
if (Regex.IsMatch(filename, zipPattern, RegexOptions.IgnoreCase))
{
match = Regex.Match(filename, zipPattern, RegexOptions.IgnoreCase);
nextPartFunc = (i) =>
{
return string.Concat(
match.Groups[1].Value,
Regex.Replace(match.Groups[2].Value, @"[^xz]", ""),
$"{i:D2}");
};
}
else if (Regex.IsMatch(filename, genericPattern, RegexOptions.IgnoreCase))
{
match = Regex.Match(filename, genericPattern, RegexOptions.IgnoreCase);
nextPartFunc = (i) =>
{
return string.Concat(
match.Groups[1].Value,
$"{i + 1}".PadLeft(match.Groups[2].Value.Length, '0')
);
};
}
else
{
return [firstPart];
}
// Loop and add the files
parts.Add(firstPart);
for (int i = 1; ; i++)
{
string nextPart = nextPartFunc(i);
if (directory != null)
nextPart = Path.Combine(directory, nextPart);
if (!File.Exists(nextPart))
break;
parts.Add(nextPart);
}
return parts;
}
#endregion
}
}

View File

@@ -14,18 +14,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public PlayJAudioFile(Models.PlayJ.AudioFile? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public PlayJAudioFile(Models.PlayJ.AudioFile model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public PlayJAudioFile(Models.PlayJ.AudioFile? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public PlayJAudioFile(Models.PlayJ.AudioFile model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public PlayJAudioFile(Models.PlayJ.AudioFile model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public PlayJAudioFile(Models.PlayJ.AudioFile model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public PlayJAudioFile(Models.PlayJ.AudioFile model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public PlayJAudioFile(Models.PlayJ.AudioFile model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a PlayJ audio file from a byte array and offset
@@ -64,12 +72,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.PlayJAudio.DeserializeStream(data);
var model = new Deserializers.PlayJAudio().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new PlayJAudioFile(model, data);
return new PlayJAudioFile(model, data, currentOffset);
}
catch
{

View File

@@ -14,18 +14,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public PlayJPlaylist(Models.PlayJ.Playlist? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public PlayJPlaylist(Models.PlayJ.Playlist model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public PlayJPlaylist(Models.PlayJ.Playlist? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public PlayJPlaylist(Models.PlayJ.Playlist model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public PlayJPlaylist(Models.PlayJ.Playlist model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public PlayJPlaylist(Models.PlayJ.Playlist model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public PlayJPlaylist(Models.PlayJ.Playlist model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public PlayJPlaylist(Models.PlayJ.Playlist model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a PlayJ playlist from a byte array and offset
@@ -64,12 +72,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.PlayJPlaylist.DeserializeStream(data);
var model = new Deserializers.PlayJPlaylist().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new PlayJPlaylist(model, data);
return new PlayJPlaylist(model, data, currentOffset);
}
catch
{

View File

@@ -0,0 +1,559 @@
using System;
using System.IO;
using SabreTools.IO.Compression.zlib;
using SabreTools.IO.Extensions;
using SabreTools.Matching;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class PortableExecutable : IExtractable
{
/// <inheritdoc/>
/// <remarks>
/// This extracts the following data:
/// - Archives and executables in the overlay
/// - Archives and executables in resource data
/// - CExe-compressed resource data
/// - SecuROM Matroschka package sections
/// - SFX archives (7z, MS-CAB, PKZIP, RAR)
/// - Wise installers
/// </remarks>
public bool Extract(string outputDirectory, bool includeDebug)
{
bool cexe = ExtractCExe(outputDirectory, includeDebug);
bool matroschka = ExtractMatroschka(outputDirectory, includeDebug);
bool overlay = ExtractFromOverlay(outputDirectory, includeDebug);
bool resources = ExtractFromResources(outputDirectory, includeDebug);
bool wise = ExtractWise(outputDirectory, includeDebug);
return cexe || matroschka || overlay || resources || wise;
}
/// <summary>
/// Extract a CExe-compressed executable
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractCExe(string outputDirectory, bool includeDebug)
{
try
{
// Get all resources of type 99 with index 2
var resources = FindResourceByNamedType("99, 2");
if (resources == null || resources.Count == 0)
return false;
// Get the first resource of type 99 with index 2
var resource = resources[0];
if (resource == null || resource.Length == 0)
return false;
// Create the output data buffer
byte[]? data = [];
// If we had the decompression DLL included, it's zlib
if (FindResourceByNamedType("99, 1").Count > 0)
data = DecompressCExeZlib(resource);
else
data = DecompressCExeLZ(resource);
// If we have no data
if (data == null)
return false;
// Create the temp filename
string tempFile = string.IsNullOrEmpty(Filename) ? "temp.sxe" : $"{Path.GetFileNameWithoutExtension(Filename)}.sxe";
tempFile = Path.Combine(outputDirectory, tempFile);
var directoryName = Path.GetDirectoryName(tempFile);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Write the file data to a temp file
var tempStream = File.Open(tempFile, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
tempStream.Write(data, 0, data.Length);
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Extract data from the overlay
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractFromOverlay(string outputDirectory, bool includeDebug)
{
try
{
// Cache the overlay data for easier reading
var overlayData = OverlayData;
if (overlayData.Length == 0)
return false;
// Set the output variables
int overlayOffset = 0;
string extension = string.Empty;
// Only process the overlay if it is recognized
for (; overlayOffset < 0x400 && overlayOffset < overlayData.Length - 0x10; overlayOffset++)
{
int temp = overlayOffset;
byte[] overlaySample = overlayData.ReadBytes(ref temp, 0x10);
if (overlaySample.StartsWith([0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]))
{
extension = "7z";
break;
}
else if (overlaySample.StartsWith([0x3B, 0x21, 0x40, 0x49, 0x6E, 0x73, 0x74, 0x61, 0x6C, 0x6C]))
{
// 7-zip SFX script -- ";!@Install" to ";!@InstallEnd@!"
overlayOffset = overlayData.FirstPosition([0x3B, 0x21, 0x40, 0x49, 0x6E, 0x73, 0x74, 0x61, 0x6C, 0x6C, 0x45, 0x6E, 0x64, 0x40, 0x21]);
if (overlayOffset == -1)
return false;
overlayOffset += 15;
extension = "7z";
break;
}
else if (overlaySample.StartsWith([0x42, 0x5A, 0x68]))
{
extension = "bz2";
break;
}
else if (overlaySample.StartsWith([0x1F, 0x8B]))
{
extension = "gz";
break;
}
else if (overlaySample.StartsWith(Models.MicrosoftCabinet.Constants.SignatureBytes))
{
extension = "cab";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.LocalFileHeaderSignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.EndOfCentralDirectoryRecordSignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.EndOfCentralDirectoryRecord64SignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.DataDescriptorSignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00]))
{
extension = "rar";
break;
}
else if (overlaySample.StartsWith([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01, 0x00]))
{
extension = "rar";
break;
}
else if (overlaySample.StartsWith([0x55, 0x48, 0x41, 0x06]))
{
extension = "uha";
break;
}
else if (overlaySample.StartsWith([0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]))
{
extension = "xz";
break;
}
else if (overlaySample.StartsWith(Models.MSDOS.Constants.SignatureBytes))
{
extension = "bin"; // exe/dll
break;
}
}
// If the extension is unset
if (extension.Length == 0)
return false;
// Create the temp filename
string tempFile = $"embedded_overlay.{extension}";
if (Filename != null)
tempFile = $"{Path.GetFileName(Filename)}-{tempFile}";
tempFile = Path.Combine(outputDirectory, tempFile);
var directoryName = Path.GetDirectoryName(tempFile);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Write the resource data to a temp file
using var tempStream = File.Open(tempFile, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
tempStream?.Write(overlayData, overlayOffset, overlayData.Length - overlayOffset);
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Extract data from the resources
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractFromResources(string outputDirectory, bool includeDebug)
{
try
{
// Cache the resource data for easier reading
var resourceData = ResourceData;
if (resourceData.Count == 0)
return false;
// Get the resources that have an archive signature
int i = 0;
foreach (var kvp in resourceData)
{
// Get the key and value
string resourceKey = kvp.Key;
var value = kvp.Value;
if (value == null || value is not byte[] ba || ba.Length == 0)
continue;
// Set the output variables
int resourceOffset = 0;
string extension = string.Empty;
// Only process the resource if it a recognized signature
for (; resourceOffset < 0x400 && resourceOffset < ba.Length - 0x10; resourceOffset++)
{
int temp = resourceOffset;
byte[] resourceSample = ba.ReadBytes(ref temp, 0x10);
if (resourceSample.StartsWith([0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]))
{
extension = "7z";
break;
}
else if (resourceSample.StartsWith([0x42, 0x4D]))
{
extension = "bmp";
break;
}
else if (resourceSample.StartsWith([0x42, 0x5A, 0x68]))
{
extension = "bz2";
break;
}
else if (resourceSample.StartsWith([0x47, 0x49, 0x46, 0x38]))
{
extension = "gif";
break;
}
else if (resourceSample.StartsWith([0x1F, 0x8B]))
{
extension = "gz";
break;
}
else if (resourceSample.StartsWith([0xFF, 0xD8, 0xFF, 0xE0]))
{
extension = "jpg";
break;
}
else if (resourceSample.StartsWith([0x3C, 0x68, 0x74, 0x6D, 0x6C]))
{
extension = "html";
break;
}
else if (resourceSample.StartsWith(Models.MicrosoftCabinet.Constants.SignatureBytes))
{
extension = "cab";
break;
}
else if (resourceSample.StartsWith(Models.PKZIP.Constants.LocalFileHeaderSignatureBytes))
{
extension = "zip";
break;
}
else if (resourceSample.StartsWith(Models.PKZIP.Constants.EndOfCentralDirectoryRecordSignatureBytes))
{
extension = "zip";
break;
}
else if (resourceSample.StartsWith(Models.PKZIP.Constants.EndOfCentralDirectoryRecord64SignatureBytes))
{
extension = "zip";
break;
}
else if (resourceSample.StartsWith(Models.PKZIP.Constants.DataDescriptorSignatureBytes))
{
extension = "zip";
break;
}
else if (resourceSample.StartsWith([0x89, 0x50, 0x4E, 0x47]))
{
extension = "png";
break;
}
else if (resourceSample.StartsWith([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00]))
{
extension = "rar";
break;
}
else if (resourceSample.StartsWith([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01, 0x00]))
{
extension = "rar";
break;
}
else if (resourceSample.StartsWith([0x55, 0x48, 0x41, 0x06]))
{
extension = "uha";
break;
}
else if (resourceSample.StartsWith([0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]))
{
extension = "xz";
break;
}
else if (resourceSample.StartsWith(Models.MSDOS.Constants.SignatureBytes))
{
extension = "bin"; // exe/dll
break;
}
}
// If the extension is unset
if (extension.Length == 0)
continue;
try
{
// Create the temp filename
string tempFile = $"embedded_resource_{i++} ({resourceKey}).{extension}";
if (Filename != null)
tempFile = $"{Path.GetFileName(Filename)}-{tempFile}";
tempFile = Path.Combine(outputDirectory, tempFile);
var directoryName = Path.GetDirectoryName(tempFile);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Write the resource data to a temp file
using var tempStream = File.Open(tempFile, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
tempStream?.Write(ba, resourceOffset, ba.Length - resourceOffset);
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
}
}
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Extract data from a SecuROM Matroschka Package
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractMatroschka(string outputDirectory, bool includeDebug)
{
// Check if executable contains Matroschka package or not
if (MatroschkaPackage == null)
return false;
// Attempt to extract package
return MatroschkaPackage.Extract(outputDirectory, includeDebug);
}
/// <summary>
/// Extract data from a Wise installer
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractWise(string outputDirectory, bool includeDebug)
{
// Get the source data for reading
Stream source = _dataSource;
if (Filename != null)
{
// Try to open a multipart file
if (WiseOverlayHeader.OpenFile(Filename, includeDebug, out var temp) && temp != null)
source = temp;
}
// Try to find the overlay header
long offset = FindWiseOverlayHeader();
if (offset > 0 && offset < Length)
return ExtractWiseOverlay(outputDirectory, includeDebug, source, offset);
// Try to find the section header
if (WiseSection != null)
return ExtractWiseSection(outputDirectory, includeDebug);
// Everything else could not extract
return false;
}
/// <summary>
/// Decompress CExe data compressed with LZ
/// </summary>
/// <param name="resource">Resource data to inflate</param>
/// <returns>Inflated data on success, null otherwise</returns>
private static byte[]? DecompressCExeLZ(byte[] resource)
{
try
{
var decompressor = IO.Compression.SZDD.Decompressor.CreateSZDD(resource);
using var dataStream = new MemoryStream();
decompressor.CopyTo(dataStream);
return dataStream.ToArray();
}
catch
{
// Reset the data
return null;
}
}
/// <summary>
/// Decompress CExe data compressed with zlib
/// </summary>
/// <param name="resource">Resource data to inflate</param>
/// <returns>Inflated data on success, null otherwise</returns>
private static byte[]? DecompressCExeZlib(byte[] resource)
{
try
{
// Inflate the data into the buffer
var zstream = new ZLib.z_stream_s();
byte[] data = new byte[resource.Length * 4];
unsafe
{
fixed (byte* payloadPtr = resource)
fixed (byte* dataPtr = data)
{
zstream.next_in = payloadPtr;
zstream.avail_in = (uint)resource.Length;
zstream.total_in = (uint)resource.Length;
zstream.next_out = dataPtr;
zstream.avail_out = (uint)data.Length;
zstream.total_out = 0;
ZLib.inflateInit_(zstream, ZLib.zlibVersion(), resource.Length);
int zret = ZLib.inflate(zstream, 1);
ZLib.inflateEnd(zstream);
}
}
// Trim the buffer to the proper size
uint read = zstream.total_out;
#if NETFRAMEWORK
var temp = new byte[read];
Array.Copy(data, temp, read);
data = temp;
#else
data = new ReadOnlySpan<byte>(data, 0, (int)read).ToArray();
#endif
return data;
}
catch
{
// Reset the data
return null;
}
}
/// <summary>
/// Extract using Wise overlay
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <param name="source">Potentially multi-part stream to read</param>
/// <param name="offset">Offset to the start of the overlay header</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
private bool ExtractWiseOverlay(string outputDirectory, bool includeDebug, Stream source, long offset)
{
// Seek to the overlay and parse
source.Seek(offset, SeekOrigin.Begin);
var header = WiseOverlayHeader.Create(source);
if (header == null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse the overlay header");
return false;
}
// Extract the header-defined files
bool extracted = header.ExtractHeaderDefinedFiles(outputDirectory, includeDebug);
if (!extracted)
{
if (includeDebug) Console.Error.WriteLine("Could not extract header-defined files");
return false;
}
// Open the script file from the output directory
var scriptStream = File.OpenRead(Path.Combine(outputDirectory, "WiseScript.bin"));
var script = WiseScript.Create(scriptStream);
if (script == null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse WiseScript.bin");
return false;
}
// Get the source directory
string? sourceDirectory = null;
if (Filename != null)
sourceDirectory = Path.GetDirectoryName(Path.GetFullPath(Filename));
// Process the state machine
return script.ProcessStateMachine(header, sourceDirectory, outputDirectory, includeDebug);
}
/// <summary>
/// Extract using Wise section
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
private bool ExtractWiseSection(string outputDirectory, bool includeDebug)
{
// Get the section header
var header = WiseSection;
if (header == null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse the section header");
return false;
}
// Attempt to extract section
return header.Extract(outputDirectory, includeDebug);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,116 @@
using System;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class Quantum : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no files
if (FileList == null || FileList.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < FileList.Length; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the Quantum archive to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If we have no files
if (Header == null || FileCount == 0 || FileList == null || FileList.Length == 0)
return false;
// If we have an invalid index
if (index < 0 || index >= FileList.Length)
return false;
// Get the file information
var fileDescriptor = FileList[index];
// Read the entire compressed data
int compressedDataOffset = (int)CompressedDataOffset;
long compressedDataLength = Length - compressedDataOffset;
var compressedData = ReadRangeFromSource(compressedDataOffset, (int)compressedDataLength);
// Print a debug reminder
if (includeDebug) Console.WriteLine("Quantum archive extraction is unsupported");
// TODO: Figure out decompression
// - Single-file archives seem to work
// - Single-file archives with files that span a window boundary seem to work
// - The first files in each archive seem to work
return false;
// // Setup the decompression state
// State state = new State();
// Decompressor.InitState(state, TableSize, CompressionFlags);
// // Decompress the entire array
// int decompressedDataLength = (int)FileList.Sum(fd => fd.ExpandedFileSize);
// byte[] decompressedData = new byte[decompressedDataLength];
// Decompressor.Decompress(state, compressedData.Length, compressedData, decompressedData.Length, decompressedData);
// // Read the data
// int offset = (int)FileList.Take(index).Sum(fd => fd.ExpandedFileSize);
// byte[] data = new byte[fileDescriptor.ExpandedFileSize];
// Array.Copy(decompressedData, offset, data, 0, data.Length);
// // Loop through all files before the current
// for (int i = 0; i < index; i++)
// {
// // Decompress the next block of data
// byte[] tempData = new byte[FileList[i].ExpandedFileSize];
// int lastRead = Decompressor.Decompress(state, compressedData.Length, compressedData, tempData.Length, tempData);
// compressedData = new ReadOnlySpan<byte>(compressedData, (lastRead), compressedData.Length - (lastRead)).ToArray();
// }
// // Read the data
// byte[] data = new byte[fileDescriptor.ExpandedFileSize];
// _ = Decompressor.Decompress(state, compressedData.Length, compressedData, data.Length, data);
// // Create the filename
// string filename = fileDescriptor.FileName;
// // If we have an invalid output directory
// if (string.IsNullOrEmpty(outputDirectory))
// return false;
// // Create the full output path
// filename = Path.Combine(outputDirectory, filename);
// // Ensure the output directory is created
// Directory.CreateDirectory(Path.GetDirectoryName(filename));
// // Try to write the data
// try
// {
// // Open the output file for writing
// using (Stream fs = File.OpenWrite(filename))
// {
// fs.Write(data, 0, data.Length);
// }
// }
// catch
// {
// return false;
// }
// return true;
}
}
}

View File

@@ -1,12 +1,9 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Models.Quantum;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class Quantum : WrapperBase<Archive>, IExtractable
public partial class Quantum : WrapperBase<Archive>
{
#region Descriptive Properties
@@ -34,18 +31,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public Quantum(Archive? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public Quantum(Archive model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public Quantum(Archive? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public Quantum(Archive model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public Quantum(Archive model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public Quantum(Archive model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public Quantum(Archive model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public Quantum(Archive model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a Quantum archive from a byte array and offset
@@ -84,12 +89,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.Quantum.DeserializeStream(data);
var model = new Deserializers.Quantum().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new Quantum(model, data);
return new Quantum(model, data, currentOffset);
}
catch
{
@@ -98,117 +102,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no files
if (FileList == null || FileList.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < FileList.Length; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the Quantum archive to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If we have no files
if (Header == null || FileCount == 0 || FileList == null || FileList.Length == 0)
return false;
// If we have an invalid index
if (index < 0 || index >= FileList.Length)
return false;
// Get the file information
var fileDescriptor = FileList[index];
// Read the entire compressed data
int compressedDataOffset = (int)CompressedDataOffset;
long compressedDataLength = Length - compressedDataOffset;
var compressedData = _dataSource.ReadFrom(compressedDataOffset, (int)compressedDataLength, retainPosition: true);
// Print a debug reminder
if (includeDebug) Console.WriteLine("Quantum archive extraction is unsupported");
// TODO: Figure out decompression
// - Single-file archives seem to work
// - Single-file archives with files that span a window boundary seem to work
// - The first files in each archive seem to work
return false;
// // Setup the decompression state
// State state = new State();
// Decompressor.InitState(state, TableSize, CompressionFlags);
// // Decompress the entire array
// int decompressedDataLength = (int)FileList.Sum(fd => fd.ExpandedFileSize);
// byte[] decompressedData = new byte[decompressedDataLength];
// Decompressor.Decompress(state, compressedData.Length, compressedData, decompressedData.Length, decompressedData);
// // Read the data
// int offset = (int)FileList.Take(index).Sum(fd => fd.ExpandedFileSize);
// byte[] data = new byte[fileDescriptor.ExpandedFileSize];
// Array.Copy(decompressedData, offset, data, 0, data.Length);
// // Loop through all files before the current
// for (int i = 0; i < index; i++)
// {
// // Decompress the next block of data
// byte[] tempData = new byte[FileList[i].ExpandedFileSize];
// int lastRead = Decompressor.Decompress(state, compressedData.Length, compressedData, tempData.Length, tempData);
// compressedData = new ReadOnlySpan<byte>(compressedData, (lastRead), compressedData.Length - (lastRead)).ToArray();
// }
// // Read the data
// byte[] data = new byte[fileDescriptor.ExpandedFileSize];
// _ = Decompressor.Decompress(state, compressedData.Length, compressedData, data.Length, data);
// // Create the filename
// string filename = fileDescriptor.FileName;
// // If we have an invalid output directory
// if (string.IsNullOrEmpty(outputDirectory))
// return false;
// // Create the full output path
// filename = Path.Combine(outputDirectory, filename);
// // Ensure the output directory is created
// Directory.CreateDirectory(Path.GetDirectoryName(filename));
// // Try to write the data
// try
// {
// // Open the output file for writing
// using (Stream fs = File.OpenWrite(filename))
// {
// fs.Write(data, 0, data.Length);
// }
// }
// catch
// {
// return false;
// }
// return true;
}
#endregion
}
}

View File

@@ -0,0 +1,222 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using SabreTools.Serialization.Interfaces;
#if NET462_OR_GREATER || NETCOREAPP
using SharpCompress.Archives;
using SharpCompress.Archives.Rar;
using SharpCompress.Common;
using SharpCompress.Readers;
#endif
namespace SabreTools.Serialization.Wrappers
{
/// <summary>
/// This is a shell wrapper; one that does not contain
/// any actual parsing. It is used as a placeholder for
/// types that typically do not have models.
/// </summary>
public partial class RAR : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
=> Extract(outputDirectory, lookForHeader: false, includeDebug);
/// <inheritdoc cref="Extract(string, bool)"/>
public bool Extract(string outputDirectory, bool lookForHeader, bool includeDebug)
{
if (_dataSource == null || !_dataSource.CanRead)
return false;
#if NET462_OR_GREATER || NETCOREAPP
try
{
var readerOptions = new ReaderOptions() { LookForHeader = lookForHeader };
RarArchive rarFile = RarArchive.Open(_dataSource, readerOptions);
// If the file exists
if (!string.IsNullOrEmpty(Filename) && File.Exists(Filename!))
{
// Find all file parts
FileInfo[] parts = [.. ArchiveFactory.GetFileParts(new FileInfo(Filename))];
// If there are multiple parts
if (parts.Length > 1)
rarFile = RarArchive.Open(parts, readerOptions);
// Try to read the file path if no entries are found
else if (rarFile.Entries.Count == 0)
rarFile = RarArchive.Open(parts, readerOptions);
}
if (rarFile.IsSolid)
return ExtractSolid(rarFile, outputDirectory, includeDebug);
else
return ExtractNonSolid(rarFile, outputDirectory, includeDebug);
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
return false;
}
#else
Console.WriteLine("Extraction is not supported for this framework!");
Console.WriteLine();
return false;
#endif
}
/// <summary>
/// Try to find all parts of the archive, if possible
/// </summary>
/// <param name="firstPart">Path of the first archive part</param>
/// <returns>List of all found parts, if possible</returns>
public static List<string> FindParts(string firstPart)
{
// Define the regex patterns
const string rarNewPattern = @"^(.*\.part)([0-9]+)(\.rar)$";
const string rarOldPattern = @"^(.*\.)([r-z{])(ar|[0-9]+)$";
const string genericPattern = @"^(.*\.)([0-9]+)$";
// Ensure the full path is available
firstPart = Path.GetFullPath(firstPart);
string filename = Path.GetFileName(firstPart);
string? directory = Path.GetDirectoryName(firstPart);
// Make the output list
List<string> parts = [];
// Determine which pattern is being used
Match match;
Func<int, string> nextPartFunc;
if (Regex.IsMatch(filename, rarNewPattern, RegexOptions.IgnoreCase))
{
match = Regex.Match(filename, rarNewPattern, RegexOptions.IgnoreCase);
nextPartFunc = (i) =>
{
return string.Concat(
match.Groups[1].Value,
$"{i + 1}".PadLeft(match.Groups[2].Value.Length, '0'),
match.Groups[3].Value);
};
}
else if (Regex.IsMatch(filename, rarOldPattern, RegexOptions.IgnoreCase))
{
match = Regex.Match(filename, rarOldPattern, RegexOptions.IgnoreCase);
nextPartFunc = (i) =>
{
return string.Concat(
match.Groups[1].Value,
(char)(match.Groups[2].Value[0] + ((i - 1) / 100))
+ (i - 1).ToString("D4").Substring(2));
};
}
else if (Regex.IsMatch(filename, genericPattern, RegexOptions.IgnoreCase))
{
match = Regex.Match(filename, genericPattern, RegexOptions.IgnoreCase);
nextPartFunc = (i) =>
{
return string.Concat(
match.Groups[1].Value,
$"{i + 1}".PadLeft(match.Groups[2].Value.Length, '0')
);
};
}
else
{
return [firstPart];
}
// Loop and add the files
parts.Add(firstPart);
for (int i = 1; ; i++)
{
string nextPart = nextPartFunc(i);
if (directory != null)
nextPart = Path.Combine(directory, nextPart);
if (!File.Exists(nextPart))
break;
parts.Add(nextPart);
}
return parts;
}
#if NET462_OR_GREATER || NETCOREAPP
/// <summary>
/// Extraction method for non-solid archives. This iterates over each entry in the archive to extract every
/// file individually, in order to extract all valid files from the archive.
/// </summary>
private static bool ExtractNonSolid(RarArchive rarFile, string outDir, bool includeDebug)
{
foreach (var entry in rarFile.Entries)
{
try
{
// If the entry is a directory
if (entry.IsDirectory)
continue;
// If the entry has an invalid key
if (entry.Key == null)
continue;
// If we have a partial entry due to an incomplete multi-part archive, skip it
if (!entry.IsComplete)
continue;
// Ensure directory separators are consistent
string filename = entry.Key;
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outDir, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
entry.WriteToFile(filename);
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
}
}
return true;
}
/// <summary>
/// Extraction method for solid archives. Uses ExtractAllEntries because extraction for solid archives must be
/// done sequentially, and files beyond a corrupted point in a solid archive will be unreadable anyways.
/// </summary>
private static bool ExtractSolid(RarArchive rarFile, string outDir, bool includeDebug)
{
try
{
if (!Directory.Exists(outDir))
Directory.CreateDirectory(outDir);
rarFile.WriteToDirectory(outDir, new ExtractionOptions()
{
ExtractFullPath = true,
Overwrite = true,
});
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
}
return true;
}
#endif
}
}

View File

@@ -1,14 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using SabreTools.Serialization.Interfaces;
#if NET462_OR_GREATER || NETCOREAPP
using SharpCompress.Archives;
using SharpCompress.Archives.Rar;
using SharpCompress.Common;
using SharpCompress.Readers;
#endif
namespace SabreTools.Serialization.Wrappers
{
@@ -17,7 +7,7 @@ namespace SabreTools.Serialization.Wrappers
/// any actual parsing. It is used as a placeholder for
/// types that typically do not have models.
/// </summary>
public class RAR : WrapperBase, IExtractable
public partial class RAR : WrapperBase
{
#region Descriptive Properties
@@ -29,18 +19,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public RAR(byte[]? data, int offset)
: base(data, offset)
{
// All logic is handled by the base class
}
public RAR(byte[] data) : base(data) { }
/// <inheritdoc/>
public RAR(Stream? data)
: base(data)
{
// All logic is handled by the base class
}
public RAR(byte[] data, int offset) : base(data, offset) { }
/// <inheritdoc/>
public RAR(byte[] data, int offset, int length) : base(data, offset, length) { }
/// <inheritdoc/>
public RAR(Stream data) : base(data) { }
/// <inheritdoc/>
public RAR(Stream data, long offset) : base(data, offset) { }
/// <inheritdoc/>
public RAR(Stream data, long offset, long length) : base(data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a RAR archive (or derived format) from a byte array and offset
@@ -87,210 +85,5 @@ namespace SabreTools.Serialization.Wrappers
#endif
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
=> Extract(outputDirectory, lookForHeader: false, includeDebug);
/// <inheritdoc cref="Extract(string, bool)"/>
public bool Extract(string outputDirectory, bool lookForHeader, bool includeDebug)
{
if (_dataSource == null || !_dataSource.CanRead)
return false;
#if NET462_OR_GREATER || NETCOREAPP
try
{
var readerOptions = new ReaderOptions() { LookForHeader = lookForHeader };
RarArchive rarFile = RarArchive.Open(_dataSource, readerOptions);
// If the file exists
if (!string.IsNullOrEmpty(Filename) && File.Exists(Filename!))
{
// Find all file parts
FileInfo[] parts = [.. ArchiveFactory.GetFileParts(new FileInfo(Filename))];
// If there are multiple parts
if (parts.Length > 1)
rarFile = RarArchive.Open(parts, readerOptions);
// Try to read the file path if no entries are found
else if (rarFile.Entries.Count == 0)
rarFile = RarArchive.Open(parts, readerOptions);
}
if (rarFile.IsSolid)
return ExtractSolid(rarFile, outputDirectory, includeDebug);
else
return ExtractNonSolid(rarFile, outputDirectory, includeDebug);
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
return false;
}
#else
Console.WriteLine("Extraction is not supported for this framework!");
Console.WriteLine();
return false;
#endif
}
/// <summary>
/// Try to find all parts of the archive, if possible
/// </summary>
/// <param name="firstPart">Path of the first archive part</param>
/// <returns>List of all found parts, if possible</returns>
public static List<string> FindParts(string firstPart)
{
// Define the regex patterns
const string rarNewPattern = @"^(.*\.part)([0-9]+)(\.rar)$";
const string rarOldPattern = @"^(.*\.)([r-z{])(ar|[0-9]+)$";
const string genericPattern = @"^(.*\.)([0-9]+)$";
// Ensure the full path is available
firstPart = Path.GetFullPath(firstPart);
string filename = Path.GetFileName(firstPart);
string? directory = Path.GetDirectoryName(firstPart);
// Make the output list
List<string> parts = [];
// Determine which pattern is being used
Match match;
Func<int, string> nextPartFunc;
if (Regex.IsMatch(filename, rarNewPattern, RegexOptions.IgnoreCase))
{
match = Regex.Match(filename, rarNewPattern, RegexOptions.IgnoreCase);
nextPartFunc = (i) =>
{
return string.Concat(
match.Groups[1].Value,
$"{i + 1}".PadLeft(match.Groups[2].Value.Length, '0'),
match.Groups[3].Value);
};
}
else if (Regex.IsMatch(filename, rarOldPattern, RegexOptions.IgnoreCase))
{
match = Regex.Match(filename, rarOldPattern, RegexOptions.IgnoreCase);
nextPartFunc = (i) =>
{
return string.Concat(
match.Groups[1].Value,
(char)(match.Groups[2].Value[0] + ((i - 1) / 100))
+ (i - 1).ToString("D4").Substring(2));
};
}
else if (Regex.IsMatch(filename, genericPattern, RegexOptions.IgnoreCase))
{
match = Regex.Match(filename, genericPattern, RegexOptions.IgnoreCase);
nextPartFunc = (i) =>
{
return string.Concat(
match.Groups[1].Value,
$"{i + 1}".PadLeft(match.Groups[2].Value.Length, '0')
);
};
}
else
{
return [firstPart];
}
// Loop and add the files
parts.Add(firstPart);
for (int i = 1; ; i++)
{
string nextPart = nextPartFunc(i);
if (directory != null)
nextPart = Path.Combine(directory, nextPart);
if (!File.Exists(nextPart))
break;
parts.Add(nextPart);
}
return parts;
}
#if NET462_OR_GREATER || NETCOREAPP
/// <summary>
/// Extraction method for non-solid archives. This iterates over each entry in the archive to extract every
/// file individually, in order to extract all valid files from the archive.
/// </summary>
private static bool ExtractNonSolid(RarArchive rarFile, string outDir, bool includeDebug)
{
foreach (var entry in rarFile.Entries)
{
try
{
// If the entry is a directory
if (entry.IsDirectory)
continue;
// If the entry has an invalid key
if (entry.Key == null)
continue;
// If we have a partial entry due to an incomplete multi-part archive, skip it
if (!entry.IsComplete)
continue;
// Ensure directory separators are consistent
string filename = entry.Key;
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outDir, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
entry.WriteToFile(filename);
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
}
}
return true;
}
/// <summary>
/// Extraction method for solid archives. Uses ExtractAllEntries because extraction for solid archives must be
/// done sequentially, and files beyond a corrupted point in a solid archive will be unreadable anyways.
/// </summary>
private static bool ExtractSolid(RarArchive rarFile, string outDir, bool includeDebug)
{
try
{
if (!Directory.Exists(outDir))
Directory.CreateDirectory(outDir);
rarFile.WriteToDirectory(outDir, new ExtractionOptions()
{
ExtractFullPath = true,
Overwrite = true,
});
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
}
return true;
}
#endif
#endregion
}
}

View File

@@ -0,0 +1,152 @@
using System;
using System.Collections.Generic;
using System.IO;
using SabreTools.IO.Compression.zlib;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class SGA : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Get the file count
int fileCount = FileCount;
if (fileCount == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < fileCount; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the SGA to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// Get the file count
int fileCount = FileCount;
if (fileCount == 0)
return false;
// If the files index is invalid
if (index < 0 || index >= fileCount)
return false;
// Create the filename
var filename = GetFileName(index);
if (filename == null)
return false;
// Loop through and get all parent directories
var parentNames = new List<string> { filename };
// Get the parent directory
string? folderName = GetParentName(index);
if (folderName != null)
parentNames.Add(folderName);
// TODO: Should the section name/alias be used in the path as well?
// Reverse and assemble the filename
parentNames.Reverse();
#if NET20 || NET35
filename = parentNames[0];
for (int i = 1; i < parentNames.Count; i++)
{
filename = Path.Combine(filename, parentNames[i]);
}
#else
filename = Path.Combine([.. parentNames]);
#endif
// Get and adjust the file offset
long fileOffset = GetFileOffset(index);
fileOffset += FileDataOffset;
if (fileOffset < 0)
return false;
// Get the file sizes
long fileSize = GetCompressedSize(index);
long outputFileSize = GetUncompressedSize(index);
// Read the compressed data directly
var compressedData = ReadRangeFromSource((int)fileOffset, (int)fileSize);
if (compressedData.Length == 0)
return false;
// If the compressed and uncompressed sizes match
byte[] data;
if (fileSize == outputFileSize)
{
data = compressedData;
}
else
{
// Inflate the data into the buffer
var zstream = new ZLib.z_stream_s();
data = new byte[outputFileSize];
unsafe
{
fixed (byte* payloadPtr = compressedData)
fixed (byte* dataPtr = data)
{
zstream.next_in = payloadPtr;
zstream.avail_in = (uint)compressedData.Length;
zstream.total_in = (uint)compressedData.Length;
zstream.next_out = dataPtr;
zstream.avail_out = (uint)data.Length;
zstream.total_out = 0;
ZLib.inflateInit_(zstream, ZLib.zlibVersion(), compressedData.Length);
int zret = ZLib.inflate(zstream, 1);
ZLib.inflateEnd(zstream);
}
}
}
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !System.IO.Directory.Exists(directoryName))
System.IO.Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = System.IO.File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return false;
}
}
}

View File

@@ -1,14 +1,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using SabreTools.IO.Compression.zlib;
using SabreTools.IO.Extensions;
using SabreTools.Models.SGA;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class SGA : WrapperBase<Archive>, IExtractable
public partial class SGA : WrapperBase<Archive>
{
#region Descriptive Properties
@@ -63,18 +59,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public SGA(Archive? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public SGA(Archive model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public SGA(Archive? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public SGA(Archive model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public SGA(Archive model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public SGA(Archive model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public SGA(Archive model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public SGA(Archive model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create an SGA from a byte array and offset
@@ -113,12 +117,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.SGA.DeserializeStream(data);
var model = new Deserializers.SGA().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new SGA(model, data);
return new SGA(model, data, currentOffset);
}
catch
{
@@ -128,151 +131,6 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Get the file count
int fileCount = FileCount;
if (fileCount == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < fileCount; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the SGA to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// Get the file count
int fileCount = FileCount;
if (fileCount == 0)
return false;
// If the files index is invalid
if (index < 0 || index >= fileCount)
return false;
// Create the filename
var filename = GetFileName(index);
if (filename == null)
return false;
// Loop through and get all parent directories
var parentNames = new List<string> { filename };
// Get the parent directory
string? folderName = GetParentName(index);
if (folderName != null)
parentNames.Add(folderName);
// TODO: Should the section name/alias be used in the path as well?
// Reverse and assemble the filename
parentNames.Reverse();
#if NET20 || NET35
filename = parentNames[0];
for (int i = 1; i < parentNames.Count; i++)
{
filename = Path.Combine(filename, parentNames[i]);
}
#else
filename = Path.Combine([.. parentNames]);
#endif
// Get and adjust the file offset
long fileOffset = GetFileOffset(index);
fileOffset += FileDataOffset;
if (fileOffset < 0)
return false;
// Get the file sizes
long fileSize = GetCompressedSize(index);
long outputFileSize = GetUncompressedSize(index);
// Read the compressed data directly
var compressedData = _dataSource.ReadFrom((int)fileOffset, (int)fileSize, retainPosition: true);
if (compressedData == null)
return false;
// If the compressed and uncompressed sizes match
byte[] data;
if (fileSize == outputFileSize)
{
data = compressedData;
}
else
{
// Inflate the data into the buffer
var zstream = new ZLib.z_stream_s();
data = new byte[outputFileSize];
unsafe
{
fixed (byte* payloadPtr = compressedData)
fixed (byte* dataPtr = data)
{
zstream.next_in = payloadPtr;
zstream.avail_in = (uint)compressedData.Length;
zstream.total_in = (uint)compressedData.Length;
zstream.next_out = dataPtr;
zstream.avail_out = (uint)data.Length;
zstream.total_out = 0;
ZLib.inflateInit_(zstream, ZLib.zlibVersion(), compressedData.Length);
int zret = ZLib.inflate(zstream, 1);
ZLib.inflateEnd(zstream);
}
}
}
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !System.IO.Directory.Exists(directoryName))
System.IO.Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = System.IO.File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return false;
}
#endregion
#region File
/// <summary>

View File

@@ -15,18 +15,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public SecuROMDFA(DFAFile? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public SecuROMDFA(DFAFile model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public SecuROMDFA(DFAFile? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public SecuROMDFA(DFAFile model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public SecuROMDFA(DFAFile model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public SecuROMDFA(DFAFile model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public SecuROMDFA(DFAFile model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public SecuROMDFA(DFAFile model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a SecuROM DFA file from a byte array and offset
@@ -65,12 +73,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.SecuROMDFA.DeserializeStream(data);
var model = new Deserializers.SecuROMDFA().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new SecuROMDFA(model, data);
return new SecuROMDFA(model, data, currentOffset);
}
catch
{

View File

@@ -0,0 +1,133 @@
using System;
using System.IO;
using System.Text;
using SabreTools.Hashing;
using SabreTools.Models.SecuROM;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class SecuROMMatroschkaPackage : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no entries
if (Entries == null || Entries.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (var i = 0; i < Entries.Length; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the package to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If we have no entries
if (Entries == null || Entries.Length == 0)
return false;
// If the entry index is invalid
if (index < 0 || index >= Entries.Length)
return false;
// Get the entry
var entry = Entries[index];
if (entry.Path == null)
return false;
// Ensure directory separators are consistent
string filename = Encoding.ASCII.GetString(entry.Path).TrimEnd('\0');
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
if (includeDebug) Console.WriteLine($"Attempting to extract {filename}");
// Read the file
var data = ReadFileData(entry, includeDebug);
if (data == null)
return false;
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
/// <summary>
/// Read file and check bytes to be extracted against MD5 checksum
/// </summary>
/// <param name="entry">Entry being extracted</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>Byte array of the file data if successful, null otherwise</returns>
/// <remarks>Marked as public because it is needed outside of file extraction</remarks>
public byte[]? ReadFileData(MatroshkaEntry entry, bool includeDebug)
{
// Skip if the entry is incomplete
if (entry.Path == null || entry.MD5 == null)
return null;
// Cache the expected MD5
string expectedMd5 = BitConverter.ToString(entry.MD5);
expectedMd5 = expectedMd5.ToLowerInvariant().Replace("-", string.Empty);
// Debug output
if (includeDebug) Console.WriteLine($"Offset: {entry.Offset:X8}, Expected Size: {entry.Size}, Expected MD5: {expectedMd5}");
// Attempt to read from the offset
var fileData = ReadRangeFromSource(entry.Offset, (int)entry.Size);
if (fileData.Length == 0)
{
if (includeDebug) Console.Error.WriteLine($"Could not read {entry.Size} bytes from {entry.Offset:X8}");
return null;
}
// Get the actual MD5 of the data
string actualMd5 = HashTool.GetByteArrayHash(fileData, HashType.MD5) ?? string.Empty;
// Debug output
if (includeDebug) Console.WriteLine($"Actual MD5: {actualMd5}");
// Do not return on a hash mismatch
if (actualMd5 != expectedMd5)
{
string filename = Encoding.ASCII.GetString(entry.Path).TrimEnd('\0');
if (includeDebug) Console.Error.WriteLine($"MD5 checksum failure for file {filename})");
return null;
}
return fileData;
}
}
}

View File

@@ -0,0 +1,118 @@
using System.IO;
using SabreTools.Models.SecuROM;
namespace SabreTools.Serialization.Wrappers
{
public partial class SecuROMMatroschkaPackage : WrapperBase<MatroshkaPackage>
{
#region Descriptive Properties
/// <inheritdoc/>
public override string DescriptionString => "SecuROM Matroschka Package";
#endregion
#region Extension Properties
/// <inheritdoc cref="MatroshkaPackage.Signature"/>
public string? Signature => Model.Signature;
/// <inheritdoc cref="MatroshkaPackage.EntryCount"/>
public uint EntryCount => Model.EntryCount;
/// <inheritdoc cref="MatroshkaPackage.UnknownRCValue1"/>
public uint? UnknownRCValue1 => Model.UnknownRCValue1;
/// <inheritdoc cref="MatroshkaPackage.UnknownRCValue2"/>
public uint? UnknownRCValue2 => Model.UnknownRCValue2;
/// <inheritdoc cref="MatroshkaPackage.UnknownRCValue3"/>
public uint? UnknownRCValue3 => Model.UnknownRCValue3;
/// <inheritdoc cref="MatroshkaPackage.KeyHexString"/>
public string? KeyHexString => Model.KeyHexString;
/// <inheritdoc cref="MatroshkaPackage.Padding"/>
public uint? Padding => Model.Padding;
/// <inheritdoc cref="MatroshkaPackage.Entries"/>
public MatroshkaEntry[]? Entries => Model.Entries;
#endregion
#region Constructors
/// <inheritdoc/>
public SecuROMMatroschkaPackage(MatroshkaPackage model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public SecuROMMatroschkaPackage(MatroshkaPackage model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public SecuROMMatroschkaPackage(MatroshkaPackage model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public SecuROMMatroschkaPackage(MatroshkaPackage model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public SecuROMMatroschkaPackage(MatroshkaPackage model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public SecuROMMatroschkaPackage(MatroshkaPackage model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a SecuROM Matroschka package from a byte array and offset
/// </summary>
/// <param name="data">Byte array representing the package</param>
/// <param name="offset">Offset within the array to parse</param>
/// <returns>A SecuROM Matroschka package wrapper on success, null on failure</returns>
public static SecuROMMatroschkaPackage? Create(byte[]? data, int offset)
{
// If the data is invalid
if (data == null || data.Length == 0)
return null;
// If the offset is out of bounds
if (offset < 0 || offset >= data.Length)
return null;
// Create a memory stream and use that
var dataStream = new MemoryStream(data, offset, data.Length - offset);
return Create(dataStream);
}
/// <summary>
/// Create a SecuROM Matroschka package from a Stream
/// </summary>
/// <param name="data">Stream representing the package</param>
/// <returns>A SecuROM Matroschka package wrapper on success, null on failure</returns>
public static SecuROMMatroschkaPackage? Create(Stream? data)
{
// If the data is invalid
if (data == null || !data.CanRead)
return null;
try
{
// Cache the current offset
long currentOffset = data.Position;
var model = new Deserializers.SecuROMMatroschkaPackage().Deserialize(data);
if (model == null)
return null;
return new SecuROMMatroschkaPackage(model, data, currentOffset);
}
catch
{
return null;
}
}
#endregion
}
}

View File

@@ -0,0 +1,224 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using SabreTools.Serialization.Interfaces;
#if NET462_OR_GREATER || NETCOREAPP
using SharpCompress.Archives;
using SharpCompress.Archives.SevenZip;
using SharpCompress.Common;
using SharpCompress.Readers;
#endif
namespace SabreTools.Serialization.Wrappers
{
/// <summary>
/// This is a shell wrapper; one that does not contain
/// any actual parsing. It is used as a placeholder for
/// types that typically do not have models.
/// </summary>
public partial class SevenZip : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
=> Extract(outputDirectory, lookForHeader: false, includeDebug);
/// <inheritdoc cref="Extract(string, bool)"/>
public bool Extract(string outputDirectory, bool lookForHeader, bool includeDebug)
{
if (_dataSource == null || !_dataSource.CanRead)
return false;
#if NET462_OR_GREATER || NETCOREAPP
try
{
var readerOptions = new ReaderOptions() { LookForHeader = lookForHeader };
var sevenZip = SevenZipArchive.Open(_dataSource, readerOptions);
// If the file exists
if (!string.IsNullOrEmpty(Filename) && File.Exists(Filename!))
{
// Find all file parts
FileInfo[] parts = [.. ArchiveFactory.GetFileParts(new FileInfo(Filename))];
// If there are multiple parts
if (parts.Length > 1)
sevenZip = SevenZipArchive.Open(parts, readerOptions);
// Try to read the file path if no entries are found
else if (sevenZip.Entries.Count == 0)
sevenZip = SevenZipArchive.Open(parts, readerOptions);
}
// Currently doesn't flag solid 7z archives with only 1 solid block as solid, but practically speaking
// this is not much of a concern.
if (sevenZip.IsSolid)
return ExtractSolid(sevenZip, outputDirectory, includeDebug);
else
return ExtractNonSolid(sevenZip, outputDirectory, includeDebug);
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
return false;
}
#else
Console.WriteLine("Extraction is not supported for this framework!");
Console.WriteLine();
return false;
#endif
}
/// <summary>
/// Try to find all parts of the archive, if possible
/// </summary>
/// <param name="firstPart">Path of the first archive part</param>
/// <returns>List of all found parts, if possible</returns>
public static List<string> FindParts(string firstPart)
{
// Define the regex patterns
const string genericPattern = @"^(.*\.)([0-9]+)$";
// Ensure the full path is available
firstPart = Path.GetFullPath(firstPart);
string filename = Path.GetFileName(firstPart);
string? directory = Path.GetDirectoryName(firstPart);
// Make the output list
List<string> parts = [];
// Determine which pattern is being used
Match match;
Func<int, string> nextPartFunc;
if (Regex.IsMatch(filename, genericPattern, RegexOptions.IgnoreCase))
{
match = Regex.Match(filename, genericPattern, RegexOptions.IgnoreCase);
nextPartFunc = (i) =>
{
return string.Concat(
match.Groups[1].Value,
$"{i + 1}".PadLeft(match.Groups[2].Value.Length, '0')
);
};
}
else
{
return [firstPart];
}
// Loop and add the files
parts.Add(firstPart);
for (int i = 1; ; i++)
{
string nextPart = nextPartFunc(i);
if (directory != null)
nextPart = Path.Combine(directory, nextPart);
if (!File.Exists(nextPart))
break;
parts.Add(nextPart);
}
return parts;
}
#if NET462_OR_GREATER || NETCOREAPP
/// <summary>
/// Extraction method for non-solid archives. This iterates over each entry in the archive to extract every
/// file individually, in order to extract all valid files from the archive.
/// </summary>
private static bool ExtractNonSolid(SevenZipArchive sevenZip, string outputDirectory, bool includeDebug)
{
foreach (var entry in sevenZip.Entries)
{
try
{
// If the entry is a directory
if (entry.IsDirectory)
continue;
// If the entry has an invalid key
if (entry.Key == null)
continue;
// If we have a partial entry due to an incomplete multi-part archive, skip it
if (!entry.IsComplete)
continue;
// Ensure directory separators are consistent
string filename = entry.Key;
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
entry.WriteToFile(filename);
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
}
}
return true;
}
/// <summary>
/// Extraction method for solid archives. Uses ExtractAllEntries because extraction for solid archives must be
/// done sequentially, and files beyond a corrupted point in a solid archive will be unreadable anyways.
/// </summary>
private static bool ExtractSolid(SevenZipArchive sevenZip, string outputDirectory, bool includeDebug)
{
try
{
if (!Directory.Exists(outputDirectory))
Directory.CreateDirectory(outputDirectory);
int index = 0;
var entries = sevenZip.ExtractAllEntries();
while (entries.MoveToNextEntry())
{
var entry = entries.Entry;
if (entry.IsDirectory)
continue;
// Ensure directory separators are consistent
string filename = entry.Key ?? $"extracted_file_{index}";
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Write to file
using var fs = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None);
entries.WriteEntryTo(fs);
fs.Flush();
// Increment the index
index++;
}
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
}
return true;
}
#endif
}
}

View File

@@ -1,14 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using SabreTools.Serialization.Interfaces;
#if NET462_OR_GREATER || NETCOREAPP
using SharpCompress.Archives;
using SharpCompress.Archives.SevenZip;
using SharpCompress.Common;
using SharpCompress.Readers;
#endif
namespace SabreTools.Serialization.Wrappers
{
@@ -17,7 +7,7 @@ namespace SabreTools.Serialization.Wrappers
/// any actual parsing. It is used as a placeholder for
/// types that typically do not have models.
/// </summary>
public class SevenZip : WrapperBase, IExtractable
public partial class SevenZip : WrapperBase
{
#region Descriptive Properties
@@ -29,18 +19,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public SevenZip(byte[]? data, int offset)
: base(data, offset)
{
// All logic is handled by the base class
}
public SevenZip(byte[] data) : base(data) { }
/// <inheritdoc/>
public SevenZip(Stream? data)
: base(data)
{
// All logic is handled by the base class
}
public SevenZip(byte[] data, int offset) : base(data, offset) { }
/// <inheritdoc/>
public SevenZip(byte[] data, int offset, int length) : base(data, offset, length) { }
/// <inheritdoc/>
public SevenZip(Stream data) : base(data) { }
/// <inheritdoc/>
public SevenZip(Stream data, long offset) : base(data, offset) { }
/// <inheritdoc/>
public SevenZip(Stream data, long offset, long length) : base(data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a SevenZip archive (or derived format) from a byte array and offset
@@ -87,211 +85,5 @@ namespace SabreTools.Serialization.Wrappers
#endif
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
=> Extract(outputDirectory, lookForHeader: false, includeDebug);
/// <inheritdoc cref="Extract(string, bool)"/>
public bool Extract(string outputDirectory, bool lookForHeader, bool includeDebug)
{
if (_dataSource == null || !_dataSource.CanRead)
return false;
#if NET462_OR_GREATER || NETCOREAPP
try
{
var readerOptions = new ReaderOptions() { LookForHeader = lookForHeader };
var sevenZip = SevenZipArchive.Open(_dataSource, readerOptions);
// If the file exists
if (!string.IsNullOrEmpty(Filename) && File.Exists(Filename!))
{
// Find all file parts
FileInfo[] parts = [.. ArchiveFactory.GetFileParts(new FileInfo(Filename))];
// If there are multiple parts
if (parts.Length > 1)
sevenZip = SevenZipArchive.Open(parts, readerOptions);
// Try to read the file path if no entries are found
else if (sevenZip.Entries.Count == 0)
sevenZip = SevenZipArchive.Open(parts, readerOptions);
}
// Currently doesn't flag solid 7z archives with only 1 solid block as solid, but practically speaking
// this is not much of a concern.
if (sevenZip.IsSolid)
return ExtractSolid(sevenZip, outputDirectory, includeDebug);
else
return ExtractNonSolid(sevenZip, outputDirectory, includeDebug);
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
return false;
}
#else
Console.WriteLine("Extraction is not supported for this framework!");
Console.WriteLine();
return false;
#endif
}
/// <summary>
/// Try to find all parts of the archive, if possible
/// </summary>
/// <param name="firstPart">Path of the first archive part</param>
/// <returns>List of all found parts, if possible</returns>
public static List<string> FindParts(string firstPart)
{
// Define the regex patterns
const string genericPattern = @"^(.*\.)([0-9]+)$";
// Ensure the full path is available
firstPart = Path.GetFullPath(firstPart);
string filename = Path.GetFileName(firstPart);
string? directory = Path.GetDirectoryName(firstPart);
// Make the output list
List<string> parts = [];
// Determine which pattern is being used
Match match;
Func<int, string> nextPartFunc;
if (Regex.IsMatch(filename, genericPattern, RegexOptions.IgnoreCase))
{
match = Regex.Match(filename, genericPattern, RegexOptions.IgnoreCase);
nextPartFunc = (i) =>
{
return string.Concat(
match.Groups[1].Value,
$"{i + 1}".PadLeft(match.Groups[2].Value.Length, '0')
);
};
}
else
{
return [firstPart];
}
// Loop and add the files
parts.Add(firstPart);
for (int i = 1; ; i++)
{
string nextPart = nextPartFunc(i);
if (directory != null)
nextPart = Path.Combine(directory, nextPart);
if (!File.Exists(nextPart))
break;
parts.Add(nextPart);
}
return parts;
}
#if NET462_OR_GREATER || NETCOREAPP
/// <summary>
/// Extraction method for non-solid archives. This iterates over each entry in the archive to extract every
/// file individually, in order to extract all valid files from the archive.
/// </summary>
private static bool ExtractNonSolid(SevenZipArchive sevenZip, string outputDirectory, bool includeDebug)
{
foreach (var entry in sevenZip.Entries)
{
try
{
// If the entry is a directory
if (entry.IsDirectory)
continue;
// If the entry has an invalid key
if (entry.Key == null)
continue;
// If we have a partial entry due to an incomplete multi-part archive, skip it
if (!entry.IsComplete)
continue;
// Ensure directory separators are consistent
string filename = entry.Key;
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
entry.WriteToFile(filename);
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
}
}
return true;
}
/// <summary>
/// Extraction method for solid archives. Uses ExtractAllEntries because extraction for solid archives must be
/// done sequentially, and files beyond a corrupted point in a solid archive will be unreadable anyways.
/// </summary>
private static bool ExtractSolid(SevenZipArchive sevenZip, string outputDirectory, bool includeDebug)
{
try
{
if (!Directory.Exists(outputDirectory))
Directory.CreateDirectory(outputDirectory);
int index = 0;
var entries = sevenZip.ExtractAllEntries();
while (entries.MoveToNextEntry())
{
var entry = entries.Entry;
if (entry.IsDirectory)
continue;
// Ensure directory separators are consistent
string filename = entry.Key ?? $"extracted_file_{index}";
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Write to file
using var fs = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None);
entries.WriteEntryTo(fs);
fs.Flush();
// Increment the index
index++;
}
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
}
return true;
}
#endif
#endregion
}
}

View File

@@ -0,0 +1,170 @@
using System;
using System.IO;
using SabreTools.Models.TAR;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class TapeArchive : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Ensure there are entries to extract
if (Entries == null || Entries.Length == 0)
return false;
try
{
// Loop through and extract the data
for (int i = 0; i < Entries.Length; i++)
{
var entry = Entries[i];
if (entry.Header == null)
{
if (includeDebug) Console.Error.WriteLine($"Invalid entry {i} found! Skipping...");
continue;
}
// Handle special entries
var header = entry.Header;
switch (header.TypeFlag)
{
// Skipped types
case TypeFlag.LNKTYPE:
case TypeFlag.SYMTYPE:
case TypeFlag.CHRTYPE:
case TypeFlag.BLKTYPE:
case TypeFlag.FIFOTYPE:
case TypeFlag.XHDTYPE:
case TypeFlag.XGLTYPE:
if (includeDebug) Console.WriteLine($"Unsupported entry type: {header.TypeFlag}");
continue;
// Skipped vendor types
case TypeFlag.VendorSpecificA:
case TypeFlag.VendorSpecificB:
case TypeFlag.VendorSpecificC:
case TypeFlag.VendorSpecificD:
case TypeFlag.VendorSpecificE:
case TypeFlag.VendorSpecificF:
case TypeFlag.VendorSpecificG:
case TypeFlag.VendorSpecificH:
case TypeFlag.VendorSpecificI:
case TypeFlag.VendorSpecificJ:
case TypeFlag.VendorSpecificK:
case TypeFlag.VendorSpecificL:
case TypeFlag.VendorSpecificM:
case TypeFlag.VendorSpecificN:
case TypeFlag.VendorSpecificO:
case TypeFlag.VendorSpecificP:
case TypeFlag.VendorSpecificQ:
case TypeFlag.VendorSpecificR:
case TypeFlag.VendorSpecificS:
case TypeFlag.VendorSpecificT:
case TypeFlag.VendorSpecificU:
case TypeFlag.VendorSpecificV:
case TypeFlag.VendorSpecificW:
case TypeFlag.VendorSpecificX:
case TypeFlag.VendorSpecificY:
case TypeFlag.VendorSpecificZ:
if (includeDebug) Console.WriteLine($"Unsupported vendor entry type: {header.TypeFlag}");
continue;
// Directories
case TypeFlag.DIRTYPE:
string? entryDirectory = header.FileName?.TrimEnd('\0');
if (entryDirectory == null)
{
if (includeDebug) Console.Error.WriteLine($"Entry {i} reported as directory, but no path found! Skipping...");
continue;
}
// Ensure directory separators are consistent
entryDirectory = Path.Combine(outputDirectory, entryDirectory);
if (Path.DirectorySeparatorChar == '\\')
entryDirectory = entryDirectory.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
entryDirectory = entryDirectory.Replace('\\', '/');
// Create the director
Directory.CreateDirectory(entryDirectory);
continue;
}
// Ensure there are blocks to extract
if (entry.Blocks == null)
{
if (includeDebug) Console.WriteLine($"Entry {i} had no block data");
continue;
}
// Get the file size
string sizeOctalString = header.Size!.TrimEnd('\0');
if (sizeOctalString.Length == 0)
{
if (includeDebug) Console.WriteLine($"Entry {i} has an invalid size, skipping...");
continue;
}
int entrySize = Convert.ToInt32(sizeOctalString, 8);
// Setup the temporary buffer
byte[] dataBytes = new byte[entrySize];
int dataBytesPtr = 0;
// Loop through and copy the bytes to the array for writing
int blockNumber = 0;
while (entrySize > 0)
{
// Exit early if block number is invalid
if (blockNumber >= entry.Blocks.Length)
{
if (includeDebug) Console.Error.WriteLine($"Invalid block number {i + 1} of {entry.Blocks.Length}, file may be incomplete!");
break;
}
// Exit early if the block has no data
var block = entry.Blocks[blockNumber++];
if (block.Data == null || block.Data.Length != 512)
{
if (includeDebug) Console.Error.WriteLine($"Invalid data for block number {i + 1}, file may be incomplete!");
break;
}
int nextBytes = Math.Min(512, entrySize);
entrySize -= nextBytes;
Array.Copy(block.Data, 0, dataBytes, dataBytesPtr, nextBytes);
dataBytesPtr += nextBytes;
}
// Ensure directory separators are consistent
string filename = header.FileName?.TrimEnd('\0') ?? $"entry_{i}";
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Write the file
using var fs = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None);
fs.Write(dataBytes, 0, dataBytes.Length);
fs.Flush();
}
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
}
}

View File

@@ -1,11 +1,9 @@
using System;
using System.IO;
using SabreTools.Models.TAR;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class TapeArchive : WrapperBase<Archive>, IExtractable
public partial class TapeArchive : WrapperBase<Archive>
{
#region Descriptive Properties
@@ -24,18 +22,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public TapeArchive(Archive? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public TapeArchive(Archive model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public TapeArchive(Archive? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public TapeArchive(Archive model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public TapeArchive(Archive model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public TapeArchive(Archive model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public TapeArchive(Archive model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public TapeArchive(Archive model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a tape archive (or derived format) from a byte array and offset
@@ -74,12 +80,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.TapeArchive.DeserializeStream(data);
var model = new Deserializers.TapeArchive().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new TapeArchive(model, data);
return new TapeArchive(model, data, currentOffset);
}
catch
{
@@ -88,169 +93,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Ensure there are entries to extract
if (Entries == null || Entries.Length == 0)
return false;
try
{
// Loop through and extract the data
for (int i = 0; i < Entries.Length; i++)
{
var entry = Entries[i];
if (entry.Header == null)
{
if (includeDebug) Console.Error.WriteLine($"Invalid entry {i} found! Skipping...");
continue;
}
// Handle special entries
var header = entry.Header;
switch (header.TypeFlag)
{
// Skipped types
case TypeFlag.LNKTYPE:
case TypeFlag.SYMTYPE:
case TypeFlag.CHRTYPE:
case TypeFlag.BLKTYPE:
case TypeFlag.FIFOTYPE:
case TypeFlag.XHDTYPE:
case TypeFlag.XGLTYPE:
if (includeDebug) Console.WriteLine($"Unsupported entry type: {header.TypeFlag}");
continue;
// Skipped vendor types
case TypeFlag.VendorSpecificA:
case TypeFlag.VendorSpecificB:
case TypeFlag.VendorSpecificC:
case TypeFlag.VendorSpecificD:
case TypeFlag.VendorSpecificE:
case TypeFlag.VendorSpecificF:
case TypeFlag.VendorSpecificG:
case TypeFlag.VendorSpecificH:
case TypeFlag.VendorSpecificI:
case TypeFlag.VendorSpecificJ:
case TypeFlag.VendorSpecificK:
case TypeFlag.VendorSpecificL:
case TypeFlag.VendorSpecificM:
case TypeFlag.VendorSpecificN:
case TypeFlag.VendorSpecificO:
case TypeFlag.VendorSpecificP:
case TypeFlag.VendorSpecificQ:
case TypeFlag.VendorSpecificR:
case TypeFlag.VendorSpecificS:
case TypeFlag.VendorSpecificT:
case TypeFlag.VendorSpecificU:
case TypeFlag.VendorSpecificV:
case TypeFlag.VendorSpecificW:
case TypeFlag.VendorSpecificX:
case TypeFlag.VendorSpecificY:
case TypeFlag.VendorSpecificZ:
if (includeDebug) Console.WriteLine($"Unsupported vendor entry type: {header.TypeFlag}");
continue;
// Directories
case TypeFlag.DIRTYPE:
string? entryDirectory = header.FileName?.TrimEnd('\0');
if (entryDirectory == null)
{
if (includeDebug) Console.Error.WriteLine($"Entry {i} reported as directory, but no path found! Skipping...");
continue;
}
// Ensure directory separators are consistent
entryDirectory = Path.Combine(outputDirectory, entryDirectory);
if (Path.DirectorySeparatorChar == '\\')
entryDirectory = entryDirectory.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
entryDirectory = entryDirectory.Replace('\\', '/');
// Create the director
Directory.CreateDirectory(entryDirectory);
continue;
}
// Ensure there are blocks to extract
if (entry.Blocks == null)
{
if (includeDebug) Console.WriteLine($"Entry {i} had no block data");
continue;
}
// Get the file size
string sizeOctalString = header.Size!.TrimEnd('\0');
if (sizeOctalString.Length == 0)
{
if (includeDebug) Console.WriteLine($"Entry {i} has an invalid size, skipping...");
continue;
}
int entrySize = Convert.ToInt32(sizeOctalString, 8);
// Setup the temporary buffer
byte[] dataBytes = new byte[entrySize];
int dataBytesPtr = 0;
// Loop through and copy the bytes to the array for writing
int blockNumber = 0;
while (entrySize > 0)
{
// Exit early if block number is invalid
if (blockNumber >= entry.Blocks.Length)
{
if (includeDebug) Console.Error.WriteLine($"Invalid block number {i + 1} of {entry.Blocks.Length}, file may be incomplete!");
break;
}
// Exit early if the block has no data
var block = entry.Blocks[blockNumber++];
if (block.Data == null || block.Data.Length != 512)
{
if (includeDebug) Console.Error.WriteLine($"Invalid data for block number {i + 1}, file may be incomplete!");
break;
}
int nextBytes = Math.Min(512, entrySize);
entrySize -= nextBytes;
Array.Copy(block.Data, 0, dataBytes, dataBytesPtr, nextBytes);
dataBytesPtr += nextBytes;
}
// Ensure directory separators are consistent
string filename = header.FileName?.TrimEnd('\0') ?? $"entry_{i}";
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Write the file
using var fs = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None);
fs.Write(dataBytes, 0, dataBytes.Length);
fs.Flush();
}
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
#endregion
}
}

View File

@@ -0,0 +1,95 @@
using System;
using System.IO;
using SabreTools.Models.BSP;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class VBSP : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no lumps
if (Lumps == null || Lumps.Length == 0)
return false;
// Loop through and extract all lumps to the output
bool allExtracted = true;
for (int i = 0; i < Lumps.Length; i++)
{
allExtracted &= ExtractLump(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a lump from the VBSP to an output directory by index
/// </summary>
/// <param name="index">Lump index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the lump extracted, false otherwise</returns>
public bool ExtractLump(int index, string outputDirectory, bool includeDebug)
{
// If we have no lumps
if (Lumps == null || Lumps.Length == 0)
return false;
// If the lumps index is invalid
if (index < 0 || index >= Lumps.Length)
return false;
// Read the data
var lump = Lumps[index];
var data = ReadRangeFromSource(lump.Offset, lump.Length);
if (data.Length == 0)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Create the filename
string filename = $"lump_{index}.bin";
switch ((LumpType)index)
{
case LumpType.LUMP_ENTITIES:
filename = "entities.ent";
break;
case LumpType.LUMP_PAKFILE:
filename = "pakfile.zip";
break;
}
// Ensure directory separators are consistent
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
}
}

View File

@@ -1,12 +1,9 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Models.BSP;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class VBSP : WrapperBase<VbspFile>, IExtractable
public partial class VBSP : WrapperBase<VbspFile>
{
#region Descriptive Properties
@@ -25,18 +22,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public VBSP(VbspFile? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public VBSP(VbspFile model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public VBSP(VbspFile? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public VBSP(VbspFile model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public VBSP(VbspFile model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public VBSP(VbspFile model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public VBSP(VbspFile model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public VBSP(VbspFile model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a VBSP from a byte array and offset
@@ -75,12 +80,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.VBSP.DeserializeStream(data);
var model = new Deserializers.VBSP().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new VBSP(model, data);
return new VBSP(model, data, currentOffset);
}
catch
{
@@ -89,94 +93,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no lumps
if (Lumps == null || Lumps.Length == 0)
return false;
// Loop through and extract all lumps to the output
bool allExtracted = true;
for (int i = 0; i < Lumps.Length; i++)
{
allExtracted &= ExtractLump(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a lump from the VBSP to an output directory by index
/// </summary>
/// <param name="index">Lump index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the lump extracted, false otherwise</returns>
public bool ExtractLump(int index, string outputDirectory, bool includeDebug)
{
// If we have no lumps
if (Lumps == null || Lumps.Length == 0)
return false;
// If the lumps index is invalid
if (index < 0 || index >= Lumps.Length)
return false;
// Read the data
var lump = Lumps[index];
var data = _dataSource.ReadFrom(lump.Offset, lump.Length, retainPosition: true);
if (data == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Create the filename
string filename = $"lump_{index}.bin";
switch ((LumpType)index)
{
case LumpType.LUMP_ENTITIES:
filename = "entities.ent";
break;
case LumpType.LUMP_PAKFILE:
filename = "pakfile.zip";
break;
}
// Ensure directory separators are consistent
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
#endregion
}
}

View File

@@ -0,0 +1,146 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Serialization.Interfaces;
using static SabreTools.Models.VPK.Constants;
namespace SabreTools.Serialization.Wrappers
{
public partial class VPK : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no directory items
if (DirectoryItems == null || DirectoryItems.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < DirectoryItems.Length; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the VPK to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If we have no directory items
if (DirectoryItems == null || DirectoryItems.Length == 0)
return false;
// If the directory item index is invalid
if (index < 0 || index >= DirectoryItems.Length)
return false;
// Get the directory item
var directoryItem = DirectoryItems[index];
if (directoryItem.DirectoryEntry == null)
return false;
// If we have an item with no archive
byte[] data = [];
if (directoryItem.DirectoryEntry.ArchiveIndex == HL_VPK_NO_ARCHIVE)
{
if (directoryItem.PreloadData == null)
return false;
data = directoryItem.PreloadData;
}
else
{
// If we have invalid archives
if (ArchiveFilenames == null || ArchiveFilenames.Length == 0)
return false;
// If we have an invalid index
if (directoryItem.DirectoryEntry.ArchiveIndex < 0 || directoryItem.DirectoryEntry.ArchiveIndex >= ArchiveFilenames.Length)
return false;
// Get the archive filename
string archiveFileName = ArchiveFilenames[directoryItem.DirectoryEntry.ArchiveIndex];
if (string.IsNullOrEmpty(archiveFileName))
return false;
// If the archive doesn't exist
if (!File.Exists(archiveFileName))
return false;
// Try to open the archive
var archiveStream = default(Stream);
try
{
// Open the archive
archiveStream = File.Open(archiveFileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// Seek to the data
archiveStream.Seek(directoryItem.DirectoryEntry.EntryOffset, SeekOrigin.Begin);
// Read the directory item bytes
data = archiveStream.ReadBytes((int)directoryItem.DirectoryEntry.EntryLength);
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
finally
{
archiveStream?.Close();
}
// If we have preload data, prepend it
if (data != null && directoryItem.PreloadData != null)
data = [.. directoryItem.PreloadData, .. data];
}
// If there is nothing to write out
if (data == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
string filename = $"{directoryItem.Name}.{directoryItem.Extension}";
if (!string.IsNullOrEmpty(directoryItem.Path))
filename = Path.Combine(directoryItem.Path, filename);
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
}
}

View File

@@ -1,12 +1,10 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Serialization.Interfaces;
using static SabreTools.Models.VPK.Constants;
namespace SabreTools.Serialization.Wrappers
{
public class VPK : WrapperBase<Models.VPK.File>, IExtractable
public partial class VPK : WrapperBase<Models.VPK.File>
{
#region Descriptive Properties
@@ -89,18 +87,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public VPK(Models.VPK.File? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public VPK(Models.VPK.File model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public VPK(Models.VPK.File? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public VPK(Models.VPK.File model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public VPK(Models.VPK.File model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public VPK(Models.VPK.File model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public VPK(Models.VPK.File model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public VPK(Models.VPK.File model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a VPK from a byte array and offset
@@ -139,12 +145,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.VPK.DeserializeStream(data);
var model = new Deserializers.VPK().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new VPK(model, data);
return new VPK(model, data, currentOffset);
}
catch
{
@@ -153,144 +158,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no directory items
if (DirectoryItems == null || DirectoryItems.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < DirectoryItems.Length; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the VPK to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If we have no directory items
if (DirectoryItems == null || DirectoryItems.Length == 0)
return false;
// If the directory item index is invalid
if (index < 0 || index >= DirectoryItems.Length)
return false;
// Get the directory item
var directoryItem = DirectoryItems[index];
if (directoryItem.DirectoryEntry == null)
return false;
// If we have an item with no archive
byte[] data = [];
if (directoryItem.DirectoryEntry.ArchiveIndex == HL_VPK_NO_ARCHIVE)
{
if (directoryItem.PreloadData == null)
return false;
data = directoryItem.PreloadData;
}
else
{
// If we have invalid archives
if (ArchiveFilenames == null || ArchiveFilenames.Length == 0)
return false;
// If we have an invalid index
if (directoryItem.DirectoryEntry.ArchiveIndex < 0 || directoryItem.DirectoryEntry.ArchiveIndex >= ArchiveFilenames.Length)
return false;
// Get the archive filename
string archiveFileName = ArchiveFilenames[directoryItem.DirectoryEntry.ArchiveIndex];
if (string.IsNullOrEmpty(archiveFileName))
return false;
// If the archive doesn't exist
if (!File.Exists(archiveFileName))
return false;
// Try to open the archive
var archiveStream = default(Stream);
try
{
// Open the archive
archiveStream = File.Open(archiveFileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// Seek to the data
archiveStream.Seek(directoryItem.DirectoryEntry.EntryOffset, SeekOrigin.Begin);
// Read the directory item bytes
data = archiveStream.ReadBytes((int)directoryItem.DirectoryEntry.EntryLength);
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
finally
{
archiveStream?.Close();
}
// If we have preload data, prepend it
if (data != null && directoryItem.PreloadData != null)
data = [.. directoryItem.PreloadData, .. data];
}
// If there is nothing to write out
if (data == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
string filename = $"{directoryItem.Name}.{directoryItem.Extension}";
if (!string.IsNullOrEmpty(directoryItem.Path))
filename = Path.Combine(directoryItem.Path, filename);
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
#endregion
}
}

View File

@@ -0,0 +1,83 @@
using System;
using System.IO;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class WAD3 : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no lumps
if (DirEntries == null || DirEntries.Length == 0)
return false;
// Loop through and extract all lumps to the output
bool allExtracted = true;
for (int i = 0; i < DirEntries.Length; i++)
{
allExtracted &= ExtractLump(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a lump from the WAD3 to an output directory by index
/// </summary>
/// <param name="index">Lump index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the lump extracted, false otherwise</returns>
public bool ExtractLump(int index, string outputDirectory, bool includeDebug)
{
// If we have no lumps
if (DirEntries == null || DirEntries.Length == 0)
return false;
// If the lumps index is invalid
if (index < 0 || index >= DirEntries.Length)
return false;
// Read the data -- TODO: Handle uncompressed lumps (see BSP.ExtractTexture)
var lump = DirEntries[index];
var data = ReadRangeFromSource((int)lump.Offset, (int)lump.Length);
if (data.Length == 0)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
string filename = $"{lump.Name}.lmp";
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
}
}

View File

@@ -1,11 +1,8 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class WAD3 : WrapperBase<Models.WAD3.File>, IExtractable
public partial class WAD3 : WrapperBase<Models.WAD3.File>
{
#region Descriptive Properties
@@ -24,18 +21,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public WAD3(Models.WAD3.File? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public WAD3(Models.WAD3.File model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public WAD3(Models.WAD3.File? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public WAD3(Models.WAD3.File model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public WAD3(Models.WAD3.File model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public WAD3(Models.WAD3.File model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public WAD3(Models.WAD3.File model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public WAD3(Models.WAD3.File model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a WAD3 from a byte array and offset
@@ -74,12 +79,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.WAD3.DeserializeStream(data);
var model = new Deserializers.WAD3().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new WAD3(model, data);
return new WAD3(model, data, currentOffset);
}
catch
{
@@ -88,83 +92,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no lumps
if (DirEntries == null || DirEntries.Length == 0)
return false;
// Loop through and extract all lumps to the output
bool allExtracted = true;
for (int i = 0; i < DirEntries.Length; i++)
{
allExtracted &= ExtractLump(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a lump from the WAD3 to an output directory by index
/// </summary>
/// <param name="index">Lump index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the lump extracted, false otherwise</returns>
public bool ExtractLump(int index, string outputDirectory, bool includeDebug)
{
// If we have no lumps
if (DirEntries == null || DirEntries.Length == 0)
return false;
// If the lumps index is invalid
if (index < 0 || index >= DirEntries.Length)
return false;
// Read the data -- TODO: Handle uncompressed lumps (see BSP.ExtractTexture)
var lump = DirEntries[index];
var data = _dataSource.ReadFrom((int)lump.Offset, (int)lump.Length, retainPosition: true);
if (data == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
string filename = $"{lump.Name}.lmp";
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
#endregion
}
}

View File

@@ -0,0 +1,313 @@
using System;
using System.IO;
using SabreTools.IO.Compression.Deflate;
using SabreTools.IO.Streams;
using SabreTools.Models.WiseInstaller.Actions;
namespace SabreTools.Serialization.Wrappers
{
public partial class WiseOverlayHeader
{
/// <summary>
/// Extract the predefined, static files defined in the header
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the files extracted successfully, false otherwise</returns>
/// <remarks>On success, this sets <see cref="InstallerDataOffset"/></remarks>
public bool ExtractHeaderDefinedFiles(string outputDirectory, bool includeDebug)
{
lock (_dataSourceLock)
{
// Seek to the compressed data offset
_dataSource.Seek(CompressedDataOffset, SeekOrigin.Begin);
if (includeDebug) Console.WriteLine($"Beginning of header-defined files: {CompressedDataOffset}");
// Extract WiseColors.dib, if it exists
var expected = new DeflateInfo { InputSize = DibDeflatedSize, OutputSize = DibInflatedSize, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, "WiseColors.dib", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract WiseScript.bin
expected = new DeflateInfo { InputSize = WiseScriptDeflatedSize, OutputSize = WiseScriptInflatedSize, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, "WiseScript.bin", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract WISE0001.DLL, if it exists
expected = new DeflateInfo { InputSize = WiseDllDeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, IsPKZIP ? null : "WISE0001.DLL", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract CTL3D32.DLL, if it exists
expected = new DeflateInfo { InputSize = Ctl3d32DeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, IsPKZIP ? null : "CTL3D32.DLL", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract FILE0004, if it exists
expected = new DeflateInfo { InputSize = SomeData4DeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, IsPKZIP ? null : "FILE0004", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract Ocxreg32.EXE, if it exists
expected = new DeflateInfo { InputSize = RegToolDeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, IsPKZIP ? null : "Ocxreg32.EXE", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract PROGRESS.DLL, if it exists
expected = new DeflateInfo { InputSize = ProgressDllDeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, IsPKZIP ? null : "PROGRESS.DLL", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract FILE0007, if it exists
expected = new DeflateInfo { InputSize = SomeData7DeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, IsPKZIP ? null : "FILE0007", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract FILE0008, if it exists
expected = new DeflateInfo { InputSize = SomeData8DeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, IsPKZIP ? null : "FILE0008", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract FILE0009, if it exists
expected = new DeflateInfo { InputSize = SomeData9DeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, IsPKZIP ? null : "FILE0009", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract FILE000A, if it exists
expected = new DeflateInfo { InputSize = SomeData10DeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, IsPKZIP ? null : "FILE000A", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract install script, if it exists
expected = new DeflateInfo { InputSize = InstallScriptDeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, IsPKZIP ? null : "INSTALL_SCRIPT", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract FILE000{n}.DAT, if it exists
expected = new DeflateInfo { InputSize = FinalFileDeflatedSize, OutputSize = FinalFileInflatedSize, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, IsPKZIP ? null : "FILE00XX.DAT", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
InstallerDataOffset = _dataSource.Position;
}
return true;
}
/// <summary>
/// Attempt to extract a file defined by a file header
/// </summary>
/// <param name="obj">Deflate information</param>
/// <param name="index">File index for automatic naming</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted successfully, false otherwise</returns>
/// <remarks>Requires <see cref="InstallerDataOffset"/> to be set</remarks>
public ExtractionStatus ExtractFile(InstallFile obj, int index, string outputDirectory, bool includeDebug)
{
// Get expected values
var expected = new DeflateInfo
{
InputSize = obj.DeflateEnd - obj.DeflateStart,
OutputSize = obj.InflatedSize,
Crc32 = obj.Crc32,
};
// Perform path replacements
string filename = obj.DestinationPathname ?? $"WISE{index:X4}";
filename = filename.Replace("%", string.Empty);
lock (_dataSourceLock)
{
_dataSource.Seek(InstallerDataOffset + obj.DeflateStart, SeekOrigin.Begin);
return InflateWrapper.ExtractFile(_dataSource,
filename,
outputDirectory,
expected,
IsPKZIP,
includeDebug);
}
}
/// <summary>
/// Attempt to extract a file defined by a file header
/// </summary>
/// <param name="obj">Deflate information</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted successfully, false otherwise</returns>
/// <remarks>Requires <see cref="InstallerDataOffset"/> to be set</remarks>
public ExtractionStatus ExtractFile(DisplayBillboard obj, string outputDirectory, bool includeDebug)
{
// Get the generated base name
string baseName = $"CustomBillboardSet_{obj.Flags:X4}-{obj.Operand_2}-{obj.Operand_3}";
// If there are no deflate objects
if (obj.DeflateInfo == null)
{
if (includeDebug) Console.WriteLine($"Skipping {baseName} because the deflate object array is null!");
return ExtractionStatus.FAIL;
}
// Loop through the values
for (int i = 0; i < obj.DeflateInfo.Length; i++)
{
// Get the deflate info object
var info = obj.DeflateInfo[i];
// Get expected values
var expected = new DeflateInfo
{
InputSize = info.DeflateEnd - info.DeflateStart,
OutputSize = info.InflatedSize,
Crc32 = 0,
};
// Perform path replacements
string filename = $"{baseName}{i:X4}";
lock (_dataSourceLock)
{
_dataSource.Seek(InstallerDataOffset + info.DeflateStart, SeekOrigin.Begin);
_ = InflateWrapper.ExtractFile(_dataSource, filename, outputDirectory, expected, IsPKZIP, includeDebug);
}
}
// Always return good -- TODO: Fix this
return ExtractionStatus.GOOD;
}
/// <summary>
/// Attempt to extract a file defined by a file header
/// </summary>
/// <param name="obj">Deflate information</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted successfully, false otherwise</returns>
/// <remarks>Requires <see cref="InstallerDataOffset"/> to be set</remarks>
public ExtractionStatus ExtractFile(CustomDialogSet obj, string outputDirectory, bool includeDebug)
{
// Get expected values
var expected = new DeflateInfo
{
InputSize = obj.DeflateEnd - obj.DeflateStart,
OutputSize = obj.InflatedSize,
Crc32 = 0,
};
// Perform path replacements
string filename = $"CustomDialogSet_{obj.DisplayVariable}-{obj.Name}";
filename = filename.Replace("%", string.Empty);
lock (_dataSourceLock)
{
_dataSource.Seek(InstallerDataOffset + obj.DeflateStart, SeekOrigin.Begin);
return InflateWrapper.ExtractFile(_dataSource, filename, outputDirectory, expected, IsPKZIP, includeDebug);
}
}
/// <summary>
/// Open a potential WISE installer file and any additional files
/// </summary>
/// <param name="filename">Input filename or base name to read from</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file could be opened, false otherwise</returns>
public static bool OpenFile(string filename, bool includeDebug, out ReadOnlyCompositeStream? stream)
{
// If the file exists as-is
if (File.Exists(filename))
{
var fileStream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
stream = new ReadOnlyCompositeStream([fileStream]);
// Debug statement
if (includeDebug) Console.WriteLine($"File {filename} was found and opened");
// Strip the extension and rebuild
string? directory = Path.GetDirectoryName(filename);
filename = Path.GetFileNameWithoutExtension(filename);
if (directory != null)
filename = Path.Combine(directory, filename);
}
// If the base name was provided, try to open the associated exe
else if (File.Exists($"{filename}.EXE"))
{
var fileStream = File.Open($"{filename}.EXE", FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
stream = new ReadOnlyCompositeStream([fileStream]);
// Debug statement
if (includeDebug) Console.WriteLine($"File {filename}.EXE was found and opened");
}
else if (File.Exists($"{filename}.exe"))
{
var fileStream = File.Open($"{filename}.exe", FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
stream = new ReadOnlyCompositeStream([fileStream]);
// Debug statement
if (includeDebug) Console.WriteLine($"File {filename}.exe was found and opened");
}
// Otherwise, the file cannot be opened
else
{
stream = null;
return false;
}
// Get the pattern for file naming
string filePattern = string.Empty;
bool longDigits = false;
byte fileno = 0;
bool foundStart = false;
for (; fileno < 3; fileno++)
{
if (File.Exists($"{filename}.W0{fileno}"))
{
foundStart = true;
filePattern = $"{filename}.W";
longDigits = false;
break;
}
else if (File.Exists($"{filename}.w0{fileno}"))
{
foundStart = true;
filePattern = $"{filename}.w";
longDigits = false;
break;
}
else if (File.Exists($"{filename}.00{fileno}"))
{
foundStart = true;
filePattern = $"{filename}.";
longDigits = true;
break;
}
}
// If no starting part has been found
if (!foundStart)
return true;
// Loop through and try to read all additional files
for (; ; fileno++)
{
string nextPart = longDigits ? $"{filePattern}{fileno:D3}" : $"{filePattern}{fileno:D2}";
if (!File.Exists(nextPart))
{
if (includeDebug) Console.WriteLine($"Part {nextPart} was not found");
break;
}
// Debug statement
if (includeDebug) Console.WriteLine($"Part {nextPart} was found and appended");
var fileStream = File.Open(nextPart, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
stream.AddStream(fileStream);
}
return true;
}
}
}

View File

@@ -1,13 +1,9 @@
using System;
using System.IO;
using SabreTools.IO.Compression.Deflate;
using SabreTools.IO.Streams;
using SabreTools.Models.WiseInstaller;
using SabreTools.Models.WiseInstaller.Actions;
namespace SabreTools.Serialization.Wrappers
{
public class WiseOverlayHeader : WrapperBase<OverlayHeader>
public partial class WiseOverlayHeader : WrapperBase<OverlayHeader>
{
#region Descriptive Properties
@@ -156,18 +152,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public WiseOverlayHeader(OverlayHeader? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public WiseOverlayHeader(OverlayHeader model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public WiseOverlayHeader(OverlayHeader? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public WiseOverlayHeader(OverlayHeader model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public WiseOverlayHeader(OverlayHeader model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public WiseOverlayHeader(OverlayHeader model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public WiseOverlayHeader(OverlayHeader model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public WiseOverlayHeader(OverlayHeader model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a Wise installer overlay header from a byte array and offset
@@ -206,12 +210,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.WiseOverlayHeader.DeserializeStream(data);
var model = new Deserializers.WiseOverlayHeader().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new WiseOverlayHeader(model, data);
return new WiseOverlayHeader(model, data, currentOffset);
}
catch
{
@@ -220,296 +223,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <summary>
/// Extract the predefined, static files defined in the header
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the files extracted successfully, false otherwise</returns>
/// <remarks>On success, this sets <see cref="InstallerDataOffset"/></remarks>
public bool ExtractHeaderDefinedFiles(string outputDirectory, bool includeDebug)
{
// Seek to the compressed data offset
_dataSource.Seek(CompressedDataOffset, SeekOrigin.Begin);
if (includeDebug) Console.WriteLine($"Beginning of header-defined files: {CompressedDataOffset}");
// Extract WiseColors.dib, if it exists
var expected = new DeflateInfo { InputSize = DibDeflatedSize, OutputSize = DibInflatedSize, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, "WiseColors.dib", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract WiseScript.bin
expected = new DeflateInfo { InputSize = WiseScriptDeflatedSize, OutputSize = WiseScriptInflatedSize, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, "WiseScript.bin", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract WISE0001.DLL, if it exists
expected = new DeflateInfo { InputSize = WiseDllDeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, IsPKZIP ? null : "WISE0001.DLL", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract CTL3D32.DLL, if it exists
expected = new DeflateInfo { InputSize = Ctl3d32DeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, IsPKZIP ? null : "CTL3D32.DLL", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract FILE0004, if it exists
expected = new DeflateInfo { InputSize = SomeData4DeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, IsPKZIP ? null : "FILE0004", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract Ocxreg32.EXE, if it exists
expected = new DeflateInfo { InputSize = RegToolDeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, IsPKZIP ? null : "Ocxreg32.EXE", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract PROGRESS.DLL, if it exists
expected = new DeflateInfo { InputSize = ProgressDllDeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, IsPKZIP ? null : "PROGRESS.DLL", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract FILE0007, if it exists
expected = new DeflateInfo { InputSize = SomeData7DeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, IsPKZIP ? null : "FILE0007", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract FILE0008, if it exists
expected = new DeflateInfo { InputSize = SomeData8DeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, IsPKZIP ? null : "FILE0008", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract FILE0009, if it exists
expected = new DeflateInfo { InputSize = SomeData9DeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, IsPKZIP ? null : "FILE0009", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract FILE000A, if it exists
expected = new DeflateInfo { InputSize = SomeData10DeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, IsPKZIP ? null : "FILE000A", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract install script, if it exists
expected = new DeflateInfo { InputSize = InstallScriptDeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, IsPKZIP ? null : "INSTALL_SCRIPT", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract FILE000{n}.DAT, if it exists
expected = new DeflateInfo { InputSize = FinalFileDeflatedSize, OutputSize = FinalFileInflatedSize, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, IsPKZIP ? null : "FILE00XX.DAT", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
InstallerDataOffset = _dataSource.Position;
return true;
}
/// <summary>
/// Attempt to extract a file defined by a file header
/// </summary>
/// <param name="obj">Deflate information</param>
/// <param name="index">File index for automatic naming</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted successfully, false otherwise</returns>
/// <remarks>Requires <see cref="InstallerDataOffset"/> to be set</remarks>
public ExtractionStatus ExtractFile(InstallFile obj, int index, string outputDirectory, bool includeDebug)
{
// Get expected values
var expected = new DeflateInfo
{
InputSize = obj.DeflateEnd - obj.DeflateStart,
OutputSize = obj.InflatedSize,
Crc32 = obj.Crc32,
};
// Perform path replacements
string filename = obj.DestinationPathname ?? $"WISE{index:X4}";
filename = filename.Replace("%", string.Empty);
_dataSource.Seek(InstallerDataOffset + obj.DeflateStart, SeekOrigin.Begin);
return InflateWrapper.ExtractFile(_dataSource,
filename,
outputDirectory,
expected,
IsPKZIP,
includeDebug);
}
/// <summary>
/// Attempt to extract a file defined by a file header
/// </summary>
/// <param name="obj">Deflate information</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted successfully, false otherwise</returns>
/// <remarks>Requires <see cref="InstallerDataOffset"/> to be set</remarks>
public ExtractionStatus ExtractFile(DisplayBillboard obj, string outputDirectory, bool includeDebug)
{
// Get the generated base name
string baseName = $"CustomBillboardSet_{obj.Flags:X4}-{obj.Operand_2}-{obj.Operand_3}";
// If there are no deflate objects
if (obj.DeflateInfo == null)
{
if (includeDebug) Console.WriteLine($"Skipping {baseName} because the deflate object array is null!");
return ExtractionStatus.FAIL;
}
// Loop through the values
for (int i = 0; i < obj.DeflateInfo.Length; i++)
{
// Get the deflate info object
var info = obj.DeflateInfo[i];
// Get expected values
var expected = new DeflateInfo
{
InputSize = info.DeflateEnd - info.DeflateStart,
OutputSize = info.InflatedSize,
Crc32 = 0,
};
// Perform path replacements
string filename = $"{baseName}{i:X4}";
_dataSource.Seek(InstallerDataOffset + info.DeflateStart, SeekOrigin.Begin);
_ = InflateWrapper.ExtractFile(_dataSource, filename, outputDirectory, expected, IsPKZIP, includeDebug);
}
// Always return good -- TODO: Fix this
return ExtractionStatus.GOOD;
}
/// <summary>
/// Attempt to extract a file defined by a file header
/// </summary>
/// <param name="obj">Deflate information</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted successfully, false otherwise</returns>
/// <remarks>Requires <see cref="InstallerDataOffset"/> to be set</remarks>
public ExtractionStatus ExtractFile(CustomDialogSet obj, string outputDirectory, bool includeDebug)
{
// Get expected values
var expected = new DeflateInfo
{
InputSize = obj.DeflateEnd - obj.DeflateStart,
OutputSize = obj.InflatedSize,
Crc32 = 0,
};
// Perform path replacements
string filename = $"CustomDialogSet_{obj.DisplayVariable}-{obj.Name}";
filename = filename.Replace("%", string.Empty);
_dataSource.Seek(InstallerDataOffset + obj.DeflateStart, SeekOrigin.Begin);
return InflateWrapper.ExtractFile(_dataSource, filename, outputDirectory, expected, IsPKZIP, includeDebug);
}
/// <summary>
/// Open a potential WISE installer file and any additional files
/// </summary>
/// <param name="filename">Input filename or base name to read from</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file could be opened, false otherwise</returns>
public static bool OpenFile(string filename, bool includeDebug, out ReadOnlyCompositeStream? stream)
{
// If the file exists as-is
if (File.Exists(filename))
{
var fileStream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
stream = new ReadOnlyCompositeStream([fileStream]);
// Debug statement
if (includeDebug) Console.WriteLine($"File {filename} was found and opened");
// Strip the extension and rebuild
string? directory = Path.GetDirectoryName(filename);
filename = Path.GetFileNameWithoutExtension(filename);
if (directory != null)
filename = Path.Combine(directory, filename);
}
// If the base name was provided, try to open the associated exe
else if (File.Exists($"{filename}.EXE"))
{
var fileStream = File.Open($"{filename}.EXE", FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
stream = new ReadOnlyCompositeStream([fileStream]);
// Debug statement
if (includeDebug) Console.WriteLine($"File {filename}.EXE was found and opened");
}
else if (File.Exists($"{filename}.exe"))
{
var fileStream = File.Open($"{filename}.exe", FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
stream = new ReadOnlyCompositeStream([fileStream]);
// Debug statement
if (includeDebug) Console.WriteLine($"File {filename}.exe was found and opened");
}
// Otherwise, the file cannot be opened
else
{
stream = null;
return false;
}
// Get the pattern for file naming
string filePattern = string.Empty;
bool longDigits = false;
byte fileno = 0;
bool foundStart = false;
for (; fileno < 3; fileno++)
{
if (File.Exists($"{filename}.W0{fileno}"))
{
foundStart = true;
filePattern = $"{filename}.W";
longDigits = false;
break;
}
else if (File.Exists($"{filename}.w0{fileno}"))
{
foundStart = true;
filePattern = $"{filename}.w";
longDigits = false;
break;
}
else if (File.Exists($"{filename}.00{fileno}"))
{
foundStart = true;
filePattern = $"{filename}.";
longDigits = true;
break;
}
}
// If no starting part has been found
if (!foundStart)
return true;
// Loop through and try to read all additional files
for (; ; fileno++)
{
string nextPart = longDigits ? $"{filePattern}{fileno:D3}" : $"{filePattern}{fileno:D2}";
if (!File.Exists(nextPart))
{
if (includeDebug) Console.WriteLine($"Part {nextPart} was not found");
break;
}
// Debug statement
if (includeDebug) Console.WriteLine($"Part {nextPart} was found and appended");
var fileStream = File.Open(nextPart, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
stream.AddStream(fileStream);
}
return true;
}
#endregion
}
}

View File

@@ -62,18 +62,26 @@ namespace SabreTools.Serialization.Wrappers
#region Constructors
/// <inheritdoc/>
public WiseScript(ScriptFile? model, byte[]? data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public WiseScript(ScriptFile model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public WiseScript(ScriptFile? model, Stream? data)
: base(model, data)
{
// All logic is handled by the base class
}
public WiseScript(ScriptFile model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public WiseScript(ScriptFile model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public WiseScript(ScriptFile model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public WiseScript(ScriptFile model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public WiseScript(ScriptFile model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a Wise installer script file from a byte array and offset
@@ -112,12 +120,11 @@ namespace SabreTools.Serialization.Wrappers
// Cache the current offset
long currentOffset = data.Position;
var model = Deserializers.WiseScript.DeserializeStream(data);
var model = new Deserializers.WiseScript().Deserialize(data);
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new WiseScript(model, data);
return new WiseScript(model, data, currentOffset);
}
catch
{

View File

@@ -0,0 +1,216 @@
using System;
using System.IO;
using SabreTools.Hashing;
using SabreTools.IO.Compression.Deflate;
using SabreTools.IO.Extensions;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class WiseSectionHeader : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Extract the header-defined files
bool extracted = ExtractHeaderDefinedFiles(outputDirectory, includeDebug);
if (!extracted)
{
if (includeDebug) Console.Error.WriteLine("Could not extract header-defined files");
return false;
}
return true;
}
// Currently unaware of any NE samples. That said, as they wouldn't have a .WISE section, it's unclear how such
// samples could be identified.
/// <summary>
/// Extract the predefined, static files defined in the header
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the files extracted successfully, false otherwise</returns>
private bool ExtractHeaderDefinedFiles(string outputDirectory, bool includeDebug)
{
lock (_dataSourceLock)
{
// Seek to the compressed data offset
_dataSource.Seek(CompressedDataOffset, SeekOrigin.Begin);
bool successful = true;
// Extract first executable, if it exists
if (ExtractFile("FirstExecutable.exe", outputDirectory, FirstExecutableFileEntryLength, includeDebug) != ExtractionStatus.GOOD)
successful = false;
// Extract second executable, if it exists
// If there's a size provided for the second executable but no size for the first executable, the size of
// the second executable appears to be some unrelated value that's larger than the second executable
// actually is. Currently unable to extract properly in these cases, as no header value in such installers
// seems to actually correspond to the real size of the second executable.
if (ExtractFile("SecondExecutable.exe", outputDirectory, SecondExecutableFileEntryLength, includeDebug) != ExtractionStatus.GOOD)
successful = false;
// Extract third executable, if it exists
if (ExtractFile("ThirdExecutable.exe", outputDirectory, ThirdExecutableFileEntryLength, includeDebug) != ExtractionStatus.GOOD)
successful = false;
// Extract main MSI file
if (ExtractFile("ExtractedMsi.msi", outputDirectory, MsiFileEntryLength, includeDebug) != ExtractionStatus.GOOD)
{
// Fallback- seek to the position that's the length of the MSI file entry from the end, then try and
// extract from there.
_dataSource.Seek(-MsiFileEntryLength + 1, SeekOrigin.End);
if (ExtractFile("ExtractedMsi.msi", outputDirectory, MsiFileEntryLength, includeDebug) != ExtractionStatus.GOOD)
return false; // The fallback also failed.
}
return successful;
}
}
/// <summary>
/// Attempt to extract a file defined by a filename
/// </summary>
/// <param name="filename">Output filename, null to auto-generate</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="entrySize">Expected size of the file plus crc32</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>Extraction status representing the final state</returns>
/// <remarks>Assumes that the current stream position is the end of where the data lives</remarks>
private ExtractionStatus ExtractFile(string filename,
string outputDirectory,
uint entrySize,
bool includeDebug)
{
if (includeDebug) Console.WriteLine($"Attempting to extract {filename}");
// Extract the file
var destination = new MemoryStream();
ExtractionStatus status;
if (!(Version != null && Version[1] == 0x01))
{
status = ExtractStreamWithChecksum(destination, entrySize, includeDebug);
}
else // hack for Codesited5.exe , very early and very strange.
{
status = ExtractStreamWithoutChecksum(destination, entrySize, includeDebug);
}
// If the extracted data is invalid
if (status != ExtractionStatus.GOOD || destination == null)
return status;
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Write the output file
File.WriteAllBytes(filename, destination.ToArray());
return status;
}
/// <summary>
/// Extract source data with a trailing CRC-32 checksum
/// </summary>
/// <param name="destination">Stream where the file data will be written</param>
/// <param name="entrySize">Expected size of the file plus crc32</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns></returns>
private ExtractionStatus ExtractStreamWithChecksum(Stream destination, uint entrySize, bool includeDebug)
{
// Debug output
if (includeDebug) Console.WriteLine($"Offset: {_dataSource.Position:X8}, Expected Read: {entrySize}, Expected Write:{entrySize - 4}"); // clamp to zero
// Check the validity of the inputs
if (entrySize == 0)
{
if (includeDebug) Console.Error.WriteLine("Not attempting to extract, expected to read 0 bytes");
return ExtractionStatus.GOOD; // If size is 0, then it shouldn't be extracted
}
else if (entrySize > (_dataSource.Length - _dataSource.Position))
{
if (includeDebug) Console.Error.WriteLine($"Not attempting to extract, expected to read {entrySize} bytes but only {_dataSource.Position} bytes remain");
return ExtractionStatus.INVALID;
}
// Extract the file
try
{
byte[] actual = _dataSource.ReadBytes((int)entrySize - 4);
uint expectedCrc32 = _dataSource.ReadUInt32();
// Debug output
if (includeDebug) Console.WriteLine($"Expected CRC-32: {expectedCrc32:X8}");
byte[]? hashBytes = HashTool.GetByteArrayHashArray(actual, HashType.CRC32);
if (hashBytes != null)
{
uint actualCrc32 = BitConverter.ToUInt32(hashBytes, 0);
// Debug output
if (includeDebug) Console.WriteLine($"Actual CRC-32: {actualCrc32:X8}");
if (expectedCrc32 != actualCrc32)
{
if (includeDebug) Console.Error.WriteLine("Mismatched CRC-32 values!");
return ExtractionStatus.BAD_CRC;
}
}
destination.Write(actual, 0, actual.Length);
return ExtractionStatus.GOOD;
}
catch
{
if (includeDebug) Console.Error.WriteLine("Could not extract");
return ExtractionStatus.FAIL;
}
}
/// <summary>
/// Extract source data without a trailing CRC-32 checksum
/// </summary>
/// <param name="destination">Stream where the file data will be written</param>
/// <param name="entrySize">Expected size of the file</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns></returns>
private ExtractionStatus ExtractStreamWithoutChecksum(Stream destination, uint entrySize, bool includeDebug)
{
// Debug output
if (includeDebug) Console.WriteLine($"Offset: {_dataSource.Position:X8}, Expected Read: {entrySize}, Expected Write:{entrySize - 4}");
// Check the validity of the inputs
if (entrySize == 0)
{
if (includeDebug) Console.Error.WriteLine("Not attempting to extract, expected to read 0 bytes");
return ExtractionStatus.GOOD; // If size is 0, then it shouldn't be extracted
}
else if (entrySize > (_dataSource.Length - _dataSource.Position))
{
if (includeDebug) Console.Error.WriteLine($"Not attempting to extract, expected to read {entrySize} bytes but only {_dataSource.Position} bytes remain");
return ExtractionStatus.INVALID;
}
// Extract the file
try
{
byte[] actual = _dataSource.ReadBytes((int)entrySize);
// Debug output
if (includeDebug) Console.WriteLine("No CRC-32!");
destination.Write(actual, 0, actual.Length);
return ExtractionStatus.GOOD;
}
catch
{
if (includeDebug) Console.Error.WriteLine("Could not extract");
return ExtractionStatus.FAIL;
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More