mirror of
https://github.com/aaru-dps/Aaru.git
synced 2026-04-05 21:44:17 +00:00
Merge pull request #925 from aaru-dps/fakeshemp/scp-refactor
Refactor SCP
This commit is contained in:
@@ -47,4 +47,10 @@ public sealed partial class SuperCardPro
|
||||
readonly byte[] _scpSignature = "SCP"u8.ToArray();
|
||||
/// <summary>SuperCardPro track header signature: "TRK"</summary>
|
||||
readonly byte[] _trkSignature = "TRK"u8.ToArray();
|
||||
|
||||
/// <summary>Per SCP spec: Extended mode TDH table offset (when FLAGS bit 6 is set)</summary>
|
||||
const uint EXTENDED_MODE_TDH_OFFSET = 0x80;
|
||||
|
||||
/// <summary>Per SCP spec: Extended mode reserved bytes size (bytes 0x10-0x7F when FLAGS bit 6 is set)</summary>
|
||||
const uint EXTENDED_MODE_RESERVED_SIZE = 0x70;
|
||||
}
|
||||
@@ -51,6 +51,8 @@ public sealed partial class SuperCardPro
|
||||
AtariFMEx = 0x12,
|
||||
AtariSTSS = 0x14,
|
||||
AtariSTDS = 0x15,
|
||||
AtariSTSSHD = 0x16, // Per SCP spec v2.5: Atari ST SS HD
|
||||
AtariSTDSHD = 0x17, // Per SCP spec v2.5: Atari ST DS HD
|
||||
AppleII = 0x20,
|
||||
AppleIIPro = 0x21,
|
||||
Apple400K = 0x24,
|
||||
@@ -85,21 +87,21 @@ public sealed partial class SuperCardPro
|
||||
[Flags]
|
||||
public enum ScpFlags : byte
|
||||
{
|
||||
/// <summary>If set, flux starts at index pulse</summary>
|
||||
StartsAtIndex = 0x00,
|
||||
/// <summary>If set, drive is 96tpi, else 48tpi</summary>
|
||||
/// <summary>Per SCP spec: If set (bit 0), flux starts at index pulse</summary>
|
||||
StartsAtIndex = 0x01,
|
||||
/// <summary>Per SCP spec: If set (bit 1), drive is 96tpi, else 48tpi</summary>
|
||||
Tpi = 0x02,
|
||||
/// <summary>If set, drive is 360rpm, else 300rpm</summary>
|
||||
/// <summary>Per SCP spec: If set (bit 2), drive is 360rpm, else 300rpm</summary>
|
||||
Rpm = 0x04,
|
||||
/// <summary>If set, image contains normalized data</summary>
|
||||
/// <summary>Per SCP spec: If set (bit 3), image contains normalized data</summary>
|
||||
Normalized = 0x08,
|
||||
/// <summary>If set, image is read/write capable</summary>
|
||||
/// <summary>Per SCP spec: If set (bit 4), image is read/write capable</summary>
|
||||
Writable = 0x10,
|
||||
/// <summary>If set, image has footer</summary>
|
||||
/// <summary>Per SCP spec: If set (bit 5), image has footer</summary>
|
||||
HasFooter = 0x20,
|
||||
/// <summary>If set, image is not floppy</summary>
|
||||
/// <summary>Per SCP spec: If set (bit 6), image is not floppy (extended mode)</summary>
|
||||
NotFloppy = 0x40,
|
||||
/// <summary>If set, image was not generated by SuperCard Pro device</summary>
|
||||
/// <summary>Per SCP spec: If set (bit 7), image was not generated by SuperCard Pro device</summary>
|
||||
CreatedByOtherDevice = 0x80
|
||||
}
|
||||
|
||||
|
||||
@@ -59,17 +59,28 @@ public sealed partial class SuperCardPro
|
||||
|
||||
/// <summary>
|
||||
/// Takes a Head, Track and Sub-Track representation and converts it to the Track representation used by SCP.
|
||||
/// For single-sided disks: side 0 uses even entries (0,2,4...), side 1 uses odd entries (1,3,5...).
|
||||
/// For double-sided disks: standard head + track * 2.
|
||||
/// </summary>
|
||||
/// <param name="head">The head number</param>
|
||||
/// <param name="track">The track number</param>
|
||||
/// <param name="subTrack">The sub-track number</param>
|
||||
/// <param name="heads">Per SCP spec: 0=both heads, 1=side 0 only, 2=side 1 only</param>
|
||||
/// <returns>SCP format track number</returns>
|
||||
static long HeadTrackSubToScpTrack(uint head, ushort track, byte subTrack, byte heads)
|
||||
{
|
||||
// Per SCP spec: For single-sided disks, entries are skipped
|
||||
// Side 0 only: uses even entries (0,2,4,6...)
|
||||
// Side 1 only: uses odd entries (1,3,5,7...)
|
||||
if(heads == 1) // Side 0 only
|
||||
return track * 2;
|
||||
|
||||
// ReSharper disable once UnusedParameter.Local
|
||||
static long HeadTrackSubToScpTrack(uint head, ushort track, byte subTrack) =>
|
||||
if(heads == 2) // Side 1 only
|
||||
return track * 2 + 1;
|
||||
|
||||
// TODO: Support single-sided disks
|
||||
head + track * 2;
|
||||
// Double-sided: standard head + track * 2
|
||||
return head + track * 2;
|
||||
}
|
||||
|
||||
static byte[] UInt32ToFluxRepresentation(uint ticks)
|
||||
{
|
||||
@@ -164,4 +175,134 @@ public sealed partial class SuperCardPro
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads flux data with overflow handling.
|
||||
/// When a bit cell time is 0x0000, it indicates no flux transition for at least 65536*25ns.
|
||||
/// Multiple consecutive 0x0000 entries accumulate (each adds 65536*25ns).
|
||||
/// </summary>
|
||||
static void ReadFluxDataWithOverflow(BinaryReader reader, ulong trackLength, List<byte> output)
|
||||
{
|
||||
for(ulong j = 0; j < trackLength; j++)
|
||||
{
|
||||
ushort rawValue = BigEndianBitConverter.ToUInt16(reader.ReadBytes(2), 0);
|
||||
|
||||
// Per SCP spec: 0x0000 indicates overflow (no flux transition for >= 65536*25ns)
|
||||
// When this occurs, the next bit cell time will be added to 65536 (or more if multiple 0x0000)
|
||||
// e.g., 0x0000, 0x0000, 0x7FFF = 65536 + 65536 + 32767 = 163839
|
||||
if(rawValue == 0)
|
||||
{
|
||||
// Count consecutive 0x0000 entries to accumulate overflow
|
||||
ulong overflowCount = 1;
|
||||
|
||||
// Look ahead to find next non-zero value or end of track
|
||||
while(j + overflowCount < trackLength)
|
||||
{
|
||||
long savedPosition = reader.BaseStream.Position;
|
||||
ushort nextValue = BigEndianBitConverter.ToUInt16(reader.ReadBytes(2), 0);
|
||||
|
||||
if(nextValue == 0)
|
||||
{
|
||||
overflowCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Found non-zero value - restore position so we can read it again
|
||||
reader.BaseStream.Position = savedPosition;
|
||||
|
||||
// Per SCP spec: Next non-zero value is added to accumulated overflow
|
||||
// overflowCount * 65536 + nextValue = total bit cell time
|
||||
nextValue = BigEndianBitConverter.ToUInt16(reader.ReadBytes(2), 0);
|
||||
uint overflowTotal = (uint)(overflowCount * 65536) + nextValue;
|
||||
output.AddRange(UInt32ToFluxRepresentation(overflowTotal));
|
||||
|
||||
// We've consumed overflowCount overflow entries (0x0000) plus 1 next value entry
|
||||
// Total entries consumed: overflowCount + 1
|
||||
// j will be incremented by loop, so advance by (overflowCount + 1 - 1) = overflowCount
|
||||
j += overflowCount;
|
||||
|
||||
goto continueLoop;
|
||||
}
|
||||
}
|
||||
|
||||
// End of track reached while in overflow - output just the overflow
|
||||
// This shouldn't normally happen in valid SCP files, but handle it gracefully
|
||||
uint overflowTotalOnly = (uint)(overflowCount * 65536);
|
||||
output.AddRange(UInt32ToFluxRepresentation(overflowTotalOnly));
|
||||
|
||||
// We've consumed overflowCount entries, j will increment, so advance by overflowCount - 1
|
||||
j += overflowCount - 1;
|
||||
|
||||
if(j + 1 >= trackLength) break;
|
||||
|
||||
continueLoop:
|
||||
continue;
|
||||
}
|
||||
|
||||
output.AddRange(UInt16ToFluxRepresentation(rawValue));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads ASCII timestamp string between track data and footer.
|
||||
/// Timestamp format varies by locale (e.g., "1/05/2014 5:15:21 PM").
|
||||
/// Valid ASCII range is 0x30-0x5F. Returns null if no valid timestamp found.
|
||||
/// </summary>
|
||||
static string ReadTimestamp(Stream stream, long afterTrackDataPosition)
|
||||
{
|
||||
// Per SCP spec: Timestamp appears after track data, before footer
|
||||
// Check if we're at a valid position to read timestamp
|
||||
if(stream.Position < afterTrackDataPosition)
|
||||
stream.Position = afterTrackDataPosition;
|
||||
|
||||
// Per SCP spec: Timestamp is ASCII, valid range 0x30-0x5F
|
||||
// Check if first byte is valid ASCII
|
||||
long savedPosition = stream.Position;
|
||||
|
||||
if(stream.Position >= stream.Length) return null;
|
||||
|
||||
int firstByte = stream.ReadByte();
|
||||
|
||||
if(firstByte < 0) return null;
|
||||
|
||||
// Per SCP spec: Valid ASCII timestamp characters are 0x30-0x5F
|
||||
// But we should allow wider range for actual timestamp strings (printable ASCII)
|
||||
if(firstByte < 0x20 || firstByte > 0x7E)
|
||||
{
|
||||
stream.Position = savedPosition;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
stream.Position = savedPosition;
|
||||
|
||||
// Read until we hit invalid ASCII or null terminator or reasonable length limit
|
||||
var timestampBytes = new List<byte>();
|
||||
int maxLength = 256; // Reasonable max timestamp length
|
||||
|
||||
for(int i = 0; i < maxLength; i++)
|
||||
{
|
||||
if(stream.Position >= stream.Length) break;
|
||||
|
||||
int b = stream.ReadByte();
|
||||
|
||||
if(b < 0) break; // EOF
|
||||
|
||||
if(b == 0) break; // Null terminator
|
||||
|
||||
// Per SCP spec: ASCII characters - allow printable range
|
||||
if(b < 0x20 || b > 0x7E) // Outside printable ASCII
|
||||
{
|
||||
stream.Position--; // Unread this byte
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
timestampBytes.Add((byte)b);
|
||||
}
|
||||
|
||||
if(timestampBytes.Count == 0) return null;
|
||||
|
||||
return Encoding.ASCII.GetString(timestampBytes.ToArray()).Trim();
|
||||
}
|
||||
}
|
||||
@@ -88,8 +88,66 @@ public sealed partial class SuperCardPro
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<MediaType> SupportedMediaTypes =>
|
||||
[
|
||||
// TODO: SCP supports a lot more formats, please add more whence tested.
|
||||
MediaType.DOS_35_DS_DD_9, MediaType.DOS_35_HD, MediaType.DOS_525_DS_DD_9, MediaType.DOS_525_HD,
|
||||
// Apple formats
|
||||
MediaType.Apple32SS, MediaType.Apple32DS, MediaType.Apple33SS, MediaType.Apple33DS,
|
||||
MediaType.AppleSonySS, MediaType.AppleSonyDS, MediaType.AppleFileWare,
|
||||
|
||||
// IBM PC/DOS formats - 5.25"
|
||||
MediaType.DOS_525_SS_DD_8, MediaType.DOS_525_SS_DD_9,
|
||||
MediaType.DOS_525_DS_DD_8, MediaType.DOS_525_DS_DD_9, MediaType.DOS_525_HD,
|
||||
|
||||
// IBM PC/DOS formats - 3.5"
|
||||
MediaType.DOS_35_SS_DD_8, MediaType.DOS_35_SS_DD_9,
|
||||
MediaType.DOS_35_DS_DD_8, MediaType.DOS_35_DS_DD_9, MediaType.DOS_35_HD, MediaType.DOS_35_ED,
|
||||
|
||||
// Microsoft formats
|
||||
MediaType.DMF, MediaType.DMF_82, MediaType.XDF_525, MediaType.XDF_35,
|
||||
|
||||
// Atari formats
|
||||
MediaType.ATARI_525_SD, MediaType.ATARI_525_DD, MediaType.ATARI_525_ED,
|
||||
MediaType.ATARI_35_SS_DD, MediaType.ATARI_35_DS_DD,
|
||||
MediaType.ATARI_35_SS_DD_11, MediaType.ATARI_35_DS_DD_11,
|
||||
|
||||
// Commodore/Amiga formats
|
||||
MediaType.CBM_35_DD, MediaType.CBM_AMIGA_35_DD, MediaType.CBM_AMIGA_35_HD,
|
||||
MediaType.CBM_1540, MediaType.CBM_1540_Ext, MediaType.CBM_1571,
|
||||
|
||||
// NEC/Sharp formats
|
||||
MediaType.NEC_525_SS, MediaType.NEC_525_DS, MediaType.NEC_525_HD,
|
||||
MediaType.NEC_35_HD_8, MediaType.NEC_35_HD_15, MediaType.NEC_35_TD,
|
||||
MediaType.SHARP_525, MediaType.SHARP_525_9, MediaType.SHARP_35, MediaType.SHARP_35_9,
|
||||
|
||||
// 8" formats
|
||||
MediaType.NEC_8_SD, MediaType.NEC_8_DD,
|
||||
MediaType.ECMA_99_8, MediaType.ECMA_69_8,
|
||||
|
||||
// IBM formats
|
||||
MediaType.IBM23FD, MediaType.IBM33FD_128, MediaType.IBM33FD_256, MediaType.IBM33FD_512,
|
||||
MediaType.IBM43FD_128, MediaType.IBM43FD_256,
|
||||
MediaType.IBM53FD_256, MediaType.IBM53FD_512, MediaType.IBM53FD_1024,
|
||||
|
||||
// DEC formats
|
||||
MediaType.RX01, MediaType.RX02, MediaType.RX03, MediaType.RX50,
|
||||
|
||||
// Acorn formats
|
||||
MediaType.ACORN_525_SS_SD_40, MediaType.ACORN_525_SS_SD_80,
|
||||
MediaType.ACORN_525_SS_DD_40, MediaType.ACORN_525_SS_DD_80, MediaType.ACORN_525_DS_DD,
|
||||
MediaType.ACORN_35_DS_DD, MediaType.ACORN_35_DS_HD,
|
||||
|
||||
// ECMA standard formats
|
||||
MediaType.ECMA_54, MediaType.ECMA_59, MediaType.ECMA_66, MediaType.ECMA_69_8,
|
||||
MediaType.ECMA_69_15, MediaType.ECMA_69_26, MediaType.ECMA_70, MediaType.ECMA_78,
|
||||
MediaType.ECMA_78_2, MediaType.ECMA_99_15, MediaType.ECMA_99_26, MediaType.ECMA_100,
|
||||
MediaType.ECMA_125, MediaType.ECMA_147,
|
||||
|
||||
// FDFORMAT formats
|
||||
MediaType.FDFORMAT_525_DD, MediaType.FDFORMAT_525_HD,
|
||||
MediaType.FDFORMAT_35_DD, MediaType.FDFORMAT_35_HD,
|
||||
|
||||
// Other formats
|
||||
MediaType.Apricot_35, MediaType.MetaFloppy_Mod_I, MediaType.MetaFloppy_Mod_II,
|
||||
|
||||
// Unknown/fallback
|
||||
MediaType.Unknown
|
||||
];
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ public sealed partial class SuperCardPro
|
||||
#region IWritableFluxImage Members
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Opens and parses SuperCard Pro flux image file</summary>
|
||||
public ErrorNumber Open(IFilter imageFilter)
|
||||
{
|
||||
Header = new ScpHeader();
|
||||
@@ -56,8 +57,12 @@ public sealed partial class SuperCardPro
|
||||
|
||||
_scpFilter = imageFilter;
|
||||
|
||||
// Per SCP spec: Minimum file size is header size (bytes 0x00-0x0F + TDH offset table)
|
||||
if(_scpStream.Length < Marshal.SizeOf<ScpHeader>()) return ErrorNumber.InvalidArgument;
|
||||
|
||||
// Per SCP spec: Read header starting at byte 0x00
|
||||
// Bytes 0x00-0x02: "SCP" signature
|
||||
// Byte 0x03: Version/revision (Version<<4|Revision)
|
||||
var hdr = new byte[Marshal.SizeOf<ScpHeader>()];
|
||||
_scpStream.EnsureRead(hdr, 0, Marshal.SizeOf<ScpHeader>());
|
||||
|
||||
@@ -76,32 +81,59 @@ public sealed partial class SuperCardPro
|
||||
AaruLogging.Debug(MODULE_NAME, "header.resolution = {0}", Header.resolution);
|
||||
AaruLogging.Debug(MODULE_NAME, "header.checksum = 0x{0:X8}", Header.checksum);
|
||||
|
||||
AaruLogging.Debug(MODULE_NAME, "header.flags.StartsAtIndex = {0}", Header.flags == ScpFlags.StartsAtIndex);
|
||||
// Per SCP spec: FLAGS byte (0x08) bit definitions
|
||||
// Bit 0 (INDEX): cleared = no index reference, set = flux starts at index
|
||||
// Bit 1 (TPI): cleared = 48TPI, set = 96TPI (5.25" drives only)
|
||||
// Bit 2 (RPM): cleared = 300 RPM, set = 360 RPM
|
||||
// Bit 3 (TYPE): cleared = original flux, set = normalized flux
|
||||
// Bit 4 (MODE): cleared = read-only, set = read/write capable
|
||||
// Bit 5 (FOOTER): cleared = no footer, set = footer present
|
||||
// Bit 6 (EXTENDED MODE): cleared = floppy only, set = extended mode (tapes/hard drives)
|
||||
// Bit 7 (FLUX CREATOR): cleared = SuperCard Pro, set = other device
|
||||
AaruLogging.Debug(MODULE_NAME, "header.flags.StartsAtIndex = {0}", Header.flags.HasFlag(ScpFlags.StartsAtIndex));
|
||||
|
||||
AaruLogging.Debug(MODULE_NAME, "header.flags.Tpi = {0}", Header.flags == ScpFlags.Tpi ? "96tpi" : "48tpi");
|
||||
AaruLogging.Debug(MODULE_NAME, "header.flags.Tpi = {0}", Header.flags.HasFlag(ScpFlags.Tpi) ? "96tpi" : "48tpi");
|
||||
|
||||
AaruLogging.Debug(MODULE_NAME, "header.flags.Rpm = {0}", Header.flags == ScpFlags.Rpm ? "360rpm" : "300rpm");
|
||||
AaruLogging.Debug(MODULE_NAME, "header.flags.Rpm = {0}", Header.flags.HasFlag(ScpFlags.Rpm) ? "360rpm" : "300rpm");
|
||||
|
||||
AaruLogging.Debug(MODULE_NAME, "header.flags.Normalized = {0}", Header.flags == ScpFlags.Normalized);
|
||||
AaruLogging.Debug(MODULE_NAME, "header.flags.Normalized = {0}", Header.flags.HasFlag(ScpFlags.Normalized));
|
||||
|
||||
AaruLogging.Debug(MODULE_NAME, "header.flags.Writable = {0}", Header.flags == ScpFlags.Writable);
|
||||
AaruLogging.Debug(MODULE_NAME, "header.flags.Writable = {0}", Header.flags.HasFlag(ScpFlags.Writable));
|
||||
|
||||
AaruLogging.Debug(MODULE_NAME, "header.flags.HasFooter = {0}", Header.flags == ScpFlags.HasFooter);
|
||||
AaruLogging.Debug(MODULE_NAME, "header.flags.HasFooter = {0}", Header.flags.HasFlag(ScpFlags.HasFooter));
|
||||
|
||||
AaruLogging.Debug(MODULE_NAME, "header.flags.NotFloppy = {0}", Header.flags == ScpFlags.NotFloppy);
|
||||
AaruLogging.Debug(MODULE_NAME, "header.flags.NotFloppy = {0}", Header.flags.HasFlag(ScpFlags.NotFloppy));
|
||||
|
||||
AaruLogging.Debug(MODULE_NAME,
|
||||
"header.flags.CreatedByOtherDevice = {0}",
|
||||
Header.flags == ScpFlags.CreatedByOtherDevice);
|
||||
Header.flags.HasFlag(ScpFlags.CreatedByOtherDevice));
|
||||
|
||||
// Per SCP spec: First 3 bytes must be "SCP" signature
|
||||
if(!_scpSignature.SequenceEqual(Header.signature)) return ErrorNumber.InvalidArgument;
|
||||
|
||||
// Extended mode (FLAGS bit 6) indicates non-floppy media (tapes/hard drives)
|
||||
// These remain unimplemented for now
|
||||
if(Header.flags.HasFlag(ScpFlags.NotFloppy)) return ErrorNumber.NotImplemented;
|
||||
|
||||
// Since we only support floppy disks, always set MetadataMediaType to BlockMedia
|
||||
_imageInfo.MetadataMediaType = MetadataMediaType.BlockMedia;
|
||||
|
||||
ScpTracks = new Dictionary<byte, TrackHeader>();
|
||||
|
||||
// Per SCP spec: For single-sided disks, skip TDH entries appropriately
|
||||
// heads = 0: both heads (read all tracks)
|
||||
// heads = 1: side 0 only (read even entries: 0,2,4,6...)
|
||||
// heads = 2: side 1 only (read odd entries: 1,3,5,7...)
|
||||
for(byte t = Header.start; t <= Header.end; t++)
|
||||
{
|
||||
if(t >= Header.offsets.Length) break;
|
||||
|
||||
// Skip entries based on single-sided disk configuration
|
||||
if(Header.heads == 1 && (t % 2) != 0) continue; // Side 0 only - skip odd entries
|
||||
if(Header.heads == 2 && (t % 2) == 0) continue; // Side 1 only - skip even entries
|
||||
|
||||
if(Header.offsets[t] == 0) continue; // Per SCP spec: 0x00000000 means no flux data for this track
|
||||
|
||||
_scpStream.Position = Header.offsets[t];
|
||||
|
||||
var trk = new TrackHeader
|
||||
@@ -110,9 +142,11 @@ public sealed partial class SuperCardPro
|
||||
Entries = new TrackEntry[Header.revolutions]
|
||||
};
|
||||
|
||||
// Per SCP spec: Track Data Header (TDH) starts with "TRK" signature (3 bytes) + track number (1 byte)
|
||||
_scpStream.EnsureRead(trk.Signature, 0, trk.Signature.Length);
|
||||
trk.TrackNumber = (byte)_scpStream.ReadByte();
|
||||
|
||||
// Per SCP spec: Validate TDH signature for recovery from corrupt files
|
||||
if(!trk.Signature.SequenceEqual(_trkSignature))
|
||||
{
|
||||
AaruLogging.Debug(MODULE_NAME,
|
||||
@@ -135,6 +169,8 @@ public sealed partial class SuperCardPro
|
||||
|
||||
AaruLogging.Debug(MODULE_NAME, Localization.Found_track_0_at_1, t, Header.offsets[t]);
|
||||
|
||||
// Per SCP spec: Each revolution has 3 longwords: indexTime, trackLength, dataOffset
|
||||
// dataOffset is relative to start of TDH (not file start)
|
||||
for(byte r = 0; r < Header.revolutions; r++)
|
||||
{
|
||||
var rev = new byte[Marshal.SizeOf<TrackEntry>()];
|
||||
@@ -142,15 +178,13 @@ public sealed partial class SuperCardPro
|
||||
|
||||
trk.Entries[r] = Marshal.ByteArrayToStructureLittleEndian<TrackEntry>(rev);
|
||||
|
||||
// De-relative offsets
|
||||
// Per SCP spec: dataOffset is relative to TDH start, convert to absolute file offset
|
||||
trk.Entries[r].dataOffset += Header.offsets[t];
|
||||
}
|
||||
|
||||
ScpTracks.Add(t, trk);
|
||||
}
|
||||
|
||||
_imageInfo.MetadataMediaType = MetadataMediaType.BlockMedia;
|
||||
|
||||
switch(Header.type)
|
||||
{
|
||||
case ScpDiskType.Commodore64:
|
||||
@@ -184,7 +218,7 @@ public sealed partial class SuperCardPro
|
||||
|
||||
break;
|
||||
case ScpDiskType.AtariFMEx:
|
||||
break;
|
||||
return ErrorNumber.NotImplemented;
|
||||
case ScpDiskType.AtariSTSS:
|
||||
_imageInfo.MediaType = MediaType.ATARI_35_SS_DD;
|
||||
_imageInfo.Cylinders = (uint)int.Max((Header.end + 1) / 2, 80);
|
||||
@@ -197,6 +231,9 @@ public sealed partial class SuperCardPro
|
||||
_imageInfo.Heads = 2;
|
||||
|
||||
break;
|
||||
case ScpDiskType.AtariSTSSHD: // Per SCP spec v2.5: Atari ST SS HD
|
||||
case ScpDiskType.AtariSTDSHD: // Per SCP spec v2.5: Atari ST DS HD
|
||||
return ErrorNumber.NotImplemented;
|
||||
case ScpDiskType.AppleII:
|
||||
_imageInfo.MediaType = MediaType.Apple32DS;
|
||||
_imageInfo.Cylinders = (uint)int.Max((Header.end + 1) / 2, 40);
|
||||
@@ -204,7 +241,7 @@ public sealed partial class SuperCardPro
|
||||
|
||||
break;
|
||||
case ScpDiskType.AppleIIPro:
|
||||
break;
|
||||
return ErrorNumber.NotImplemented;
|
||||
case ScpDiskType.Apple400K:
|
||||
_imageInfo.MediaType = MediaType.AppleSonySS;
|
||||
_imageInfo.Cylinders = (uint)int.Max((Header.end + 1) / 2, 80);
|
||||
@@ -253,15 +290,10 @@ public sealed partial class SuperCardPro
|
||||
|
||||
break;
|
||||
case ScpDiskType.TandySSDD:
|
||||
return ErrorNumber.NotImplemented;
|
||||
case ScpDiskType.TandyDSSD:
|
||||
return ErrorNumber.NotImplemented;
|
||||
case ScpDiskType.TandyDSDD:
|
||||
return ErrorNumber.NotImplemented;
|
||||
case ScpDiskType.Ti994A:
|
||||
return ErrorNumber.NotImplemented;
|
||||
case ScpDiskType.RolandD20:
|
||||
return ErrorNumber.NotImplemented;
|
||||
case ScpDiskType.AmstradCPC:
|
||||
return ErrorNumber.NotImplemented;
|
||||
case ScpDiskType.Generic360K:
|
||||
@@ -292,14 +324,12 @@ public sealed partial class SuperCardPro
|
||||
case ScpDiskType.TapeGCR1:
|
||||
case ScpDiskType.TapeGCR2:
|
||||
case ScpDiskType.TapeMFM:
|
||||
_imageInfo.MediaType = MediaType.UnknownTape;
|
||||
|
||||
break;
|
||||
// Tapes remain unimplemented for now
|
||||
return ErrorNumber.NotImplemented;
|
||||
case ScpDiskType.HddMFM:
|
||||
case ScpDiskType.HddRLL:
|
||||
_imageInfo.MediaType = MediaType.GENERIC_HDD;
|
||||
|
||||
break;
|
||||
// Hard drives remain unimplemented for now
|
||||
return ErrorNumber.NotImplemented;
|
||||
default:
|
||||
_imageInfo.MediaType = MediaType.Unknown;
|
||||
|
||||
@@ -311,9 +341,21 @@ public sealed partial class SuperCardPro
|
||||
break;
|
||||
}
|
||||
|
||||
// Per SCP spec: Timestamp (if present) appears after track data, before footer
|
||||
// Read timestamp if present
|
||||
long lastTrackDataPosition = _scpStream.Position;
|
||||
string timestamp = ReadTimestamp(_scpStream, lastTrackDataPosition);
|
||||
|
||||
if(timestamp != null)
|
||||
{
|
||||
AaruLogging.Debug(MODULE_NAME, "Found timestamp: \"{0}\"", timestamp);
|
||||
// Timestamp is informative only - we use footer timestamps if footer is present
|
||||
}
|
||||
|
||||
// Per SCP spec: Footer detection - look for "FPCS" signature
|
||||
if(Header.flags.HasFlag(ScpFlags.HasFooter))
|
||||
{
|
||||
long position = _scpStream.Position;
|
||||
long position = timestamp != null ? _scpStream.Position : lastTrackDataPosition;
|
||||
_scpStream.Seek(-4, SeekOrigin.End);
|
||||
|
||||
while(_scpStream.Position >= position)
|
||||
@@ -416,7 +458,18 @@ public sealed partial class SuperCardPro
|
||||
_imageInfo.DriveFirmwareRevision =
|
||||
$"{(footer.firmwareVersion & 0xF0) >> 4}.{footer.firmwareVersion & 0xF}";
|
||||
|
||||
_imageInfo.Version = $"{(footer.imageVersion & 0xF0) >> 4}.{footer.imageVersion & 0xF}";
|
||||
// Per SCP spec: When FOOTER bit is set and version byte is 0, use footer version
|
||||
if(Header.version == 0)
|
||||
{
|
||||
_imageInfo.Version = $"{(footer.imageVersion & 0xF0) >> 4}.{footer.imageVersion & 0xF}";
|
||||
AaruLogging.Debug(MODULE_NAME,
|
||||
"Using footer version (header version was 0): {0}",
|
||||
_imageInfo.Version);
|
||||
}
|
||||
else
|
||||
{
|
||||
_imageInfo.Version = $"{(footer.imageVersion & 0xF0) >> 4}.{footer.imageVersion & 0xF}";
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -430,7 +483,6 @@ public sealed partial class SuperCardPro
|
||||
_imageInfo.ApplicationVersion = $"{(Header.version & 0xF0) >> 4}.{Header.version & 0xF}";
|
||||
_imageInfo.CreationTime = imageFilter.CreationTime;
|
||||
_imageInfo.LastModificationTime = imageFilter.LastWriteTime;
|
||||
_imageInfo.Version = "2.4";
|
||||
}
|
||||
|
||||
return ErrorNumber.NoError;
|
||||
@@ -485,16 +537,22 @@ public sealed partial class SuperCardPro
|
||||
{
|
||||
buffer = null;
|
||||
|
||||
// Non-floppy media (tapes/hard drives) not supported
|
||||
if(Header.flags.HasFlag(ScpFlags.NotFloppy)) return ErrorNumber.NotImplemented;
|
||||
|
||||
if(captureIndex > 0) return ErrorNumber.OutOfRange;
|
||||
|
||||
List<byte> tmpBuffer = [];
|
||||
|
||||
// Per SCP spec: If flux starts at index, add initial 0 marker
|
||||
if(Header.flags.HasFlag(ScpFlags.StartsAtIndex)) tmpBuffer.Add(0);
|
||||
|
||||
TrackHeader scpTrack = ScpTracks[(byte)HeadTrackSubToScpTrack(head, track, subTrack)];
|
||||
byte scpTrackNum = (byte)HeadTrackSubToScpTrack(head, track, subTrack, Header.heads);
|
||||
|
||||
if(!ScpTracks.TryGetValue(scpTrackNum, out TrackHeader scpTrack))
|
||||
return ErrorNumber.OutOfRange;
|
||||
|
||||
// Per SCP spec: indexTime is duration in nanoseconds/25ns for one revolution
|
||||
for(var i = 0; i < Header.revolutions; i++)
|
||||
tmpBuffer.AddRange(UInt32ToFluxRepresentation(scpTrack.Entries[i].indexTime));
|
||||
|
||||
@@ -507,15 +565,20 @@ public sealed partial class SuperCardPro
|
||||
{
|
||||
buffer = null;
|
||||
|
||||
// Non-floppy media (tapes/hard drives) not supported
|
||||
if(Header.flags.HasFlag(ScpFlags.NotFloppy)) return ErrorNumber.NotImplemented;
|
||||
|
||||
if(HeadTrackSubToScpTrack(head, track, subTrack) > Header.end) return ErrorNumber.OutOfRange;
|
||||
byte scpTrackNum = (byte)HeadTrackSubToScpTrack(head, track, subTrack, Header.heads);
|
||||
|
||||
if(scpTrackNum > Header.end) return ErrorNumber.OutOfRange;
|
||||
|
||||
if(captureIndex > 0) return ErrorNumber.OutOfRange;
|
||||
|
||||
// Per SCP spec: bitCellEncoding (byte 0x09) = 0 means 16 bits, other values are for future expansion
|
||||
if(Header.bitCellEncoding != 0 && Header.bitCellEncoding != 16) return ErrorNumber.NotImplemented;
|
||||
|
||||
TrackHeader scpTrack = ScpTracks[(byte)HeadTrackSubToScpTrack(head, track, subTrack)];
|
||||
if(!ScpTracks.TryGetValue(scpTrackNum, out TrackHeader scpTrack))
|
||||
return ErrorNumber.OutOfRange;
|
||||
|
||||
Stream stream = _scpFilter.GetDataForkStream();
|
||||
var br = new BinaryReader(stream);
|
||||
@@ -526,9 +589,8 @@ public sealed partial class SuperCardPro
|
||||
{
|
||||
br.BaseStream.Seek(scpTrack.Entries[i].dataOffset, SeekOrigin.Begin);
|
||||
|
||||
// TODO: Check for 0x0000
|
||||
for(ulong j = 0; j < scpTrack.Entries[i].trackLength; j++)
|
||||
tmpBuffer.AddRange(UInt16ToFluxRepresentation(BigEndianBitConverter.ToUInt16(br.ReadBytes(2), 0)));
|
||||
// Per SCP spec: Handle 0x0000 overflow entries in flux data (strongbits protection)
|
||||
ReadFluxDataWithOverflow(br, scpTrack.Entries[i].trackLength, tmpBuffer);
|
||||
}
|
||||
|
||||
buffer = tmpBuffer.ToArray();
|
||||
@@ -569,11 +631,28 @@ public sealed partial class SuperCardPro
|
||||
{
|
||||
byte scpTrack = kvp.Key;
|
||||
|
||||
// Reverse HeadTrackSubToScpTrack: scpTrack = head + track * 2
|
||||
uint head = (uint)(scpTrack % 2);
|
||||
ushort track = (ushort)(scpTrack / 2);
|
||||
// Reverse HeadTrackSubToScpTrack based on heads configuration
|
||||
// Per SCP spec: Single-sided disks use specific entry patterns
|
||||
uint head;
|
||||
ushort track;
|
||||
const byte subTrack = 0; // SuperCardPro always has subTrack = 0
|
||||
|
||||
if(Header.heads == 1) // Side 0 only - even entries
|
||||
{
|
||||
track = (ushort)(scpTrack / 2);
|
||||
head = 0;
|
||||
}
|
||||
else if(Header.heads == 2) // Side 1 only - odd entries
|
||||
{
|
||||
track = (ushort)(scpTrack / 2);
|
||||
head = 1;
|
||||
}
|
||||
else // Double-sided - standard mapping
|
||||
{
|
||||
head = (uint)(scpTrack % 2);
|
||||
track = (ushort)(scpTrack / 2);
|
||||
}
|
||||
|
||||
return new FluxCapture
|
||||
{
|
||||
Head = head,
|
||||
|
||||
@@ -58,6 +58,21 @@ public sealed partial class SuperCardPro
|
||||
|
||||
#endregion
|
||||
|
||||
#region Nested type: ExtendedModeHeader
|
||||
|
||||
/// <summary>
|
||||
/// Per SCP spec: Extended mode header structure (bytes 0x10-0x7F when FLAGS bit 6 is set).
|
||||
/// Reserved for future use with hard drives and tape drives.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
readonly struct ExtendedModeHeader
|
||||
{
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 112)]
|
||||
public readonly byte[] reserved; // 0x70 bytes reserved for extended mode variables
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Nested type: ScpHeader
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
|
||||
@@ -58,8 +58,8 @@ public sealed partial class SuperCardPro
|
||||
|
||||
if(subTrack != 0) return ErrorNumber.NotSupported;
|
||||
|
||||
Header.start = byte.Min((byte)HeadTrackSubToScpTrack(head, track, subTrack), Header.start);
|
||||
Header.end = byte.Max((byte)HeadTrackSubToScpTrack(head, track, subTrack), Header.end);
|
||||
Header.start = byte.Min((byte)HeadTrackSubToScpTrack(head, track, subTrack, Header.heads), Header.start);
|
||||
Header.end = byte.Max((byte)HeadTrackSubToScpTrack(head, track, subTrack, Header.heads), Header.end);
|
||||
|
||||
ulong scpResolution = dataResolution / DEFAULT_RESOLUTION - 1;
|
||||
|
||||
@@ -72,7 +72,7 @@ public sealed partial class SuperCardPro
|
||||
// SCP can only have one resolution for all tracks
|
||||
if(Header.resolution != scpResolution) return ErrorNumber.NotSupported;
|
||||
|
||||
long scpTrack = HeadTrackSubToScpTrack(head, track, subTrack);
|
||||
long scpTrack = HeadTrackSubToScpTrack(head, track, subTrack, Header.heads);
|
||||
|
||||
_writingStream.Seek(0x10 + 4 * scpTrack, SeekOrigin.Begin);
|
||||
_writingStream.Write(BitConverter.GetBytes(_trackOffset), 0, 4);
|
||||
|
||||
Reference in New Issue
Block a user