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..72f385d 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 < 32) + { + 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..a313794 --- /dev/null +++ b/tests/Spectron.Files.Tests/Rzx/RzxFileTests.cs @@ -0,0 +1,33 @@ +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("test.rzx"); + var rzxFile = RzxFile.Load(file); + + rzxFile.Creator?.CreatorName.ShouldStartWith("Spectaculator"); + rzxFile.Creator?.MajorVersion.ShouldBe(52); + rzxFile.Creator?.MinorVersion.ShouldBe(371); + + rzxFile.Snapshots.ShouldHaveSingleItem(); + 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) + { + 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 diff --git a/tests/Spectron.Files.Tests/TestFiles/test.rzx b/tests/Spectron.Files.Tests/TestFiles/test.rzx new file mode 100644 index 0000000..a35d008 Binary files /dev/null and b/tests/Spectron.Files.Tests/TestFiles/test.rzx differ