Add QD file support

This commit is contained in:
Matt Nadareski
2026-03-10 18:11:42 -04:00
parent 1e1bf2e27c
commit a6656fd5e6
10 changed files with 315 additions and 2 deletions

View File

@@ -0,0 +1,73 @@
using System.IO;
using System.Linq;
using SabreTools.Serialization.Readers;
using Xunit;
namespace SabreTools.Serialization.Test.Readers
{
public class QDTests
{
[Fact]
public void NullArray_Null()
{
byte[]? data = null;
int offset = 0;
var deserializer = new QD();
var actual = deserializer.Deserialize(data, offset);
Assert.Null(actual);
}
[Fact]
public void EmptyArray_Null()
{
byte[]? data = [];
int offset = 0;
var deserializer = new QD();
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 QD();
var actual = deserializer.Deserialize(data, offset);
Assert.Null(actual);
}
[Fact]
public void NullStream_Null()
{
Stream? data = null;
var deserializer = new QD();
var actual = deserializer.Deserialize(data);
Assert.Null(actual);
}
[Fact]
public void EmptyStream_Null()
{
Stream? data = new MemoryStream([]);
var deserializer = new QD();
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 QD();
var actual = deserializer.Deserialize(data);
Assert.Null(actual);
}
}
}

View File

@@ -0,0 +1,61 @@
using System.IO;
using System.Linq;
using SabreTools.Serialization.Wrappers;
using Xunit;
namespace SabreTools.Serialization.Test.Wrappers
{
public class QDTests
{
[Fact]
public void NullArray_Null()
{
byte[]? data = null;
int offset = 0;
var actual = QD.Create(data, offset);
Assert.Null(actual);
}
[Fact]
public void EmptyArray_Null()
{
byte[]? data = [];
int offset = 0;
var actual = QD.Create(data, offset);
Assert.Null(actual);
}
[Fact]
public void InvalidArray_Null()
{
byte[]? data = [.. Enumerable.Repeat<byte>(0xFF, 1024)];
int offset = 0;
var actual = QD.Create(data, offset);
Assert.Null(actual);
}
[Fact]
public void NullStream_Null()
{
Stream? data = null;
var actual = QD.Create(data);
Assert.Null(actual);
}
[Fact]
public void EmptyStream_Null()
{
Stream? data = new MemoryStream([]);
var actual = QD.Create(data);
Assert.Null(actual);
}
[Fact]
public void InvalidStream_Null()
{
Stream? data = new MemoryStream([.. Enumerable.Repeat<byte>(0xFF, 1024)]);
var actual = QD.Create(data);
Assert.Null(actual);
}
}
}

View File

@@ -0,0 +1,14 @@
namespace SabreTools.Data.Models.NES
{
/// <summary>
/// Quick Disk Famicom Disk System image
/// </summary>
/// <see href="https://www.nesdev.org/wiki/QD"/>
public class QD
{
/// <summary>
/// Disk data (65536 * x bytes)
/// </summary>
public byte[] Data { get; set; } = [];
}
}

View File

@@ -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) | FDS, NESCart |
| [Nesdev Wiki](https://www.nesdev.org/wiki/Nesdev_Wiki) | FDS, NESCart, QD |
| [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 |

View File

@@ -0,0 +1,39 @@
using System.IO;
using SabreTools.IO.Extensions;
namespace SabreTools.Serialization.Readers
{
public class QD : BaseBinaryReader<Data.Models.NES.QD>
{
/// <inheritdoc/>
public override Data.Models.NES.QD? 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 QD file to fill
var QD = new Data.Models.NES.QD();
// If the size is invalid
if ((data.Length - data.Position) == 0 || (data.Length - data.Position) % 65536 != 0)
return null;
// Read the disk data
QD.Data = data.ReadBytes((int)(data.Length - data.Position));
return QD;
}
catch
{
// Ignore the actual error
return null;
}
}
}
}

View File

@@ -50,6 +50,7 @@ namespace SabreTools.Serialization
WrapperType.PKZIP => PKZIP.Create(data),
WrapperType.PlayJAudioFile => PlayJAudioFile.Create(data),
WrapperType.PlayJPlaylist => PlayJPlaylist.Create(data),
WrapperType.QD => QD.Create(data),
WrapperType.Quantum => Quantum.Create(data),
WrapperType.RAR => RAR.Create(data),
WrapperType.RealArcadeInstaller => RealArcadeInstaller.Create(data),
@@ -673,6 +674,13 @@ namespace SabreTools.Serialization
#endregion
#region QD
if (extension.Equals("qd", StringComparison.OrdinalIgnoreCase))
return WrapperType.QD;
#endregion
#region Quantum
if (magic.StartsWith(Data.Models.Quantum.Constants.SignatureBytes))

View File

@@ -14,7 +14,7 @@ namespace SabreTools.Serialization.Wrappers
/// <inheritdoc/>
public void PrintInformation(StringBuilder builder)
{
builder.AppendLine("Nintendo Entertainment System Cart Information:");
builder.AppendLine("fwNES FDS File Information:");
builder.AppendLine("-------------------------");
builder.AppendLine();

View File

@@ -0,0 +1,24 @@
using System.Text;
using SabreTools.Data.Extensions;
namespace SabreTools.Serialization.Wrappers
{
public partial class QD : IPrintable
{
#if NETCOREAPP
/// <inheritdoc/>
public string ExportJSON() => System.Text.Json.JsonSerializer.Serialize(Model, _jsonSerializerOptions);
#endif
/// <inheritdoc/>
public void PrintInformation(StringBuilder builder)
{
builder.AppendLine("Quick Disk Image Information:");
builder.AppendLine("-------------------------");
builder.AppendLine();
//builder.AppendLine(Model.Data, "Disk Data");
builder.AppendLine(Model.Data.Length, "Disk Data Length");
}
}
}

View File

@@ -0,0 +1,89 @@
using System.IO;
namespace SabreTools.Serialization.Wrappers
{
public partial class QD : WrapperBase<Data.Models.NES.QD>
{
#region Descriptive Properties
/// <inheritdoc/>
public override string DescriptionString => "Quick Disk Famicom Disk System image";
#endregion
#region Constructors
/// <inheritdoc/>
public QD(Data.Models.NES.QD model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public QD(Data.Models.NES.QD model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public QD(Data.Models.NES.QD model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public QD(Data.Models.NES.QD model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public QD(Data.Models.NES.QD model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public QD(Data.Models.NES.QD 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 QD? 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 QD? 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.QD().Deserialize(data);
if (model is null)
return null;
return new QD(model, data, currentOffset);
}
catch
{
return null;
}
}
#endregion
}
}

View File

@@ -197,6 +197,11 @@ namespace SabreTools.Serialization.Wrappers
/// </summary>
Quantum,
/// <summary>
/// Quick Disk Famicom Disk System image
/// </summary>
QD,
/// <summary>
/// RAR archive
/// </summary>