Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down
7 changes: 7 additions & 0 deletions src/Spectron.Files/Extensions/BytesExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,17 @@ internal static class BytesExtensions
internal static string ToAsciiString(this IEnumerable<byte> bytes)
{
var s = new StringBuilder();

foreach (var b in bytes)
{
if (b < 32)
{
continue;
}

s.Append((char)b);
}

return s.ToString();
}
}
41 changes: 41 additions & 0 deletions src/Spectron.Files/Extensions/StreamExtensions.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
35 changes: 4 additions & 31 deletions src/Spectron.Files/IO/ByteStreamReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,54 +11,27 @@ internal sealed class ByteStreamReader
/// Create a new instance of the byte reader.
/// </summary>
/// <param name="stream">The stream that provides data for the reader.</param>
public ByteStreamReader(Stream stream)
{
_stream = stream;
}
public ByteStreamReader(Stream stream) => _stream = stream;

/// <summary>
/// Reads a byte from the stream and advances the position within the stream by one byte.
/// </summary>
/// <returns>The byte retrieved from the stream.</returns>
/// <exception cref="EndOfStreamException">Thrown when not enough data is in the stream.</exception>
public byte ReadByte()
{
if (!TryReadByte( out var data))
{
throw new EndOfStreamException();
}

return data;
}
public byte ReadByte() => !TryReadByte( out var data) ? throw new EndOfStreamException() : data;

/// <summary>
/// Reads a word from the stream and advances the position within the stream by two bytes.
/// </summary>
/// <returns>The word retrieved from the stream.</returns>
/// <exception cref="EndOfStreamException">Thrown when not enough data is in the stream.</exception>
public Word ReadWord()
{
if (!TryReadWord(out var data))
{
throw new EndOfStreamException();
}

return data;
}
public Word ReadWord() => !TryReadWord(out var data) ? throw new EndOfStreamException() : data;

/// <summary>
/// Reads a dword from the stream and advances the position within the stream by four bytes.
/// </summary>
/// <returns>The dword retrieved from the stream.</returns>
public DWord ReadDWord()
{
if (!TryReadDWord(out var data))
{
throw new EndOfStreamException();
}

return data;
}
public DWord ReadDWord() => !TryReadDWord(out var data) ? throw new EndOfStreamException() : data;

/// <summary>
/// Reads a sequence of bytes from the stream and advances the position within the stream by 'count' bytes.
Expand Down
2 changes: 1 addition & 1 deletion src/Spectron.Files/IO/DataWriter.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
10 changes: 10 additions & 0 deletions src/Spectron.Files/Rzx/BlockIds.cs
Original file line number Diff line number Diff line change
@@ -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;
}
19 changes: 19 additions & 0 deletions src/Spectron.Files/Rzx/Blocks/BlockHeader.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
43 changes: 43 additions & 0 deletions src/Spectron.Files/Rzx/Blocks/CreatorBlock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using OldBit.Spectron.Files.Extensions;
using OldBit.Spectron.Files.IO;

namespace OldBit.Spectron.Files.Rzx.Blocks;

/// <summary>
/// Information about the program which created the RZX.
/// </summary>
public class CreatorBlock
{
/// <summary>
/// Creator's identification string.
/// </summary>≠≠
public string CreatorName { get; private set; } = string.Empty;

/// <summary>
/// Creator's major version number.
/// </summary>
public Word MajorVersion { get; private set; }

/// <summary>
/// Creator's minor version number.
/// </summary>
public Word MinorVersion { get; private set; }

/// <summary>
/// Creator's custom data (may be absent).
/// </summary>
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;
}
}
80 changes: 80 additions & 0 deletions src/Spectron.Files/Rzx/Blocks/RecordingBlock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using OldBit.Spectron.Files.IO;
using OldBit.Spectron.Files.Szx;

namespace OldBit.Spectron.Files.Rzx.Blocks;

/// <summary>
/// Actual input recording data.
/// </summary>
public class RecordingBlock
{
/// <summary>
/// Number of frames in the block.
/// </summary>
public DWord FrameCount { get; private init; }

/// <summary>
/// Reserved.
/// </summary>
public byte Reserved { get; private set; }

/// <summary>
/// T-STATES counter at the beginning.
/// </summary>
public DWord TStatesCounter { get; private set; }

/// <summary>
/// Flags (b0: Protected (frames are encrypted with x-key), b1: Compressed data.)
/// </summary>
public DWord Flags { get; private set; }

public List<RecordingFrame> Frames { get; } = [];

Check warning on line 31 in src/Spectron.Files/Rzx/Blocks/RecordingBlock.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'RecordingBlock.Frames'

Check warning on line 31 in src/Spectron.Files/Rzx/Blocks/RecordingBlock.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'RecordingBlock.Frames'

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);
}
}
}
41 changes: 41 additions & 0 deletions src/Spectron.Files/Rzx/Blocks/RecordingFrame.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using OldBit.Spectron.Files.IO;

namespace OldBit.Spectron.Files.Rzx.Blocks;

/// <summary>
/// Layout of the input log for the frame.
/// </summary>
public class RecordingFrame
{
/// <summary>
/// Fetch counter till next interrupt (i.e. number of R increments, INTA excluded)
/// </summary>
public Word FetchCounter { get; private set; }

/// <summary>
/// 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.
/// </summary>
public Word InCounter { get; private set; }

/// <summary>
/// Return values for the CPU I/O port reads.
/// </summary>
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;
}
}
Loading
Loading