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