diff --git a/Aaru.CommonTypes b/Aaru.CommonTypes index a5fe3e6d9..69b26f6cd 160000 --- a/Aaru.CommonTypes +++ b/Aaru.CommonTypes @@ -1 +1 @@ -Subproject commit a5fe3e6d91240c6287e5b43f512fe1c898b7fec4 +Subproject commit 69b26f6cd54fad1e3e5d9f9facbbd3db781bde77 diff --git a/Aaru.Core/ImageInfo.cs b/Aaru.Core/ImageInfo.cs index c59b78dbd..1124dde25 100644 --- a/Aaru.Core/ImageInfo.cs +++ b/Aaru.Core/ImageInfo.cs @@ -805,6 +805,9 @@ public static class ImageInfo } } + if(imageFormat is IFluxImage fluxImage) + AaruConsole.WriteLine(Localization.Core.Image_flux_captures); + if(imageFormat is not IOpticalMediaImage opticalImage) return; diff --git a/Aaru.Images/SuperCardPro/Constants.cs b/Aaru.Images/SuperCardPro/Constants.cs index 57c223e09..852b9e319 100644 --- a/Aaru.Images/SuperCardPro/Constants.cs +++ b/Aaru.Images/SuperCardPro/Constants.cs @@ -46,4 +46,11 @@ public sealed partial class SuperCardPro { 0x54, 0x52, 0x4B }; + + /// SuperCardPro device default capture resolution: 25 nanoseconds. + const ushort DEFAULT_RESOLUTION = 25000; + /// SuperCardPro format full (with track offsets) header length. + const uint FULL_HEADER_OFFSET = 0x2b0; + /// SuperCardPro format header length (without track offsets). + const byte HEADER_OFFSET = 0x10; } \ No newline at end of file diff --git a/Aaru.Images/SuperCardPro/Enums.cs b/Aaru.Images/SuperCardPro/Enums.cs index 24be535c1..d9419bf97 100644 --- a/Aaru.Images/SuperCardPro/Enums.cs +++ b/Aaru.Images/SuperCardPro/Enums.cs @@ -40,30 +40,37 @@ public sealed partial class SuperCardPro [SuppressMessage("ReSharper", "InconsistentNaming")] public enum ScpDiskType : byte { - Commodore64 = 0x00, CommodoreAmiga = 0x04, AtariFMSS = 0x10, - AtariFMDS = 0x11, AtariFSEx = 0x12, AtariSTSS = 0x14, - AtariSTDS = 0x15, AppleII = 0x20, AppleIIPro = 0x21, - Apple400K = 0x24, Apple800K = 0x25, Apple144 = 0x26, - PC360K = 0x30, PC720K = 0x31, PC12M = 0x32, - PC144M = 0x33, TandySSSD = 0x40, TandySSDD = 0x41, - TandyDSSD = 0x42, TandyDSDD = 0x43, Ti994A = 0x50, - RolandD20 = 0x60 + Commodore64 = 0x00, CommodoreAmiga = 0x04, CommodoreAmigaHD = 0x08, + AtariFMSS = 0x10, AtariFMDS = 0x11, AtariFMEx = 0x12, + AtariSTSS = 0x14, AtariSTDS = 0x15, AppleII = 0x20, + AppleIIPro = 0x21, Apple400K = 0x24, Apple800K = 0x25, + Apple144 = 0x26, PC360K = 0x30, PC720K = 0x31, + PC12M = 0x32, PC144M = 0x33, TandySSSD = 0x40, + TandySSDD = 0x41, TandyDSSD = 0x42, TandyDSDD = 0x43, + Ti994A = 0x50, RolandD20 = 0x60, AmstradCPC = 0x70, + Generic360K = 0x80, Generic12M = 0x81, Generic720K = 0x84, + Generic144M = 0x85, TapeGCR1 = 0xE0, TapeGCR2 = 0xE1, + TapeMFM = 0xE2, HddMFM = 0xF0, HddRLL = 0xF1 } [Flags] public enum ScpFlags : byte { - /// If set flux starts at index pulse - Index = 0x00, - /// If set drive is 96tpi + /// If set, flux starts at index pulse + StartsAtIndex = 0x00, + /// If set, drive is 96tpi, else 48tpi Tpi = 0x02, - /// If set drive is 360rpm + /// If set, drive is 360rpm, else 300rpm Rpm = 0x04, - /// If set image contains normalized data + /// If set, image contains normalized data Normalized = 0x08, - /// If set image is read/write capable + /// If set, image is read/write capable Writable = 0x10, /// If set, image has footer - HasFooter = 0x20 + HasFooter = 0x20, + /// If set, image is not floppy + NotFloppy = 0x40, + /// If set, image was not generated by SuperCard Pro device + CreatedByOtherDevice = 0x80 } } \ No newline at end of file diff --git a/Aaru.Images/SuperCardPro/Helpers.cs b/Aaru.Images/SuperCardPro/Helpers.cs index 9586467bf..fcd2353ee 100644 --- a/Aaru.Images/SuperCardPro/Helpers.cs +++ b/Aaru.Images/SuperCardPro/Helpers.cs @@ -31,6 +31,7 @@ // ****************************************************************************/ using System; +using System.Collections.Generic; using System.IO; using System.Text; using Aaru.Helpers; @@ -58,4 +59,113 @@ public sealed partial class SuperCardPro return Encoding.UTF8.GetString(str); } + + /// + /// Takes a Head, Track and Sub-Track representation and converts it to the Track representation used by SCP. + /// + /// The head number + /// The track number + /// The sub-track number + /// SCP format track number + static long HeadTrackSubToScpTrack(uint head, ushort track, byte subTrack) => + + // TODO: Support single-sided disks + head + (track * 2); + + static byte[] UInt32ToFluxRepresentation(uint ticks) + { + uint over = ticks / 255; + + if(over == 0) + return new[] + { + (byte)ticks + }; + + byte[] expanded = new byte[over + 1]; + + Array.Fill(expanded, (byte)255, 0, (int)over); + expanded[^1] = (byte)(ticks % 255); + + return expanded; + } + + static byte[] UInt16ToFluxRepresentation(ushort ticks) => UInt32ToFluxRepresentation(ticks); + + static List FluxRepresentationsToUInt32List(IEnumerable flux) + { + List scpData = new(); + uint tick = 0; + + foreach(byte b in flux) + if(b == 255) + tick += 255; + else + { + tick += b; + scpData.Add(tick); + tick = 0; + } + + return scpData; + } + + static List FluxRepresentationsToUInt16List(IEnumerable flux, IReadOnlyList indices, + out uint[] trackLengths) + { + List scpData = new(); + ushort tick = 0; + + List revolutionLength = new(); + uint revolutionTicks = 0; + uint revolutionCells = 0; + ushort index = 0; + + foreach(byte b in flux) + { + if(b == 255) + { + tick += 255; + revolutionTicks += 255; + } + else + { + tick += b; + scpData.AddRange(BigEndianBitConverter.GetBytes(tick)); + tick = 0; + + revolutionTicks += b; + + if(revolutionTicks > indices[index] - 1) + { + revolutionLength.Add(revolutionCells); + revolutionTicks = 0; + revolutionCells = 0; + index++; + } + + revolutionCells++; + } + } + + revolutionLength.Add(revolutionCells); + + trackLengths = revolutionLength.ToArray(); + + return scpData; + } + + static uint CalculateChecksum(Stream stream) + { + byte[] wholeFile = new byte[stream.Length]; + uint sum = 0; + + stream.Position = 0; + stream.EnsureRead(wholeFile, 0, wholeFile.Length); + + for(int i = HEADER_OFFSET; i < wholeFile.Length; i++) + sum += wholeFile[i]; + + return sum; + } } \ No newline at end of file diff --git a/Aaru.Images/SuperCardPro/Properties.cs b/Aaru.Images/SuperCardPro/Properties.cs index 50b07eee4..ebf8654d5 100644 --- a/Aaru.Images/SuperCardPro/Properties.cs +++ b/Aaru.Images/SuperCardPro/Properties.cs @@ -32,7 +32,9 @@ using System; using System.Collections.Generic; +using Aaru.CommonTypes; using Aaru.CommonTypes.AaruMetadata; +using Aaru.CommonTypes.Enums; using Aaru.CommonTypes.Structs; namespace Aaru.DiscImages; @@ -53,4 +55,37 @@ public sealed partial class SuperCardPro public List DumpHardware => null; /// public Metadata AaruMetadata => null; + /// + public IEnumerable SupportedMediaTags => null; + /// + public IEnumerable SupportedMediaTypes => new[] + { + // 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, + MediaType.Unknown + }; + /// + public IEnumerable SupportedSectorTags => Array.Empty(); + /// + public IEnumerable<(string name, Type type, string description, object @default)> SupportedOptions => + Array.Empty<(string name, Type type, string description, object @default)>(); + /// + public IEnumerable KnownExtensions => new[] + { + ".scp" + }; + /// + public bool IsWriting { get; private set; } + /// + public string ErrorMessage { get; private set; } + /// + /// SCP can only have one resolution. This is to help avoid changing the resolution and therefore create broken + /// SCP files. + /// + bool IsResolutionSet { get; set; } + /// + /// SCP can only have the same amount of revolutions for all tracks. This is to help avoid changing the number of + /// revolutions and therefore create broken SCP files. + /// + bool IsRevolutionsSet { get; set; } } \ No newline at end of file diff --git a/Aaru.Images/SuperCardPro/Read.cs b/Aaru.Images/SuperCardPro/Read.cs index 9e953e889..2102106d2 100644 --- a/Aaru.Images/SuperCardPro/Read.cs +++ b/Aaru.Images/SuperCardPro/Read.cs @@ -34,6 +34,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Aaru.CommonTypes; using Aaru.CommonTypes.Enums; using Aaru.CommonTypes.Interfaces; using Aaru.Console; @@ -50,6 +51,8 @@ public sealed partial class SuperCardPro _scpStream = imageFilter.GetDataForkStream(); _scpStream.Seek(0, SeekOrigin.Begin); + _scpFilter = imageFilter; + if(_scpStream.Length < Marshal.SizeOf()) return ErrorNumber.InvalidArgument; @@ -68,12 +71,35 @@ public sealed partial class SuperCardPro AaruConsole.DebugWriteLine("SuperCardPro plugin", "header.revolutions = {0}", Header.revolutions); AaruConsole.DebugWriteLine("SuperCardPro plugin", "header.start = {0}", Header.start); AaruConsole.DebugWriteLine("SuperCardPro plugin", "header.end = {0}", Header.end); - AaruConsole.DebugWriteLine("SuperCardPro plugin", "header.flags = {0}", Header.flags); AaruConsole.DebugWriteLine("SuperCardPro plugin", "header.bitCellEncoding = {0}", Header.bitCellEncoding); AaruConsole.DebugWriteLine("SuperCardPro plugin", "header.heads = {0}", Header.heads); - AaruConsole.DebugWriteLine("SuperCardPro plugin", "header.reserved = {0}", Header.reserved); + AaruConsole.DebugWriteLine("SuperCardPro plugin", "header.resolution = {0}", Header.resolution); AaruConsole.DebugWriteLine("SuperCardPro plugin", "header.checksum = 0x{0:X8}", Header.checksum); + AaruConsole.DebugWriteLine("SuperCardPro plugin", "header.flags.StartsAtIndex = {0}", + Header.flags == ScpFlags.StartsAtIndex); + + AaruConsole.DebugWriteLine("SuperCardPro plugin", "header.flags.Tpi = {0}", + Header.flags == ScpFlags.Tpi ? "96tpi" : "48tpi"); + + AaruConsole.DebugWriteLine("SuperCardPro plugin", "header.flags.Rpm = {0}", + Header.flags == ScpFlags.Rpm ? "360rpm" : "300rpm"); + + AaruConsole.DebugWriteLine("SuperCardPro plugin", "header.flags.Normalized = {0}", + Header.flags == ScpFlags.Normalized); + + AaruConsole.DebugWriteLine("SuperCardPro plugin", "header.flags.Writable = {0}", + Header.flags == ScpFlags.Writable); + + AaruConsole.DebugWriteLine("SuperCardPro plugin", "header.flags.HasFooter = {0}", + Header.flags == ScpFlags.HasFooter); + + AaruConsole.DebugWriteLine("SuperCardPro plugin", "header.flags.NotFloppy = {0}", + Header.flags == ScpFlags.NotFloppy); + + AaruConsole.DebugWriteLine("SuperCardPro plugin", "header.flags.CreatedByOtherDevice = {0}", + Header.flags == ScpFlags.CreatedByOtherDevice); + if(!_scpSignature.SequenceEqual(Header.signature)) return ErrorNumber.InvalidArgument; @@ -128,6 +154,161 @@ public sealed partial class SuperCardPro ScpTracks.Add(t, trk); } + _imageInfo.MetadataMediaType = MetadataMediaType.BlockMedia; + + switch(Header.type) + { + case ScpDiskType.Commodore64: + _imageInfo.MediaType = MediaType.CBM_1540; + _imageInfo.Cylinders = (uint)int.Max(((Header.end + 1) / 2), 40); + _imageInfo.Heads = 1; + + break; + case ScpDiskType.CommodoreAmiga: + _imageInfo.MediaType = MediaType.CBM_AMIGA_35_DD; + _imageInfo.Cylinders = (uint)int.Max(((Header.end + 1) / 2), 80); + _imageInfo.Heads = 2; + + break; + case ScpDiskType.CommodoreAmigaHD: + _imageInfo.MediaType = MediaType.CBM_AMIGA_35_HD; + _imageInfo.Cylinders = (uint)int.Max(((Header.end + 1) / 2), 80); + _imageInfo.Heads = 2; + + break; + case ScpDiskType.AtariFMSS: + _imageInfo.MediaType = MediaType.ATARI_525_SD; + _imageInfo.Cylinders = (uint)int.Max(((Header.end + 1) / 2), 40); + _imageInfo.Heads = 1; + + break; + case ScpDiskType.AtariFMDS: + _imageInfo.MediaType = MediaType.Unknown; + _imageInfo.Cylinders = (uint)int.Max(((Header.end + 1) / 2), 40); + _imageInfo.Heads = 2; + + break; + case ScpDiskType.AtariFMEx: break; + case ScpDiskType.AtariSTSS: + _imageInfo.MediaType = MediaType.ATARI_35_SS_DD; + _imageInfo.Cylinders = (uint)int.Max(((Header.end + 1) / 2), 80); + _imageInfo.Heads = 1; + + break; + case ScpDiskType.AtariSTDS: + _imageInfo.MediaType = MediaType.ATARI_35_DS_DD; + _imageInfo.Cylinders = (uint)int.Max(((Header.end + 1) / 2), 80); + _imageInfo.Heads = 2; + + break; + case ScpDiskType.AppleII: + _imageInfo.MediaType = MediaType.Apple32DS; + _imageInfo.Cylinders = (uint)int.Max(((Header.end + 1) / 2), 40); + _imageInfo.Heads = 2; + + break; + case ScpDiskType.AppleIIPro: break; + case ScpDiskType.Apple400K: + _imageInfo.MediaType = MediaType.AppleSonySS; + _imageInfo.Cylinders = (uint)int.Max(((Header.end + 1) / 2), 80); + _imageInfo.Heads = 1; + _imageInfo.SectorsPerTrack = 10; + + break; + case ScpDiskType.Apple800K: + _imageInfo.MediaType = MediaType.AppleSonyDS; + _imageInfo.Cylinders = (uint)int.Max(((Header.end + 1) / 2), 80); + _imageInfo.Heads = 2; + _imageInfo.SectorsPerTrack = 10; + + break; + case ScpDiskType.Apple144: + _imageInfo.MediaType = MediaType.DOS_525_HD; + _imageInfo.Cylinders = (uint)int.Max(((Header.end + 1) / 2), 80); + _imageInfo.Heads = 2; + _imageInfo.SectorsPerTrack = 18; + + break; + case ScpDiskType.PC360K: + _imageInfo.MediaType = MediaType.DOS_525_DS_DD_9; + _imageInfo.Cylinders = (uint)int.Max(((Header.end + 1) / 2), 40); + _imageInfo.Heads = 2; + _imageInfo.SectorsPerTrack = 9; + + break; + case ScpDiskType.PC720K: + _imageInfo.MediaType = MediaType.DOS_35_DS_DD_9; + _imageInfo.Cylinders = (uint)int.Max(((Header.end + 1) / 2), 80); + _imageInfo.Heads = 2; + + break; + case ScpDiskType.PC12M: + _imageInfo.MediaType = MediaType.DOS_525_HD; + _imageInfo.Cylinders = (uint)int.Max(((Header.end + 1) / 2), 80); + _imageInfo.Heads = 2; + + break; + case ScpDiskType.PC144M: + _imageInfo.MediaType = MediaType.DOS_35_HD; + _imageInfo.Cylinders = (uint)int.Max(((Header.end + 1) / 2), 80); + _imageInfo.Heads = 2; + _imageInfo.SectorsPerTrack = 18; + + break; + case ScpDiskType.TandySSSD: break; + case ScpDiskType.TandySSDD: break; + case ScpDiskType.TandyDSSD: break; + case ScpDiskType.TandyDSDD: break; + case ScpDiskType.Ti994A: break; + case ScpDiskType.RolandD20: break; + case ScpDiskType.AmstradCPC: break; + case ScpDiskType.Generic360K: + _imageInfo.MediaType = MediaType.DOS_525_DS_DD_9; + _imageInfo.Cylinders = (uint)int.Max(((Header.end + 1) / 2), 40); + _imageInfo.Heads = 2; + + break; + case ScpDiskType.Generic12M: + _imageInfo.MediaType = MediaType.DOS_525_HD; + _imageInfo.Cylinders = (uint)int.Max(((Header.end + 1) / 2), 80); + _imageInfo.Heads = 2; + + break; + + case ScpDiskType.Generic720K: + _imageInfo.MediaType = MediaType.DOS_35_DS_DD_9; + _imageInfo.Cylinders = (uint)int.Max(((Header.end + 1) / 2), 80); + _imageInfo.Heads = 2; + + break; + case ScpDiskType.Generic144M: + _imageInfo.MediaType = MediaType.DOS_35_HD; + _imageInfo.Cylinders = (uint)int.Max(((Header.end + 1) / 2), 80); + _imageInfo.Heads = 2; + + break; + case ScpDiskType.TapeGCR1: + case ScpDiskType.TapeGCR2: + case ScpDiskType.TapeMFM: + _imageInfo.MediaType = MediaType.UnknownTape; + + break; + case ScpDiskType.HddMFM: + case ScpDiskType.HddRLL: + _imageInfo.MediaType = MediaType.GENERIC_HDD; + + break; + default: + _imageInfo.MediaType = MediaType.Unknown; + + _imageInfo.Cylinders = + (uint)int.Max((Header.end + 1) / 2, Header.flags.HasFlag(ScpFlags.Tpi) ? 80 : 40); + + _imageInfo.Heads = Header.heads == 0 ? 2 : (uint)1; + + break; + } + if(Header.flags.HasFlag(ScpFlags.HasFooter)) { long position = _scpStream.Position; @@ -245,16 +426,14 @@ public sealed partial class SuperCardPro } else { - _imageInfo.Application = "SuperCardPro"; - _imageInfo.ApplicationVersion = $"{(Header.version & 0xF0) >> 4}.{Header.version & 0xF}"; - _imageInfo.CreationTime = imageFilter.CreationTime; + _imageInfo.Application = (Header.flags & ScpFlags.CreatedByOtherDevice) == 0 ? "SuperCardPro" : null; + _imageInfo.ApplicationVersion = $"{(Header.version & 0xF0) >> 4}.{Header.version & 0xF}"; + _imageInfo.CreationTime = imageFilter.CreationTime; _imageInfo.LastModificationTime = imageFilter.LastWriteTime; - _imageInfo.Version = "1.5"; + _imageInfo.Version = "2.4"; } - AaruConsole.ErrorWriteLine(Localization.Flux_decoding_is_not_yet_implemented); - - return ErrorNumber.NotImplemented; + return ErrorNumber.NoError; } /// @@ -299,4 +478,135 @@ public sealed partial class SuperCardPro return ErrorNumber.NotSupported; } + + public ErrorNumber SubTrackLength(uint head, ushort track, out byte length) + { + length = 1; + + return ErrorNumber.NoError; + } + + public ErrorNumber CapturesLength(uint head, ushort track, byte subTrack, out uint length) + { + length = 1; + + return ErrorNumber.NoError; + } + + public ErrorNumber ReadFluxIndexResolution(uint head, ushort track, byte subTrack, uint captureIndex, + out ulong resolution) + { + resolution = (ulong)((Header.resolution + 1) * DEFAULT_RESOLUTION); + + return ErrorNumber.NoError; + } + + public ErrorNumber ReadFluxDataResolution(uint head, ushort track, byte subTrack, uint captureIndex, + out ulong resolution) + { + resolution = (ulong)((Header.resolution + 1) * DEFAULT_RESOLUTION); + + return ErrorNumber.NoError; + } + + public ErrorNumber ReadFluxResolution(uint head, ushort track, byte subTrack, uint captureIndex, + out ulong indexResolution, out ulong dataResolution) + { + indexResolution = dataResolution = 0; + + ErrorNumber indexError = ReadFluxIndexResolution(head, track, subTrack, captureIndex, out indexResolution); + + if(indexError != ErrorNumber.NoError) + return indexError; + + ErrorNumber dataError = ReadFluxDataResolution(head, track, subTrack, captureIndex, out dataResolution); + + return dataError; + } + + public ErrorNumber ReadFluxIndexCapture(uint head, ushort track, byte subTrack, uint captureIndex, + out byte[] buffer) + { + buffer = null; + + if(Header.flags.HasFlag(ScpFlags.NotFloppy)) + return ErrorNumber.NotImplemented; + + if(captureIndex > 0) + return ErrorNumber.OutOfRange; + + List tmpBuffer = new(); + + if(Header.flags.HasFlag(ScpFlags.StartsAtIndex)) + tmpBuffer.Add(0); + + TrackHeader scpTrack = ScpTracks[(byte)HeadTrackSubToScpTrack(head, track, subTrack)]; + + for(int i = 0; i < Header.revolutions; i++) + tmpBuffer.AddRange(UInt32ToFluxRepresentation(scpTrack.Entries[i].indexTime)); + + buffer = tmpBuffer.ToArray(); + + return ErrorNumber.NoError; + } + + public ErrorNumber ReadFluxDataCapture(uint head, ushort track, byte subTrack, uint captureIndex, out byte[] buffer) + { + buffer = null; + + if(Header.flags.HasFlag(ScpFlags.NotFloppy)) + return ErrorNumber.NotImplemented; + + if(HeadTrackSubToScpTrack(head, track, subTrack) > Header.end) + return ErrorNumber.OutOfRange; + + if(captureIndex > 0) + return ErrorNumber.OutOfRange; + + if(Header.bitCellEncoding != 0 && + Header.bitCellEncoding != 16) + return ErrorNumber.NotImplemented; + + TrackHeader scpTrack = ScpTracks[(byte)HeadTrackSubToScpTrack(head, track, subTrack)]; + + Stream stream = _scpFilter.GetDataForkStream(); + var br = new BinaryReader(stream); + + List tmpBuffer = new(); + + for(int i = 0; i < Header.revolutions; i++) + { + 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))); + } + + buffer = tmpBuffer.ToArray(); + + return ErrorNumber.NoError; + } + + public ErrorNumber ReadFluxCapture(uint head, ushort track, byte subTrack, uint captureIndex, + out ulong indexResolution, out ulong dataResolution, out byte[] indexBuffer, + out byte[] dataBuffer) + { + dataBuffer = indexBuffer = null; + + ErrorNumber error = + ReadFluxResolution(head, track, subTrack, captureIndex, out indexResolution, out dataResolution); + + if(error != ErrorNumber.NoError) + return error; + + error = ReadFluxDataCapture(head, track, subTrack, captureIndex, out dataBuffer); + + if(error != ErrorNumber.NoError) + return error; + + ErrorNumber indexCapture = ReadFluxIndexCapture(head, track, subTrack, captureIndex, out indexBuffer); + + return indexCapture; + } } \ No newline at end of file diff --git a/Aaru.Images/SuperCardPro/Structs.cs b/Aaru.Images/SuperCardPro/Structs.cs index 91e04b8b0..686b1ce36 100644 --- a/Aaru.Images/SuperCardPro/Structs.cs +++ b/Aaru.Images/SuperCardPro/Structs.cs @@ -47,10 +47,10 @@ public sealed partial class SuperCardPro public byte start; public byte end; public ScpFlags flags; - public byte bitCellEncoding; - public byte heads; - public byte reserved; - public uint checksum; + public byte bitCellEncoding; // 0 = 16 bit, otherwise value represent bits + public byte heads; // 1 = bottom side, 2 = top side, 0 = both + public byte resolution; // 25ns increments, ergo algorithm is 25ns + resolution * 25ns + public uint checksum; // for data starting at 0x10 [MarshalAs(UnmanagedType.ByValArray, SizeConst = 168)] public uint[] offsets; } diff --git a/Aaru.Images/SuperCardPro/SuperCardPro.cs b/Aaru.Images/SuperCardPro/SuperCardPro.cs index db5c48699..eacdcb806 100644 --- a/Aaru.Images/SuperCardPro/SuperCardPro.cs +++ b/Aaru.Images/SuperCardPro/SuperCardPro.cs @@ -30,6 +30,7 @@ // Copyright © 2011-2023 Natalia Portillo // ****************************************************************************/ +using System; using System.Collections.Generic; using System.IO; using Aaru.CommonTypes.Enums; @@ -40,10 +41,14 @@ namespace Aaru.DiscImages; /// /// Implements reading SuperCardPro flux images -public sealed partial class SuperCardPro : IMediaImage, IVerifiableImage, IVerifiableSectorsImage +public sealed partial class SuperCardPro : IFluxImage, IMediaImage, IWritableImage, IVerifiableImage, + IVerifiableSectorsImage, IWritableFluxImage { - ImageInfo _imageInfo; - Stream _scpStream; + ImageInfo _imageInfo; + Stream _scpStream; + IFilter _scpFilter; + FileStream _writingStream; + uint _trackOffset; // TODO: These variables have been made public so create-sidecar can access to this information until I define an API >4.0 public ScpHeader Header; diff --git a/Aaru.Images/SuperCardPro/Write.cs b/Aaru.Images/SuperCardPro/Write.cs new file mode 100644 index 000000000..8ea23f378 --- /dev/null +++ b/Aaru.Images/SuperCardPro/Write.cs @@ -0,0 +1,254 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Write.cs +// Author(s) : Rebecca Wallander +// +// Component : Disk image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Writes SuperCardPro flux images. +// +// --[ License ] -------------------------------------------------------------- +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2023 Rebecca Wallander +// ****************************************************************************/ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Aaru.CommonTypes; +using Aaru.CommonTypes.AaruMetadata; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Structs; +using Track = Aaru.CommonTypes.AaruMetadata.Track; + +namespace Aaru.DiscImages; + +public sealed partial class SuperCardPro +{ + public bool Create(string path, MediaType mediaType, Dictionary options, ulong sectors, + uint sectorSize) + { + try + { + _writingStream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); + } + catch(IOException e) + { + ErrorMessage = string.Format(Localization.Could_not_create_new_image_file_exception_0, e.Message); + + return false; + } + + _trackOffset = FULL_HEADER_OFFSET; + + IsWriting = true; + ErrorMessage = null; + + Header.signature = _scpSignature; + + Header.type = mediaType switch + { + MediaType.DOS_525_DS_DD_9 => ScpDiskType.PC360K, + MediaType.Unknown => ScpDiskType.Generic720K, + _ => Header.type + }; + + return true; + } + + public bool Close() + { + if(!IsWriting) + { + ErrorMessage = Localization.Image_is_not_opened_for_writing; + + return false; + } + + _writingStream.Seek(0, SeekOrigin.Begin); + + _writingStream.Write(Header.signature, 0, 3); + _writingStream.WriteByte(Header.version); + _writingStream.WriteByte((byte)Header.type); + _writingStream.WriteByte(Header.revolutions); + _writingStream.WriteByte(Header.start); + _writingStream.WriteByte(Header.end); + _writingStream.WriteByte((byte)Header.flags); + _writingStream.WriteByte(Header.bitCellEncoding); + _writingStream.WriteByte(Header.heads); + _writingStream.WriteByte(Header.resolution); + + _writingStream.Seek(0, SeekOrigin.End); + string date = DateTime.Now.ToString("G"); + _writingStream.Write(Encoding.ASCII.GetBytes(date), 0, date.Length); + + Header.checksum = CalculateChecksum(_writingStream); + _writingStream.Seek(0x0C, SeekOrigin.Begin); + _writingStream.Write(BitConverter.GetBytes(Header.checksum), 0, 4); + + _writingStream.Flush(); + _writingStream.Close(); + + IsWriting = false; + ErrorMessage = ""; + + return true; + } + + public bool SetGeometry(uint cylinders, uint heads, uint sectorsPerTrack) => true; + + public bool WriteSectorTag(byte[] data, ulong sectorAddress, SectorTagType tag) => false; + + public bool WriteSectorsTag(byte[] data, ulong sectorAddress, uint length, SectorTagType tag) => false; + + public bool SetDumpHardware(List dumpHardware) => false; + + public bool SetMetadata(Metadata metadata) => false; + + public bool WriteMediaTag(byte[] data, MediaTagType tag) => false; + + public bool WriteSector(byte[] data, ulong sectorAddress) + { + ErrorMessage = Localization.Flux_decoding_is_not_yet_implemented; + + return false; + } + + public bool WriteSectors(byte[] data, ulong sectorAddress, uint length) + { + ErrorMessage = Localization.Flux_decoding_is_not_yet_implemented; + + return false; + } + + /// + public bool WriteSectorLong(byte[] data, ulong sectorAddress) + { + ErrorMessage = Localization.Flux_decoding_is_not_yet_implemented; + + return false; + } + + /// + public bool WriteSectorsLong(byte[] data, ulong sectorAddress, uint length) + { + ErrorMessage = Localization.Flux_decoding_is_not_yet_implemented; + + return false; + } + + public bool SetTracks(List tracks) => false; + + public bool SetImageInfo(ImageInfo imageInfo) + { + string[] version = imageInfo.ApplicationVersion.Split('.'); + + Header.version = (byte)((byte.Parse(version[0]) << 4) + byte.Parse(version[1])); + Header.start = byte.MaxValue; + Header.end = byte.MinValue; + Header.bitCellEncoding = 0; + Header.heads = 0; // TODO: Support single sided disks + + return true; + } + + public ErrorNumber WriteFluxCapture(ulong indexResolution, ulong dataResolution, byte[] indexBuffer, + byte[] dataBuffer, uint head, ushort track, byte subTrack, uint captureIndex) + { + if(!IsWriting) + { + ErrorMessage = Localization.Tried_to_write_on_a_non_writable_image; + + return ErrorNumber.WriteError; + } + + 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); + + ulong scpResolution = (dataResolution / DEFAULT_RESOLUTION) - 1; + + if(!IsResolutionSet) + { + Header.resolution = (byte)scpResolution; + IsResolutionSet = true; + } + + // SCP can only have one resolution for all tracks + if(Header.resolution != scpResolution) + return ErrorNumber.NotSupported; + + long scpTrack = HeadTrackSubToScpTrack(head, track, subTrack); + + _writingStream.Seek(0x10 + (4 * scpTrack), SeekOrigin.Begin); + _writingStream.Write(BitConverter.GetBytes(_trackOffset), 0, 4); + + _writingStream.Seek(_trackOffset, SeekOrigin.Begin); + _writingStream.Write(_trkSignature, 0, 3); + _writingStream.WriteByte((byte)scpTrack); + + List scpIndices = FluxRepresentationsToUInt32List(indexBuffer); + + if(scpIndices[0] == 0) + { + // Stream starts at index + Header.flags |= ScpFlags.StartsAtIndex; + scpIndices.RemoveAt(0); + } + + if(!IsRevolutionsSet) + { + Header.revolutions = (byte)scpIndices.Count; + IsRevolutionsSet = true; + } + + // SCP can only have the same number of revolutions for every tracks + if(Header.revolutions != scpIndices.Count) + return ErrorNumber.NotSupported; + + List scpData = FluxRepresentationsToUInt16List(dataBuffer, scpIndices, out uint[] trackLengths); + + uint offset = (uint)(4 + (12 * Header.revolutions)); + + for(int i = 0; i < Header.revolutions; i++) + { + _writingStream.Write(BitConverter.GetBytes(scpIndices[i]), 0, 4); + _writingStream.Write(BitConverter.GetBytes(trackLengths[i]), 0, 4); + _writingStream.Write(BitConverter.GetBytes(offset), 0, 4); + + offset += trackLengths[i] * 2; + } + + _writingStream.Write(scpData.ToArray(), 0, scpData.Count); + _trackOffset = (uint)_writingStream.Position; + + return ErrorNumber.NoError; + } + + public ErrorNumber WriteFluxIndexCapture(ulong resolution, byte[] index, uint head, ushort track, byte subTrack, + uint captureIndex) => ErrorNumber.NotImplemented; + + public ErrorNumber WriteFluxDataCapture(ulong resolution, byte[] data, uint head, ushort track, byte subTrack, + uint captureIndex) => ErrorNumber.NotImplemented; +} \ No newline at end of file diff --git a/Aaru.Localization/Core.Designer.cs b/Aaru.Localization/Core.Designer.cs index 5b7b28d8f..ff0d8f8d6 100644 --- a/Aaru.Localization/Core.Designer.cs +++ b/Aaru.Localization/Core.Designer.cs @@ -6881,5 +6881,11 @@ namespace Aaru.Localization { return ResourceManager.GetString("Output_format_does_not_support_hidden_tracks", resourceCulture); } } + + public static string Image_flux_captures { + get { + return ResourceManager.GetString("Image_flux_captures", resourceCulture); + } + } } } diff --git a/Aaru.Localization/Core.resx b/Aaru.Localization/Core.resx index 689997f7f..a9b92be4f 100644 --- a/Aaru.Localization/Core.resx +++ b/Aaru.Localization/Core.resx @@ -3502,4 +3502,7 @@ It has no sense to do it, and it will put too much strain on the tape. Output format does not properly support storing hidden tracks, this will end in a loss of data, not continuing... + + This image contains low-level flux captures. + \ No newline at end of file diff --git a/Aaru/Commands/Image/Convert.cs b/Aaru/Commands/Image/Convert.cs index b4ba79c8f..05a763bca 100644 --- a/Aaru/Commands/Image/Convert.cs +++ b/Aaru/Commands/Image/Convert.cs @@ -1388,8 +1388,6 @@ sealed class ConvertImageCommand : Command while(doneSectors < inputFormat.Info.Sectors) { - byte[] sector; - uint sectorsToDo; if(inputFormat.Info.Sectors - doneSectors >= (ulong)count) @@ -1403,7 +1401,8 @@ sealed class ConvertImageCommand : Command bool result; - errno = sectorsToDo == 1 ? inputFormat.ReadSectorTag(doneSectors, tag, out sector) + errno = sectorsToDo == 1 + ? inputFormat.ReadSectorTag(doneSectors, tag, out byte[] sector) : inputFormat.ReadSectorsTag(doneSectors, sectorsToDo, tag, out sector); if(errno == ErrorNumber.NoError) @@ -1447,6 +1446,43 @@ sealed class ConvertImageCommand : Command tagsTask.StopTask(); } + if(inputFormat is IFluxImage inputFlux && + outputFormat is IWritableFluxImage outputFlux) + { + for(ushort track = 0; track < inputFlux.Info.Cylinders; track++) + { + for(uint head = 0; head < inputFlux.Info.Heads; head++) + { + ErrorNumber error = inputFlux.SubTrackLength(head, track, out byte subTrackLen); + + if(error != ErrorNumber.NoError) + continue; + + for(byte subTrackIndex = 0; subTrackIndex < subTrackLen; subTrackIndex++) + { + error = inputFlux.CapturesLength(head, track, subTrackIndex, + out uint capturesLen); + + if(error != ErrorNumber.NoError) + continue; + + for(uint captureIndex = 0; captureIndex < capturesLen; captureIndex++) + { + inputFlux.ReadFluxCapture(head, track, subTrackIndex, captureIndex, + out ulong indexResolution, + out ulong dataResolution, + out byte[] indexBuffer, + out byte[] dataBuffer); + + outputFlux.WriteFluxCapture(indexResolution, dataResolution, + indexBuffer, dataBuffer, head, track, 0, + captureIndex); + } + } + } + } + } + if(inputTape == null || outputTape == null || !inputTape.IsTape)