Files
Aaru-aaru-dps/Aaru.Images/KryoFlux/Read.cs
Rebecca Wallander d6939712b6 Remove ICK
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-04-05 11:37:11 +02:00

769 lines
28 KiB
C#

// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Read.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : Disk image plugins.
//
// --[ Description ] ----------------------------------------------------------
//
// Reads 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using System;
using System.Collections.Generic;
using System.Globalization;
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;
using Aaru.CommonTypes;
namespace Aaru.Images;
public sealed partial class KryoFlux
{
#region IMediaImage Members
/// <inheritdoc />
public ErrorNumber Open(IFilter imageFilter)
{
Stream stream = imageFilter.GetDataForkStream();
stream.Seek(0, SeekOrigin.Begin);
if(stream.Length < Marshal.SizeOf<OobBlock>()) return ErrorNumber.InvalidArgument;
var hdr = new byte[Marshal.SizeOf<OobBlock>()];
stream.EnsureRead(hdr, 0, Marshal.SizeOf<OobBlock>());
OobBlock header = Marshal.ByteArrayToStructureLittleEndian<OobBlock>(hdr);
stream.Seek(-Marshal.SizeOf<OobBlock>(), SeekOrigin.End);
hdr = new byte[Marshal.SizeOf<OobBlock>()];
stream.EnsureRead(hdr, 0, Marshal.SizeOf<OobBlock>());
OobBlock footer = Marshal.ByteArrayToStructureLittleEndian<OobBlock>(hdr);
if(header.blockId != BlockIds.Oob ||
header.blockType != OobTypes.KFInfo ||
footer.blockId != BlockIds.Oob ||
footer.blockType != OobTypes.EOF ||
footer.length != 0x0D0D)
return ErrorNumber.InvalidArgument;
// TODO: This is supposing NoFilter, shouldn't
tracks = new SortedDictionary<byte, IFilter>();
_trackCaptures = [];
byte step = 1;
byte heads = 2;
var topHead = false;
string basename = Path.Combine(imageFilter.ParentFolder, imageFilter.Filename[..^8]);
for(byte t = 0; t < 166; t += step)
{
int cylinder = t / heads;
int head = topHead ? 1 : t % heads;
string trackfile = Directory.Exists(basename)
? Path.Combine(basename, $"{cylinder:D2}.{head:D1}.raw")
: $"{basename}{cylinder:D2}.{head:D1}.raw";
if(!File.Exists(trackfile))
{
switch(cylinder)
{
case 0 when head == 0:
AaruLogging.Debug(MODULE_NAME,
Localization.Cannot_find_cyl_0_hd_0_supposing_only_top_head_was_dumped);
topHead = true;
heads = 1;
continue;
case 0:
AaruLogging.Debug(MODULE_NAME,
Localization.Cannot_find_cyl_0_hd_1_supposing_only_bottom_head_was_dumped);
heads = 1;
continue;
case 1:
AaruLogging.Debug(MODULE_NAME, Localization.Cannot_find_cyl_1_supposing_double_stepping);
step = 2;
continue;
}
AaruLogging.Debug(MODULE_NAME, Localization.Arrived_end_of_disk_at_cylinder_0, cylinder);
break;
}
var trackFilter = new ZZZNoFilter();
ErrorNumber errno = trackFilter.Open(trackfile);
if(errno != ErrorNumber.NoError) return errno;
_imageInfo.CreationTime = DateTime.MaxValue;
_imageInfo.LastModificationTime = DateTime.MinValue;
ErrorNumber processError = ProcessTrackFile(trackfile, (uint)head, (ushort)cylinder, trackFilter);
if(processError != ErrorNumber.NoError) return processError;
tracks.Add(t, trackFilter);
}
_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;
}
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);
byte[] fileData = new byte[trackStream.Length];
trackStream.EnsureRead(fileData, 0, (int)trackStream.Length);
int fileSize = fileData.Length;
uint[] cellValues = new uint[fileSize];
uint[] cellStreamPositions = new uint[fileSize];
uint cellAccumulator = 0;
int streamOfs = 0;
uint streamPos = 0;
int cellPos = 0;
// Sample Frequency
double sck = SCK;
List<(uint streamPosition, uint timer, uint sysTime)> indexEvents = [];
int oobHeaderSize = Marshal.SizeOf<OobBlock>();
while(streamOfs < fileSize)
{
byte curOp = fileData[streamOfs];
int curOpLen;
if(curOp == (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<OobBlock>(oobBytes);
if(oobBlk.blockType == OobTypes.EOF)
break;
curOpLen = oobHeaderSize + oobBlk.length;
if(fileSize - streamOfs < curOpLen)
return ErrorNumber.InvalidArgument;
int oobDataStart = streamOfs + oobHeaderSize;
switch(oobBlk.blockType)
{
case OobTypes.KFInfo:
{
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))
{
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:
// 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;
}
}
if(foundDate)
{
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);
if(blockTimestamp < Info.CreationTime) _imageInfo.CreationTime = blockTimestamp;
if(blockTimestamp > Info.LastModificationTime)
_imageInfo.LastModificationTime = blockTimestamp;
}
break;
}
case OobTypes.StreamInfo:
{
if(oobDataStart + Marshal.SizeOf<OobStreamRead>() <= fileSize)
{
byte[] streamReadBytes = new byte[Marshal.SizeOf<OobStreamRead>()];
Array.Copy(fileData, oobDataStart, streamReadBytes, 0,
Marshal.SizeOf<OobStreamRead>());
OobStreamRead oobStreamRead =
Marshal.ByteArrayToStructureLittleEndian<OobStreamRead>(streamReadBytes);
AaruLogging.Debug(MODULE_NAME,
"Stream Read at position {0}, elapsed time {1} ms",
oobStreamRead.streamPosition,
oobStreamRead.trTime);
}
break;
}
case OobTypes.Index:
{
if(oobDataStart + Marshal.SizeOf<OobIndex>() <= fileSize)
{
byte[] indexBytes = new byte[Marshal.SizeOf<OobIndex>()];
Array.Copy(fileData, oobDataStart, indexBytes, 0, Marshal.SizeOf<OobIndex>());
OobIndex oobIndex = Marshal.ByteArrayToStructureLittleEndian<OobIndex>(indexBytes);
AaruLogging.Debug(MODULE_NAME,
"Index signal at stream position {0}, timer {1}, sysTime {2}",
oobIndex.streamPosition,
oobIndex.timer,
oobIndex.sysTime);
indexEvents.Add((oobIndex.streamPosition, oobIndex.timer, oobIndex.sysTime));
}
break;
}
case OobTypes.StreamEnd:
{
if(oobDataStart + Marshal.SizeOf<OobStreamEnd>() <= fileSize)
{
byte[] streamEndBytes = new byte[Marshal.SizeOf<OobStreamEnd>()];
Array.Copy(fileData, oobDataStart, streamEndBytes, 0,
Marshal.SizeOf<OobStreamEnd>());
OobStreamEnd oobStreamEnd =
Marshal.ByteArrayToStructureLittleEndian<OobStreamEnd>(streamEndBytes);
AaruLogging.Debug(MODULE_NAME,
"Stream End at position {0}, result {1}",
oobStreamEnd.streamPosition,
oobStreamEnd.result);
}
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:
if(curOp >= 0x0E)
{
curOpLen = 1;
cellAccumulator += curOp;
newCell = true;
}
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;
}
// 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<uint> indexPositions = [];
if(indexEvents.Count > 0)
{
int nextIndex = 0;
uint nextIndexStreamPos = indexEvents[nextIndex].streamPosition;
for(int i = 0; i < totalCells; i++)
{
if(nextIndex >= indexEvents.Count)
break;
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;
}
}
}
// 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.Length,
indexPositions.Count,
resolution);
TrackCapture capture = new TrackCapture
{
head = head,
track = track,
resolution = resolution,
fluxPulses = fluxPulses,
indexPositions = indexPositions.ToArray()
};
_trackCaptures.Add(capture);
return ErrorNumber.NoError;
}
/// <inheritdoc />
public ErrorNumber ReadMediaTag(MediaTagType tag, out byte[] buffer)
{
buffer = null;
return ErrorNumber.NotImplemented;
}
/// <inheritdoc />
public ErrorNumber ReadSector(ulong sectorAddress, bool negative, out byte[] buffer, out SectorStatus sectorStatus)
{
sectorStatus = SectorStatus.NotDumped;
return ReadSectors(sectorAddress, negative, 1, out buffer, out _);
}
/// <inheritdoc />
public ErrorNumber ReadSectorTag(ulong sectorAddress, bool negative, SectorTagType tag, out byte[] buffer) =>
ReadSectorsTag(sectorAddress, negative, 1, tag, out buffer);
/// <inheritdoc />
public ErrorNumber ReadSectors(ulong sectorAddress, bool negative, uint length, out byte[] buffer,
out SectorStatus[] sectorStatus)
{
buffer = null;
sectorStatus = null;
return ErrorNumber.NotImplemented;
}
/// <inheritdoc />
public ErrorNumber ReadSectorsTag(ulong sectorAddress, bool negative, uint length, SectorTagType tag,
out byte[] buffer)
{
buffer = null;
return ErrorNumber.NotImplemented;
}
/// <inheritdoc />
public ErrorNumber ReadSectorLong(ulong sectorAddress, bool negative, out byte[] buffer,
out SectorStatus sectorStatus)
{
sectorStatus = SectorStatus.NotDumped;
return ReadSectorsLong(sectorAddress, negative, 1, out buffer, out _);
}
/// <inheritdoc />
public ErrorNumber ReadSectorsLong(ulong sectorAddress, bool negative, uint length, out byte[] buffer,
out SectorStatus[] sectorStatus)
{
buffer = null;
sectorStatus = null;
return ErrorNumber.NotImplemented;
}
#endregion
#region IFluxImage Members
/// <inheritdoc />
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;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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<byte>();
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;
}
/// <inheritdoc />
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<byte>();
foreach(uint pulse in capture.fluxPulses) tmpBuffer.AddRange(UInt32ToFluxRepresentation(pulse));
buffer = tmpBuffer.ToArray();
return ErrorNumber.NoError;
}
/// <inheritdoc />
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<TrackCapture> 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;
}
/// <inheritdoc />
public ErrorNumber GetAllFluxCaptures(out List<FluxCapture> 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
}