From 1df97c53d07ad73ce83dc171bd8a19a1054f9c46 Mon Sep 17 00:00:00 2001 From: voytas Date: Wed, 29 Apr 2026 21:00:48 +0100 Subject: [PATCH 1/3] Add support for RZX file format --- .../Extensions/ByteStreamReaderExtensions.cs | 2 +- .../Extensions/BytesExtensions.cs | 7 ++ .../Extensions/StreamExtensions.cs | 41 ++++++++++ src/Spectron.Files/IO/ByteStreamReader.cs | 35 +------- src/Spectron.Files/IO/DataWriter.cs | 2 +- src/Spectron.Files/Rzx/BlockIds.cs | 10 +++ src/Spectron.Files/Rzx/Blocks/BlockHeader.cs | 19 +++++ src/Spectron.Files/Rzx/Blocks/CreatorBlock.cs | 43 ++++++++++ .../Rzx/Blocks/RecordingBlock.cs | 80 +++++++++++++++++++ .../Rzx/Blocks/RecordingFrame.cs | 41 ++++++++++ .../Rzx/Blocks/SnapshotBlock.cs | 60 ++++++++++++++ src/Spectron.Files/Rzx/RzxFile.cs | 69 ++++++++++++++++ src/Spectron.Files/Rzx/RzxHeader.cs | 43 ++++++++++ src/Spectron.Files/Szx/Blocks/AyBlock.cs | 2 +- src/Spectron.Files/Szx/Blocks/BlockHeader.cs | 2 +- src/Spectron.Files/Szx/Blocks/CreatorBlock.cs | 2 +- .../Szx/Blocks/CustomRomBlock.cs | 2 +- .../Szx/Blocks/JoystickBlock.cs | 2 +- .../Szx/Blocks/KeyboardBlock.cs | 2 +- src/Spectron.Files/Szx/Blocks/PaletteBlock.cs | 2 +- src/Spectron.Files/Szx/Blocks/RamPageBlock.cs | 2 +- .../Szx/Blocks/SpecRegsBlock.cs | 2 +- src/Spectron.Files/Szx/Blocks/TapeBlock.cs | 2 +- src/Spectron.Files/Szx/Blocks/Z80RegsBlock.cs | 2 +- .../Szx/Blocks/ZxPrinterBlock.cs | 2 +- .../Szx/Extensions/StreamExtensions.cs | 36 --------- src/Spectron.Files/Szx/SzxHeader.cs | 2 +- .../Spectron.Files.Tests/Rzx/RzxFileTests.cs | 30 +++++++ .../Spectron.Files.Tests.csproj | 8 +- 29 files changed, 463 insertions(+), 89 deletions(-) rename src/Spectron.Files/{Szx => }/Extensions/ByteStreamReaderExtensions.cs (86%) create mode 100644 src/Spectron.Files/Extensions/StreamExtensions.cs create mode 100644 src/Spectron.Files/Rzx/BlockIds.cs create mode 100644 src/Spectron.Files/Rzx/Blocks/BlockHeader.cs create mode 100644 src/Spectron.Files/Rzx/Blocks/CreatorBlock.cs create mode 100644 src/Spectron.Files/Rzx/Blocks/RecordingBlock.cs create mode 100644 src/Spectron.Files/Rzx/Blocks/RecordingFrame.cs create mode 100644 src/Spectron.Files/Rzx/Blocks/SnapshotBlock.cs create mode 100644 src/Spectron.Files/Rzx/RzxFile.cs create mode 100644 src/Spectron.Files/Rzx/RzxHeader.cs delete mode 100644 src/Spectron.Files/Szx/Extensions/StreamExtensions.cs create mode 100644 tests/Spectron.Files.Tests/Rzx/RzxFileTests.cs diff --git a/src/Spectron.Files/Szx/Extensions/ByteStreamReaderExtensions.cs b/src/Spectron.Files/Extensions/ByteStreamReaderExtensions.cs similarity index 86% rename from src/Spectron.Files/Szx/Extensions/ByteStreamReaderExtensions.cs rename to src/Spectron.Files/Extensions/ByteStreamReaderExtensions.cs index 2cadd26..94b1849 100644 --- a/src/Spectron.Files/Szx/Extensions/ByteStreamReaderExtensions.cs +++ b/src/Spectron.Files/Extensions/ByteStreamReaderExtensions.cs @@ -1,7 +1,7 @@ using System.Text; using OldBit.Spectron.Files.IO; -namespace OldBit.Spectron.Files.Szx.Extensions; +namespace OldBit.Spectron.Files.Extensions; internal static class ByteStreamReaderExtensions { diff --git a/src/Spectron.Files/Extensions/BytesExtensions.cs b/src/Spectron.Files/Extensions/BytesExtensions.cs index f21602c..83580e8 100644 --- a/src/Spectron.Files/Extensions/BytesExtensions.cs +++ b/src/Spectron.Files/Extensions/BytesExtensions.cs @@ -7,10 +7,17 @@ internal static class BytesExtensions internal static string ToAsciiString(this IEnumerable bytes) { var s = new StringBuilder(); + foreach (var b in bytes) { + if (b == 0) + { + continue; + } + s.Append((char)b); } + return s.ToString(); } } \ No newline at end of file diff --git a/src/Spectron.Files/Extensions/StreamExtensions.cs b/src/Spectron.Files/Extensions/StreamExtensions.cs new file mode 100644 index 0000000..75f1f4a --- /dev/null +++ b/src/Spectron.Files/Extensions/StreamExtensions.cs @@ -0,0 +1,41 @@ +using System.Text; + +namespace OldBit.Spectron.Files.Extensions; + +internal static class StreamExtensions +{ + extension(Stream stream) + { + internal void WriteWord(int value) + { + stream.WriteByte((byte)value); + stream.WriteByte((byte)(value >> 8)); + } + + internal void WriteDWord(DWord value) + { + stream.WriteByte((byte)value); + stream.WriteByte((byte)(value >> 8)); + stream.WriteByte((byte)(value >> 16)); + stream.WriteByte((byte)(value >> 24)); + } + + internal void WriteBytes(byte[] value) => stream.Write(value, 0, value.Length); + + internal void WriteChars(string value, int length) + { + if (length == 0) + { + return; + } + + var bytes = Encoding.ASCII.GetBytes(value); + stream.Write(bytes, 0, Math.Min(bytes.Length, length)); + + if (bytes.Length < length) + { + stream.Write(new byte[length - bytes.Length], 0, length - bytes.Length); + } + } + } +} \ No newline at end of file diff --git a/src/Spectron.Files/IO/ByteStreamReader.cs b/src/Spectron.Files/IO/ByteStreamReader.cs index 3d7fd58..7b405d0 100644 --- a/src/Spectron.Files/IO/ByteStreamReader.cs +++ b/src/Spectron.Files/IO/ByteStreamReader.cs @@ -11,54 +11,27 @@ internal sealed class ByteStreamReader /// Create a new instance of the byte reader. /// /// The stream that provides data for the reader. - public ByteStreamReader(Stream stream) - { - _stream = stream; - } + public ByteStreamReader(Stream stream) => _stream = stream; /// /// Reads a byte from the stream and advances the position within the stream by one byte. /// /// The byte retrieved from the stream. /// Thrown when not enough data is in the stream. - public byte ReadByte() - { - if (!TryReadByte( out var data)) - { - throw new EndOfStreamException(); - } - - return data; - } + public byte ReadByte() => !TryReadByte( out var data) ? throw new EndOfStreamException() : data; /// /// Reads a word from the stream and advances the position within the stream by two bytes. /// /// The word retrieved from the stream. /// Thrown when not enough data is in the stream. - public Word ReadWord() - { - if (!TryReadWord(out var data)) - { - throw new EndOfStreamException(); - } - - return data; - } + public Word ReadWord() => !TryReadWord(out var data) ? throw new EndOfStreamException() : data; /// /// Reads a dword from the stream and advances the position within the stream by four bytes. /// /// The dword retrieved from the stream. - public DWord ReadDWord() - { - if (!TryReadDWord(out var data)) - { - throw new EndOfStreamException(); - } - - return data; - } + public DWord ReadDWord() => !TryReadDWord(out var data) ? throw new EndOfStreamException() : data; /// /// Reads a sequence of bytes from the stream and advances the position within the stream by 'count' bytes. diff --git a/src/Spectron.Files/IO/DataWriter.cs b/src/Spectron.Files/IO/DataWriter.cs index 7ea8984..6273395 100644 --- a/src/Spectron.Files/IO/DataWriter.cs +++ b/src/Spectron.Files/IO/DataWriter.cs @@ -1,5 +1,5 @@ +using OldBit.Spectron.Files.Extensions; using OldBit.Spectron.Files.Serialization; -using OldBit.Spectron.Files.Szx.Extensions; using OldBit.Spectron.Files.Tap; using OldBit.Spectron.Files.Tzx.Blocks; diff --git a/src/Spectron.Files/Rzx/BlockIds.cs b/src/Spectron.Files/Rzx/BlockIds.cs new file mode 100644 index 0000000..a65c548 --- /dev/null +++ b/src/Spectron.Files/Rzx/BlockIds.cs @@ -0,0 +1,10 @@ +namespace OldBit.Spectron.Files.Rzx; + +internal static class BlockIds +{ + internal const byte Creator = 0x10; + internal const byte Security = 0x20; + internal const byte Signature = 0x21; + internal const byte Snapshot = 0x30; + internal const byte Recording = 0x80; +} \ No newline at end of file diff --git a/src/Spectron.Files/Rzx/Blocks/BlockHeader.cs b/src/Spectron.Files/Rzx/Blocks/BlockHeader.cs new file mode 100644 index 0000000..1d2ae16 --- /dev/null +++ b/src/Spectron.Files/Rzx/Blocks/BlockHeader.cs @@ -0,0 +1,19 @@ +using OldBit.Spectron.Files.IO; + +namespace OldBit.Spectron.Files.Rzx.Blocks; + +internal sealed class BlockHeader(byte blockId, DWord size) +{ + internal byte BlockId { get; } = blockId; + internal DWord Size { get; } = size; + + internal static BlockHeader? Read(ByteStreamReader reader) + { + if (reader.TryReadByte(out var blockId) && reader.TryReadDWord(out var size)) + { + return new BlockHeader(blockId, size); + } + + return null; + } +} \ No newline at end of file diff --git a/src/Spectron.Files/Rzx/Blocks/CreatorBlock.cs b/src/Spectron.Files/Rzx/Blocks/CreatorBlock.cs new file mode 100644 index 0000000..b38c363 --- /dev/null +++ b/src/Spectron.Files/Rzx/Blocks/CreatorBlock.cs @@ -0,0 +1,43 @@ +using OldBit.Spectron.Files.Extensions; +using OldBit.Spectron.Files.IO; + +namespace OldBit.Spectron.Files.Rzx.Blocks; + +/// +/// Information about the program which created the RZX. +/// +public class CreatorBlock +{ + /// + /// Creator's identification string. + /// ≠≠ + public string CreatorName { get; private set; } = string.Empty; + + /// + /// Creator's major version number. + /// + public Word MajorVersion { get; private set; } + + /// + /// Creator's minor version number. + /// + public Word MinorVersion { get; private set; } + + /// + /// Creator's custom data (may be absent). + /// + public byte[]? Data { get; private set; } + + internal static CreatorBlock Read(ByteStreamReader reader, DWord blockLength) + { + var block = new CreatorBlock(); + + var bytes = reader.ReadBytes(20); + block.CreatorName = bytes.ToAsciiString().Trim(); + block.MajorVersion = reader.ReadWord(); + block.MinorVersion = reader.ReadWord(); + block.Data = reader.ReadBytes((int)(blockLength - 29)); + + return block; + } +} \ No newline at end of file diff --git a/src/Spectron.Files/Rzx/Blocks/RecordingBlock.cs b/src/Spectron.Files/Rzx/Blocks/RecordingBlock.cs new file mode 100644 index 0000000..6367879 --- /dev/null +++ b/src/Spectron.Files/Rzx/Blocks/RecordingBlock.cs @@ -0,0 +1,80 @@ +using OldBit.Spectron.Files.IO; +using OldBit.Spectron.Files.Szx; + +namespace OldBit.Spectron.Files.Rzx.Blocks; + +/// +/// Actual input recording data. +/// +public class RecordingBlock +{ + /// + /// Number of frames in the block. + /// + public DWord FrameCount { get; private init; } + + /// + /// Reserved. + /// + public byte Reserved { get; private set; } + + /// + /// T-STATES counter at the beginning. + /// + public DWord TStatesCounter { get; private set; } + + /// + /// Flags (b0: Protected (frames are encrypted with x-key), b1: Compressed data.) + /// + public DWord Flags { get; private set; } + + public List Frames { get; } = []; + + internal static RecordingBlock Read(ByteStreamReader reader, DWord blockLength) + { + var block = new RecordingBlock + { + FrameCount = reader.ReadDWord(), + Reserved = reader.ReadByte(), + TStatesCounter = reader.ReadDWord(), + Flags = reader.ReadDWord() + }; + + var isProtected = (block.Flags & 0x01) == 0x01; + if (isProtected) + { + throw new NotSupportedException("Protected recording blocks are not supported."); + } + + var data = reader.ReadBytes((int)(blockLength - 18)); + + var isCompressed = (block.Flags & 0x02) == 0x02; + if (isCompressed) + { + data = ZLibHelper.Decompress(data); + } + + ReadFrames(block, data); + + return block; + } + + private static void ReadFrames(RecordingBlock block, byte[] data) + { + var memoryStream = new MemoryStream(data); + var reader = new ByteStreamReader(memoryStream); + + for (var i = 0; i < block.FrameCount; i++) + { + var frame = RecordingFrame.Read(reader); + + if (frame.InCounter == 65535) + { + // Repeated frame, copy the values from the previous frame + frame.Values = block.Frames.LastOrDefault()?.Values ?? []; + } + + block.Frames.Add(frame); + } + } +} \ No newline at end of file diff --git a/src/Spectron.Files/Rzx/Blocks/RecordingFrame.cs b/src/Spectron.Files/Rzx/Blocks/RecordingFrame.cs new file mode 100644 index 0000000..1ba523f --- /dev/null +++ b/src/Spectron.Files/Rzx/Blocks/RecordingFrame.cs @@ -0,0 +1,41 @@ +using OldBit.Spectron.Files.IO; + +namespace OldBit.Spectron.Files.Rzx.Blocks; + +/// +/// Layout of the input log for the frame. +/// +public class RecordingFrame +{ + /// + /// Fetch counter till next interrupt (i.e. number of R increments, INTA excluded) + /// + public Word FetchCounter { get; private set; } + + /// + /// IN counter. Number of I/O port reads performed by the CPU in this frame (their return values follow). + /// If equal to 65,535, this was a repeated frame, i.e. the port reads were exactly the same of the last frame. + /// + public Word InCounter { get; private set; } + + /// + /// Return values for the CPU I/O port reads. + /// + public byte[] Values { get; internal set; } = []; + + internal static RecordingFrame Read(ByteStreamReader reader) + { + var block = new RecordingFrame + { + FetchCounter = reader.ReadWord(), + InCounter = reader.ReadWord() + }; + + if (block.InCounter != 65535) + { + block.Values = reader.ReadBytes(block.InCounter); + } + + return block; + } +} \ No newline at end of file diff --git a/src/Spectron.Files/Rzx/Blocks/SnapshotBlock.cs b/src/Spectron.Files/Rzx/Blocks/SnapshotBlock.cs new file mode 100644 index 0000000..176f33c --- /dev/null +++ b/src/Spectron.Files/Rzx/Blocks/SnapshotBlock.cs @@ -0,0 +1,60 @@ +using OldBit.Spectron.Files.Extensions; +using OldBit.Spectron.Files.IO; +using OldBit.Spectron.Files.Szx; + +namespace OldBit.Spectron.Files.Rzx.Blocks; + +/// +/// Snapshot block. +/// +public class SnapshotBlock +{ + /// + /// Flags. + /// + public DWord Flags { get; private init; } + + /// + /// Snapshot filename extension ("SNA", "Z80", etc). + /// + public string Extension { get; private set; } = string.Empty; + + /// + /// Uncompressed snapshot length (same as SL is the snapshot is not compressed + /// + public DWord UncompressedSize { get; private set; } + + /// + /// Input recording block. + /// + public RecordingBlock? Recording { get; internal set; } + + /// + /// Snapshot data. + /// + public byte[]? Data { get; set; } + + internal static SnapshotBlock Read(ByteStreamReader reader, DWord blockLength) + { + var block = new SnapshotBlock + { + Flags = reader.ReadDWord() + }; + + if ((block.Flags & 0x01) == 0x01) + { + throw new NotSupportedException("Only compressed snapshots are supported."); + } + + var isCompressed = (block.Flags & 0x02) == 0x02; + + var bytes = reader.ReadBytes(4); + block.Extension = bytes.ToAsciiString().Trim(); + block.UncompressedSize = reader.ReadDWord(); + + var data = reader.ReadBytes((int)(blockLength - 17)); + block.Data = isCompressed ? ZLibHelper.Decompress(data) : data; + + return block; + } +} \ No newline at end of file diff --git a/src/Spectron.Files/Rzx/RzxFile.cs b/src/Spectron.Files/Rzx/RzxFile.cs new file mode 100644 index 0000000..aabc20e --- /dev/null +++ b/src/Spectron.Files/Rzx/RzxFile.cs @@ -0,0 +1,69 @@ +using OldBit.Spectron.Files.IO; +using OldBit.Spectron.Files.Rzx.Blocks; + +namespace OldBit.Spectron.Files.Rzx; + +/// +/// Represents a .rzx file. +/// +public sealed class RzxFile +{ + /// + /// Gets the RZX file header. + /// + public RzxHeader Header { get; private set; } = new(); + + /// + /// Gets the program that created this RZX file. + /// + public CreatorBlock? Creator { get; private set; } + + /// + /// Gets a list of snapshots contained in the RZX file. + /// + public List Snapshots { get; private set; } = []; + + /// + /// Loads a RZX file from the given stream. + /// + /// The stream containing the RZX data. + /// The loaded RzxFile object. + public static RzxFile Load(Stream stream) + { + var reader = new ByteStreamReader(stream); + var header = RzxHeader.Read(reader); + + if (header.Signature != "RZX!") + { + throw new InvalidDataException("Not a valid RZX file. Invalid header signature."); + } + + var file = new RzxFile { Header = header }; + + while (BlockHeader.Read(reader) is { } blockHeader) + { + switch (blockHeader.BlockId) + { + case BlockIds.Creator: + file.Creator = CreatorBlock.Read(reader, blockHeader.Size); + break; + + case BlockIds.Snapshot: + file.Snapshots.Add(SnapshotBlock.Read(reader, blockHeader.Size)); + break; + + case BlockIds.Recording: + var recording = RecordingBlock.Read(reader, blockHeader.Size); + file.Snapshots.LastOrDefault()?.Recording = recording; + break; + + default: + // Ignore this block, not supported + reader.ReadBytes((int)blockHeader.Size - 5); + break; + } + } + + return file; + } +} \ No newline at end of file diff --git a/src/Spectron.Files/Rzx/RzxHeader.cs b/src/Spectron.Files/Rzx/RzxHeader.cs new file mode 100644 index 0000000..1a19b2d --- /dev/null +++ b/src/Spectron.Files/Rzx/RzxHeader.cs @@ -0,0 +1,43 @@ +using OldBit.Spectron.Files.Extensions; +using OldBit.Spectron.Files.IO; + +namespace OldBit.Spectron.Files.Rzx; + +/// +/// Represents the RZX file header. +/// +public sealed class RzxHeader +{ + /// + /// RZX file signature. + /// + public string Signature { get; private set; } = "RZX!"; + + /// + /// Major version numbers. + /// + public byte MajorVersion { get; private set; } + + /// + /// Minor version numbers. + /// + public byte MinorVersion { get; private set; } = 13; + + /// + /// Flags. + /// + public DWord Flags { get; private set; } + + internal static RzxHeader Read(ByteStreamReader reader) + { + var header = new RzxHeader(); + + var bytes = reader.ReadBytes(4); + header.Signature = bytes.ToAsciiString(); + header.MajorVersion = reader.ReadByte(); + header.MinorVersion = reader.ReadByte(); + header.Flags = reader.ReadDWord(); + + return header; + } +} \ No newline at end of file diff --git a/src/Spectron.Files/Szx/Blocks/AyBlock.cs b/src/Spectron.Files/Szx/Blocks/AyBlock.cs index bdbc464..0c7ba8b 100644 --- a/src/Spectron.Files/Szx/Blocks/AyBlock.cs +++ b/src/Spectron.Files/Szx/Blocks/AyBlock.cs @@ -1,5 +1,5 @@ +using OldBit.Spectron.Files.Extensions; using OldBit.Spectron.Files.IO; -using OldBit.Spectron.Files.Szx.Extensions; namespace OldBit.Spectron.Files.Szx.Blocks; diff --git a/src/Spectron.Files/Szx/Blocks/BlockHeader.cs b/src/Spectron.Files/Szx/Blocks/BlockHeader.cs index dd98bad..f0584c8 100644 --- a/src/Spectron.Files/Szx/Blocks/BlockHeader.cs +++ b/src/Spectron.Files/Szx/Blocks/BlockHeader.cs @@ -1,5 +1,5 @@ +using OldBit.Spectron.Files.Extensions; using OldBit.Spectron.Files.IO; -using OldBit.Spectron.Files.Szx.Extensions; namespace OldBit.Spectron.Files.Szx.Blocks; diff --git a/src/Spectron.Files/Szx/Blocks/CreatorBlock.cs b/src/Spectron.Files/Szx/Blocks/CreatorBlock.cs index c0c80fd..3f7f558 100644 --- a/src/Spectron.Files/Szx/Blocks/CreatorBlock.cs +++ b/src/Spectron.Files/Szx/Blocks/CreatorBlock.cs @@ -1,5 +1,5 @@ +using OldBit.Spectron.Files.Extensions; using OldBit.Spectron.Files.IO; -using OldBit.Spectron.Files.Szx.Extensions; namespace OldBit.Spectron.Files.Szx.Blocks; diff --git a/src/Spectron.Files/Szx/Blocks/CustomRomBlock.cs b/src/Spectron.Files/Szx/Blocks/CustomRomBlock.cs index 19b8b6d..a89576f 100644 --- a/src/Spectron.Files/Szx/Blocks/CustomRomBlock.cs +++ b/src/Spectron.Files/Szx/Blocks/CustomRomBlock.cs @@ -1,6 +1,6 @@ using System.IO.Compression; +using OldBit.Spectron.Files.Extensions; using OldBit.Spectron.Files.IO; -using OldBit.Spectron.Files.Szx.Extensions; namespace OldBit.Spectron.Files.Szx.Blocks; diff --git a/src/Spectron.Files/Szx/Blocks/JoystickBlock.cs b/src/Spectron.Files/Szx/Blocks/JoystickBlock.cs index 4251d4e..be5cc6e 100644 --- a/src/Spectron.Files/Szx/Blocks/JoystickBlock.cs +++ b/src/Spectron.Files/Szx/Blocks/JoystickBlock.cs @@ -1,5 +1,5 @@ +using OldBit.Spectron.Files.Extensions; using OldBit.Spectron.Files.IO; -using OldBit.Spectron.Files.Szx.Extensions; namespace OldBit.Spectron.Files.Szx.Blocks; diff --git a/src/Spectron.Files/Szx/Blocks/KeyboardBlock.cs b/src/Spectron.Files/Szx/Blocks/KeyboardBlock.cs index ab1ebfd..375fe77 100644 --- a/src/Spectron.Files/Szx/Blocks/KeyboardBlock.cs +++ b/src/Spectron.Files/Szx/Blocks/KeyboardBlock.cs @@ -1,5 +1,5 @@ +using OldBit.Spectron.Files.Extensions; using OldBit.Spectron.Files.IO; -using OldBit.Spectron.Files.Szx.Extensions; namespace OldBit.Spectron.Files.Szx.Blocks; diff --git a/src/Spectron.Files/Szx/Blocks/PaletteBlock.cs b/src/Spectron.Files/Szx/Blocks/PaletteBlock.cs index 6de58f8..a49dfa5 100644 --- a/src/Spectron.Files/Szx/Blocks/PaletteBlock.cs +++ b/src/Spectron.Files/Szx/Blocks/PaletteBlock.cs @@ -1,5 +1,5 @@ +using OldBit.Spectron.Files.Extensions; using OldBit.Spectron.Files.IO; -using OldBit.Spectron.Files.Szx.Extensions; namespace OldBit.Spectron.Files.Szx.Blocks; diff --git a/src/Spectron.Files/Szx/Blocks/RamPageBlock.cs b/src/Spectron.Files/Szx/Blocks/RamPageBlock.cs index 5777d36..4cb6ee7 100644 --- a/src/Spectron.Files/Szx/Blocks/RamPageBlock.cs +++ b/src/Spectron.Files/Szx/Blocks/RamPageBlock.cs @@ -1,6 +1,6 @@ using System.IO.Compression; +using OldBit.Spectron.Files.Extensions; using OldBit.Spectron.Files.IO; -using OldBit.Spectron.Files.Szx.Extensions; namespace OldBit.Spectron.Files.Szx.Blocks; diff --git a/src/Spectron.Files/Szx/Blocks/SpecRegsBlock.cs b/src/Spectron.Files/Szx/Blocks/SpecRegsBlock.cs index ea3249f..264db0b 100644 --- a/src/Spectron.Files/Szx/Blocks/SpecRegsBlock.cs +++ b/src/Spectron.Files/Szx/Blocks/SpecRegsBlock.cs @@ -1,5 +1,5 @@ +using OldBit.Spectron.Files.Extensions; using OldBit.Spectron.Files.IO; -using OldBit.Spectron.Files.Szx.Extensions; namespace OldBit.Spectron.Files.Szx.Blocks; diff --git a/src/Spectron.Files/Szx/Blocks/TapeBlock.cs b/src/Spectron.Files/Szx/Blocks/TapeBlock.cs index 0942d15..5be8d2a 100644 --- a/src/Spectron.Files/Szx/Blocks/TapeBlock.cs +++ b/src/Spectron.Files/Szx/Blocks/TapeBlock.cs @@ -1,6 +1,6 @@ using System.IO.Compression; +using OldBit.Spectron.Files.Extensions; using OldBit.Spectron.Files.IO; -using OldBit.Spectron.Files.Szx.Extensions; namespace OldBit.Spectron.Files.Szx.Blocks; diff --git a/src/Spectron.Files/Szx/Blocks/Z80RegsBlock.cs b/src/Spectron.Files/Szx/Blocks/Z80RegsBlock.cs index 4f3f3a4..9213eb2 100644 --- a/src/Spectron.Files/Szx/Blocks/Z80RegsBlock.cs +++ b/src/Spectron.Files/Szx/Blocks/Z80RegsBlock.cs @@ -1,5 +1,5 @@ +using OldBit.Spectron.Files.Extensions; using OldBit.Spectron.Files.IO; -using OldBit.Spectron.Files.Szx.Extensions; namespace OldBit.Spectron.Files.Szx.Blocks; diff --git a/src/Spectron.Files/Szx/Blocks/ZxPrinterBlock.cs b/src/Spectron.Files/Szx/Blocks/ZxPrinterBlock.cs index f8ba330..d7cc78c 100644 --- a/src/Spectron.Files/Szx/Blocks/ZxPrinterBlock.cs +++ b/src/Spectron.Files/Szx/Blocks/ZxPrinterBlock.cs @@ -1,5 +1,5 @@ +using OldBit.Spectron.Files.Extensions; using OldBit.Spectron.Files.IO; -using OldBit.Spectron.Files.Szx.Extensions; namespace OldBit.Spectron.Files.Szx.Blocks; diff --git a/src/Spectron.Files/Szx/Extensions/StreamExtensions.cs b/src/Spectron.Files/Szx/Extensions/StreamExtensions.cs deleted file mode 100644 index 618babf..0000000 --- a/src/Spectron.Files/Szx/Extensions/StreamExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Text; - -namespace OldBit.Spectron.Files.Szx.Extensions; - -internal static class StreamExtensions -{ - internal static void WriteWord(this Stream stream, int value) - { - stream.WriteByte((byte)value); - stream.WriteByte((byte)(value >> 8)); - } - - internal static void WriteDWord(this Stream stream, DWord value) - { - stream.WriteByte((byte)value); - stream.WriteByte((byte)(value >> 8)); - stream.WriteByte((byte)(value >> 16)); - stream.WriteByte((byte)(value >> 24)); - } - - internal static void WriteBytes(this Stream stream, byte[] value) - { - stream.Write(value, 0, value.Length); - } - - internal static void WriteChars(this Stream stream, string value, int length) - { - var bytes = Encoding.ASCII.GetBytes(value); - stream.Write(bytes, 0, Math.Min(bytes.Length, length)); - - if (bytes.Length < length) - { - stream.Write(new byte[length - bytes.Length], 0, length - bytes.Length); - } - } -} \ No newline at end of file diff --git a/src/Spectron.Files/Szx/SzxHeader.cs b/src/Spectron.Files/Szx/SzxHeader.cs index f0583c2..ac322d0 100644 --- a/src/Spectron.Files/Szx/SzxHeader.cs +++ b/src/Spectron.Files/Szx/SzxHeader.cs @@ -1,5 +1,5 @@ +using OldBit.Spectron.Files.Extensions; using OldBit.Spectron.Files.IO; -using OldBit.Spectron.Files.Szx.Extensions; namespace OldBit.Spectron.Files.Szx; diff --git a/tests/Spectron.Files.Tests/Rzx/RzxFileTests.cs b/tests/Spectron.Files.Tests/Rzx/RzxFileTests.cs new file mode 100644 index 0000000..53afdd1 --- /dev/null +++ b/tests/Spectron.Files.Tests/Rzx/RzxFileTests.cs @@ -0,0 +1,30 @@ +using System.Reflection; +using OldBit.Spectron.Files.Rzx; + +namespace OldBit.Spectron.Files.Tests.Rzx; + +public class RzxFileTests +{ + [Fact] + public void RzxFile_ShouldLoad() + { + using var file = LoadTestFile("manic.rzx"); + var rzxFile = RzxFile.Load(file); + + rzxFile.Creator?.CreatorName.ShouldBe("SPIN"); + rzxFile.Creator?.MajorVersion.ShouldBe(0); + rzxFile.Creator?.MinorVersion.ShouldBe(3); + + rzxFile.Snapshots.ShouldHaveSingleItem(); + rzxFile.Snapshots[0].Extension.ShouldBe("Z80"); + } + + private static FileStream LoadTestFile(string fileName) + { + var location = typeof(RzxFileTests).GetTypeInfo().Assembly.Location; + var dir = Path.GetDirectoryName(location) ?? throw new InvalidOperationException(); + var path = Path.Combine(dir, "TestFiles", fileName); + + return File.OpenRead(path); + } +} \ No newline at end of file diff --git a/tests/Spectron.Files.Tests/Spectron.Files.Tests.csproj b/tests/Spectron.Files.Tests/Spectron.Files.Tests.csproj index 1c20f36..d235e66 100644 --- a/tests/Spectron.Files.Tests/Spectron.Files.Tests.csproj +++ b/tests/Spectron.Files.Tests/Spectron.Files.Tests.csproj @@ -30,13 +30,7 @@ - - Always - - - Always - - + Always From 08b8cca0ef7cd621a922fc7f430df2ca803be7e1 Mon Sep 17 00:00:00 2001 From: voytas Date: Wed, 29 Apr 2026 21:08:27 +0100 Subject: [PATCH 2/3] Add test RZX file --- .../Extensions/BytesExtensions.cs | 2 +- tests/Spectron.Files.Tests/Rzx/RzxFileTests.cs | 13 ++++++++----- tests/Spectron.Files.Tests/TestFiles/test.rzx | Bin 0 -> 33031 bytes 3 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 tests/Spectron.Files.Tests/TestFiles/test.rzx diff --git a/src/Spectron.Files/Extensions/BytesExtensions.cs b/src/Spectron.Files/Extensions/BytesExtensions.cs index 83580e8..602b83c 100644 --- a/src/Spectron.Files/Extensions/BytesExtensions.cs +++ b/src/Spectron.Files/Extensions/BytesExtensions.cs @@ -10,7 +10,7 @@ internal static string ToAsciiString(this IEnumerable bytes) foreach (var b in bytes) { - if (b == 0) + if (b is < 32 or > 126) { continue; } diff --git a/tests/Spectron.Files.Tests/Rzx/RzxFileTests.cs b/tests/Spectron.Files.Tests/Rzx/RzxFileTests.cs index 53afdd1..4a6ff89 100644 --- a/tests/Spectron.Files.Tests/Rzx/RzxFileTests.cs +++ b/tests/Spectron.Files.Tests/Rzx/RzxFileTests.cs @@ -8,15 +8,18 @@ public class RzxFileTests [Fact] public void RzxFile_ShouldLoad() { - using var file = LoadTestFile("manic.rzx"); + using var file = LoadTestFile("test.rzx"); var rzxFile = RzxFile.Load(file); - rzxFile.Creator?.CreatorName.ShouldBe("SPIN"); - rzxFile.Creator?.MajorVersion.ShouldBe(0); - rzxFile.Creator?.MinorVersion.ShouldBe(3); + rzxFile.Creator?.CreatorName.ShouldBe("SpectaculatorD"); + rzxFile.Creator?.MajorVersion.ShouldBe(52); + rzxFile.Creator?.MinorVersion.ShouldBe(371); rzxFile.Snapshots.ShouldHaveSingleItem(); - rzxFile.Snapshots[0].Extension.ShouldBe("Z80"); + rzxFile.Snapshots[0].Extension.ShouldBe("z80"); + + rzxFile.Snapshots[0].Recording.ShouldNotBeNull(); + rzxFile.Snapshots[0].Recording!.Frames.Count.ShouldBe(21381); } private static FileStream LoadTestFile(string fileName) diff --git a/tests/Spectron.Files.Tests/TestFiles/test.rzx b/tests/Spectron.Files.Tests/TestFiles/test.rzx new file mode 100644 index 0000000000000000000000000000000000000000..a35d0086e851dea1172ff39b3983643b0138ec47 GIT binary patch literal 33031 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 Date: Wed, 29 Apr 2026 21:11:50 +0100 Subject: [PATCH 3/3] Fix test --- src/Spectron.Files/Extensions/BytesExtensions.cs | 2 +- tests/Spectron.Files.Tests/Rzx/RzxFileTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Spectron.Files/Extensions/BytesExtensions.cs b/src/Spectron.Files/Extensions/BytesExtensions.cs index 602b83c..72f385d 100644 --- a/src/Spectron.Files/Extensions/BytesExtensions.cs +++ b/src/Spectron.Files/Extensions/BytesExtensions.cs @@ -10,7 +10,7 @@ internal static string ToAsciiString(this IEnumerable bytes) foreach (var b in bytes) { - if (b is < 32 or > 126) + if (b < 32) { continue; } diff --git a/tests/Spectron.Files.Tests/Rzx/RzxFileTests.cs b/tests/Spectron.Files.Tests/Rzx/RzxFileTests.cs index 4a6ff89..a313794 100644 --- a/tests/Spectron.Files.Tests/Rzx/RzxFileTests.cs +++ b/tests/Spectron.Files.Tests/Rzx/RzxFileTests.cs @@ -11,7 +11,7 @@ public void RzxFile_ShouldLoad() using var file = LoadTestFile("test.rzx"); var rzxFile = RzxFile.Load(file); - rzxFile.Creator?.CreatorName.ShouldBe("SpectaculatorD"); + rzxFile.Creator?.CreatorName.ShouldStartWith("Spectaculator"); rzxFile.Creator?.MajorVersion.ShouldBe(52); rzxFile.Creator?.MinorVersion.ShouldBe(371);