mirror of
https://github.com/aaru-dps/Aaru.git
synced 2026-07-08 18:16:24 +00:00
784 lines
32 KiB
C#
784 lines
32 KiB
C#
// /***************************************************************************
|
|
// Aaru Data Preservation Suite
|
|
// ----------------------------------------------------------------------------
|
|
//
|
|
// Filename : Read.cs
|
|
// Author(s) : Rebecca Wallander <sakcheen+github@gmail.com>
|
|
//
|
|
// Component : Disk image plugins.
|
|
//
|
|
// --[ Description ] ----------------------------------------------------------
|
|
//
|
|
// Reads HxC Stream 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 <http://www.gnu.org/licenses/>.
|
|
//
|
|
// ----------------------------------------------------------------------------
|
|
// Copyright © 2011-2026 Rebecca Wallander
|
|
// ****************************************************************************/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using Aaru.CommonTypes.Enums;
|
|
using Aaru.CommonTypes.Interfaces;
|
|
using Aaru.CommonTypes.Structs;
|
|
using Aaru.Compression;
|
|
using Aaru.Helpers;
|
|
using Aaru.Logging;
|
|
|
|
namespace Aaru.Images;
|
|
|
|
public sealed partial class HxCStream
|
|
{
|
|
#region IFluxImage Members
|
|
|
|
/// <inheritdoc />
|
|
public ErrorNumber Open(IFilter imageFilter)
|
|
{
|
|
_trackCaptures = [];
|
|
_trackFilePaths = [];
|
|
_mediaTags = [];
|
|
_imageInfo.Heads = 0;
|
|
_imageInfo.Cylinders = 0;
|
|
|
|
string filename = imageFilter.Filename;
|
|
string parentFolder = imageFilter.ParentFolder;
|
|
|
|
// We always open a single file - extract basename to find related track files
|
|
if(!filename.EndsWith(".hxcstream", StringComparison.OrdinalIgnoreCase)) return ErrorNumber.InvalidArgument;
|
|
|
|
// Extract basename - remove the track number pattern (e.g., "track00.0.hxcstream" -> "track")
|
|
// The pattern is {basename}{cylinder:D2}.{head:D1}.hxcstream
|
|
string basename = filename[..^14]; // Remove ".XX.X.hxcstream" (14 chars)
|
|
string fullBasename = Path.Combine(parentFolder, basename);
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "Opening HxCStream image from file: {0}", filename);
|
|
AaruLogging.Debug(MODULE_NAME, "Basename: {0}", basename);
|
|
AaruLogging.Debug(MODULE_NAME, "Full basename path: {0}", fullBasename);
|
|
|
|
// Discover track files by trying different cylinder/head combinations
|
|
var trackFiles = new Dictionary<(int cylinder, int head), string>();
|
|
int minCylinder = int.MaxValue;
|
|
int maxCylinder = int.MinValue;
|
|
int minHead = int.MaxValue;
|
|
int maxHead = int.MinValue;
|
|
|
|
// Search for related track files
|
|
for(var cylinder = 0; cylinder < 166; cylinder++)
|
|
{
|
|
for(var head = 0; head < 2; head++)
|
|
{
|
|
var trackfile = $"{fullBasename}{cylinder:D2}.{head:D1}.hxcstream";
|
|
|
|
if(File.Exists(trackfile))
|
|
{
|
|
trackFiles[(cylinder, head)] = trackfile;
|
|
minCylinder = Math.Min(minCylinder, cylinder);
|
|
maxCylinder = Math.Max(maxCylinder, cylinder);
|
|
minHead = Math.Min(minHead, head);
|
|
maxHead = Math.Max(maxHead, head);
|
|
}
|
|
}
|
|
}
|
|
|
|
if(trackFiles.Count == 0) return ErrorNumber.NoData;
|
|
|
|
_imageInfo.Cylinders = (uint)(maxCylinder - minCylinder + 1);
|
|
_imageInfo.Heads = (uint)(maxHead - minHead + 1);
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "Found {0} track files", trackFiles.Count);
|
|
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"Cylinder range: {0} to {1} ({2} cylinders)",
|
|
minCylinder,
|
|
maxCylinder,
|
|
_imageInfo.Cylinders);
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "Head range: {0} to {1} ({2} heads)", minHead, maxHead, _imageInfo.Heads);
|
|
|
|
// Process each track file
|
|
var trackIndex = 0;
|
|
int totalTracks = trackFiles.Count;
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "Processing {0} track files...", totalTracks);
|
|
|
|
foreach((int cylinder, int head) key in trackFiles.Keys.OrderBy(k => k.cylinder).ThenBy(k => k.head).ToList())
|
|
{
|
|
trackIndex++;
|
|
string trackfile = trackFiles[key];
|
|
_trackFilePaths.Add(trackfile);
|
|
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"Processing track {0}/{1}: cylinder {2}, head {3}",
|
|
trackIndex,
|
|
totalTracks,
|
|
key.cylinder,
|
|
key.head);
|
|
|
|
ErrorNumber error = ProcessTrackFile(trackfile, (uint)key.head, (ushort)key.cylinder);
|
|
|
|
if(error != ErrorNumber.NoError) return error;
|
|
}
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "Successfully processed all {0} track files", totalTracks);
|
|
|
|
_imageInfo.MetadataMediaType = MetadataMediaType.BlockMedia;
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
ErrorNumber ProcessTrackFile(string trackfile, uint head, ushort track)
|
|
{
|
|
if(!File.Exists(trackfile)) return ErrorNumber.NoSuchFile;
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "Processing track file: {0} (head {1}, track {2})", trackfile, head, track);
|
|
|
|
using FileStream fileStream = File.OpenRead(trackfile);
|
|
var fileData = new byte[fileStream.Length];
|
|
fileStream.EnsureRead(fileData, 0, (int)fileStream.Length);
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "Track file size: {0} bytes", fileData.Length);
|
|
|
|
uint samplePeriod = DEFAULT_RESOLUTION; // Default 40,000 ns (25 MHz)
|
|
var fluxPulses = new List<uint>();
|
|
var ioStream = new List<ushort>();
|
|
string metadata = null;
|
|
|
|
long fileOffset = 0;
|
|
|
|
while(fileOffset < fileData.Length)
|
|
{
|
|
if(fileOffset + Marshal.SizeOf<HxCStreamChunkHeader>() > fileData.Length)
|
|
return ErrorNumber.InvalidArgument;
|
|
|
|
HxCStreamChunkHeader chunkHeader =
|
|
Marshal.ByteArrayToStructureLittleEndian<HxCStreamChunkHeader>(fileData,
|
|
(int)fileOffset,
|
|
Marshal.SizeOf<HxCStreamChunkHeader>());
|
|
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"Chunk at offset {0}: signature = \"{1}\", size = {2}, packetNumber = {3}",
|
|
fileOffset,
|
|
StringHandlers.CToString(chunkHeader.signature),
|
|
chunkHeader.size,
|
|
chunkHeader.packetNumber);
|
|
|
|
if(!_hxcStreamSignature.SequenceEqual(chunkHeader.signature)) return ErrorNumber.InvalidArgument;
|
|
|
|
if(chunkHeader.size > fileData.Length - fileOffset) return ErrorNumber.InvalidArgument;
|
|
|
|
// Verify CRC32 - calculate CRC of chunk data (excluding the CRC itself)
|
|
var chunkData = new byte[chunkHeader.size - 4];
|
|
Array.Copy(fileData, (int)fileOffset, chunkData, 0, (int)(chunkHeader.size - 4));
|
|
|
|
var storedCrc = BitConverter.ToUInt32(fileData, (int)(fileOffset + chunkHeader.size - 4));
|
|
|
|
if(!VerifyChunkCrc32(chunkData, storedCrc))
|
|
{
|
|
AaruLogging.Error(MODULE_NAME, "CRC32 mismatch in chunk at offset {0}", fileOffset);
|
|
|
|
return ErrorNumber.InvalidArgument;
|
|
}
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "Chunk CRC32 verified successfully");
|
|
|
|
long packetOffset = fileOffset + Marshal.SizeOf<HxCStreamChunkHeader>();
|
|
long chunkEnd = fileOffset + chunkHeader.size - 4;
|
|
|
|
while(packetOffset < chunkEnd)
|
|
{
|
|
if(packetOffset + 4 > fileData.Length) break;
|
|
|
|
var type = BitConverter.ToUInt32(fileData, (int)packetOffset);
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "Packet at offset {0}: type = 0x{1:X8}", packetOffset, type);
|
|
|
|
switch(type)
|
|
{
|
|
case 0x0: // Metadata
|
|
{
|
|
if(packetOffset + Marshal.SizeOf<HxCStreamMetadataHeader>() > fileData.Length)
|
|
return ErrorNumber.InvalidArgument;
|
|
|
|
HxCStreamMetadataHeader metadataHeader =
|
|
Marshal.ByteArrayToStructureLittleEndian<HxCStreamMetadataHeader>(fileData,
|
|
(int)packetOffset,
|
|
Marshal.SizeOf<HxCStreamMetadataHeader>());
|
|
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"Metadata packet: type = 0x{0:X8}, payloadSize = {1}",
|
|
metadataHeader.type,
|
|
metadataHeader.payloadSize);
|
|
|
|
if(packetOffset + Marshal.SizeOf<HxCStreamMetadataHeader>() + metadataHeader.payloadSize >
|
|
fileData.Length)
|
|
return ErrorNumber.InvalidArgument;
|
|
|
|
var metadataBytes = new byte[metadataHeader.payloadSize];
|
|
|
|
Array.Copy(fileData,
|
|
(int)packetOffset + Marshal.SizeOf<HxCStreamMetadataHeader>(),
|
|
metadataBytes,
|
|
0,
|
|
(int)metadataHeader.payloadSize);
|
|
|
|
metadata = Encoding.UTF8.GetString(metadataBytes);
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "Metadata content: {0}", metadata);
|
|
|
|
// Parse metadata and populate ImageInfo (only parse once, from first chunk)
|
|
if(string.IsNullOrEmpty(_imageInfo.Application)) ParseMetadata(metadata, _imageInfo);
|
|
|
|
// Check for sample rate in metadata
|
|
if(metadata.Contains("sample_rate_hz 25000000"))
|
|
{
|
|
samplePeriod = 40000;
|
|
AaruLogging.Debug(MODULE_NAME, "Sample rate detected: 25 MHz (40000 ns period)");
|
|
}
|
|
else if(metadata.Contains("sample_rate_hz 50000000"))
|
|
{
|
|
samplePeriod = 20000;
|
|
AaruLogging.Debug(MODULE_NAME, "Sample rate detected: 50 MHz (20000 ns period)");
|
|
}
|
|
else
|
|
AaruLogging.Debug(MODULE_NAME, "Using default sample rate: 25 MHz (40000 ns period)");
|
|
|
|
packetOffset += Marshal.SizeOf<HxCStreamMetadataHeader>() + metadataHeader.payloadSize;
|
|
|
|
// Align to 4 bytes
|
|
if(packetOffset % 4 != 0) packetOffset += 4 - packetOffset % 4;
|
|
|
|
break;
|
|
}
|
|
case 0x1: // Packed IO stream
|
|
{
|
|
if(packetOffset + Marshal.SizeOf<HxCStreamPackedIoHeader>() > fileData.Length)
|
|
return ErrorNumber.InvalidArgument;
|
|
|
|
HxCStreamPackedIoHeader ioHeader =
|
|
Marshal.ByteArrayToStructureLittleEndian<HxCStreamPackedIoHeader>(fileData,
|
|
(int)packetOffset,
|
|
Marshal.SizeOf<HxCStreamPackedIoHeader>());
|
|
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"Packed IO stream packet: type = 0x{0:X8}, payloadSize = {1}, packedSize = {2}, unpackedSize = {3}",
|
|
ioHeader.type,
|
|
ioHeader.payloadSize,
|
|
ioHeader.packedSize,
|
|
ioHeader.unpackedSize);
|
|
|
|
if(packetOffset + Marshal.SizeOf<HxCStreamPackedIoHeader>() + ioHeader.packedSize >
|
|
fileData.Length)
|
|
return ErrorNumber.InvalidArgument;
|
|
|
|
var packedData = new byte[ioHeader.packedSize];
|
|
|
|
Array.Copy(fileData,
|
|
(int)packetOffset + Marshal.SizeOf<HxCStreamPackedIoHeader>(),
|
|
packedData,
|
|
0,
|
|
(int)ioHeader.packedSize);
|
|
|
|
var unpackedData = new byte[ioHeader.unpackedSize];
|
|
int decoded = LZ4.DecodeBuffer(packedData, unpackedData);
|
|
|
|
if(decoded != ioHeader.unpackedSize) return ErrorNumber.InvalidArgument;
|
|
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"Decompressed IO stream: {0} bytes -> {1} bytes ({2} 16-bit values)",
|
|
ioHeader.packedSize,
|
|
decoded,
|
|
decoded / 2);
|
|
|
|
// Convert to ushort array
|
|
for(var i = 0; i < unpackedData.Length; i += 2)
|
|
if(i + 1 < unpackedData.Length)
|
|
ioStream.Add(BitConverter.ToUInt16(unpackedData, i));
|
|
|
|
packetOffset += Marshal.SizeOf<HxCStreamPackedIoHeader>() + ioHeader.packedSize;
|
|
|
|
// Align to 4 bytes
|
|
if(packetOffset % 4 != 0) packetOffset += 4 - packetOffset % 4;
|
|
|
|
break;
|
|
}
|
|
case 0x2: // Packed flux stream
|
|
{
|
|
if(packetOffset + Marshal.SizeOf<HxCStreamPackedStreamHeader>() > fileData.Length)
|
|
return ErrorNumber.InvalidArgument;
|
|
|
|
HxCStreamPackedStreamHeader streamHeader =
|
|
Marshal.ByteArrayToStructureLittleEndian<HxCStreamPackedStreamHeader>(fileData,
|
|
(int)packetOffset,
|
|
Marshal.SizeOf<HxCStreamPackedStreamHeader>());
|
|
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"Packed flux stream packet: type = 0x{0:X8}, payloadSize = {1}, packedSize = {2}, unpackedSize = {3}, numberOfPulses = {4}",
|
|
streamHeader.type,
|
|
streamHeader.payloadSize,
|
|
streamHeader.packedSize,
|
|
streamHeader.unpackedSize,
|
|
streamHeader.numberOfPulses);
|
|
|
|
if(packetOffset + Marshal.SizeOf<HxCStreamPackedStreamHeader>() + streamHeader.packedSize >
|
|
fileData.Length)
|
|
return ErrorNumber.InvalidArgument;
|
|
|
|
var packedData = new byte[streamHeader.packedSize];
|
|
|
|
Array.Copy(fileData,
|
|
(int)packetOffset + Marshal.SizeOf<HxCStreamPackedStreamHeader>(),
|
|
packedData,
|
|
0,
|
|
(int)streamHeader.packedSize);
|
|
|
|
var unpackedData = new byte[streamHeader.unpackedSize];
|
|
int decoded = LZ4.DecodeBuffer(packedData, unpackedData);
|
|
|
|
if(decoded != streamHeader.unpackedSize) return ErrorNumber.InvalidArgument;
|
|
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"Decompressed flux stream: {0} bytes -> {1} bytes",
|
|
streamHeader.packedSize,
|
|
decoded);
|
|
|
|
// Decode variable-length pulses
|
|
uint numberOfPulses = streamHeader.numberOfPulses;
|
|
|
|
uint[] pulses = DecodeVariableLengthPulses(unpackedData,
|
|
streamHeader.unpackedSize,
|
|
ref numberOfPulses);
|
|
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"Decoded {0} flux pulses (expected {1})",
|
|
pulses.Length,
|
|
numberOfPulses);
|
|
|
|
if(pulses.Length != numberOfPulses) return ErrorNumber.InvalidArgument;
|
|
|
|
fluxPulses.AddRange(pulses);
|
|
|
|
packetOffset += Marshal.SizeOf<HxCStreamPackedStreamHeader>() + streamHeader.packedSize;
|
|
|
|
// Align to 4 bytes
|
|
if(packetOffset % 4 != 0) packetOffset += 4 - packetOffset % 4;
|
|
|
|
break;
|
|
}
|
|
default:
|
|
AaruLogging.Error(MODULE_NAME, "Unknown packet type: 0x{0:X8}", type);
|
|
|
|
return ErrorNumber.InvalidArgument;
|
|
}
|
|
}
|
|
|
|
fileOffset += chunkHeader.size;
|
|
}
|
|
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"Finished processing chunks. Total flux pulses: {0}, IO stream values: {1}",
|
|
fluxPulses.Count,
|
|
ioStream.Count);
|
|
|
|
// Extract write protect status from IO stream
|
|
// The write protect flag should be constant for a capture, so we only need to check the first value
|
|
if(ioStream.Count > 0 && !_mediaTags.ContainsKey(MediaTagType.Floppy_WriteProtection))
|
|
{
|
|
IoStreamState firstState = DecodeIoStreamValue(ioStream[0]);
|
|
bool writeProtected = firstState.WriteProtect;
|
|
|
|
// Store as boolean: 1 byte where 0 = false (not write protected), 1 = true (write protected)
|
|
_mediaTags[MediaTagType.Floppy_WriteProtection] = writeProtected ? [1] : [0];
|
|
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"Write protect status from IO stream: {0}",
|
|
writeProtected ? "write protected" : "not write protected");
|
|
}
|
|
|
|
// Extract index signals from IO stream
|
|
var indexPositions = new List<uint>();
|
|
|
|
if(ioStream.Count > 0)
|
|
{
|
|
IoStreamState previousState = DecodeIoStreamValue(ioStream[0]);
|
|
bool oldIndex = previousState.IndexSignal;
|
|
bool startsAtIndex = previousState.IndexSignal;
|
|
if(startsAtIndex) indexPositions.Add(0);
|
|
|
|
uint totalTicks = 0;
|
|
var pulseIndex = 0;
|
|
|
|
for(var i = 0; i < ioStream.Count; i++)
|
|
{
|
|
IoStreamState currentState = DecodeIoStreamValue(ioStream[i]);
|
|
bool currentIndex = currentState.IndexSignal;
|
|
|
|
if(currentIndex != oldIndex && currentIndex)
|
|
{
|
|
// Index signal transition to high
|
|
// Map to flux stream position
|
|
var targetTicks = (uint)(i * 16);
|
|
|
|
while(pulseIndex < fluxPulses.Count && totalTicks < targetTicks)
|
|
{
|
|
totalTicks += fluxPulses[pulseIndex];
|
|
pulseIndex++;
|
|
}
|
|
|
|
if(pulseIndex < fluxPulses.Count) indexPositions.Add((uint)pulseIndex);
|
|
}
|
|
|
|
oldIndex = currentIndex;
|
|
}
|
|
|
|
AaruLogging.Debug(MODULE_NAME, "Extracted {0} index positions from IO stream", indexPositions.Count);
|
|
}
|
|
else
|
|
AaruLogging.Debug(MODULE_NAME, "No IO stream data available, no index positions extracted");
|
|
|
|
// Create track capture
|
|
// Note: HxCStream doesn't support subtracks, so subTrack is always 0
|
|
var capture = new TrackCapture
|
|
{
|
|
head = head,
|
|
track = track,
|
|
resolution = samplePeriod,
|
|
fluxPulses = fluxPulses.ToArray(),
|
|
indexPositions = indexPositions.ToArray()
|
|
};
|
|
|
|
AaruLogging.Debug(MODULE_NAME,
|
|
"Created track capture: head = {0}, track = {1}, resolution = {2} ns, fluxPulses = {3}, indexPositions = {4}",
|
|
capture.head,
|
|
capture.track,
|
|
capture.resolution,
|
|
capture.fluxPulses.Length,
|
|
capture.indexPositions.Length);
|
|
|
|
_trackCaptures.Add(capture);
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ErrorNumber CapturesLength(uint head, ushort track, byte subTrack, out uint length)
|
|
{
|
|
length = 0;
|
|
|
|
if(_trackCaptures == null) return ErrorNumber.NotOpened;
|
|
|
|
// HxCStream doesn't support subtracks - only subTrack 0 is valid
|
|
if(subTrack != 0) return ErrorNumber.OutOfRange;
|
|
|
|
// HxCStream has one file per track/head, which results in exactly one capture
|
|
// Check if a capture exists for this track
|
|
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;
|
|
|
|
// HxCStream doesn't support subtracks - only subTrack 0 is valid
|
|
if(subTrack != 0) return ErrorNumber.OutOfRange;
|
|
|
|
// HxCStream 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;
|
|
|
|
// HxCStream doesn't support subtracks - only subTrack 0 is valid
|
|
if(subTrack != 0) return ErrorNumber.OutOfRange;
|
|
|
|
// HxCStream 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;
|
|
|
|
// HxCStream doesn't support subtracks - only subTrack 0 is valid
|
|
if(subTrack != 0) return ErrorNumber.OutOfRange;
|
|
|
|
// HxCStream 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;
|
|
|
|
// HxCStream doesn't support subtracks - only subTrack 0 is valid
|
|
if(subTrack != 0) return ErrorNumber.OutOfRange;
|
|
|
|
// HxCStream 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;
|
|
|
|
// HxCStream doesn't support subtracks - only subTrack 0 is valid
|
|
if(subTrack != 0) return ErrorNumber.OutOfRange;
|
|
|
|
// HxCStream 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 previousTicks = 0;
|
|
|
|
foreach(uint indexPos in capture.indexPositions)
|
|
{
|
|
// Convert index position to ticks
|
|
uint ticks = 0;
|
|
for(uint i = 0; i < indexPos && i < capture.fluxPulses.Length; i++) ticks += capture.fluxPulses[i];
|
|
|
|
uint deltaTicks = ticks - previousTicks;
|
|
tmpBuffer.AddRange(UInt32ToFluxRepresentation(deltaTicks));
|
|
previousTicks = ticks;
|
|
}
|
|
|
|
buffer = tmpBuffer.ToArray();
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ErrorNumber ReadFluxDataCapture(uint head, ushort track, byte subTrack, uint captureIndex, out byte[] buffer)
|
|
{
|
|
buffer = null;
|
|
|
|
// HxCStream doesn't support subtracks - only subTrack 0 is valid
|
|
if(subTrack != 0) return ErrorNumber.OutOfRange;
|
|
|
|
// HxCStream 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;
|
|
|
|
// HxCStream 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 HxCStream 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: HxCStream 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, // HxCStream doesn't support subtracks
|
|
CaptureIndex = captureIndex++,
|
|
IndexResolution = trackCapture.resolution,
|
|
DataResolution = trackCapture.resolution
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IMediaImage Members
|
|
|
|
/// <inheritdoc />
|
|
public ErrorNumber ReadMediaTag(MediaTagType tag, out byte[] buffer)
|
|
{
|
|
buffer = null;
|
|
|
|
if(_mediaTags == null) return ErrorNumber.NotOpened;
|
|
|
|
if(_mediaTags.TryGetValue(tag, out byte[] tagData))
|
|
{
|
|
buffer = new byte[tagData.Length];
|
|
Array.Copy(tagData, buffer, tagData.Length);
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
return ErrorNumber.NoData;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ErrorNumber ReadSector(ulong sectorAddress, bool negative, out byte[] buffer, out SectorStatus sectorStatus)
|
|
{
|
|
buffer = null;
|
|
sectorStatus = SectorStatus.NotDumped;
|
|
|
|
return ErrorNumber.NotImplemented;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ErrorNumber ReadSectorLong(ulong sectorAddress, bool negative, out byte[] buffer,
|
|
out SectorStatus sectorStatus)
|
|
{
|
|
buffer = null;
|
|
sectorStatus = SectorStatus.NotDumped;
|
|
|
|
return ErrorNumber.NotImplemented;
|
|
}
|
|
|
|
/// <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 ReadSectorsLong(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 ReadSectorTag(ulong sectorAddress, bool negative, SectorTagType tag, out byte[] buffer)
|
|
{
|
|
buffer = null;
|
|
|
|
return ErrorNumber.NotImplemented;
|
|
}
|
|
|
|
#endregion
|
|
} |