mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-02-05 21:34:02 +00:00
Compare commits
113 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf35b7c10b | ||
|
|
4026b8ca09 | ||
|
|
6d2e2d8c3b | ||
|
|
a2b08157cc | ||
|
|
0108ecf4c1 | ||
|
|
4921da0bb5 | ||
|
|
0c836bb3b1 | ||
|
|
a8e41c1505 | ||
|
|
f67e1c9d2b | ||
|
|
f0ce58a79e | ||
|
|
b7f782c1b7 | ||
|
|
5e39e169b2 | ||
|
|
104c5ccad4 | ||
|
|
d15b4d7d23 | ||
|
|
60e6a75d5e | ||
|
|
484415d0e5 | ||
|
|
0ef9b447c4 | ||
|
|
8f64e2defd | ||
|
|
fbdadce129 | ||
|
|
d3e7abfaa3 | ||
|
|
b2279e97b2 | ||
|
|
7cf969336f | ||
|
|
f73d48166a | ||
|
|
53af618fe4 | ||
|
|
5d2cf58477 | ||
|
|
664e7dce28 | ||
|
|
14a8f00864 | ||
|
|
0b889fdc06 | ||
|
|
e336efc149 | ||
|
|
4cd52162eb | ||
|
|
eab9fff711 | ||
|
|
d4f3511060 | ||
|
|
ed12bbb35c | ||
|
|
aa4629fe99 | ||
|
|
1950f23cf4 | ||
|
|
ca7c88cef6 | ||
|
|
10848e6c51 | ||
|
|
f5d0f065c1 | ||
|
|
17b0573b0b | ||
|
|
7f1d843d96 | ||
|
|
cc4837c1d1 | ||
|
|
588ee5bfe4 | ||
|
|
e9b1b2750f | ||
|
|
1d6fa06e97 | ||
|
|
2c22924239 | ||
|
|
eb01dd1e25 | ||
|
|
0a3cb79b1c | ||
|
|
da9eace8cc | ||
|
|
526a02b8b6 | ||
|
|
658c7a1c3b | ||
|
|
af84474795 | ||
|
|
42913c6732 | ||
|
|
2cdf544518 | ||
|
|
652ec58238 | ||
|
|
f8531daa5c | ||
|
|
e9e89b0b43 | ||
|
|
55e788a894 | ||
|
|
b28bb93ccb | ||
|
|
367aab0f83 | ||
|
|
9dcf3b9e0a | ||
|
|
3c514110ce | ||
|
|
c9b0c2dace | ||
|
|
d575b6977e | ||
|
|
a00e6a5e2d | ||
|
|
1b9ae83e8c | ||
|
|
8b91eb1caf | ||
|
|
2a6a7b5e9a | ||
|
|
a85943866e | ||
|
|
797fb519c1 | ||
|
|
3ba9d56363 | ||
|
|
04cd4e4056 | ||
|
|
348e170654 | ||
|
|
f5a4ca6276 | ||
|
|
672c010aa7 | ||
|
|
2459d88951 | ||
|
|
350d1c8d31 | ||
|
|
98a3842a3e | ||
|
|
b52a4469ee | ||
|
|
e3143e21ba | ||
|
|
1bf2181fd3 | ||
|
|
1460635aab | ||
|
|
935ec00c86 | ||
|
|
473b6de09b | ||
|
|
ba75f2ac2c | ||
|
|
a230b39fbc | ||
|
|
8e963ac62a | ||
|
|
eaaa89847d | ||
|
|
ef76166978 | ||
|
|
72912586a1 | ||
|
|
fb241a4036 | ||
|
|
368c8b0533 | ||
|
|
4010325e65 | ||
|
|
11dd75ad95 | ||
|
|
d0480a1311 | ||
|
|
2be33b845d | ||
|
|
2ad42e3a0f | ||
|
|
5d1f83800b | ||
|
|
30e89a7943 | ||
|
|
61f5dc4cf2 | ||
|
|
d056c179ed | ||
|
|
b9c4bfc67e | ||
|
|
6ab5ee0ae0 | ||
|
|
94c1a86702 | ||
|
|
af6dd6a7fc | ||
|
|
45d4926d4c | ||
|
|
ce016c5eb0 | ||
|
|
2225c1f2d8 | ||
|
|
2d0c0d5845 | ||
|
|
60f1756cbb | ||
|
|
738a1d250a | ||
|
|
c8e65e1e30 | ||
|
|
ecb09ce6f2 | ||
|
|
72a1484a71 |
@@ -10,7 +10,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Version>1.9.0</Version>
|
||||
<Version>1.9.5</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Support All Frameworks -->
|
||||
@@ -66,8 +66,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SabreTools.IO" Version="1.7.1" />
|
||||
<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`))" />
|
||||
<PackageReference Include="SabreTools.IO" Version="1.7.3" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.9" Condition="!$(TargetFramework.StartsWith(`net2`)) AND !$(TargetFramework.StartsWith(`net3`)) AND !$(TargetFramework.StartsWith(`net40`)) AND !$(TargetFramework.StartsWith(`net452`))" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -10,7 +10,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Version>1.9.0</Version>
|
||||
<Version>1.9.5</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.3" />
|
||||
<PackageReference Include="SabreTools.Hashing" Version="1.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -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
7
LICENSE
Normal 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.
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
@@ -415,7 +420,14 @@ namespace SabreTools.Serialization.Deserializers
|
||||
obj.RelocationRecords = new RelocationRecord[obj.RelocationRecordCount];
|
||||
for (int i = 0; i < obj.RelocationRecords.Length; i++)
|
||||
{
|
||||
obj.RelocationRecords[i] = ParseRelocationRecord(data);
|
||||
if (data.Position >= data.Length)
|
||||
break;
|
||||
|
||||
var record = ParseRelocationRecord(data);
|
||||
if (record == null)
|
||||
break;
|
||||
|
||||
obj.RelocationRecords[i] = record;
|
||||
}
|
||||
|
||||
return obj;
|
||||
@@ -428,12 +440,20 @@ namespace SabreTools.Serialization.Deserializers
|
||||
/// <returns>Filled RelocationRecord on success, null on error</returns>
|
||||
public static RelocationRecord ParseRelocationRecord(Stream data)
|
||||
{
|
||||
// Handle partial relocation sections
|
||||
if (data.Position > data.Length - 4)
|
||||
return null;
|
||||
|
||||
var obj = new RelocationRecord();
|
||||
|
||||
obj.SourceType = (RelocationRecordSourceType)data.ReadByteValue();
|
||||
obj.Flags = (RelocationRecordFlag)data.ReadByteValue();
|
||||
obj.Offset = data.ReadUInt16LittleEndian();
|
||||
|
||||
// Handle incomplete entries
|
||||
if (data.Position > data.Length - 4)
|
||||
return obj;
|
||||
|
||||
switch (obj.Flags & RelocationRecordFlag.TARGET_MASK)
|
||||
{
|
||||
case RelocationRecordFlag.INTERNALREF:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
85
SabreTools.Serialization/Deserializers/SecuROMAddD.cs
Normal file
85
SabreTools.Serialization/Deserializers/SecuROMAddD.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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, ',');
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -721,7 +729,7 @@ namespace SabreTools.Serialization
|
||||
#region Creation data
|
||||
|
||||
dialogItemTemplate.CreationDataSize = entry.Data.ReadUInt16LittleEndian(ref offset);
|
||||
if (dialogItemTemplate.CreationDataSize != 0)
|
||||
if (dialogItemTemplate.CreationDataSize != 0 && dialogItemTemplate.CreationDataSize + offset < entry.Data.Length)
|
||||
dialogItemTemplate.CreationData = entry.Data.ReadBytes(ref offset, dialogItemTemplate.CreationDataSize);
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -36,14 +36,14 @@ namespace SabreTools.Serialization.Printers
|
||||
Print(builder, executable.COFFSymbolTable);
|
||||
Print(builder, executable.COFFStringTable);
|
||||
Print(builder, executable.AttributeCertificateTable);
|
||||
Print(builder, executable.DelayLoadDirectoryTable);
|
||||
Print(builder, executable.DelayLoadDirectoryTable, executable.SectionTable);
|
||||
|
||||
// Named Sections
|
||||
Print(builder, executable.BaseRelocationTable, executable.SectionTable);
|
||||
Print(builder, executable.DebugTable);
|
||||
Print(builder, executable.ExportTable);
|
||||
Print(builder, executable.ExportTable, executable.SectionTable);
|
||||
Print(builder, executable.ImportTable, executable.SectionTable);
|
||||
Print(builder, executable.ResourceDirectoryTable);
|
||||
Print(builder, executable.ResourceDirectoryTable, executable.SectionTable);
|
||||
}
|
||||
|
||||
private static void Print(StringBuilder builder, Models.MSDOS.ExecutableHeader? header)
|
||||
@@ -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)
|
||||
@@ -502,7 +502,7 @@ namespace SabreTools.Serialization.Printers
|
||||
}
|
||||
}
|
||||
|
||||
private static void Print(StringBuilder builder, DelayLoadDirectoryTable? table)
|
||||
private static void Print(StringBuilder builder, DelayLoadDirectoryTable? table, SectionHeader[]? sections)
|
||||
{
|
||||
builder.AppendLine(" Delay-Load Directory Table Information:");
|
||||
builder.AppendLine(" -------------------------");
|
||||
@@ -515,16 +515,21 @@ namespace SabreTools.Serialization.Printers
|
||||
|
||||
builder.AppendLine(table.Attributes, " Attributes");
|
||||
builder.AppendLine(table.Name, " Name RVA");
|
||||
builder.AppendLine(table.Name.ConvertVirtualAddress(sections), " Name physical address");
|
||||
builder.AppendLine(table.ModuleHandle, " Module handle");
|
||||
builder.AppendLine(table.DelayImportAddressTable, " Delay import address table RVA");
|
||||
builder.AppendLine(table.DelayImportAddressTable.ConvertVirtualAddress(sections), " Delay import address table physical address");
|
||||
builder.AppendLine(table.DelayImportNameTable, " Delay import name table RVA");
|
||||
builder.AppendLine(table.DelayImportNameTable.ConvertVirtualAddress(sections), " Delay import name table physical address");
|
||||
builder.AppendLine(table.BoundDelayImportTable, " Bound delay import table RVA");
|
||||
builder.AppendLine(table.BoundDelayImportTable.ConvertVirtualAddress(sections), " Bound delay import table physical address");
|
||||
builder.AppendLine(table.UnloadDelayImportTable, " Unload delay import table RVA");
|
||||
builder.AppendLine(table.UnloadDelayImportTable.ConvertVirtualAddress(sections), " Unload delay import table physical address");
|
||||
builder.AppendLine(table.TimeStamp, " Timestamp");
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
private static void Print(StringBuilder builder, BaseRelocationBlock[]? entries, SectionHeader[]? table)
|
||||
private static void Print(StringBuilder builder, BaseRelocationBlock[]? entries, SectionHeader[]? sections)
|
||||
{
|
||||
builder.AppendLine(" Base Relocation Table Information:");
|
||||
builder.AppendLine(" -------------------------");
|
||||
@@ -541,14 +546,16 @@ namespace SabreTools.Serialization.Printers
|
||||
|
||||
builder.AppendLine($" Base Relocation Table Entry {i}");
|
||||
builder.AppendLine(baseRelocationTableEntry.PageRVA, " Page RVA");
|
||||
builder.AppendLine(baseRelocationTableEntry.PageRVA.ConvertVirtualAddress(table), " Page physical address");
|
||||
builder.AppendLine(baseRelocationTableEntry.PageRVA.ConvertVirtualAddress(sections), " 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;
|
||||
}
|
||||
|
||||
@@ -595,7 +602,7 @@ namespace SabreTools.Serialization.Printers
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
private static void Print(StringBuilder builder, ExportTable? table)
|
||||
private static void Print(StringBuilder builder, ExportTable? table, SectionHeader[]? sections)
|
||||
{
|
||||
builder.AppendLine(" Export Table Information:");
|
||||
builder.AppendLine(" -------------------------");
|
||||
@@ -619,13 +626,17 @@ namespace SabreTools.Serialization.Printers
|
||||
builder.AppendLine(table.ExportDirectoryTable.MajorVersion, " Major version");
|
||||
builder.AppendLine(table.ExportDirectoryTable.MinorVersion, " Minor version");
|
||||
builder.AppendLine(table.ExportDirectoryTable.NameRVA, " Name RVA");
|
||||
builder.AppendLine(table.ExportDirectoryTable.NameRVA.ConvertVirtualAddress(sections), " Name physical address");
|
||||
builder.AppendLine(table.ExportDirectoryTable.Name, " Name");
|
||||
builder.AppendLine(table.ExportDirectoryTable.OrdinalBase, " Ordinal base");
|
||||
builder.AppendLine(table.ExportDirectoryTable.AddressTableEntries, " Address table entries");
|
||||
builder.AppendLine(table.ExportDirectoryTable.NumberOfNamePointers, " Number of name pointers");
|
||||
builder.AppendLine(table.ExportDirectoryTable.ExportAddressTableRVA, " Export address table RVA");
|
||||
builder.AppendLine(table.ExportDirectoryTable.ExportAddressTableRVA.ConvertVirtualAddress(sections), " Export address table physical address");
|
||||
builder.AppendLine(table.ExportDirectoryTable.NamePointerRVA, " Name pointer table RVA");
|
||||
builder.AppendLine(table.ExportDirectoryTable.NamePointerRVA.ConvertVirtualAddress(sections), " Name pointer table physical address");
|
||||
builder.AppendLine(table.ExportDirectoryTable.OrdinalTableRVA, " Ordinal table RVA");
|
||||
builder.AppendLine(table.ExportDirectoryTable.OrdinalTableRVA.ConvertVirtualAddress(sections), " Ordinal table physical address");
|
||||
}
|
||||
builder.AppendLine();
|
||||
|
||||
@@ -642,7 +653,8 @@ namespace SabreTools.Serialization.Printers
|
||||
var entry = table.ExportAddressTable[i];
|
||||
|
||||
builder.AppendLine($" Export Address Table Entry {i}");
|
||||
builder.AppendLine(entry.ExportRVA, " Export RVA / Forwarder RVA");
|
||||
builder.AppendLine(entry.ExportRVA, " Export / Forwarder RVA");
|
||||
builder.AppendLine(entry.ExportRVA.ConvertVirtualAddress(sections), " Export / Forwarder physical address");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -706,7 +718,7 @@ namespace SabreTools.Serialization.Printers
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
private static void Print(StringBuilder builder, ImportTable? table, SectionHeader[]? sectionTable)
|
||||
private static void Print(StringBuilder builder, ImportTable? table, SectionHeader[]? sections)
|
||||
{
|
||||
builder.AppendLine(" Import Table Information:");
|
||||
builder.AppendLine(" -------------------------");
|
||||
@@ -732,13 +744,14 @@ namespace SabreTools.Serialization.Printers
|
||||
|
||||
builder.AppendLine($" Import Directory Table Entry {i}");
|
||||
builder.AppendLine(entry.ImportLookupTableRVA, " Import lookup table RVA");
|
||||
builder.AppendLine(entry.ImportLookupTableRVA.ConvertVirtualAddress(sectionTable), " Import lookup table Physical Address");
|
||||
builder.AppendLine(entry.ImportLookupTableRVA.ConvertVirtualAddress(sections), " Import lookup table physical address");
|
||||
builder.AppendLine(entry.TimeDateStamp, " Time/Date stamp");
|
||||
builder.AppendLine(entry.ForwarderChain, " Forwarder chain");
|
||||
builder.AppendLine(entry.NameRVA, " Name RVA");
|
||||
builder.AppendLine(entry.NameRVA.ConvertVirtualAddress(sections), " Name physical address");
|
||||
builder.AppendLine(entry.Name, " Name");
|
||||
builder.AppendLine(entry.ImportAddressTableRVA, " Import address table RVA");
|
||||
builder.AppendLine(entry.ImportAddressTableRVA.ConvertVirtualAddress(sectionTable), " Import address table Physical Address");
|
||||
builder.AppendLine(entry.ImportAddressTableRVA.ConvertVirtualAddress(sections), " Import address table physical address");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -779,7 +792,7 @@ namespace SabreTools.Serialization.Printers
|
||||
else
|
||||
{
|
||||
builder.AppendLine(entry.HintNameTableRVA, " Hint/Name table RVA");
|
||||
builder.AppendLine(entry.HintNameTableRVA.ConvertVirtualAddress(sectionTable), " Hint/Name table Physical Address");
|
||||
builder.AppendLine(entry.HintNameTableRVA.ConvertVirtualAddress(sections), " Hint/Name table physical address");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -822,7 +835,7 @@ namespace SabreTools.Serialization.Printers
|
||||
else
|
||||
{
|
||||
builder.AppendLine(entry.HintNameTableRVA, " Hint/Name table RVA");
|
||||
builder.AppendLine(entry.HintNameTableRVA.ConvertVirtualAddress(sectionTable), " Hint/Name table Physical Address");
|
||||
builder.AppendLine(entry.HintNameTableRVA.ConvertVirtualAddress(sections), " Hint/Name table physical address");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -851,7 +864,7 @@ namespace SabreTools.Serialization.Printers
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
private static void Print(StringBuilder builder, ResourceDirectoryTable? table)
|
||||
private static void Print(StringBuilder builder, ResourceDirectoryTable? table, SectionHeader[]? sections)
|
||||
{
|
||||
builder.AppendLine(" Resource Directory Table Information:");
|
||||
builder.AppendLine(" -------------------------");
|
||||
@@ -862,11 +875,11 @@ namespace SabreTools.Serialization.Printers
|
||||
return;
|
||||
}
|
||||
|
||||
Print(table, level: 0, types: [], builder);
|
||||
Print(builder, table, level: 0, types: [], sections);
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
private static void Print(ResourceDirectoryTable table, int level, List<object> types, StringBuilder builder)
|
||||
private static void Print(StringBuilder builder, ResourceDirectoryTable table, int level, List<object> types, SectionHeader[]? sections)
|
||||
{
|
||||
string padding = new(' ', (level + 1) * 2);
|
||||
|
||||
@@ -901,12 +914,12 @@ namespace SabreTools.Serialization.Printers
|
||||
else
|
||||
newTypes.Add(entry.IntegerID);
|
||||
|
||||
PrintResourceDirectoryEntry(entry, level + 1, newTypes, builder);
|
||||
Print(builder, entry, level + 1, newTypes, sections);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void PrintResourceDirectoryEntry(ResourceDirectoryEntry entry, int level, List<object> types, StringBuilder builder)
|
||||
private static void Print(StringBuilder builder, ResourceDirectoryEntry entry, int level, List<object> types, SectionHeader[]? sections)
|
||||
{
|
||||
string padding = new(' ', (level + 1) * 2);
|
||||
|
||||
@@ -922,12 +935,12 @@ namespace SabreTools.Serialization.Printers
|
||||
}
|
||||
|
||||
if (entry.DataEntry != null)
|
||||
PrintResourceDataEntry(entry.DataEntry, level: level + 1, types, builder);
|
||||
Print(builder, entry.DataEntry, level: level + 1, types, sections);
|
||||
else if (entry.Subdirectory != null)
|
||||
Print(entry.Subdirectory, level: level + 1, types, builder);
|
||||
Print(builder, entry.Subdirectory, level: level + 1, types, sections);
|
||||
}
|
||||
|
||||
private static void PrintResourceDataEntry(ResourceDataEntry entry, int level, List<object> types, StringBuilder builder)
|
||||
private static void Print(StringBuilder builder, ResourceDataEntry entry, int level, List<object> types, SectionHeader[]? sections)
|
||||
{
|
||||
string padding = new(' ', (level + 1) * 2);
|
||||
|
||||
@@ -937,6 +950,7 @@ namespace SabreTools.Serialization.Printers
|
||||
|
||||
builder.AppendLine(level, $"{padding}Entry level");
|
||||
builder.AppendLine(entry.DataRVA, $"{padding}Data RVA");
|
||||
builder.AppendLine(entry.DataRVA.ConvertVirtualAddress(sections), $"{padding}Data physical address");
|
||||
builder.AppendLine(entry.Size, $"{padding}Size");
|
||||
builder.AppendLine(entry.Codepage, $"{padding}Codepage");
|
||||
builder.AppendLine(entry.Reserved, $"{padding}Reserved");
|
||||
|
||||
@@ -8,11 +8,13 @@
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<!-- Added due to StormLibSharp -->
|
||||
<NoWarn>CS8600;CS8601;CS8603;CS8604;CS8618;CS8625;CS8634;IL3000</NoWarn>
|
||||
<Nullable>enable</Nullable>
|
||||
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Version>1.9.0</Version>
|
||||
<Version>1.9.5</Version>
|
||||
|
||||
<!-- Package Properties -->
|
||||
<Authors>Matt Nadareski</Authors>
|
||||
@@ -26,6 +28,28 @@
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Support All Frameworks -->
|
||||
<PropertyGroup Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`)) OR $(TargetFramework.StartsWith(`net4`))">
|
||||
<RuntimeIdentifiers>win-x86;win-x64</RuntimeIdentifiers>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="$(TargetFramework.StartsWith(`netcoreapp`)) OR $(TargetFramework.StartsWith(`net5`))">
|
||||
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64</RuntimeIdentifiers>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="$(TargetFramework.StartsWith(`net6`)) OR $(TargetFramework.StartsWith(`net7`)) OR $(TargetFramework.StartsWith(`net8`)) OR $(TargetFramework.StartsWith(`net9`))">
|
||||
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</RuntimeIdentifiers>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="$(RuntimeIdentifier.StartsWith(`osx-arm`))">
|
||||
<TargetFrameworks>net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Set a build flag for Windows specifically -->
|
||||
<PropertyGroup Condition="'$(RuntimeIdentifier)'=='win-x86'">
|
||||
<DefineConstants>$(DefineConstants);WINX86</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(RuntimeIdentifier)'=='win-x64'">
|
||||
<DefineConstants>$(DefineConstants);WINX64</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Exclude certain parts of external modules for by default -->
|
||||
<PropertyGroup>
|
||||
<DefaultItemExcludes>
|
||||
@@ -36,9 +60,8 @@
|
||||
</DefaultItemExcludes>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Exclude all external modules for .NET Framework 2.0, .NET Framework 3.5, or non-Windows
|
||||
builds -->
|
||||
<PropertyGroup Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`)) OR !$(RuntimeIdentifier.StartsWith(`win-x86`))">
|
||||
<!-- Exclude all external modules for .NET Framework 2.0, .NET Framework 3.5, or non-Windows builds -->
|
||||
<PropertyGroup Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`)) OR !($(RuntimeIdentifier.StartsWith(`win-x86`)) OR $(RuntimeIdentifier.StartsWith(`win-x64`)))">
|
||||
<DefaultItemExcludes>
|
||||
$(DefaultItemExcludes);
|
||||
_EXTERNAL\**
|
||||
@@ -63,10 +86,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.3" />
|
||||
<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.3" />
|
||||
<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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
111
SabreTools.Serialization/Wrappers/BFPK.Extraction.cs
Normal file
111
SabreTools.Serialization/Wrappers/BFPK.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
95
SabreTools.Serialization/Wrappers/BSP.Extraction.cs
Normal file
95
SabreTools.Serialization/Wrappers/BSP.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
53
SabreTools.Serialization/Wrappers/BZip2.Extraction.cs
Normal file
53
SabreTools.Serialization/Wrappers/BZip2.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
135
SabreTools.Serialization/Wrappers/CFB.Extraction.cs
Normal file
135
SabreTools.Serialization/Wrappers/CFB.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
114
SabreTools.Serialization/Wrappers/GCF.Extraction.cs
Normal file
114
SabreTools.Serialization/Wrappers/GCF.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
71
SabreTools.Serialization/Wrappers/GZip.Extraction.cs
Normal file
71
SabreTools.Serialization/Wrappers/GZip.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
66
SabreTools.Serialization/Wrappers/LZKWAJ.Extraction.cs
Normal file
66
SabreTools.Serialization/Wrappers/LZKWAJ.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
62
SabreTools.Serialization/Wrappers/LZQBasic.Extraction.cs
Normal file
62
SabreTools.Serialization/Wrappers/LZQBasic.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
102
SabreTools.Serialization/Wrappers/LZSZDD.Extraction.cs
Normal file
102
SabreTools.Serialization/Wrappers/LZSZDD.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
292
SabreTools.Serialization/Wrappers/MicrosoftCabinet.Extraction.cs
Normal file
292
SabreTools.Serialization/Wrappers/MicrosoftCabinet.Extraction.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
79
SabreTools.Serialization/Wrappers/MoPaQ.Extraction.cs
Normal file
79
SabreTools.Serialization/Wrappers/MoPaQ.Extraction.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using SabreTools.Serialization.Interfaces;
|
||||
#if (NET452_OR_GREATER || NETCOREAPP) && (WINX86 || WINX64)
|
||||
using System.IO;
|
||||
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)
|
||||
{
|
||||
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(outputDirectory, 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
65
SabreTools.Serialization/Wrappers/N3DS.Encryption.cs
Normal file
65
SabreTools.Serialization/Wrappers/N3DS.Encryption.cs
Normal 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
226
SabreTools.Serialization/Wrappers/NewExecutable.Extraction.cs
Normal file
226
SabreTools.Serialization/Wrappers/NewExecutable.Extraction.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
437
SabreTools.Serialization/Wrappers/Nitro.Encryption.cs
Normal file
437
SabreTools.Serialization/Wrappers/Nitro.Encryption.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
83
SabreTools.Serialization/Wrappers/PAK.Extraction.cs
Normal file
83
SabreTools.Serialization/Wrappers/PAK.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
88
SabreTools.Serialization/Wrappers/PFF.Extraction.cs
Normal file
88
SabreTools.Serialization/Wrappers/PFF.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
164
SabreTools.Serialization/Wrappers/PKZIP.Extraction.cs
Normal file
164
SabreTools.Serialization/Wrappers/PKZIP.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -0,0 +1,589 @@
|
||||
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([0x3C, 0x3F, 0x78, 0x6D, 0x6C]))
|
||||
{
|
||||
extension = "xml";
|
||||
break;
|
||||
}
|
||||
else if (overlaySample.StartsWith([0x3C, 0x00, 0x3F, 0x00, 0x78, 0x00, 0x6D, 0x00, 0x6C, 0x00]))
|
||||
{
|
||||
extension = "xml";
|
||||
break;
|
||||
}
|
||||
else if (overlaySample.StartsWith([0xFF, 0xFE, 0x3C, 0x00, 0x3F, 0x00, 0x78, 0x00, 0x6D, 0x00, 0x6C, 0x00]))
|
||||
{
|
||||
extension = "xml";
|
||||
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([0x3C, 0x3F, 0x78, 0x6D, 0x6C]))
|
||||
{
|
||||
extension = "xml";
|
||||
break;
|
||||
}
|
||||
else if (resourceSample.StartsWith([0x3C, 0x00, 0x3F, 0x00, 0x78, 0x00, 0x6D, 0x00, 0x6C, 0x00]))
|
||||
{
|
||||
extension = "xml";
|
||||
break;
|
||||
}
|
||||
else if (resourceSample.StartsWith([0xFF, 0xFE, 0x3C, 0x00, 0x3F, 0x00, 0x78, 0x00, 0x6D, 0x00, 0x6C, 0x00]))
|
||||
{
|
||||
extension = "xml";
|
||||
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
116
SabreTools.Serialization/Wrappers/Quantum.Extraction.cs
Normal file
116
SabreTools.Serialization/Wrappers/Quantum.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
222
SabreTools.Serialization/Wrappers/RAR.Extraction.cs
Normal file
222
SabreTools.Serialization/Wrappers/RAR.Extraction.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
152
SabreTools.Serialization/Wrappers/SGA.Extraction.cs
Normal file
152
SabreTools.Serialization/Wrappers/SGA.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
118
SabreTools.Serialization/Wrappers/SecuROMMatroschkaPackage.cs
Normal file
118
SabreTools.Serialization/Wrappers/SecuROMMatroschkaPackage.cs
Normal 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
|
||||
}
|
||||
}
|
||||
224
SabreTools.Serialization/Wrappers/SevenZip.Extraction.cs
Normal file
224
SabreTools.Serialization/Wrappers/SevenZip.Extraction.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
170
SabreTools.Serialization/Wrappers/TapeArchive.Extraction.cs
Normal file
170
SabreTools.Serialization/Wrappers/TapeArchive.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
95
SabreTools.Serialization/Wrappers/VBSP.Extraction.cs
Normal file
95
SabreTools.Serialization/Wrappers/VBSP.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
146
SabreTools.Serialization/Wrappers/VPK.Extraction.cs
Normal file
146
SabreTools.Serialization/Wrappers/VPK.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
83
SabreTools.Serialization/Wrappers/WAD3.Extraction.cs
Normal file
83
SabreTools.Serialization/Wrappers/WAD3.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user