mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-07-08 18:06:41 +00:00
Add fwNES FDS file support
This commit is contained in:
@@ -185,6 +185,11 @@ namespace ExtractionTool.Features
|
||||
cfb.Extract(OutputPath, Debug);
|
||||
break;
|
||||
|
||||
// fwNES FDS file
|
||||
case FDS fds:
|
||||
fds.Extract(OutputPath, Debug);
|
||||
break;
|
||||
|
||||
// GCF
|
||||
case GCF gcf:
|
||||
gcf.Extract(OutputPath, Debug);
|
||||
|
||||
@@ -58,6 +58,7 @@ Options:
|
||||
| BFPK custom archive format | |
|
||||
| bzip2 archive | .NET Framework 4.6.2 and greater |
|
||||
| Compound File Binary (CFB) | Only CFB common pieces extractable |
|
||||
| fwNES FDS file | Header and disk data |
|
||||
| gzip archive | |
|
||||
| Half-Life Game Cache File (GCF) | |
|
||||
| Half-Life Level (BSP) | |
|
||||
|
||||
73
SabreTools.Serialization.Test/Readers/FDSTests.cs
Normal file
73
SabreTools.Serialization.Test/Readers/FDSTests.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using SabreTools.Serialization.Readers;
|
||||
using Xunit;
|
||||
|
||||
namespace SabreTools.Serialization.Test.Readers
|
||||
{
|
||||
public class FDSTests
|
||||
{
|
||||
[Fact]
|
||||
public void NullArray_Null()
|
||||
{
|
||||
byte[]? data = null;
|
||||
int offset = 0;
|
||||
var deserializer = new FDS();
|
||||
|
||||
var actual = deserializer.Deserialize(data, offset);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyArray_Null()
|
||||
{
|
||||
byte[]? data = [];
|
||||
int offset = 0;
|
||||
var deserializer = new FDS();
|
||||
|
||||
var actual = deserializer.Deserialize(data, offset);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidArray_Null()
|
||||
{
|
||||
byte[]? data = [.. Enumerable.Repeat<byte>(0xFF, 1024)];
|
||||
int offset = 0;
|
||||
var deserializer = new FDS();
|
||||
|
||||
var actual = deserializer.Deserialize(data, offset);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NullStream_Null()
|
||||
{
|
||||
Stream? data = null;
|
||||
var deserializer = new FDS();
|
||||
|
||||
var actual = deserializer.Deserialize(data);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyStream_Null()
|
||||
{
|
||||
Stream? data = new MemoryStream([]);
|
||||
var deserializer = new FDS();
|
||||
|
||||
var actual = deserializer.Deserialize(data);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidStream_Null()
|
||||
{
|
||||
Stream? data = new MemoryStream([.. Enumerable.Repeat<byte>(0xFF, 1024)]);
|
||||
var deserializer = new FDS();
|
||||
|
||||
var actual = deserializer.Deserialize(data);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
61
SabreTools.Serialization.Test/Wrappers/FDSTests.cs
Normal file
61
SabreTools.Serialization.Test/Wrappers/FDSTests.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using SabreTools.Serialization.Wrappers;
|
||||
using Xunit;
|
||||
|
||||
namespace SabreTools.Serialization.Test.Wrappers
|
||||
{
|
||||
public class FDSTests
|
||||
{
|
||||
[Fact]
|
||||
public void NullArray_Null()
|
||||
{
|
||||
byte[]? data = null;
|
||||
int offset = 0;
|
||||
var actual = FDS.Create(data, offset);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyArray_Null()
|
||||
{
|
||||
byte[]? data = [];
|
||||
int offset = 0;
|
||||
var actual = FDS.Create(data, offset);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidArray_Null()
|
||||
{
|
||||
byte[]? data = [.. Enumerable.Repeat<byte>(0xFF, 1024)];
|
||||
int offset = 0;
|
||||
var actual = FDS.Create(data, offset);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NullStream_Null()
|
||||
{
|
||||
Stream? data = null;
|
||||
var actual = FDS.Create(data);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyStream_Null()
|
||||
{
|
||||
Stream? data = new MemoryStream([]);
|
||||
var actual = FDS.Create(data);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidStream_Null()
|
||||
{
|
||||
Stream? data = new MemoryStream([.. Enumerable.Repeat<byte>(0xFF, 1024)]);
|
||||
var actual = FDS.Create(data);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,5 +11,15 @@ namespace SabreTools.Data.Models.NES
|
||||
/// NES<EOF>
|
||||
/// </summary>
|
||||
public static readonly string CartSignatureString = "NES" + (char)0x1A;
|
||||
|
||||
/// <summary>
|
||||
/// FDS<EOF>
|
||||
/// </summary>
|
||||
public static readonly byte[] FDSSignatureBytes = [0x46, 0x44, 0x53, 0x1A];
|
||||
|
||||
/// <summary>
|
||||
/// FDS<EOF>
|
||||
/// </summary>
|
||||
public static readonly string FDSSignatureString = "FDS" + (char)0x1A;
|
||||
}
|
||||
}
|
||||
|
||||
19
SabreTools.Serialization/Models/NES/FDS.cs
Normal file
19
SabreTools.Serialization/Models/NES/FDS.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace SabreTools.Data.Models.NES
|
||||
{
|
||||
/// <summary>
|
||||
/// fwNES FDS file
|
||||
/// </summary>
|
||||
/// <see href="https://www.nesdev.org/wiki/FDS_file_format"/>
|
||||
public class FDS
|
||||
{
|
||||
/// <summary>
|
||||
/// FDS header
|
||||
/// </summary>
|
||||
public FDSHeader? Header { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Disk data (65500 * x bytes)
|
||||
/// </summary>
|
||||
public byte[] Data { get; set; } = [];
|
||||
}
|
||||
}
|
||||
24
SabreTools.Serialization/Models/NES/FDSHeader.cs
Normal file
24
SabreTools.Serialization/Models/NES/FDSHeader.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace SabreTools.Data.Models.NES
|
||||
{
|
||||
/// <summary>
|
||||
/// fwNES FDS header
|
||||
/// </summary>
|
||||
/// <see href="https://www.nesdev.org/wiki/FDS_file_format"/>
|
||||
public class FDSHeader
|
||||
{
|
||||
/// <summary>
|
||||
/// Constant $46 $44 $53 $1A ("FDS" followed by MS-DOS end-of-file)
|
||||
/// </summary>
|
||||
public byte[] IdentificationString { get; set; } = new byte[4];
|
||||
|
||||
/// <summary>
|
||||
/// Number of disk sides
|
||||
/// </summary>
|
||||
public byte DiskSides { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Zero-filled padding
|
||||
/// </summary>
|
||||
public byte[] Padding { get; set; } = new byte[11];
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ Not all of this information was able to be gathered directly from the files in q
|
||||
| [Matthew Russotto](http://www.russotto.net/quantumcomp.html) | Compression/Quantum |
|
||||
| [Microsoft Learn](https://learn.microsoft.com/en-us/) | BMP, CFB, Compression/LZX, Compression/MSZIP, MicrosoftCabinet, OLE, PortableExecutable, SecuROM, WiseInstaller |
|
||||
| [msitools](https://github.com/GNOME/msitools/) | CFB |
|
||||
| [Nesdev Wiki](https://www.nesdev.org/wiki/Nesdev_Wiki) | NESCart |
|
||||
| [Nesdev Wiki](https://www.nesdev.org/wiki/Nesdev_Wiki) | FDS, NESCart |
|
||||
| [OSDev.org](https://wiki.osdev.org/Expanded_Main_Page) | MSDOS, NewExecutable |
|
||||
| [PInvoke.net](http://www.pinvoke.net/index.aspx) | MSDOS |
|
||||
| [PKWARE(?)](https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT) | PKZIP |
|
||||
|
||||
68
SabreTools.Serialization/Readers/FDS.cs
Normal file
68
SabreTools.Serialization/Readers/FDS.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System.IO;
|
||||
using SabreTools.Data.Models.NES;
|
||||
using SabreTools.IO.Extensions;
|
||||
using static SabreTools.Data.Models.NES.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Readers
|
||||
{
|
||||
public class FDS : BaseBinaryReader<Data.Models.NES.FDS>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override Data.Models.NES.FDS? Deserialize(Stream? data)
|
||||
{
|
||||
// If the data is invalid
|
||||
if (data is null || !data.CanRead)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
// Cache the current offset
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// Create a new FDS file to fill
|
||||
var fds = new Data.Models.NES.FDS();
|
||||
|
||||
#region Header
|
||||
|
||||
// Try to parse the header
|
||||
var header = ParseHeader(data);
|
||||
if (header is null)
|
||||
return null;
|
||||
|
||||
// Set the header
|
||||
fds.Header = header;
|
||||
|
||||
#endregion
|
||||
|
||||
// Read the disk data
|
||||
fds.Data = data.ReadBytes((int)(data.Length - data.Position));
|
||||
|
||||
return fds;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore the actual error
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a FDSHeader
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled FDSHeader on success, null on error</returns>
|
||||
public static FDSHeader? ParseHeader(Stream data)
|
||||
{
|
||||
var obj = new FDSHeader();
|
||||
|
||||
obj.IdentificationString = data.ReadBytes(4);
|
||||
if (!obj.IdentificationString.EqualsExactly(FDSSignatureBytes))
|
||||
return null;
|
||||
|
||||
obj.DiskSides = data.ReadByteValue();
|
||||
obj.Padding = data.ReadBytes(11);
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,10 +79,10 @@ namespace SabreTools.Serialization.Readers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Header
|
||||
/// Parse a Stream into a CartHeader
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Header on success, null on error</returns>
|
||||
/// <returns>Filled CartHeader on success, null on error</returns>
|
||||
public static CartHeader? ParseHeader(Stream data)
|
||||
{
|
||||
// Cache data until NES 2.0 flag determined
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace SabreTools.Serialization
|
||||
WrapperType.CHD => CHD.Create(data),
|
||||
WrapperType.CIA => CIA.Create(data),
|
||||
WrapperType.Executable => CreateExecutableWrapper(data),
|
||||
WrapperType.FDS => FDS.Create(data),
|
||||
WrapperType.GCF => GCF.Create(data),
|
||||
WrapperType.GZip => GZip.Create(data),
|
||||
WrapperType.IniFile => null,// TODO: Implement wrapper
|
||||
@@ -342,6 +343,18 @@ namespace SabreTools.Serialization
|
||||
|
||||
#endregion
|
||||
|
||||
#region FDS
|
||||
|
||||
// fwNES FDS file with header
|
||||
if (magic.StartsWith(Data.Models.NES.Constants.FDSSignatureBytes))
|
||||
return WrapperType.FDS;
|
||||
|
||||
// fwNES FDS file with header
|
||||
if (extension.Equals("fds", StringComparison.OrdinalIgnoreCase))
|
||||
return WrapperType.FDS;
|
||||
|
||||
#endregion
|
||||
|
||||
// TODO: Use constants from Models here
|
||||
#region GCF
|
||||
|
||||
|
||||
81
SabreTools.Serialization/Wrappers/FDS.Extraction.cs
Normal file
81
SabreTools.Serialization/Wrappers/FDS.Extraction.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using SabreTools.IO.Extensions;
|
||||
|
||||
namespace SabreTools.Serialization.Wrappers
|
||||
{
|
||||
public partial class FDS : IExtractable
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public bool Extract(string outputDirectory, bool includeDebug)
|
||||
{
|
||||
// Get the base path
|
||||
string baseFilename = Filename is null
|
||||
? Guid.NewGuid().ToString()
|
||||
: Path.GetFileNameWithoutExtension(Filename);
|
||||
string basePath = Path.Combine(outputDirectory, baseFilename);
|
||||
|
||||
// Check if any data was extracted successfully
|
||||
bool success = false;
|
||||
|
||||
// Header data
|
||||
if (Header is not null)
|
||||
{
|
||||
string headerPath = $"{basePath}.hdr";
|
||||
if (includeDebug) Console.WriteLine($"Attempting to extract header data to {headerPath}");
|
||||
|
||||
// Try to write the data
|
||||
try
|
||||
{
|
||||
// Open the output file for writing
|
||||
using var fs = File.Open(headerPath, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
|
||||
// Bytes 0-3
|
||||
fs.Write(Header.IdentificationString);
|
||||
fs.Flush();
|
||||
|
||||
// Byte 4
|
||||
fs.Write(Header.DiskSides);
|
||||
fs.Flush();
|
||||
|
||||
// Byte 5-15
|
||||
fs.Write(Header.Padding);
|
||||
fs.Flush();
|
||||
|
||||
// Header extracted
|
||||
success = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (includeDebug) Console.Error.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Disk data
|
||||
// TODO: Convert to QD format?
|
||||
if (Model.Data.Length > 0)
|
||||
{
|
||||
string diskPath = $"{basePath}.unh";
|
||||
if (includeDebug) Console.WriteLine($"Attempting to extract disk data to {diskPath}");
|
||||
|
||||
// Try to write the data
|
||||
try
|
||||
{
|
||||
// Open the output file for writing
|
||||
using var fs = File.Open(diskPath, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
fs.Write(Model.Data, 0, Model.Data.Length);
|
||||
fs.Flush();
|
||||
|
||||
// PRG-ROM extracted
|
||||
success = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (includeDebug) Console.Error.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
}
|
||||
}
|
||||
44
SabreTools.Serialization/Wrappers/FDS.Printing.cs
Normal file
44
SabreTools.Serialization/Wrappers/FDS.Printing.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System.Text;
|
||||
using SabreTools.Data.Extensions;
|
||||
using SabreTools.Data.Models.NES;
|
||||
|
||||
namespace SabreTools.Serialization.Wrappers
|
||||
{
|
||||
public partial class FDS : IPrintable
|
||||
{
|
||||
#if NETCOREAPP
|
||||
/// <inheritdoc/>
|
||||
public string ExportJSON() => System.Text.Json.JsonSerializer.Serialize(Model, _jsonSerializerOptions);
|
||||
#endif
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void PrintInformation(StringBuilder builder)
|
||||
{
|
||||
builder.AppendLine("Nintendo Entertainment System Cart Information:");
|
||||
builder.AppendLine("-------------------------");
|
||||
builder.AppendLine();
|
||||
|
||||
Print(builder, Model.Header);
|
||||
|
||||
//builder.AppendLine(Model.Data, "Disk Data");
|
||||
builder.AppendLine(Model.Data.Length, "Disk Data Length");
|
||||
}
|
||||
|
||||
private static void Print(StringBuilder builder, FDSHeader? header)
|
||||
{
|
||||
builder.AppendLine(" FDS Header Information:");
|
||||
builder.AppendLine(" -------------------------");
|
||||
if (header is null)
|
||||
{
|
||||
builder.AppendLine(" No header present");
|
||||
builder.AppendLine();
|
||||
return;
|
||||
}
|
||||
|
||||
builder.AppendLine(header.IdentificationString, " Identification string");
|
||||
builder.AppendLine(header.DiskSides, " Disk sides");
|
||||
builder.AppendLine(header.Padding, " Padding");
|
||||
builder.AppendLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
100
SabreTools.Serialization/Wrappers/FDS.cs
Normal file
100
SabreTools.Serialization/Wrappers/FDS.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using System.IO;
|
||||
using SabreTools.Data.Models.NES;
|
||||
|
||||
namespace SabreTools.Serialization.Wrappers
|
||||
{
|
||||
public partial class FDS : WrapperBase<Data.Models.NES.FDS>
|
||||
{
|
||||
#region Descriptive Properties
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string DescriptionString => "fwNES FDS File";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Extension Properties
|
||||
|
||||
/// <inheritdoc cref="Data.Models.NES.FDS.Header"/>
|
||||
public FDSHeader Header => Model.Header;
|
||||
|
||||
/// <inheritdoc cref="FDSHeader.DiskSides"/>
|
||||
public byte DiskSides => Header.DiskSides;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <inheritdoc/>
|
||||
public FDS(Data.Models.NES.FDS model, byte[] data) : base(model, data) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public FDS(Data.Models.NES.FDS model, byte[] data, int offset) : base(model, data, offset) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public FDS(Data.Models.NES.FDS model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public FDS(Data.Models.NES.FDS model, Stream data) : base(model, data) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public FDS(Data.Models.NES.FDS model, Stream data, long offset) : base(model, data, offset) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public FDS(Data.Models.NES.FDS model, Stream data, long offset, long length) : base(model, data, offset, length) { }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Static Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Create an NES cart image from a byte array and offset
|
||||
/// </summary>
|
||||
/// <param name="data">Byte array representing the archive</param>
|
||||
/// <param name="offset">Offset within the array to parse</param>
|
||||
/// <returns>An NES cart image wrapper on success, null on failure</returns>
|
||||
public static FDS? Create(byte[]? data, int offset)
|
||||
{
|
||||
// If the data is invalid
|
||||
if (data is 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 an NES cart image from a Stream
|
||||
/// </summary>
|
||||
/// <param name="data">Stream representing the archive</param>
|
||||
/// <returns>An NES cart image wrapper on success, null on failure</returns>
|
||||
public static FDS? Create(Stream? data)
|
||||
{
|
||||
// If the data is invalid
|
||||
if (data is null || !data.CanRead)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
// Cache the current offset
|
||||
long currentOffset = data.Position;
|
||||
|
||||
var model = new Readers.FDS().Deserialize(data);
|
||||
if (model is null)
|
||||
return null;
|
||||
|
||||
return new FDS(model, data, currentOffset);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,11 @@ namespace SabreTools.Serialization.Wrappers
|
||||
/// <remarks>Includes MZ, NE, LE/LX, and PE</remarks>
|
||||
Executable,
|
||||
|
||||
/// <summary>
|
||||
/// fwNES FDS file
|
||||
/// </summary>
|
||||
FDS,
|
||||
|
||||
/// <summary>
|
||||
/// Half-Life Game Cache File
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user