From 77e4332571cf24c73193ddf38c14f2644541d572 Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Tue, 6 Jan 2026 14:37:34 +0100 Subject: [PATCH 1/5] Add preliminary KryoFlux support --- Aaru.Images/KryoFlux/Constants.cs | 8 + Aaru.Images/KryoFlux/Helpers.cs | 219 ++++++++++ Aaru.Images/KryoFlux/KryoFlux.cs | 6 +- Aaru.Images/KryoFlux/Read.cs | 663 ++++++++++++++++++++++++------ Aaru.Images/KryoFlux/Structs.cs | 75 ++++ 5 files changed, 850 insertions(+), 121 deletions(-) create mode 100644 Aaru.Images/KryoFlux/Helpers.cs diff --git a/Aaru.Images/KryoFlux/Constants.cs b/Aaru.Images/KryoFlux/Constants.cs index c90d86d30..618283fae 100644 --- a/Aaru.Images/KryoFlux/Constants.cs +++ b/Aaru.Images/KryoFlux/Constants.cs @@ -47,4 +47,12 @@ public sealed partial class KryoFlux const string KF_HW_RV = "hwrv"; const string KF_SCK = "sck"; const string KF_ICK = "ick"; + + // Per KryoFlux spec: Clock frequencies for current hardware + // mck = Master Clock Frequency = ((18432000 * 73) / 14) / 2 + // sck = Sample Frequency = mck / 2 + // ick = Index Frequency = mck / 16 + const double MCK = (18432000.0 * 73.0 / 14.0) / 2.0; + const double SCK = MCK / 2.0; + const double ICK = MCK / 16.0; } \ No newline at end of file diff --git a/Aaru.Images/KryoFlux/Helpers.cs b/Aaru.Images/KryoFlux/Helpers.cs new file mode 100644 index 000000000..eeb48cc0b --- /dev/null +++ b/Aaru.Images/KryoFlux/Helpers.cs @@ -0,0 +1,219 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Helpers.cs +// Author(s) : Natalia Portillo +// +// Component : Disk image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Contains helpers for KryoFlux STREAM 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-2026 Natalia Portillo +// ****************************************************************************/ + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Aaru.CommonTypes.Enums; +using Aaru.Helpers; + +namespace Aaru.Images; + +[SuppressMessage("ReSharper", "UnusedType.Global")] +public sealed partial class KryoFlux +{ + /// + /// Decodes KryoFlux stream format into cell values (flux transition timings). + /// Per KryoFlux spec: The stream uses encoding markers to efficiently encode flux transition timings. + /// + /// The raw stream data to decode + /// Length of the stream data + /// Output list of decoded cell values (flux transition timings in clock cycles) + /// Error number indicating success or failure + static ErrorNumber DecodeKryoFluxStream(byte[] streamData, int streamLength, out List cellValues) + { + cellValues = new List(); + + if(streamData == null || streamLength <= 0) return ErrorNumber.InvalidArgument; + + int streamPosition = 0; + + while(streamPosition < streamLength) + { + byte encodingMarker = streamData[streamPosition++]; + + if(encodingMarker <= 0x07) + { + // Value: 16-bit value where upper 8 bits are the marker, lower 8 bits are next byte + if(streamPosition >= streamLength) return ErrorNumber.InvalidArgument; + + byte lowerByte = streamData[streamPosition++]; + uint cellValue = (uint)((encodingMarker << 8) | lowerByte); + + cellValues.Add(cellValue); + } + else if(encodingMarker == (byte)BlockIds.Nop1) + { + // Nop1: Skip 1 byte + streamPosition++; + } + else if(encodingMarker == (byte)BlockIds.Nop2) + { + // Nop2: Skip 2 bytes + streamPosition += 2; + } + else if(encodingMarker == (byte)BlockIds.Nop3) + { + // Nop3: Skip 3 bytes + streamPosition += 3; + } + else if(encodingMarker == (byte)BlockIds.Ovl16) + { + // Overflow16: Next cell value is increased by 0x10000 + // Continue decoding at next stream position + // The actual value will be determined by the next encoding marker + if(streamPosition >= streamLength) return ErrorNumber.InvalidArgument; + + byte nextMarker = streamData[streamPosition++]; + uint cellValue = 0x10000; + + if(nextMarker <= 0x07) + { + // Value16 after overflow + if(streamPosition >= streamLength) return ErrorNumber.InvalidArgument; + + byte lowerByte = streamData[streamPosition++]; + cellValue += (uint)((nextMarker << 8) | lowerByte); + } + else if(nextMarker == (byte)BlockIds.Flux3) + { + // Value16: 16-bit value from next 2 bytes + if(streamPosition + 1 >= streamLength) return ErrorNumber.InvalidArgument; + + byte byte1 = streamData[streamPosition++]; + byte byte2 = streamData[streamPosition++]; + cellValue += (uint)((byte2 << 8) | byte1); + } + else if(nextMarker >= 0x0E) + { + // Sample: direct value + cellValue += (uint)(nextMarker - 0x0D); + } + else + { + // Invalid encoding after overflow + return ErrorNumber.InvalidArgument; + } + + cellValues.Add(cellValue); + } + else if(encodingMarker == (byte)BlockIds.Flux3) + { + // Value16: 16-bit value from next 2 bytes + if(streamPosition + 1 >= streamLength) return ErrorNumber.InvalidArgument; + + byte byte1 = streamData[streamPosition++]; + byte byte2 = streamData[streamPosition++]; + uint cellValue = (uint)((byte2 << 8) | byte1); + + cellValues.Add(cellValue); + } + else if(encodingMarker == (byte)BlockIds.Oob) + { + // OOB header: This should be handled by the caller, but if we encounter it here, + // we need to skip past the OOB block. The OOB block structure is: + // byte 0: 0x0D (already read) + // byte 1: OOB type + // bytes 2-3: length (little-endian) + // bytes 4+: data + if(streamPosition + 2 >= streamLength) return ErrorNumber.InvalidArgument; + + byte oobType = streamData[streamPosition++]; + ushort oobLength = (ushort)(streamData[streamPosition] | (streamData[streamPosition + 1] << 8)); + streamPosition += 2; + + // Skip OOB data + streamPosition += oobLength; + } + else if(encodingMarker >= 0x0E) + { + // Sample: direct value (marker - 0x0D) + uint cellValue = (uint)(encodingMarker - 0x0D); + cellValues.Add(cellValue); + } + else + { + // Flux2 variants (0x00-0x07 already handled above) + // These are legacy encodings, treat as regular values + // Actually, 0x00-0x07 are already handled, so this shouldn't happen + // But handle Flux2_1 through Flux2_7 just in case + if(encodingMarker >= (byte)BlockIds.Flux2_1 && encodingMarker <= (byte)BlockIds.Flux2_7) + { + // These are 2-byte values where the marker indicates the high byte + if(streamPosition >= streamLength) return ErrorNumber.InvalidArgument; + + byte lowerByte = streamData[streamPosition++]; + uint cellValue = (uint)((encodingMarker << 8) | lowerByte); + cellValues.Add(cellValue); + } + } + } + + return ErrorNumber.NoError; + } + + /// + /// Converts a uint32 cell value to Aaru's flux representation format. + /// Format: byte array where 255 = overflow, remainder = value + /// + /// The cell value in clock cycles + /// Flux representation as byte array + static byte[] UInt32ToFluxRepresentation(uint ticks) + { + uint over = ticks / 255; + + if(over == 0) return [(byte)ticks]; + + var expanded = new byte[over + 1]; + Array.Fill(expanded, (byte)255, 0, (int)over); + expanded[^1] = (byte)(ticks % 255); + + return expanded; + } + + /// + /// Calculates resolution in picoseconds from sample clock frequency. + /// Resolution = (1 / sck) * 1e12 picoseconds + /// + /// Sample clock frequency in Hz + /// Resolution in picoseconds + static ulong CalculateResolution(double sck) + { + if(sck <= 0) return 0; + + double periodSeconds = 1.0 / sck; + double periodPicoseconds = periodSeconds * 1e12; + + return (ulong)periodPicoseconds; + } +} + diff --git a/Aaru.Images/KryoFlux/KryoFlux.cs b/Aaru.Images/KryoFlux/KryoFlux.cs index 298f51dd0..b66b0a21b 100644 --- a/Aaru.Images/KryoFlux/KryoFlux.cs +++ b/Aaru.Images/KryoFlux/KryoFlux.cs @@ -37,16 +37,18 @@ using Aaru.CommonTypes.Structs; namespace Aaru.Images; -/// +/// /// Implements reading KryoFlux flux images [SuppressMessage("ReSharper", "InconsistentNaming")] -public sealed partial class KryoFlux : IMediaImage, IVerifiableSectorsImage +public sealed partial class KryoFlux : IVerifiableSectorsImage, IFluxImage { const string MODULE_NAME = "KryoFlux plugin"; // TODO: These variables have been made public so create-sidecar can access to this information until I define an API >4.0 public SortedDictionary tracks; + List _trackCaptures; + public KryoFlux() => _imageInfo = new ImageInfo { ReadableSectorTags = [], diff --git a/Aaru.Images/KryoFlux/Read.cs b/Aaru.Images/KryoFlux/Read.cs index 4f2b702bf..72f4730bd 100644 --- a/Aaru.Images/KryoFlux/Read.cs +++ b/Aaru.Images/KryoFlux/Read.cs @@ -37,6 +37,7 @@ using System.IO; using System.Linq; using Aaru.CommonTypes.Enums; using Aaru.CommonTypes.Interfaces; +using Aaru.CommonTypes.Structs; using Aaru.Filters; using Aaru.Helpers; using Aaru.Logging; @@ -76,6 +77,7 @@ public sealed partial class KryoFlux // TODO: This is supposing NoFilter, shouldn't tracks = new SortedDictionary(); + _trackCaptures = []; byte step = 1; byte heads = 2; var topHead = false; @@ -131,124 +133,9 @@ public sealed partial class KryoFlux _imageInfo.CreationTime = DateTime.MaxValue; _imageInfo.LastModificationTime = DateTime.MinValue; - Stream trackStream = trackFilter.GetDataForkStream(); + ErrorNumber processError = ProcessTrackFile(trackfile, (uint)head, (ushort)cylinder, trackFilter); - while(trackStream.Position < trackStream.Length) - { - var blockId = (byte)trackStream.ReadByte(); - - switch(blockId) - { - case (byte)BlockIds.Oob: - { - trackStream.Position--; - - var oob = new byte[Marshal.SizeOf()]; - trackStream.EnsureRead(oob, 0, Marshal.SizeOf()); - - OobBlock oobBlk = Marshal.ByteArrayToStructureLittleEndian(oob); - - if(oobBlk.blockType == OobTypes.EOF) - { - trackStream.Position = trackStream.Length; - - break; - } - - if(oobBlk.blockType != OobTypes.KFInfo) - { - trackStream.Position += oobBlk.length; - - break; - } - - var kfinfo = new byte[oobBlk.length]; - trackStream.EnsureRead(kfinfo, 0, oobBlk.length); - string kfinfoStr = StringHandlers.CToString(kfinfo); - - string[] lines = kfinfoStr.Split([','], StringSplitOptions.RemoveEmptyEntries); - - DateTime blockDate = DateTime.Now; - DateTime blockTime = DateTime.Now; - var foundDate = false; - - foreach(string[] kvp in lines.Select(static line => line.Split('=')) - .Where(static kvp => kvp.Length == 2)) - { - kvp[0] = kvp[0].Trim(); - kvp[1] = kvp[1].Trim(); - AaruLogging.Debug(MODULE_NAME, "\"{0}\" = \"{1}\"", kvp[0], kvp[1]); - - switch(kvp[0]) - { - case HOST_DATE: - if(DateTime.TryParseExact(kvp[1], - "yyyy.MM.dd", - CultureInfo.InvariantCulture, - DateTimeStyles.AssumeLocal, - out blockDate)) - foundDate = true; - - break; - case HOST_TIME: - DateTime.TryParseExact(kvp[1], - "HH:mm:ss", - CultureInfo.InvariantCulture, - DateTimeStyles.AssumeLocal, - out blockTime); - - break; - case KF_NAME: - _imageInfo.Application = kvp[1]; - - break; - case KF_VERSION: - _imageInfo.ApplicationVersion = kvp[1]; - - break; - } - } - - if(foundDate) - { - var blockTimestamp = new DateTime(blockDate.Year, - blockDate.Month, - blockDate.Day, - blockTime.Hour, - blockTime.Minute, - blockTime.Second); - - AaruLogging.Debug(MODULE_NAME, Localization.Found_timestamp_0, blockTimestamp); - - if(blockTimestamp < Info.CreationTime) _imageInfo.CreationTime = blockTimestamp; - - if(blockTimestamp > Info.LastModificationTime) - _imageInfo.LastModificationTime = blockTimestamp; - } - - break; - } - case (byte)BlockIds.Flux2: - case (byte)BlockIds.Flux2_1: - case (byte)BlockIds.Flux2_2: - case (byte)BlockIds.Flux2_3: - case (byte)BlockIds.Flux2_4: - case (byte)BlockIds.Flux2_5: - case (byte)BlockIds.Flux2_6: - case (byte)BlockIds.Flux2_7: - case (byte)BlockIds.Nop2: - trackStream.Position++; - - continue; - case (byte)BlockIds.Nop3: - case (byte)BlockIds.Flux3: - trackStream.Position += 2; - - continue; - default: - continue; - } - } + if(processError != ErrorNumber.NoError) return processError; tracks.Add(t, trackFilter); } @@ -256,9 +143,320 @@ public sealed partial class KryoFlux _imageInfo.Heads = heads; _imageInfo.Cylinders = (uint)(tracks.Count / heads); - AaruLogging.Error(Localization.Flux_decoding_is_not_yet_implemented); + return ErrorNumber.NoError; + } - return ErrorNumber.NotImplemented; + ErrorNumber ProcessTrackFile(string trackfile, uint head, ushort track, IFilter trackFilter) + { + if(!File.Exists(trackfile)) return ErrorNumber.NoSuchFile; + + AaruLogging.Debug(MODULE_NAME, "Processing track file: {0} (head {1}, track {2})", trackfile, head, track); + + Stream trackStream = trackFilter.GetDataForkStream(); + trackStream.Seek(0, SeekOrigin.Begin); + + var fluxPulses = new List(); + var indexPositions = new List(); + double sck = SCK; // Default sample clock frequency + double ick = ICK; // Default index clock frequency + bool inStreamRead = false; + + // Read entire file into memory for processing + var fileData = new byte[trackStream.Length]; + trackStream.EnsureRead(fileData, 0, (int)trackStream.Length); + + int streamPosition = 0; + var streamData = new List(); + + while(streamPosition < fileData.Length) + { + byte blockId = fileData[streamPosition++]; + + switch(blockId) + { + case (byte)BlockIds.Oob: + { + streamPosition--; + + if(streamPosition + Marshal.SizeOf() > fileData.Length) + return ErrorNumber.InvalidArgument; + + var oob = new byte[Marshal.SizeOf()]; + Array.Copy(fileData, streamPosition, oob, 0, Marshal.SizeOf()); + streamPosition += Marshal.SizeOf(); + + OobBlock oobBlk = Marshal.ByteArrayToStructureLittleEndian(oob); + + if(oobBlk.blockType == OobTypes.EOF) + { + // Process any remaining stream data + if(streamData.Count > 0 && inStreamRead) + { + ErrorNumber decodeError = DecodeKryoFluxStream(streamData.ToArray(), streamData.Count, + out List cells); + + if(decodeError != ErrorNumber.NoError) return decodeError; + + fluxPulses.AddRange(cells); + streamData.Clear(); + inStreamRead = false; + } + + streamPosition = fileData.Length; + + break; + } + + if(streamPosition + oobBlk.length > fileData.Length) return ErrorNumber.InvalidArgument; + + switch(oobBlk.blockType) + { + case OobTypes.KFInfo: + { + var kfinfo = new byte[oobBlk.length]; + Array.Copy(fileData, streamPosition, kfinfo, 0, oobBlk.length); + streamPosition += oobBlk.length; + + string kfinfoStr = StringHandlers.CToString(kfinfo); + string[] lines = kfinfoStr.Split([','], StringSplitOptions.RemoveEmptyEntries); + + DateTime blockDate = DateTime.Now; + DateTime blockTime = DateTime.Now; + var foundDate = false; + + foreach(string[] kvp in lines.Select(static line => line.Split('=')) + .Where(static kvp => kvp.Length == 2)) + { + kvp[0] = kvp[0].Trim(); + kvp[1] = kvp[1].Trim(); + AaruLogging.Debug(MODULE_NAME, "\"{0}\" = \"{1}\"", kvp[0], kvp[1]); + + switch(kvp[0]) + { + case HOST_DATE: + if(DateTime.TryParseExact(kvp[1], + "yyyy.MM.dd", + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeLocal, + out blockDate)) + foundDate = true; + + break; + case HOST_TIME: + DateTime.TryParseExact(kvp[1], + "HH:mm:ss", + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeLocal, + out blockTime); + + break; + case KF_NAME: + _imageInfo.Application = kvp[1]; + + break; + case KF_VERSION: + _imageInfo.ApplicationVersion = kvp[1]; + + break; + case KF_SCK: + if(double.TryParse(kvp[1], NumberStyles.Float, CultureInfo.InvariantCulture, + out double parsedSck)) + sck = parsedSck; + + break; + case KF_ICK: + if(double.TryParse(kvp[1], NumberStyles.Float, CultureInfo.InvariantCulture, + out double parsedIck)) + ick = parsedIck; + + break; + } + } + + if(foundDate) + { + var blockTimestamp = new DateTime(blockDate.Year, + blockDate.Month, + blockDate.Day, + blockTime.Hour, + blockTime.Minute, + blockTime.Second); + + AaruLogging.Debug(MODULE_NAME, Localization.Found_timestamp_0, blockTimestamp); + + if(blockTimestamp < Info.CreationTime) _imageInfo.CreationTime = blockTimestamp; + + if(blockTimestamp > Info.LastModificationTime) + _imageInfo.LastModificationTime = blockTimestamp; + } + + break; + } + case OobTypes.StreamInfo: + { + // Process any previous stream data + if(streamData.Count > 0 && inStreamRead) + { + ErrorNumber decodeError = DecodeKryoFluxStream(streamData.ToArray(), streamData.Count, + out List cells); + + if(decodeError != ErrorNumber.NoError) return decodeError; + + fluxPulses.AddRange(cells); + streamData.Clear(); + } + + if(streamPosition + Marshal.SizeOf() > fileData.Length) + return ErrorNumber.InvalidArgument; + + var streamRead = new byte[Marshal.SizeOf()]; + Array.Copy(fileData, streamPosition, streamRead, 0, Marshal.SizeOf()); + streamPosition += Marshal.SizeOf(); + + OobStreamRead oobStreamRead = + Marshal.ByteArrayToStructureLittleEndian(streamRead); + + AaruLogging.Debug(MODULE_NAME, + "Stream Read at position {0}, elapsed time {1} ms", + oobStreamRead.streamPosition, + oobStreamRead.trTime); + + inStreamRead = true; + + break; + } + case OobTypes.Index: + { + if(streamPosition + Marshal.SizeOf() > fileData.Length) + return ErrorNumber.InvalidArgument; + + var index = new byte[Marshal.SizeOf()]; + Array.Copy(fileData, streamPosition, index, 0, Marshal.SizeOf()); + streamPosition += Marshal.SizeOf(); + + OobIndex oobIndex = Marshal.ByteArrayToStructureLittleEndian(index); + + AaruLogging.Debug(MODULE_NAME, + "Index signal at stream position {0}, timer {1}, sysTime {2}", + oobIndex.streamPosition, + oobIndex.timer, + oobIndex.sysTime); + + // Store index position in cell buffer + // The index position is the current number of flux pulses decoded + indexPositions.Add((uint)fluxPulses.Count); + + break; + } + case OobTypes.StreamEnd: + { + if(streamPosition + Marshal.SizeOf() > fileData.Length) + return ErrorNumber.InvalidArgument; + + var streamEnd = new byte[Marshal.SizeOf()]; + Array.Copy(fileData, streamPosition, streamEnd, 0, Marshal.SizeOf()); + streamPosition += Marshal.SizeOf(); + + OobStreamEnd oobStreamEnd = + Marshal.ByteArrayToStructureLittleEndian(streamEnd); + + AaruLogging.Debug(MODULE_NAME, + "Stream End at position {0}, result {1}", + oobStreamEnd.streamPosition, + oobStreamEnd.result); + + // Process any remaining stream data + if(streamData.Count > 0 && inStreamRead) + { + ErrorNumber decodeError = DecodeKryoFluxStream(streamData.ToArray(), streamData.Count, + out List cells); + + if(decodeError != ErrorNumber.NoError) return decodeError; + + fluxPulses.AddRange(cells); + streamData.Clear(); + } + + inStreamRead = false; + + break; + } + default: + // Skip unknown OOB types + streamPosition += oobBlk.length; + + break; + } + + break; + } + default: + // Collect stream data for decoding when in Stream Read block + if(inStreamRead) + { + // Check if this is an OOB marker - if so, decode what we have and handle OOB + if(blockId == (byte)BlockIds.Oob) + { + // Decode collected stream data before handling OOB block + if(streamData.Count > 0) + { + ErrorNumber decodeError = DecodeKryoFluxStream(streamData.ToArray(), streamData.Count, + out List cells); + + if(decodeError != ErrorNumber.NoError) return decodeError; + + fluxPulses.AddRange(cells); + streamData.Clear(); + } + + // Back up to process OOB block + streamPosition--; + } + else + { + streamData.Add(blockId); + } + } + // If not in Stream Read block, skip non-stream data + // streamPosition already incremented + + break; + } + } + + // Process any remaining stream data + if(streamData.Count > 0 && inStreamRead) + { + ErrorNumber decodeError = DecodeKryoFluxStream(streamData.ToArray(), streamData.Count, + out List cells); + + if(decodeError != ErrorNumber.NoError) return decodeError; + + fluxPulses.AddRange(cells); + } + + // Calculate resolution from sample clock frequency + ulong resolution = CalculateResolution(sck); + + AaruLogging.Debug(MODULE_NAME, + "Decoded {0} flux pulses, {1} index signals, resolution {2} ps", + fluxPulses.Count, + indexPositions.Count, + resolution); + + // Create track capture + var capture = new TrackCapture + { + head = head, + track = track, + resolution = resolution, + fluxPulses = fluxPulses.ToArray(), + indexPositions = indexPositions.ToArray() + }; + + _trackCaptures.Add(capture); + + return ErrorNumber.NoError; } /// @@ -319,5 +517,232 @@ public sealed partial class KryoFlux return ErrorNumber.NotImplemented; } +#endregion + +#region IFluxImage Members + + /// + public ErrorNumber CapturesLength(uint head, ushort track, byte subTrack, out uint length) + { + length = 0; + + if(_trackCaptures == null) return ErrorNumber.NotOpened; + + // KryoFlux doesn't support subtracks - only subTrack 0 is valid + if(subTrack != 0) return ErrorNumber.OutOfRange; + + // KryoFlux has one file per track/head, which results in exactly one capture + bool hasCapture = _trackCaptures.Any(c => c.head == head && c.track == track); + + length = hasCapture ? 1u : 0u; + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber ReadFluxIndexResolution(uint head, ushort track, byte subTrack, uint captureIndex, + out ulong resolution) + { + resolution = 0; + + // KryoFlux doesn't support subtracks - only subTrack 0 is valid + if(subTrack != 0) return ErrorNumber.OutOfRange; + + // KryoFlux has one file per track/head, which results in exactly one capture (captureIndex 0) + if(captureIndex != 0) return ErrorNumber.OutOfRange; + + TrackCapture capture = _trackCaptures.Find(c => c.head == head && c.track == track); + + if(capture == null) return ErrorNumber.OutOfRange; + + resolution = capture.resolution; + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber ReadFluxDataResolution(uint head, ushort track, byte subTrack, uint captureIndex, + out ulong resolution) + { + resolution = 0; + + // KryoFlux doesn't support subtracks - only subTrack 0 is valid + if(subTrack != 0) return ErrorNumber.OutOfRange; + + // KryoFlux has one file per track/head, which results in exactly one capture (captureIndex 0) + if(captureIndex != 0) return ErrorNumber.OutOfRange; + + TrackCapture capture = _trackCaptures.Find(c => c.head == head && c.track == track); + + if(capture == null) return ErrorNumber.OutOfRange; + + resolution = capture.resolution; + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber ReadFluxResolution(uint head, ushort track, byte subTrack, uint captureIndex, + out ulong indexResolution, out ulong dataResolution) + { + indexResolution = dataResolution = 0; + + // KryoFlux doesn't support subtracks - only subTrack 0 is valid + if(subTrack != 0) return ErrorNumber.OutOfRange; + + // KryoFlux has one file per track/head, which results in exactly one capture (captureIndex 0) + if(captureIndex != 0) return ErrorNumber.OutOfRange; + + TrackCapture capture = _trackCaptures.Find(c => c.head == head && c.track == track); + + if(capture == null) return ErrorNumber.OutOfRange; + + indexResolution = dataResolution = capture.resolution; + + 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) + { + indexBuffer = dataBuffer = null; + indexResolution = dataResolution = 0; + + // KryoFlux doesn't support subtracks - only subTrack 0 is valid + if(subTrack != 0) return ErrorNumber.OutOfRange; + + // KryoFlux has one file per track/head, which results in exactly one capture (captureIndex 0) + if(captureIndex != 0) return ErrorNumber.OutOfRange; + + 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; + + error = ReadFluxIndexCapture(head, track, subTrack, captureIndex, out indexBuffer); + + return error; + } + + /// + public ErrorNumber ReadFluxIndexCapture(uint head, ushort track, byte subTrack, uint captureIndex, + out byte[] buffer) + { + buffer = null; + + // KryoFlux doesn't support subtracks - only subTrack 0 is valid + if(subTrack != 0) return ErrorNumber.OutOfRange; + + // KryoFlux has one file per track/head, which results in exactly one capture (captureIndex 0) + if(captureIndex != 0) return ErrorNumber.OutOfRange; + + TrackCapture capture = _trackCaptures.Find(c => c.head == head && c.track == track); + + if(capture == null) return ErrorNumber.OutOfRange; + + var tmpBuffer = new List { 0 }; + uint previousPosition = 0; + + foreach(uint indexPos in capture.indexPositions) + { + // Calculate ticks from start to this index position + uint ticks = 0; + for(uint i = previousPosition; i < indexPos && i < capture.fluxPulses.Length; i++) + ticks += capture.fluxPulses[i]; + + uint deltaTicks = ticks; + tmpBuffer.AddRange(UInt32ToFluxRepresentation(deltaTicks)); + previousPosition = indexPos; + } + + buffer = tmpBuffer.ToArray(); + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber + ReadFluxDataCapture(uint head, ushort track, byte subTrack, uint captureIndex, out byte[] buffer) + { + buffer = null; + + // KryoFlux doesn't support subtracks - only subTrack 0 is valid + if(subTrack != 0) return ErrorNumber.OutOfRange; + + // KryoFlux has one file per track/head, which results in exactly one capture (captureIndex 0) + if(captureIndex != 0) return ErrorNumber.OutOfRange; + + TrackCapture capture = _trackCaptures.Find(c => c.head == head && c.track == track); + + if(capture == null) return ErrorNumber.OutOfRange; + + var tmpBuffer = new List(); + + foreach(uint pulse in capture.fluxPulses) tmpBuffer.AddRange(UInt32ToFluxRepresentation(pulse)); + + buffer = tmpBuffer.ToArray(); + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber SubTrackLength(uint head, ushort track, out byte length) + { + length = 0; + + if(_trackCaptures == null) return ErrorNumber.NotOpened; + + // KryoFlux doesn't support subtracks - filenames only contain cylinder and head + // Check if any captures exist for this track + List captures = _trackCaptures.FindAll(c => c.head == head && c.track == track); + + if(captures.Count <= 0) return ErrorNumber.OutOfRange; + + // Always return 1 since KryoFlux doesn't support subtracks + length = 1; + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber GetAllFluxCaptures(out List captures) + { + captures = []; + + if(_trackCaptures is { Count: > 0 }) + { + // Group captures by head/track to assign capture indices + // Note: KryoFlux doesn't support subtracks, so subTrack is always 0 + var grouped = _trackCaptures.GroupBy(c => new { c.head, c.track }) + .ToList(); + + foreach(var group in grouped) + { + uint captureIndex = 0; + + foreach(TrackCapture trackCapture in group) + { + captures.Add(new FluxCapture + { + Head = trackCapture.head, + Track = trackCapture.track, + SubTrack = 0, // KryoFlux doesn't support subtracks + CaptureIndex = captureIndex++, + IndexResolution = trackCapture.resolution, + DataResolution = trackCapture.resolution + }); + } + } + } + + return ErrorNumber.NoError; + } + #endregion } \ No newline at end of file diff --git a/Aaru.Images/KryoFlux/Structs.cs b/Aaru.Images/KryoFlux/Structs.cs index 2556d7f18..70ce4b090 100644 --- a/Aaru.Images/KryoFlux/Structs.cs +++ b/Aaru.Images/KryoFlux/Structs.cs @@ -46,5 +46,80 @@ public sealed partial class KryoFlux public readonly ushort length; } +#endregion + +#region Nested type: OobStreamRead + + /// + /// Per KryoFlux spec: OOB Stream Read block structure (4 bytes after OOB header). + /// Contains stream position and elapsed time since last transfer. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + readonly struct OobStreamRead + { + public readonly uint streamPosition; + public readonly uint trTime; + } + +#endregion + +#region Nested type: OobIndex + + /// + /// Per KryoFlux spec: OOB Index block structure (12 bytes after OOB header). + /// Contains stream position, timer value, and system time when index was detected. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + readonly struct OobIndex + { + public readonly uint streamPosition; + public readonly uint timer; + public readonly uint sysTime; + } + +#endregion + +#region Nested type: OobStreamEnd + + /// + /// Per KryoFlux spec: OOB Stream End block structure (12 bytes after OOB header). + /// Contains stream position and result code. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + readonly struct OobStreamEnd + { + public readonly uint streamPosition; + public readonly ulong result; + } + +#endregion + +#region Nested type: TrackCapture + + /// + /// Internal structure representing a flux capture for a single track. + /// Contains decoded flux pulse data, index signal positions, and resolution information. + /// + public class TrackCapture + { + public uint head; + public ushort track; + /// + /// Resolution (sample rate) of the flux capture in picoseconds. + /// Calculated from KryoFlux clock frequencies (sck). + /// + public ulong resolution; + /// + /// Array of flux pulse intervals in clock cycles. Each value represents the time interval + /// between flux reversals, measured in KryoFlux clock cycles. + /// + public uint[] fluxPulses; + /// + /// Array of index positions. Each value is an index into the fluxPulses array + /// indicating where an index signal occurs. + /// + public uint[] indexPositions; + } + #endregion } \ No newline at end of file From 7da919f601f78d85f2aa4be169332d7da09afe0d Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Fri, 3 Apr 2026 22:59:05 +0200 Subject: [PATCH 2/5] Update Kryoflux --- Aaru.Images/KryoFlux/Helpers.cs | 143 ---------- Aaru.Images/KryoFlux/Read.cs | 451 +++++++++++++++++--------------- Aaru.Images/KryoFlux/Structs.cs | 3 +- 3 files changed, 240 insertions(+), 357 deletions(-) diff --git a/Aaru.Images/KryoFlux/Helpers.cs b/Aaru.Images/KryoFlux/Helpers.cs index eeb48cc0b..008735384 100644 --- a/Aaru.Images/KryoFlux/Helpers.cs +++ b/Aaru.Images/KryoFlux/Helpers.cs @@ -31,156 +31,13 @@ // ****************************************************************************/ using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using Aaru.CommonTypes.Enums; -using Aaru.Helpers; namespace Aaru.Images; [SuppressMessage("ReSharper", "UnusedType.Global")] public sealed partial class KryoFlux { - /// - /// Decodes KryoFlux stream format into cell values (flux transition timings). - /// Per KryoFlux spec: The stream uses encoding markers to efficiently encode flux transition timings. - /// - /// The raw stream data to decode - /// Length of the stream data - /// Output list of decoded cell values (flux transition timings in clock cycles) - /// Error number indicating success or failure - static ErrorNumber DecodeKryoFluxStream(byte[] streamData, int streamLength, out List cellValues) - { - cellValues = new List(); - - if(streamData == null || streamLength <= 0) return ErrorNumber.InvalidArgument; - - int streamPosition = 0; - - while(streamPosition < streamLength) - { - byte encodingMarker = streamData[streamPosition++]; - - if(encodingMarker <= 0x07) - { - // Value: 16-bit value where upper 8 bits are the marker, lower 8 bits are next byte - if(streamPosition >= streamLength) return ErrorNumber.InvalidArgument; - - byte lowerByte = streamData[streamPosition++]; - uint cellValue = (uint)((encodingMarker << 8) | lowerByte); - - cellValues.Add(cellValue); - } - else if(encodingMarker == (byte)BlockIds.Nop1) - { - // Nop1: Skip 1 byte - streamPosition++; - } - else if(encodingMarker == (byte)BlockIds.Nop2) - { - // Nop2: Skip 2 bytes - streamPosition += 2; - } - else if(encodingMarker == (byte)BlockIds.Nop3) - { - // Nop3: Skip 3 bytes - streamPosition += 3; - } - else if(encodingMarker == (byte)BlockIds.Ovl16) - { - // Overflow16: Next cell value is increased by 0x10000 - // Continue decoding at next stream position - // The actual value will be determined by the next encoding marker - if(streamPosition >= streamLength) return ErrorNumber.InvalidArgument; - - byte nextMarker = streamData[streamPosition++]; - uint cellValue = 0x10000; - - if(nextMarker <= 0x07) - { - // Value16 after overflow - if(streamPosition >= streamLength) return ErrorNumber.InvalidArgument; - - byte lowerByte = streamData[streamPosition++]; - cellValue += (uint)((nextMarker << 8) | lowerByte); - } - else if(nextMarker == (byte)BlockIds.Flux3) - { - // Value16: 16-bit value from next 2 bytes - if(streamPosition + 1 >= streamLength) return ErrorNumber.InvalidArgument; - - byte byte1 = streamData[streamPosition++]; - byte byte2 = streamData[streamPosition++]; - cellValue += (uint)((byte2 << 8) | byte1); - } - else if(nextMarker >= 0x0E) - { - // Sample: direct value - cellValue += (uint)(nextMarker - 0x0D); - } - else - { - // Invalid encoding after overflow - return ErrorNumber.InvalidArgument; - } - - cellValues.Add(cellValue); - } - else if(encodingMarker == (byte)BlockIds.Flux3) - { - // Value16: 16-bit value from next 2 bytes - if(streamPosition + 1 >= streamLength) return ErrorNumber.InvalidArgument; - - byte byte1 = streamData[streamPosition++]; - byte byte2 = streamData[streamPosition++]; - uint cellValue = (uint)((byte2 << 8) | byte1); - - cellValues.Add(cellValue); - } - else if(encodingMarker == (byte)BlockIds.Oob) - { - // OOB header: This should be handled by the caller, but if we encounter it here, - // we need to skip past the OOB block. The OOB block structure is: - // byte 0: 0x0D (already read) - // byte 1: OOB type - // bytes 2-3: length (little-endian) - // bytes 4+: data - if(streamPosition + 2 >= streamLength) return ErrorNumber.InvalidArgument; - - byte oobType = streamData[streamPosition++]; - ushort oobLength = (ushort)(streamData[streamPosition] | (streamData[streamPosition + 1] << 8)); - streamPosition += 2; - - // Skip OOB data - streamPosition += oobLength; - } - else if(encodingMarker >= 0x0E) - { - // Sample: direct value (marker - 0x0D) - uint cellValue = (uint)(encodingMarker - 0x0D); - cellValues.Add(cellValue); - } - else - { - // Flux2 variants (0x00-0x07 already handled above) - // These are legacy encodings, treat as regular values - // Actually, 0x00-0x07 are already handled, so this shouldn't happen - // But handle Flux2_1 through Flux2_7 just in case - if(encodingMarker >= (byte)BlockIds.Flux2_1 && encodingMarker <= (byte)BlockIds.Flux2_7) - { - // These are 2-byte values where the marker indicates the high byte - if(streamPosition >= streamLength) return ErrorNumber.InvalidArgument; - - byte lowerByte = streamData[streamPosition++]; - uint cellValue = (uint)((encodingMarker << 8) | lowerByte); - cellValues.Add(cellValue); - } - } - } - - return ErrorNumber.NoError; - } - /// /// Converts a uint32 cell value to Aaru's flux representation format. /// Format: byte array where 255 = overflow, remainder = value diff --git a/Aaru.Images/KryoFlux/Read.cs b/Aaru.Images/KryoFlux/Read.cs index 72f4730bd..8322c991d 100644 --- a/Aaru.Images/KryoFlux/Read.cs +++ b/Aaru.Images/KryoFlux/Read.cs @@ -1,4 +1,4 @@ -// /*************************************************************************** +// /*************************************************************************** // Aaru Data Preservation Suite // ---------------------------------------------------------------------------- // @@ -41,6 +41,7 @@ using Aaru.CommonTypes.Structs; using Aaru.Filters; using Aaru.Helpers; using Aaru.Logging; +using Aaru.CommonTypes; namespace Aaru.Images; @@ -142,6 +143,8 @@ public sealed partial class KryoFlux _imageInfo.Heads = heads; _imageInfo.Cylinders = (uint)(tracks.Count / heads); + // TODO: Find a way to determine the media type from the track data. + _imageInfo.MediaType = MediaType.DOS_35_HD; return ErrorNumber.NoError; } @@ -155,186 +158,165 @@ public sealed partial class KryoFlux Stream trackStream = trackFilter.GetDataForkStream(); trackStream.Seek(0, SeekOrigin.Begin); - var fluxPulses = new List(); - var indexPositions = new List(); - double sck = SCK; // Default sample clock frequency - double ick = ICK; // Default index clock frequency - bool inStreamRead = false; - - // Read entire file into memory for processing - var fileData = new byte[trackStream.Length]; + byte[] fileData = new byte[trackStream.Length]; trackStream.EnsureRead(fileData, 0, (int)trackStream.Length); - int streamPosition = 0; - var streamData = new List(); + int fileSize = fileData.Length; - while(streamPosition < fileData.Length) + uint[] cellValues = new uint[fileSize]; + uint[] cellStreamPositions = new uint[fileSize]; + + uint cellAccumulator = 0; + int streamOfs = 0; + uint streamPos = 0; + int cellPos = 0; + bool oobEnd = false; + + double sck = SCK; + double ick = ICK; + + List<(uint streamPosition, uint timer, uint sysTime)> indexEvents = []; + + int oobHeaderSize = Marshal.SizeOf(); + + while(streamOfs < fileSize && !oobEnd) { - byte blockId = fileData[streamPosition++]; + byte curOp = fileData[streamOfs]; + int curOpLen; - switch(blockId) + if(curOp == (byte)BlockIds.Oob) { - case (byte)BlockIds.Oob: + if(fileSize - streamOfs < oobHeaderSize) + return ErrorNumber.InvalidArgument; + + byte[] oobBytes = new byte[oobHeaderSize]; + Array.Copy(fileData, streamOfs, oobBytes, 0, oobHeaderSize); + OobBlock oobBlk = Marshal.ByteArrayToStructureLittleEndian(oobBytes); + + if(oobBlk.blockType == OobTypes.EOF) { - streamPosition--; + oobEnd = true; - if(streamPosition + Marshal.SizeOf() > fileData.Length) - return ErrorNumber.InvalidArgument; + break; + } - var oob = new byte[Marshal.SizeOf()]; - Array.Copy(fileData, streamPosition, oob, 0, Marshal.SizeOf()); - streamPosition += Marshal.SizeOf(); + curOpLen = oobHeaderSize + oobBlk.length; - OobBlock oobBlk = Marshal.ByteArrayToStructureLittleEndian(oob); + if(fileSize - streamOfs < curOpLen) + return ErrorNumber.InvalidArgument; - if(oobBlk.blockType == OobTypes.EOF) + int oobDataStart = streamOfs + oobHeaderSize; + + switch(oobBlk.blockType) + { + case OobTypes.KFInfo: { - // Process any remaining stream data - if(streamData.Count > 0 && inStreamRead) + byte[] kfinfo = new byte[oobBlk.length]; + Array.Copy(fileData, oobDataStart, kfinfo, 0, oobBlk.length); + + string kfinfoStr = StringHandlers.CToString(kfinfo); + string[] lines = kfinfoStr.Split([','], StringSplitOptions.RemoveEmptyEntries); + + DateTime blockDate = DateTime.Now; + DateTime blockTime = DateTime.Now; + bool foundDate = false; + + foreach(string[] kvp in lines.Select(static line => line.Split('=')) + .Where(static kvp => kvp.Length == 2)) { - ErrorNumber decodeError = DecodeKryoFluxStream(streamData.ToArray(), streamData.Count, - out List cells); + kvp[0] = kvp[0].Trim(); + kvp[1] = kvp[1].Trim(); + AaruLogging.Debug(MODULE_NAME, "\"{0}\" = \"{1}\"", kvp[0], kvp[1]); - if(decodeError != ErrorNumber.NoError) return decodeError; + switch(kvp[0]) + { + case HOST_DATE: + if(DateTime.TryParseExact(kvp[1], + "yyyy.MM.dd", + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeLocal, + out blockDate)) + foundDate = true; - fluxPulses.AddRange(cells); - streamData.Clear(); - inStreamRead = false; + break; + case HOST_TIME: + DateTime.TryParseExact(kvp[1], + "HH:mm:ss", + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeLocal, + out blockTime); + + break; + case KF_NAME: + _imageInfo.Application = kvp[1]; + + break; + case KF_VERSION: + _imageInfo.ApplicationVersion = kvp[1]; + + break; + case KF_SCK: + if(double.TryParse(kvp[1], NumberStyles.Float, CultureInfo.InvariantCulture, + out double parsedSck)) + sck = parsedSck; + + break; + case KF_ICK: + if(double.TryParse(kvp[1], NumberStyles.Float, CultureInfo.InvariantCulture, + out double parsedIck)) + ick = parsedIck; + + break; + } } - streamPosition = fileData.Length; - - break; - } - - if(streamPosition + oobBlk.length > fileData.Length) return ErrorNumber.InvalidArgument; - - switch(oobBlk.blockType) - { - case OobTypes.KFInfo: + if(foundDate) { - var kfinfo = new byte[oobBlk.length]; - Array.Copy(fileData, streamPosition, kfinfo, 0, oobBlk.length); - streamPosition += oobBlk.length; - - string kfinfoStr = StringHandlers.CToString(kfinfo); - string[] lines = kfinfoStr.Split([','], StringSplitOptions.RemoveEmptyEntries); - - DateTime blockDate = DateTime.Now; - DateTime blockTime = DateTime.Now; - var foundDate = false; - - foreach(string[] kvp in lines.Select(static line => line.Split('=')) - .Where(static kvp => kvp.Length == 2)) - { - kvp[0] = kvp[0].Trim(); - kvp[1] = kvp[1].Trim(); - AaruLogging.Debug(MODULE_NAME, "\"{0}\" = \"{1}\"", kvp[0], kvp[1]); - - switch(kvp[0]) - { - case HOST_DATE: - if(DateTime.TryParseExact(kvp[1], - "yyyy.MM.dd", - CultureInfo.InvariantCulture, - DateTimeStyles.AssumeLocal, - out blockDate)) - foundDate = true; - - break; - case HOST_TIME: - DateTime.TryParseExact(kvp[1], - "HH:mm:ss", - CultureInfo.InvariantCulture, - DateTimeStyles.AssumeLocal, - out blockTime); - - break; - case KF_NAME: - _imageInfo.Application = kvp[1]; - - break; - case KF_VERSION: - _imageInfo.ApplicationVersion = kvp[1]; - - break; - case KF_SCK: - if(double.TryParse(kvp[1], NumberStyles.Float, CultureInfo.InvariantCulture, - out double parsedSck)) - sck = parsedSck; - - break; - case KF_ICK: - if(double.TryParse(kvp[1], NumberStyles.Float, CultureInfo.InvariantCulture, - out double parsedIck)) - ick = parsedIck; - - break; - } - } - - if(foundDate) - { - var blockTimestamp = new DateTime(blockDate.Year, + DateTime blockTimestamp = new DateTime(blockDate.Year, blockDate.Month, blockDate.Day, blockTime.Hour, blockTime.Minute, blockTime.Second); - AaruLogging.Debug(MODULE_NAME, Localization.Found_timestamp_0, blockTimestamp); + AaruLogging.Debug(MODULE_NAME, Localization.Found_timestamp_0, blockTimestamp); - if(blockTimestamp < Info.CreationTime) _imageInfo.CreationTime = blockTimestamp; + if(blockTimestamp < Info.CreationTime) _imageInfo.CreationTime = blockTimestamp; - if(blockTimestamp > Info.LastModificationTime) - _imageInfo.LastModificationTime = blockTimestamp; - } - - break; + if(blockTimestamp > Info.LastModificationTime) + _imageInfo.LastModificationTime = blockTimestamp; } - case OobTypes.StreamInfo: + + break; + } + case OobTypes.StreamInfo: + { + if(oobDataStart + Marshal.SizeOf() <= fileSize) { - // Process any previous stream data - if(streamData.Count > 0 && inStreamRead) - { - ErrorNumber decodeError = DecodeKryoFluxStream(streamData.ToArray(), streamData.Count, - out List cells); + byte[] streamReadBytes = new byte[Marshal.SizeOf()]; - if(decodeError != ErrorNumber.NoError) return decodeError; - - fluxPulses.AddRange(cells); - streamData.Clear(); - } - - if(streamPosition + Marshal.SizeOf() > fileData.Length) - return ErrorNumber.InvalidArgument; - - var streamRead = new byte[Marshal.SizeOf()]; - Array.Copy(fileData, streamPosition, streamRead, 0, Marshal.SizeOf()); - streamPosition += Marshal.SizeOf(); + Array.Copy(fileData, oobDataStart, streamReadBytes, 0, + Marshal.SizeOf()); OobStreamRead oobStreamRead = - Marshal.ByteArrayToStructureLittleEndian(streamRead); + Marshal.ByteArrayToStructureLittleEndian(streamReadBytes); AaruLogging.Debug(MODULE_NAME, "Stream Read at position {0}, elapsed time {1} ms", oobStreamRead.streamPosition, oobStreamRead.trTime); - - inStreamRead = true; - - break; } - case OobTypes.Index: + + break; + } + case OobTypes.Index: + { + if(oobDataStart + Marshal.SizeOf() <= fileSize) { - if(streamPosition + Marshal.SizeOf() > fileData.Length) - return ErrorNumber.InvalidArgument; + byte[] indexBytes = new byte[Marshal.SizeOf()]; + Array.Copy(fileData, oobDataStart, indexBytes, 0, Marshal.SizeOf()); - var index = new byte[Marshal.SizeOf()]; - Array.Copy(fileData, streamPosition, index, 0, Marshal.SizeOf()); - streamPosition += Marshal.SizeOf(); - - OobIndex oobIndex = Marshal.ByteArrayToStructureLittleEndian(index); + OobIndex oobIndex = Marshal.ByteArrayToStructureLittleEndian(indexBytes); AaruLogging.Debug(MODULE_NAME, "Index signal at stream position {0}, timer {1}, sysTime {2}", @@ -342,115 +324,158 @@ public sealed partial class KryoFlux oobIndex.timer, oobIndex.sysTime); - // Store index position in cell buffer - // The index position is the current number of flux pulses decoded - indexPositions.Add((uint)fluxPulses.Count); - - break; + indexEvents.Add((oobIndex.streamPosition, oobIndex.timer, oobIndex.sysTime)); } - case OobTypes.StreamEnd: - { - if(streamPosition + Marshal.SizeOf() > fileData.Length) - return ErrorNumber.InvalidArgument; - var streamEnd = new byte[Marshal.SizeOf()]; - Array.Copy(fileData, streamPosition, streamEnd, 0, Marshal.SizeOf()); - streamPosition += Marshal.SizeOf(); + break; + } + case OobTypes.StreamEnd: + { + if(oobDataStart + Marshal.SizeOf() <= fileSize) + { + byte[] streamEndBytes = new byte[Marshal.SizeOf()]; + + Array.Copy(fileData, oobDataStart, streamEndBytes, 0, + Marshal.SizeOf()); OobStreamEnd oobStreamEnd = - Marshal.ByteArrayToStructureLittleEndian(streamEnd); + Marshal.ByteArrayToStructureLittleEndian(streamEndBytes); AaruLogging.Debug(MODULE_NAME, "Stream End at position {0}, result {1}", oobStreamEnd.streamPosition, oobStreamEnd.result); - - // Process any remaining stream data - if(streamData.Count > 0 && inStreamRead) - { - ErrorNumber decodeError = DecodeKryoFluxStream(streamData.ToArray(), streamData.Count, - out List cells); - - if(decodeError != ErrorNumber.NoError) return decodeError; - - fluxPulses.AddRange(cells); - streamData.Clear(); - } - - inStreamRead = false; - - break; } - default: - // Skip unknown OOB types - streamPosition += oobBlk.length; - break; + break; } + } + + streamOfs += curOpLen; + + continue; + } + + // Non-OOB: determine operation length and decode flux data + bool newCell = false; + + switch(curOp) + { + case (byte)BlockIds.Nop1: + curOpLen = 1; + + break; + case (byte)BlockIds.Nop2: + curOpLen = 2; + + break; + case (byte)BlockIds.Nop3: + curOpLen = 3; + + break; + case (byte)BlockIds.Ovl16: + curOpLen = 1; + cellAccumulator += 0x10000; + + break; + case (byte)BlockIds.Flux3: + curOpLen = 3; + cellAccumulator += (uint)((fileData[streamOfs + 1] << 8) | fileData[streamOfs + 2]); + newCell = true; break; - } default: - // Collect stream data for decoding when in Stream Read block - if(inStreamRead) + if(curOp >= 0x0E) { - // Check if this is an OOB marker - if so, decode what we have and handle OOB - if(blockId == (byte)BlockIds.Oob) - { - // Decode collected stream data before handling OOB block - if(streamData.Count > 0) - { - ErrorNumber decodeError = DecodeKryoFluxStream(streamData.ToArray(), streamData.Count, - out List cells); - - if(decodeError != ErrorNumber.NoError) return decodeError; - - fluxPulses.AddRange(cells); - streamData.Clear(); - } - - // Back up to process OOB block - streamPosition--; - } - else - { - streamData.Add(blockId); - } + curOpLen = 1; + cellAccumulator += curOp; + newCell = true; } - // If not in Stream Read block, skip non-stream data - // streamPosition already incremented + else if((curOp & 0xF8) == 0) + { + curOpLen = 2; + cellAccumulator += ((uint)curOp << 8) | fileData[streamOfs + 1]; + newCell = true; + } + else + return ErrorNumber.InvalidArgument; break; } + + if(fileSize - streamOfs < curOpLen) + return ErrorNumber.InvalidArgument; + + if(newCell) + { + cellValues[cellPos] = cellAccumulator; + cellStreamPositions[cellPos] = streamPos; + cellPos++; + cellAccumulator = 0; + } + + streamPos += (uint)curOpLen; + streamOfs += curOpLen; } - // Process any remaining stream data - if(streamData.Count > 0 && inStreamRead) + // Store final partial cell for index resolution boundary + cellValues[cellPos] = cellAccumulator; + cellStreamPositions[cellPos] = streamPos; + + int totalCells = cellPos; + + // Resolve index stream positions to cell positions + List indexPositions = []; + + if(indexEvents.Count > 0) { - ErrorNumber decodeError = DecodeKryoFluxStream(streamData.ToArray(), streamData.Count, - out List cells); + int nextIndex = 0; + uint nextIndexStreamPos = indexEvents[nextIndex].streamPosition; - if(decodeError != ErrorNumber.NoError) return decodeError; + for(int i = 0; i < totalCells; i++) + { + if(nextIndex >= indexEvents.Count) + break; - fluxPulses.AddRange(cells); + int nextCellPos = i + 1; + + if(nextIndexStreamPos <= cellStreamPositions[nextCellPos]) + { + if(i == 0 && cellStreamPositions[0] >= nextIndexStreamPos) + nextCellPos = 0; + + indexPositions.Add((uint)nextCellPos); + + AaruLogging.Debug(MODULE_NAME, + "Index {0} resolved to cell position {1}", + nextIndex, nextCellPos); + + nextIndex++; + + if(nextIndex < indexEvents.Count) + nextIndexStreamPos = indexEvents[nextIndex].streamPosition; + } + } } - // Calculate resolution from sample clock frequency + // Build flux pulses array + uint[] fluxPulses = new uint[totalCells]; + Array.Copy(cellValues, fluxPulses, totalCells); + ulong resolution = CalculateResolution(sck); AaruLogging.Debug(MODULE_NAME, "Decoded {0} flux pulses, {1} index signals, resolution {2} ps", - fluxPulses.Count, + fluxPulses.Length, indexPositions.Count, resolution); - // Create track capture - var capture = new TrackCapture + TrackCapture capture = new TrackCapture { head = head, track = track, resolution = resolution, - fluxPulses = fluxPulses.ToArray(), + fluxPulses = fluxPulses, indexPositions = indexPositions.ToArray() }; diff --git a/Aaru.Images/KryoFlux/Structs.cs b/Aaru.Images/KryoFlux/Structs.cs index 70ce4b090..b2a66bbc8 100644 --- a/Aaru.Images/KryoFlux/Structs.cs +++ b/Aaru.Images/KryoFlux/Structs.cs @@ -89,7 +89,8 @@ public sealed partial class KryoFlux readonly struct OobStreamEnd { public readonly uint streamPosition; - public readonly ulong result; + // The specification says it's a ulong, but the actual data is a uint + public readonly uint result; } #endregion From 3a6c759bc8f5b931083919e015fde184eeaa697d Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Sat, 4 Apr 2026 19:19:45 +0200 Subject: [PATCH 3/5] [Kryoflux] Data does not have to be index aligned --- Aaru.Images/KryoFlux/Read.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Aaru.Images/KryoFlux/Read.cs b/Aaru.Images/KryoFlux/Read.cs index 8322c991d..daf2cdcb0 100644 --- a/Aaru.Images/KryoFlux/Read.cs +++ b/Aaru.Images/KryoFlux/Read.cs @@ -671,7 +671,7 @@ public sealed partial class KryoFlux if(capture == null) return ErrorNumber.OutOfRange; - var tmpBuffer = new List { 0 }; + var tmpBuffer = new List(); uint previousPosition = 0; foreach(uint indexPos in capture.indexPositions) From a099b2dbd3502657233ad42ed47beb0fd6a49427 Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Sun, 5 Apr 2026 10:36:27 +0200 Subject: [PATCH 4/5] remove unused variables --- Aaru.Images/KryoFlux/Read.cs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Aaru.Images/KryoFlux/Read.cs b/Aaru.Images/KryoFlux/Read.cs index daf2cdcb0..2de5923df 100644 --- a/Aaru.Images/KryoFlux/Read.cs +++ b/Aaru.Images/KryoFlux/Read.cs @@ -170,16 +170,17 @@ public sealed partial class KryoFlux int streamOfs = 0; uint streamPos = 0; int cellPos = 0; - bool oobEnd = false; + // Sample Frequency double sck = SCK; + // Index Frequency double ick = ICK; List<(uint streamPosition, uint timer, uint sysTime)> indexEvents = []; int oobHeaderSize = Marshal.SizeOf(); - while(streamOfs < fileSize && !oobEnd) + while(streamOfs < fileSize) { byte curOp = fileData[streamOfs]; int curOpLen; @@ -194,11 +195,7 @@ public sealed partial class KryoFlux OobBlock oobBlk = Marshal.ByteArrayToStructureLittleEndian(oobBytes); if(oobBlk.blockType == OobTypes.EOF) - { - oobEnd = true; - break; - } curOpLen = oobHeaderSize + oobBlk.length; @@ -262,9 +259,10 @@ public sealed partial class KryoFlux break; case KF_ICK: - if(double.TryParse(kvp[1], NumberStyles.Float, CultureInfo.InvariantCulture, - out double parsedIck)) - ick = parsedIck; + // Parse KF_ICK for validation purposes; parsed value is not currently used. + // We use sample frequency space instead. + _ = double.TryParse(kvp[1], NumberStyles.Float, CultureInfo.InvariantCulture, + out _); break; } From d6939712b60f6fa565dd64d6850261a459a508ce Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Sun, 5 Apr 2026 11:37:11 +0200 Subject: [PATCH 5/5] Remove ICK Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- Aaru.Images/KryoFlux/Read.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Aaru.Images/KryoFlux/Read.cs b/Aaru.Images/KryoFlux/Read.cs index 2de5923df..3cd93a6b5 100644 --- a/Aaru.Images/KryoFlux/Read.cs +++ b/Aaru.Images/KryoFlux/Read.cs @@ -173,8 +173,6 @@ public sealed partial class KryoFlux // Sample Frequency double sck = SCK; - // Index Frequency - double ick = ICK; List<(uint streamPosition, uint timer, uint sysTime)> indexEvents = [];