diff --git a/Aaru.CommonTypes/Interfaces/IFluxImage.cs b/Aaru.CommonTypes/Interfaces/IFluxImage.cs index 794b348cc..cfb5a7c29 100644 --- a/Aaru.CommonTypes/Interfaces/IFluxImage.cs +++ b/Aaru.CommonTypes/Interfaces/IFluxImage.cs @@ -47,7 +47,7 @@ namespace Aaru.CommonTypes.Interfaces; /// Abstract class to implement flux reading plugins. [SuppressMessage("ReSharper", "UnusedMethodReturnValue.Global")] [SuppressMessage("ReSharper", "UnusedMemberInSuper.Global")] -public interface IFluxImage : IBaseImage +public interface IFluxImage : IMediaImage { /// /// An image may have more than one capture for a specific head/track/sub-track combination. This returns diff --git a/Aaru.Compression/Aaru.Compression.csproj b/Aaru.Compression/Aaru.Compression.csproj index 0b7a919fc..096c51dc2 100644 --- a/Aaru.Compression/Aaru.Compression.csproj +++ b/Aaru.Compression/Aaru.Compression.csproj @@ -49,6 +49,7 @@ + diff --git a/Aaru.Compression/LZ4.cs b/Aaru.Compression/LZ4.cs new file mode 100644 index 000000000..3fd553d88 --- /dev/null +++ b/Aaru.Compression/LZ4.cs @@ -0,0 +1,49 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : LZ4.cs +// Author(s) : Rebecca Wallander +// +// Component : Compression algorithms. +// +// --[ 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 Rebecca Wallander +// ****************************************************************************/ + +using System.Runtime.InteropServices; + +namespace Aaru.Compression; + +// ReSharper disable once InconsistentNaming +/// Implements the LZ4 compression algorithm +public partial class LZ4 +{ + /// Set to true if this algorithm is supported, false otherwise. + public static bool IsSupported => Native.IsSupported; + + [LibraryImport("libAaru.Compression.Native", SetLastError = true)] + private static partial int AARU_lz4_decode_buffer(byte[] dstBuffer, int dstSize, byte[] srcBuffer, int srcSize); + + /// Decodes a buffer compressed with LZ4 + /// Encoded buffer + /// Buffer where to write the decoded data + /// The number of decoded bytes + public static int DecodeBuffer(byte[] source, byte[] destination) => + Native.IsSupported ? AARU_lz4_decode_buffer(destination, destination.Length, source, source.Length) : 0; +} \ No newline at end of file diff --git a/Aaru.Images/HxCStream/Constants.cs b/Aaru.Images/HxCStream/Constants.cs new file mode 100644 index 000000000..085d9e873 --- /dev/null +++ b/Aaru.Images/HxCStream/Constants.cs @@ -0,0 +1,51 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Constants.cs +// Author(s) : Rebecca Wallander +// +// Component : Disk image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Contains constants for 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 . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Rebecca Wallander +// ****************************************************************************/ + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Aaru.Images; + +[SuppressMessage("ReSharper", "UnusedType.Global")] +public sealed partial class HxCStream +{ + const ushort DEFAULT_RESOLUTION = 40000; + + readonly byte[] _hxcStreamSignature = + [ + 0x43, 0x48, 0x4b, 0x48 // CHKH + ]; + + readonly uint _metadataId = 0x0; + readonly uint _ioStreamId = 0x1; + readonly uint _streamId = 0x2; +} \ No newline at end of file diff --git a/Aaru.Images/HxCStream/Helpers.cs b/Aaru.Images/HxCStream/Helpers.cs new file mode 100644 index 000000000..467062aa2 --- /dev/null +++ b/Aaru.Images/HxCStream/Helpers.cs @@ -0,0 +1,266 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Helpers.cs +// Author(s) : Rebecca Wallander +// +// Component : Disk image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Contains helpers for 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 . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Rebecca Wallander +// ****************************************************************************/ + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Diagnostics.CodeAnalysis; +using Aaru.Checksums; +using Aaru.CommonTypes.Structs; + +namespace Aaru.Images; + +[SuppressMessage("ReSharper", "UnusedType.Global")] +public sealed partial class HxCStream +{ + /// + /// Decodes variable-length encoded pulses from HxCStream format. + /// Values < 0x80: single byte value + /// 0x80-0xBF: 2-byte value (6 bits + 8 bits) + /// 0xC0-0xDF: 3-byte value (5 bits + 8 bits + 8 bits) + /// 0xE0-0xEF: 4-byte value (4 bits + 24 bits) + /// + /// The unpacked data buffer + /// Size of unpacked data + /// Number of pulses to decode (updated with actual count) + /// Array of decoded pulse values + static uint[] DecodeVariableLengthPulses(byte[] unpackedData, uint unpackedDataSize, ref uint numberOfPulses) + { + if(numberOfPulses == 0) return []; + + var pulses = new List(); + uint k = 0; + uint l = 0; + + while(l < numberOfPulses && k < unpackedDataSize) + { + byte c = unpackedData[k++]; + uint value = 0; + + if((c & 0x80) == 0) + { + // Single byte value + if(c != 0) pulses.Add(c); + } + else if((c & 0xC0) == 0x80) + { + // 2-byte value + if(k >= unpackedDataSize) break; + value = (uint)((c & 0x3F) << 8) | unpackedData[k++]; + pulses.Add(value); + } + else if((c & 0xE0) == 0xC0) + { + // 3-byte value + if(k + 1 >= unpackedDataSize) break; + value = (uint)((c & 0x1F) << 16) | ((uint)unpackedData[k++] << 8) | unpackedData[k++]; + pulses.Add(value); + } + else if((c & 0xF0) == 0xE0) + { + // 4-byte value + if(k + 2 >= unpackedDataSize) break; + value = (uint)((c & 0x0F) << 24) | ((uint)unpackedData[k++] << 16) | + ((uint)unpackedData[k++] << 8) | unpackedData[k++]; + pulses.Add(value); + } + + l++; + } + + // Add dummy pulse (300 ticks) + pulses.Add(300); + numberOfPulses = (uint)pulses.Count; + + return pulses.ToArray(); + } + + /// + /// Converts a uint32 pulse value to Aaru's flux representation format. + /// Format: byte array where 255 = overflow, remainder = value + /// + /// The pulse value in ticks + /// 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; + } + + /// + /// Decodes a raw 16-bit IO stream value into a readable IoStreamState structure. + /// This provides named properties for known signals (index, write protect) and + /// all 16 IO channels, while preserving the raw value for future extensions. + /// + /// The raw 16-bit IO stream value + /// Decoded IO stream state with named properties + public static IoStreamState DecodeIoStreamValue(ushort rawValue) => IoStreamState.FromRawValue(rawValue); + + /// + /// Parses HxCStream metadata string and populates ImageInfo fields. + /// Metadata format: key-value pairs separated by newlines, values may be quoted strings. + /// + /// The metadata string to parse + /// ImageInfo structure to populate + static void ParseMetadata(string metadata, ImageInfo imageInfo) + { + if(string.IsNullOrEmpty(metadata)) return; + + var lines = metadata.Split('\n', StringSplitOptions.RemoveEmptyEntries); + + foreach(string line in lines) + { + string trimmed = line.Trim(); + + if(string.IsNullOrEmpty(trimmed)) continue; + + // Find the first space to separate key from value + int spaceIndex = trimmed.IndexOf(' '); + + if(spaceIndex <= 0) continue; + + string key = trimmed[..spaceIndex].Trim(); + string value = trimmed[(spaceIndex + 1)..].Trim(); + + // Remove quotes if present + if(value.Length >= 2 && value[0] == '"' && value[^1] == '"') + value = value[1..^1]; + + switch(key) + { + case "format_version": + if(string.IsNullOrEmpty(imageInfo.Version)) + imageInfo.Version = value; + + break; + case "software_version": + // Extract version number (e.g., "v1.3.2.1 9 September 2021" -> "v1.3.2.1") + int versionEnd = value.IndexOf(' ', StringComparison.Ordinal); + + if(versionEnd > 0) + imageInfo.ApplicationVersion = value[..versionEnd]; + else + imageInfo.ApplicationVersion = value; + + // Set application name if not already set + if(string.IsNullOrEmpty(imageInfo.Application)) + imageInfo.Application = "HxC Floppy Emulator"; + + break; + case "dump_name": + if(string.IsNullOrEmpty(imageInfo.MediaTitle)) + imageInfo.MediaTitle = value; + + break; + case "dump_comment": + // Combine dump_comment and dump_comment2 if both exist + if(string.IsNullOrEmpty(imageInfo.Comments)) + imageInfo.Comments = value; + else + imageInfo.Comments = $"{imageInfo.Comments}\n{value}"; + + break; + case "dump_comment2": + if(!string.IsNullOrEmpty(value)) + { + if(string.IsNullOrEmpty(imageInfo.Comments)) + imageInfo.Comments = value; + else + imageInfo.Comments = $"{imageInfo.Comments}\n{value}"; + } + + break; + case "operator": + if(string.IsNullOrEmpty(imageInfo.Creator)) + imageInfo.Creator = value; + + break; + case "current_time": + // Parse date/time: "2025-11-13 16:42:29" + if(DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime dateTime)) + { + if(imageInfo.CreationTime == default) + imageInfo.CreationTime = dateTime; + + imageInfo.LastModificationTime = dateTime; + } + + break; + case "floppy_drive": + // Format: "1 \"5.25-inch Floppy drive\"" + // Extract the quoted description + int quoteStart = value.IndexOf('"'); + + if(quoteStart >= 0) + { + int quoteEnd = value.LastIndexOf('"'); + + if(quoteEnd > quoteStart) + { + string driveDesc = value[(quoteStart + 1)..quoteEnd]; + + if(string.IsNullOrEmpty(imageInfo.DriveModel)) + imageInfo.DriveModel = driveDesc; + } + } + + break; + case "drive_reference": + if(!string.IsNullOrEmpty(value) && string.IsNullOrEmpty(imageInfo.DriveSerialNumber)) + imageInfo.DriveSerialNumber = value; + + break; + } + } + } + + static bool VerifyChunkCrc32(byte[] chunkData, uint storedCrc) + { + var crc32Context = new Crc32Context(0xEDB88320, 0x00000000); + crc32Context.Update(chunkData, (uint)chunkData.Length); + byte[] crc32Bytes = crc32Context.Final(); + // Final() returns big-endian, but stored CRC is little-endian + Array.Reverse(crc32Bytes); + uint crc32 = BitConverter.ToUInt32(crc32Bytes, 0); + + crc32 ^= 0xFFFFFFFF; + + return crc32 == storedCrc; + } +} diff --git a/Aaru.Images/HxCStream/HxCStream.cs b/Aaru.Images/HxCStream/HxCStream.cs new file mode 100644 index 000000000..c1f47cdbb --- /dev/null +++ b/Aaru.Images/HxCStream/HxCStream.cs @@ -0,0 +1,75 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : HxCStream.cs +// Author(s) : Rebecca Wallander +// +// Component : Disk image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Manages 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 . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Rebecca Wallander +// ****************************************************************************/ + +using System.Collections.Generic; +using System.IO; +using Aaru.CommonTypes.Interfaces; +using Aaru.CommonTypes.Structs; + +namespace Aaru.Images; + +/// +/// Implements reading HxCStream flux images +public sealed partial class HxCStream : IFluxImage, IVerifiableImage +{ + const string MODULE_NAME = "HxCStream plugin"; + + ImageInfo _imageInfo; + + List _trackCaptures; + + List _trackFilePaths; + + public HxCStream() => _imageInfo = new ImageInfo + { + ReadableSectorTags = [], + ReadableMediaTags = [], + HasPartitions = false, + HasSessions = false, + Version = null, + Application = null, + ApplicationVersion = null, + Creator = null, + Comments = null, + MediaManufacturer = null, + MediaModel = null, + MediaSerialNumber = null, + MediaBarcode = null, + MediaPartNumber = null, + MediaSequence = 0, + LastMediaSequence = 0, + DriveManufacturer = null, + DriveModel = null, + DriveSerialNumber = null, + DriveFirmwareRevision = null + }; +} \ No newline at end of file diff --git a/Aaru.Images/HxCStream/Identify.cs b/Aaru.Images/HxCStream/Identify.cs new file mode 100644 index 000000000..f62a18385 --- /dev/null +++ b/Aaru.Images/HxCStream/Identify.cs @@ -0,0 +1,62 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Identify.cs +// Author(s) : Rebecca Wallander +// +// Component : Disk image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Identifies 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 . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Rebecca Wallander +// ****************************************************************************/ + +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using Aaru.CommonTypes.Interfaces; +using Aaru.Helpers; + +namespace Aaru.Images; + +[SuppressMessage("ReSharper", "UnusedType.Global")] +public sealed partial class HxCStream +{ +#region IFluxImage Members + + /// + public bool Identify(IFilter imageFilter) + { + Stream stream = imageFilter.GetDataForkStream(); + stream.Seek(0, SeekOrigin.Begin); + + if(stream.Length < 8) return false; + + var hdr = new byte[4]; + + stream.EnsureRead(hdr, 0, 4); + + return _hxcStreamSignature.SequenceEqual(hdr); + } + +#endregion +} \ No newline at end of file diff --git a/Aaru.Images/HxCStream/Properties.cs b/Aaru.Images/HxCStream/Properties.cs new file mode 100644 index 000000000..6689f9188 --- /dev/null +++ b/Aaru.Images/HxCStream/Properties.cs @@ -0,0 +1,107 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Properties.cs +// Author(s) : Rebecca Wallander +// +// Component : Disk image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Contains properties for 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 . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Rebecca Wallander +// ****************************************************************************/ + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Aaru.CommonTypes; +using Aaru.CommonTypes.AaruMetadata; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Structs; + +namespace Aaru.Images; + +public sealed partial class HxCStream +{ + [SuppressMessage("ReSharper", "UnusedType.Global")] + +#region IFluxImage Members + + /// + + // ReSharper disable once ConvertToAutoProperty + public ImageInfo Info => _imageInfo; + + /// + public string Name => Localization.HxCStream_Name; + + /// + public Guid Id => new("522c6f71-c5e5-4bff-9d2e-44c9af8397e7"); + + /// + public string Author => Authors.RebeccaWallander; + + /// + public string Format => "HxCStream"; + + /// + public List DumpHardware => null; + + /// + public Metadata AaruMetadata => null; + +#endregion + +#region IWritableImage Members + + /// + public IEnumerable KnownExtensions => new[] + { + ".hxcstream" + }; + + /// + public IEnumerable SupportedMediaTags => null; + + /// + public IEnumerable SupportedMediaTypes => new[] + { + // TODO: HxCStream supports a lot more formats, please add more whence tested. + MediaType.DOS_35_DS_DD_9, MediaType.DOS_35_HD, MediaType.DOS_525_DS_DD_9, MediaType.DOS_525_HD, + MediaType.Unknown + }; + + /// + public IEnumerable<(string name, Type type, string description, object @default)> SupportedOptions => + Array.Empty<(string name, Type type, string description, object @default)>(); + + /// + public IEnumerable SupportedSectorTags => Array.Empty(); + + /// + public bool IsWriting { get; private set; } + + /// + public string ErrorMessage { get; private set; } + +#endregion +} \ No newline at end of file diff --git a/Aaru.Images/HxCStream/Read.cs b/Aaru.Images/HxCStream/Read.cs new file mode 100644 index 000000000..b64b6d211 --- /dev/null +++ b/Aaru.Images/HxCStream/Read.cs @@ -0,0 +1,702 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Read.cs +// Author(s) : Rebecca Wallander +// +// 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 . +// +// ---------------------------------------------------------------------------- +// 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 + + /// + public ErrorNumber Open(IFilter imageFilter) + { + _trackCaptures = []; + _trackFilePaths = []; + _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(int cylinder = 0; cylinder < 166; cylinder++) + { + for(int head = 0; head < 2; head++) + { + string 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 + int 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); + byte[] 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(); + var ioStream = new List(); + string metadata = null; + + long fileOffset = 0; + + while(fileOffset < fileData.Length) + { + if(fileOffset + Marshal.SizeOf() > fileData.Length) + return ErrorNumber.InvalidArgument; + + HxCStreamChunkHeader chunkHeader = Marshal.ByteArrayToStructureLittleEndian( + fileData, (int)fileOffset, Marshal.SizeOf()); + + 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) + byte[] chunkData = new byte[chunkHeader.size - 4]; + Array.Copy(fileData, (int)fileOffset, chunkData, 0, (int)(chunkHeader.size - 4)); + + uint 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(); + long chunkEnd = fileOffset + chunkHeader.size - 4; + + while(packetOffset < chunkEnd) + { + if(packetOffset + 4 > fileData.Length) break; + + uint 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() > fileData.Length) + return ErrorNumber.InvalidArgument; + + HxCStreamMetadataHeader metadataHeader = + Marshal.ByteArrayToStructureLittleEndian(fileData, + (int)packetOffset, + Marshal.SizeOf()); + + AaruLogging.Debug(MODULE_NAME, "Metadata packet: type = 0x{0:X8}, payloadSize = {1}", + metadataHeader.type, metadataHeader.payloadSize); + + if(packetOffset + Marshal.SizeOf() + metadataHeader.payloadSize > + fileData.Length) + return ErrorNumber.InvalidArgument; + + byte[] metadataBytes = new byte[metadataHeader.payloadSize]; + Array.Copy(fileData, + (int)packetOffset + Marshal.SizeOf(), + 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() + metadataHeader.payloadSize; + + // Align to 4 bytes + if(packetOffset % 4 != 0) packetOffset += 4 - packetOffset % 4; + + break; + } + case 0x1: // Packed IO stream + { + if(packetOffset + Marshal.SizeOf() > fileData.Length) + return ErrorNumber.InvalidArgument; + + HxCStreamPackedIoHeader ioHeader = + Marshal.ByteArrayToStructureLittleEndian(fileData, + (int)packetOffset, + Marshal.SizeOf()); + + 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() + ioHeader.packedSize > + fileData.Length) + return ErrorNumber.InvalidArgument; + + byte[] packedData = new byte[ioHeader.packedSize]; + Array.Copy(fileData, + (int)packetOffset + Marshal.SizeOf(), + packedData, + 0, + (int)ioHeader.packedSize); + + byte[] 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(int i = 0; i < unpackedData.Length; i += 2) + { + if(i + 1 < unpackedData.Length) + ioStream.Add(BitConverter.ToUInt16(unpackedData, i)); + } + + packetOffset += Marshal.SizeOf() + ioHeader.packedSize; + + // Align to 4 bytes + if(packetOffset % 4 != 0) packetOffset += 4 - packetOffset % 4; + + break; + } + case 0x2: // Packed flux stream + { + if(packetOffset + Marshal.SizeOf() > fileData.Length) + return ErrorNumber.InvalidArgument; + + HxCStreamPackedStreamHeader streamHeader = + Marshal.ByteArrayToStructureLittleEndian(fileData, + (int)packetOffset, + Marshal.SizeOf()); + + 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() + streamHeader.packedSize > + fileData.Length) + return ErrorNumber.InvalidArgument; + + byte[] packedData = new byte[streamHeader.packedSize]; + Array.Copy(fileData, + (int)packetOffset + Marshal.SizeOf(), + packedData, + 0, + (int)streamHeader.packedSize); + + byte[] 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); + + fluxPulses.AddRange(pulses); + + packetOffset += Marshal.SizeOf() + 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 index signals from IO stream + var indexPositions = new List(); + + if(ioStream.Count > 0) + { + IoStreamState previousState = DecodeIoStreamValue(ioStream[0]); + bool oldIndex = previousState.IndexSignal; + uint totalTicks = 0; + int pulseIndex = 0; + + for(int 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 + uint 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; + } + + /// + 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; + } + + /// + 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; + } + + /// + 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; + } + + /// + 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; + } + + /// + 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; + } + + /// + 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 { 0 }; + 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; + } + + /// + 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(); + + 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; + + // HxCStream 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 HxCStream 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: 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 + + /// + public ErrorNumber ReadMediaTag(MediaTagType tag, out byte[] buffer) => throw new NotImplementedException(); + + /// + public ErrorNumber ReadSector(ulong sectorAddress, bool negative, out byte[] buffer, out SectorStatus sectorStatus) + { + buffer = null; + sectorStatus = SectorStatus.NotDumped; + return ErrorNumber.NotImplemented; + } + + /// + public ErrorNumber ReadSectorLong(ulong sectorAddress, bool negative, out byte[] buffer, + out SectorStatus sectorStatus) + { + buffer = null; + sectorStatus = SectorStatus.NotDumped; + return ErrorNumber.NotImplemented; + } + + /// + public ErrorNumber ReadSectors(ulong sectorAddress, bool negative, uint length, out byte[] buffer, + out SectorStatus[] sectorStatus) + { + buffer = null; + sectorStatus = null; + return ErrorNumber.NotImplemented; + } + + /// + public ErrorNumber ReadSectorsLong(ulong sectorAddress, bool negative, uint length, out byte[] buffer, + out SectorStatus[] sectorStatus) + { + buffer = null; + sectorStatus = null; + return ErrorNumber.NotImplemented; + } + + /// + public ErrorNumber ReadSectorsTag(ulong sectorAddress, bool negative, uint length, SectorTagType tag, + out byte[] buffer) + { + buffer = null; + return ErrorNumber.NotImplemented; + } + + /// + public ErrorNumber ReadSectorTag(ulong sectorAddress, bool negative, SectorTagType tag, out byte[] buffer) + { + buffer = null; + return ErrorNumber.NotImplemented; + } + +#endregion +} diff --git a/Aaru.Images/HxCStream/Structs.cs b/Aaru.Images/HxCStream/Structs.cs new file mode 100644 index 000000000..963353c20 --- /dev/null +++ b/Aaru.Images/HxCStream/Structs.cs @@ -0,0 +1,206 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Structs.cs +// Author(s) : Rebecca Wallander +// +// Component : Disk image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Contains structures for 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 . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Rebecca Wallander +// ****************************************************************************/ + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace Aaru.Images; + +[SuppressMessage("ReSharper", "UnusedType.Global")] +public sealed partial class HxCStream +{ +#region Nested type: HxCStreamChunkHeader + + /// + /// Represents a chunk header in an HxCStream file. A chunk is a container that holds + /// multiple packet blocks (metadata, IO stream, flux stream) and ends with a CRC32 checksum. + /// Each track file can contain multiple chunks, allowing data to be split across chunks. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct HxCStreamChunkHeader + { + /// Chunk signature, always "CHKH" (0x43, 0x48, 0x4B, 0x48) + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] signature; + /// Total size of the chunk including header, all packet blocks, and CRC32 (4 bytes) + public uint size; + /// Packet number, used for sequencing chunks + public uint packetNumber; + } + +#endregion + +#region Nested type: HxCStreamChunkBlockHeader + + /// + /// Base header structure for packet blocks within a chunk. This is the common header + /// that all packet types share. The actual packet headers extend this with additional fields. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct HxCStreamChunkBlockHeader + { + /// Packet type identifier (0x0 = metadata, 0x1 = IO stream, 0x2 = flux stream) + public uint type; + /// Size of the packet payload data (excluding this header) + public uint payloadSize; + } + +#endregion + +#region Nested type: HxCStreamMetadataHeader + + /// + /// Header for a metadata packet block (type 0x0). Contains text-based metadata information + /// such as sample rate, IO channel names, etc. The payload is UTF-8 encoded text. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct HxCStreamMetadataHeader + { + /// Packet type, always 0x0 for metadata + public uint type; + /// Size of the metadata text payload in bytes + public uint payloadSize; + } + +#endregion + +#region Nested type: HxCStreamPackedIoHeader + + /// + /// Header for a packed IO stream packet block (type 0x1). Contains LZ4-compressed + /// 16-bit IO values representing index signals, write protect status, and other + /// IO channel states. Also, see for more information. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct HxCStreamPackedIoHeader + { + /// Packet type, always 0x1 for packed IO stream + public uint type; + /// Total size of the packet including this header and packed data + public uint payloadSize; + /// Size of the LZ4-compressed data in bytes + public uint packedSize; + /// Size of the uncompressed data in bytes (should be even, as it's 16-bit values) + public uint unpackedSize; + } + +#endregion + +#region Nested type: HxCStreamPackedStreamHeader + + /// + /// Header for a packed flux stream packet block (type 0x2). Contains LZ4-compressed + /// variable-length encoded flux pulse data. The pulses represent time intervals between + /// flux reversals, encoded using a variable-length encoding scheme to save space. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct HxCStreamPackedStreamHeader + { + /// Packet type, always 0x2 for packed flux stream + public uint type; + /// Total size of the packet including this header and packed data + public uint payloadSize; + /// Size of the LZ4-compressed data in bytes + public uint packedSize; + /// Size of the uncompressed variable-length encoded pulse data in bytes + public uint unpackedSize; + /// Number of flux pulses in this packet (may be updated during decoding) + public uint numberOfPulses; + } + +#endregion + +#region Nested type: IoStreamState + + /// + /// Represents the decoded state of a 16-bit IO stream value from an HxCStream file. + /// The IO stream contains signals sampled at regular intervals. Currently, only the + /// index signal (bit 0) and write protect (bit 5) are used. The raw value is preserved + /// for future extensions and can be accessed to check other bits if needed. + /// + public struct IoStreamState + { + /// + /// Raw 16-bit IO value. Use this to access other bits that may be defined in the future. + /// Bits can be checked using: (RawValue & bitMask) != 0 + /// + public ushort RawValue { get; set; } + + /// Index signal state (bit 0). True when index signal is active/high. + public bool IndexSignal => (RawValue & 0x01) != 0; + + /// Write protect state (bit 5). True when write protect is active. + public bool WriteProtect => (RawValue & 0x20) != 0; + + /// + /// Creates an IoStreamState from a raw 16-bit value + /// + /// The raw 16-bit IO stream value + /// Decoded IO stream state + public static IoStreamState FromRawValue(ushort rawValue) => new() { RawValue = rawValue }; + } + +#endregion + +#region Nested type: TrackCapture + + /// + /// Represents a complete flux capture for a single track. Contains the decoded flux pulse + /// data, index signal positions, and resolution information. This is the internal representation + /// used to store parsed track data from HxCStream files. + /// + public class TrackCapture + { + public uint head; + public ushort track; + /// + /// Resolution (sample rate) of the flux capture in picoseconds. + /// Default is 40,000 picoseconds (40 nanoseconds = 25 MHz sample rate). + /// Can be 20,000 picoseconds (20 nanoseconds = 50 MHz sample rate) if metadata indicates 50 MHz. + /// + public uint resolution; + /// + /// Array of flux pulse intervals in ticks. Each value represents the time interval + /// between flux reversals, measured in resolution units (picoseconds). + /// + public uint[] fluxPulses; + /// + /// Array of index positions. Each value is an index into the fluxPulses array + /// indicating where an index signal occurs. These positions are extracted from + /// the IO stream (bit 0 transitions) and mapped to flux stream positions. + /// + public uint[] indexPositions; + } + +#endregion +} \ No newline at end of file diff --git a/Aaru.Images/HxCStream/Verify.cs b/Aaru.Images/HxCStream/Verify.cs new file mode 100644 index 000000000..a4989c3d9 --- /dev/null +++ b/Aaru.Images/HxCStream/Verify.cs @@ -0,0 +1,90 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Verify.cs +// Author(s) : Rebecca Wallander +// +// Component : Disk image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Verifies HxC Stream flux images. +// +// --[ License ] -------------------------------------------------------------- +// +// This library is free software; you can redistribute it and it 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 Rebecca Wallander +// ****************************************************************************/ + +using System; +using System.IO; +using System.Linq; +using Aaru.Checksums; +using Aaru.Helpers; + +namespace Aaru.Images; + +public sealed partial class HxCStream +{ + public bool? VerifyMediaImage() + { + if(_trackCaptures == null || _trackCaptures.Count == 0) return null; + + if(_trackFilePaths == null || _trackFilePaths.Count == 0) return null; + + // Verify CRC32 checksums in all track files + foreach(string trackfile in _trackFilePaths) + { + if(!File.Exists(trackfile)) return false; + + using FileStream fileStream = File.OpenRead(trackfile); + byte[] fileData = new byte[fileStream.Length]; + fileStream.EnsureRead(fileData, 0, (int)fileStream.Length); + + long fileOffset = 0; + + while(fileOffset < fileData.Length) + { + if(fileOffset + Marshal.SizeOf() > fileData.Length) return false; + + HxCStreamChunkHeader chunkHeader = Marshal.ByteArrayToStructureLittleEndian( + fileData, (int)fileOffset, Marshal.SizeOf()); + + if(!_hxcStreamSignature.SequenceEqual(chunkHeader.signature)) return false; + + if(chunkHeader.size > fileData.Length - fileOffset) return false; + + // Verify CRC32 - calculate CRC of chunk data (excluding the CRC itself) + byte[] chunkData = new byte[chunkHeader.size - 4]; + Array.Copy(fileData, (int)fileOffset, chunkData, 0, (int)(chunkHeader.size - 4)); + + uint storedCrc = BitConverter.ToUInt32(fileData, (int)(fileOffset + chunkHeader.size - 4)); + + if(!VerifyChunkCrc32(chunkData, storedCrc)) return false; + + fileOffset += chunkHeader.size; + } + } + + // Basic verification: check that all track captures have valid data + foreach(TrackCapture capture in _trackCaptures) + { + if(capture.fluxPulses == null || capture.fluxPulses.Length == 0) return false; + + if(capture.resolution == 0) return false; + } + + return true; + } +} \ No newline at end of file diff --git a/Aaru.Images/Localization/Localization.Designer.cs b/Aaru.Images/Localization/Localization.Designer.cs index bf5e86689..0a19c19bf 100644 --- a/Aaru.Images/Localization/Localization.Designer.cs +++ b/Aaru.Images/Localization/Localization.Designer.cs @@ -5907,5 +5907,11 @@ namespace Aaru.Images { return ResourceManager.GetString("Overflow_sectors_are_not_supported", resourceCulture); } } + + internal static string HxCStream_Name { + get { + return ResourceManager.GetString("HxCStream_Name", resourceCulture); + } + } } } diff --git a/Aaru.Images/Localization/Localization.resx b/Aaru.Images/Localization/Localization.resx index bbf07e24f..41a313e5c 100644 --- a/Aaru.Images/Localization/Localization.resx +++ b/Aaru.Images/Localization/Localization.resx @@ -2965,4 +2965,7 @@ Overflow sectors are not supported. + + HxCStream + \ No newline at end of file