diff --git a/Aaru.Images/Localization/Localization.Designer.cs b/Aaru.Images/Localization/Localization.Designer.cs
index a2eaf0764..e0d7152cf 100644
--- a/Aaru.Images/Localization/Localization.Designer.cs
+++ b/Aaru.Images/Localization/Localization.Designer.cs
@@ -3987,6 +3987,72 @@ namespace Aaru.Images {
}
}
+ internal static string Sxd_Name {
+ get {
+ return ResourceManager.GetString("Sxd_Name", resourceCulture);
+ }
+ }
+
+ internal static string Sxd_header_CRC_mismatch {
+ get {
+ return ResourceManager.GetString("Sxd_header_CRC_mismatch", resourceCulture);
+ }
+ }
+
+ internal static string Sxd_image_is_password_protected_and_unsupported {
+ get {
+ return ResourceManager.GetString("Sxd_image_is_password_protected_and_unsupported", resourceCulture);
+ }
+ }
+
+ internal static string Sxd_image_has_invalid_geometry {
+ get {
+ return ResourceManager.GetString("Sxd_image_has_invalid_geometry", resourceCulture);
+ }
+ }
+
+ internal static string Sxd_inconsistent_track_length {
+ get {
+ return ResourceManager.GetString("Sxd_inconsistent_track_length", resourceCulture);
+ }
+ }
+
+ internal static string Sxd_comment_CRC_mismatch_discarded {
+ get {
+ return ResourceManager.GetString("Sxd_comment_CRC_mismatch_discarded", resourceCulture);
+ }
+ }
+
+ internal static string Sxd_track_decompression_failed_0 {
+ get {
+ return ResourceManager.GetString("Sxd_track_decompression_failed_0", resourceCulture);
+ }
+ }
+
+ internal static string Sxd_track_compressed_length_out_of_range_0 {
+ get {
+ return ResourceManager.GetString("Sxd_track_compressed_length_out_of_range_0", resourceCulture);
+ }
+ }
+
+ internal static string Sxd_image_contains_a_disk_of_type_0 {
+ get {
+ return ResourceManager.GetString("Sxd_image_contains_a_disk_of_type_0", resourceCulture);
+ }
+ }
+
+ internal static string Sxd_comments_0 {
+ get {
+ return ResourceManager.GetString("Sxd_comments_0", resourceCulture);
+ }
+ }
+
+ internal static string Sxd_all_track_CRCs_match_0 {
+ get {
+ return ResourceManager.GetString("Sxd_all_track_CRCs_match_0", resourceCulture);
+ }
+ }
+
internal static string Calculated_header_checksum_equals_0_X2_1 {
get {
return ResourceManager.GetString("Calculated_header_checksum_equals_0_X2_1", resourceCulture);
diff --git a/Aaru.Images/Localization/Localization.es.resx b/Aaru.Images/Localization/Localization.es.resx
index fda6d75ab..d7684a434 100644
--- a/Aaru.Images/Localization/Localization.es.resx
+++ b/Aaru.Images/Localization/Localization.es.resx
@@ -487,6 +487,39 @@
Comentarios DXP: {0}
+
+
+ Disco Auto-extraíble Sydex CopyQM+
+
+
+ El CRC de la cabecera SXD no coincide; la imagen está dañada o no es SXD.
+
+
+ La imagen SXD está protegida por contraseña; el descifrado no está soportado.
+
+
+ La imagen SXD indica una geometría de disco inválida.
+
+
+ La longitud de pista de la cabecera SXD es inconsistente con los sectores por pista.
+
+
+ El CRC del comentario SXD no coincide; comentario descartado.
+
+
+ Falló la descompresión de la pista SXD {0}.
+
+
+ La pista SXD tiene una longitud comprimida fuera de rango {0}.
+
+
+ La imagen SXD contiene un disco de tipo {0}
+
+
+ Comentarios SXD: {0}
+
+
+ CRCs de todas las pistas SXD validados = {0}
Texto de copyright: {0}
diff --git a/Aaru.Images/Localization/Localization.resx b/Aaru.Images/Localization/Localization.resx
index 64329ec3d..e349c29ad 100644
--- a/Aaru.Images/Localization/Localization.resx
+++ b/Aaru.Images/Localization/Localization.resx
@@ -2003,6 +2003,39 @@
DXP comments: {0}
+
+
+ Sydex CopyQM+ Self-eXtracting Disk
+
+
+ SXD header CRC does not match; image is corrupt or not an SXD image.
+
+
+ SXD image is password protected; decryption is not supported.
+
+
+ SXD image reports invalid disk geometry.
+
+
+ SXD header track length is inconsistent with sectors per track.
+
+
+ SXD comment CRC mismatch; comment discarded.
+
+
+ Decompression failed for SXD track {0}.
+
+
+ SXD track has out-of-range compressed length {0}.
+
+
+ SXD image contains a disk of type {0}
+
+
+ SXD comments: {0}
+
+
+ All SXD track CRCs validated = {0}
Calculated header checksum = 0x{0:X2}, {1}
diff --git a/Aaru.Images/SXD/Constants.cs b/Aaru.Images/SXD/Constants.cs
new file mode 100644
index 000000000..b855f2083
--- /dev/null
+++ b/Aaru.Images/SXD/Constants.cs
@@ -0,0 +1,77 @@
+// /***************************************************************************
+// Aaru Data Preservation Suite
+// ----------------------------------------------------------------------------
+//
+// Filename : Constants.cs
+// Author(s) : Natalia Portillo
+//
+// Component : Disk image plugins.
+//
+// --[ Description ] ----------------------------------------------------------
+//
+// Contains constants for Sydex CopyQM+ Self-eXtracting Disk (SXD) images.
+//
+// Based on the work of Michal Necasek (fdimg).
+//
+// --[ License ] --------------------------------------------------------------
+//
+// This library is free software; you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, see .
+//
+// ----------------------------------------------------------------------------
+// Copyright © 2011-2026 Natalia Portillo
+// ****************************************************************************/
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Aaru.Images;
+
+[SuppressMessage("ReSharper", "UnusedMember.Local")]
+public sealed partial class SXD
+{
+ /// SXD header signature bytes: 'S','X','D'.
+ static readonly byte[] _sxdSignature = [(byte)'S', (byte)'X', (byte)'D'];
+
+ /// Optional "WB" block signature bytes preceding the SXD payload.
+ static readonly byte[] _wbSignature = [(byte)'W', (byte)'B'];
+
+ /// Size of the SXD header, in bytes.
+ const int SXD_HEADER_SIZE = 33;
+
+ /// Number of header bytes protected by crc_hdr.
+ const int SXD_CRC_HDR_COVER = 31;
+
+ /// Reversed ANSI CRC-16 polynomial used by the SXD format.
+ const ushort SXD_CRC_POLY = 0xA001;
+
+ /// Maximum number of cylinders we will accept in a well-formed SXD image.
+ const int NTRK_MAX = 84;
+
+ /// All SXD tracks use 512-byte sectors.
+ const int SECTOR_SIZE = 512;
+
+ /// Fill byte used for tracks not present in the image (freshly formatted).
+ const byte FMT_BYTE = 0xF6;
+
+ /// SXD drive type codes as reported by the drv_typ header field.
+ enum SxdDriveType : byte
+ {
+ Unused = 0,
+ Floppy360K = 1,
+ Floppy12M = 2,
+ Floppy720K = 3,
+ Floppy144M = 4,
+ EightInch = 5,
+ Floppy288M = 6
+ }
+}
\ No newline at end of file
diff --git a/Aaru.Images/SXD/Identify.cs b/Aaru.Images/SXD/Identify.cs
new file mode 100644
index 000000000..c0b3770c1
--- /dev/null
+++ b/Aaru.Images/SXD/Identify.cs
@@ -0,0 +1,172 @@
+// /***************************************************************************
+// Aaru Data Preservation Suite
+// ----------------------------------------------------------------------------
+//
+// Filename : Identify.cs
+// Author(s) : Natalia Portillo
+//
+// Component : Disk image plugins.
+//
+// --[ Description ] ----------------------------------------------------------
+//
+// Identifies Sydex CopyQM+ Self-eXtracting Disk (SXD) images.
+//
+// Based on the work of Michal Necasek (fdimg).
+//
+// --[ License ] --------------------------------------------------------------
+//
+// This library is free software; you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, see .
+//
+// ----------------------------------------------------------------------------
+// Copyright © 2011-2026 Natalia Portillo
+// ****************************************************************************/
+
+using System;
+using System.IO;
+using Aaru.CommonTypes.Interfaces;
+using Aaru.Helpers;
+
+namespace Aaru.Images;
+
+public sealed partial class SXD
+{
+#region IMediaImage Members
+
+ ///
+ public bool Identify(IFilter imageFilter)
+ {
+ Stream stream = imageFilter.GetDataForkStream();
+
+ return FindHeaderOffset(stream) >= 0;
+ }
+
+#endregion
+
+ ///
+ /// Locate the SXD header within . SXD images are either "bare" (the 'SXD' signature at
+ /// offset 0) or wrapped in a DOS or DOS/OS-2 self-extracting executable stub. An optional "WB" block may sit
+ /// between the stub and the SXD payload.
+ ///
+ /// Offset of the SXD header in , or -1 if not found.
+ static long FindHeaderOffset(Stream stream)
+ {
+ if(stream.Length < SXD_HEADER_SIZE) return -1;
+
+ stream.Seek(0, SeekOrigin.Begin);
+
+ var first3 = new byte[3];
+
+ if(stream.EnsureRead(first3, 0, 3) != 3) return -1;
+
+ // Bare SXD image.
+ if(SignatureMatches(first3, _sxdSignature)) return 0;
+
+ // Otherwise must start with the 'MZ' DOS executable signature.
+ if(first3[0] != 'M' || first3[1] != 'Z') return -1;
+
+ long fileSize = stream.Length;
+
+ long hdrOfs = CalcSkip(stream, fileSize, 0);
+
+ if(hdrOfs < 0) return -1;
+
+ if(hdrOfs + SXD_HEADER_SIZE > fileSize) return -1;
+
+ stream.Seek(hdrOfs, SeekOrigin.Begin);
+
+ if(stream.EnsureRead(first3, 0, 3) != 3) return -1;
+
+ if(SignatureMatches(first3, _sxdSignature)) return hdrOfs;
+
+ // A 'WB' block may precede the actual SXD payload.
+ if(first3[0] != 'W' || first3[1] != 'B') return -1;
+
+ stream.Seek(hdrOfs, SeekOrigin.Begin);
+
+ hdrOfs = CalcSkip(stream, fileSize, hdrOfs);
+
+ if(hdrOfs < 0) return -1;
+
+ if(hdrOfs + SXD_HEADER_SIZE > fileSize) return -1;
+
+ stream.Seek(hdrOfs, SeekOrigin.Begin);
+
+ if(stream.EnsureRead(first3, 0, 3) != 3) return -1;
+
+ return SignatureMatches(first3, _sxdSignature) ? hdrOfs : -1;
+ }
+
+ ///
+ /// Skip a DOS-style stub/block by reading its MZ-header length (bytes 2..3 = last-block bytes, bytes 4..5 =
+ /// number of 512-byte blocks) from the current position, and computing the absolute offset of what follows.
+ ///
+ static long CalcSkip(Stream stream, long fileSize, long startOfs)
+ {
+ // The caller already consumed the first two bytes of the stub/block signature; skip to offset 2 of it.
+ stream.Seek(startOfs + 2, SeekOrigin.Begin);
+
+ var word = new byte[2];
+
+ if(stream.EnsureRead(word, 0, 2) != 2) return -1;
+
+ var lastBlkSz = (ushort)(word[0] | word[1] << 8);
+
+ if(lastBlkSz > 512) return -1;
+
+ if(stream.EnsureRead(word, 0, 2) != 2) return -1;
+
+ var nBlks = (ushort)(word[0] | word[1] << 8);
+
+ if(nBlks > 256) return -1;
+
+ long skipSz = nBlks * 512L - (512 - lastBlkSz);
+
+ if(skipSz <= 0 || skipSz > fileSize) return -1;
+
+ return startOfs + skipSz;
+ }
+
+ static bool SignatureMatches(ReadOnlySpan data, byte[] expected)
+ {
+ if(data.Length < expected.Length) return false;
+
+ for(var i = 0; i < expected.Length; i++)
+ {
+ if(data[i] != expected[i]) return false;
+ }
+
+ return true;
+ }
+
+ /// Compute the SXD CRC-16 (reversed poly 0xA001, initial value 0, non-inverted) over .
+ static ushort SxdCrc16(ReadOnlySpan data)
+ {
+ ushort crc = 0;
+
+ for(var i = 0; i < data.Length; i++)
+ {
+ crc ^= data[i];
+
+ for(var b = 0; b < 8; b++)
+ {
+ if((crc & 1) != 0)
+ crc = (ushort)(crc >> 1 ^ SXD_CRC_POLY);
+ else
+ crc = (ushort)(crc >> 1);
+ }
+ }
+
+ return crc;
+ }
+}
\ No newline at end of file
diff --git a/Aaru.Images/SXD/Lzh.cs b/Aaru.Images/SXD/Lzh.cs
new file mode 100644
index 000000000..2f16993d6
--- /dev/null
+++ b/Aaru.Images/SXD/Lzh.cs
@@ -0,0 +1,356 @@
+// /***************************************************************************
+// Aaru Data Preservation Suite
+// ----------------------------------------------------------------------------
+//
+// Filename : Lzh.cs
+// Author(s) : Natalia Portillo
+//
+// Component : Disk image plugins.
+//
+// --[ Description ] ----------------------------------------------------------
+//
+// LZHUF / LH1-variant decompressor used by Sydex CopyQM+ Self-eXtracting
+// Disk (SXD) images. Plugin-local, buffer based, single pass.
+//
+// Based on the work of Michal Necasek (fdimg), which is itself adapted
+// from LZHUF by Haruyasu Yoshizaki (1988), as published in the English
+// port edited by Kenji Rikitake. The only difference from the standard
+// LH1 algorithm is that the sliding text buffer is pre-filled with zero
+// bytes instead of spaces.
+//
+// --[ License ] --------------------------------------------------------------
+//
+// This library is free software; you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, see .
+//
+// ----------------------------------------------------------------------------
+// Copyright © 2011-2026 Natalia Portillo
+// ****************************************************************************/
+
+namespace Aaru.Images;
+
+public sealed partial class SXD
+{
+ /// SXD LH1/LZHUF decompressor. Buffer based, single-shot; reused per image.
+ sealed class Lzh
+ {
+ // LZSS parameters.
+ const int N = 4096;
+ const int F = 60;
+ const int THRESHOLD = 2;
+
+ // Huffman coding parameters.
+ const int N_CHAR = 256 - THRESHOLD + F;
+ const int T = N_CHAR * 2 - 1;
+ const int R = T - 1;
+ const int MAX_FREQ = 0x8000;
+
+ // Tables for decoding upper 6 bits of sliding dictionary pointer.
+ static readonly byte[] _dCode =
+ [
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x08, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A,
+ 0x0B, 0x0B, 0x0B, 0x0B, 0x0B, 0x0B, 0x0B, 0x0B, 0x0C, 0x0C, 0x0C, 0x0C, 0x0D, 0x0D, 0x0D, 0x0D, 0x0E,
+ 0x0E, 0x0E, 0x0E, 0x0F, 0x0F, 0x0F, 0x0F, 0x10, 0x10, 0x10, 0x10, 0x11, 0x11, 0x11, 0x11, 0x12, 0x12,
+ 0x12, 0x12, 0x13, 0x13, 0x13, 0x13, 0x14, 0x14, 0x14, 0x14, 0x15, 0x15, 0x15, 0x15, 0x16, 0x16, 0x16,
+ 0x16, 0x17, 0x17, 0x17, 0x17, 0x18, 0x18, 0x19, 0x19, 0x1A, 0x1A, 0x1B, 0x1B, 0x1C, 0x1C, 0x1D, 0x1D,
+ 0x1E, 0x1E, 0x1F, 0x1F, 0x20, 0x20, 0x21, 0x21, 0x22, 0x22, 0x23, 0x23, 0x24, 0x24, 0x25, 0x25, 0x26,
+ 0x26, 0x27, 0x27, 0x28, 0x28, 0x29, 0x29, 0x2A, 0x2A, 0x2B, 0x2B, 0x2C, 0x2C, 0x2D, 0x2D, 0x2E, 0x2E,
+ 0x2F, 0x2F, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E,
+ 0x3F
+ ];
+
+ static readonly byte[] _dLen =
+ [
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x08
+ ];
+
+ readonly ushort[] _freq = new ushort[T + 1];
+ readonly int[] _prnt = new int[T + N_CHAR];
+ readonly short[] _son = new short[T];
+ readonly byte[] _textBuf = new byte[N + F - 1];
+
+ ushort _getbuf;
+ byte _getlen;
+
+ byte[] _in;
+ int _inEnd;
+ int _inPos;
+
+ byte[] _out;
+ int _outEnd;
+ int _outPos;
+
+ ///
+ /// Decompress a single SXD LH1 block from into at
+ /// .
+ ///
+ /// Compressed track data.
+ /// Number of valid bytes in .
+ /// Destination buffer.
+ /// Offset in at which to write the uncompressed data.
+ /// Expected uncompressed size.
+ /// Number of bytes written. Should equal on success.
+ public int Decode(byte[] src, int srcLength, byte[] dst, int dstOfs, int dstLength)
+ {
+ _in = src;
+ _inPos = 0;
+ _inEnd = srcLength;
+ _out = dst;
+ _outPos = dstOfs;
+ _outEnd = dstOfs + dstLength;
+ _getbuf = 0;
+ _getlen = 0;
+
+ StartHuff();
+
+ // Pre-initialize text buffer with zero bytes (the key difference from LH1 used in LHarc).
+ for(var i = 0; i < N - F; i++) _textBuf[i] = 0;
+
+ int r = N - F;
+ var count = 0;
+
+ while(count < dstLength)
+ {
+ int c = DecodeChar();
+
+ if(c < 256)
+ {
+ WriteByte((byte)c);
+ _textBuf[r++] = (byte)c;
+ r &= N - 1;
+ count++;
+ }
+ else
+ {
+ int i = r - DecodePosition() - 1 & N - 1;
+ int j = c - 255 + THRESHOLD;
+
+ for(var k = 0; k < j; k++)
+ {
+ byte b = _textBuf[i + k & N - 1];
+ WriteByte(b);
+ _textBuf[r++] = b;
+ r &= N - 1;
+ count++;
+
+ if(count >= dstLength) break;
+ }
+ }
+ }
+
+ return count;
+ }
+
+ int ReadByte()
+ {
+ if(_inPos < _inEnd) return _in[_inPos++];
+
+ return -1;
+ }
+
+ void WriteByte(byte c)
+ {
+ if(_outPos < _outEnd) _out[_outPos++] = c;
+ }
+
+ int GetBit()
+ {
+ while(_getlen <= 8)
+ {
+ int b = ReadByte();
+ if(b < 0) b = 0;
+ _getbuf |= (ushort)(b << 8 - _getlen);
+ _getlen += 8;
+ }
+
+ bool msb = (_getbuf & 0x8000) != 0;
+ _getbuf <<= 1;
+ _getlen--;
+
+ return msb ? 1 : 0;
+ }
+
+ int GetByte()
+ {
+ while(_getlen <= 8)
+ {
+ int b = ReadByte();
+ if(b < 0) b = 0;
+ _getbuf |= (ushort)(b << 8 - _getlen);
+ _getlen += 8;
+ }
+
+ int i = _getbuf;
+ _getbuf <<= 8;
+ _getlen -= 8;
+
+ return (ushort)i >> 8;
+ }
+
+ void StartHuff()
+ {
+ int i;
+
+ for(i = 0; i < N_CHAR; i++)
+ {
+ _freq[i] = 1;
+ _son[i] = (short)(i + T);
+ _prnt[i + T] = i;
+ }
+
+ i = 0;
+ int j = N_CHAR;
+
+ while(j <= R)
+ {
+ _freq[j] = (ushort)(_freq[i] + _freq[i + 1]);
+ _son[j] = (short)i;
+ _prnt[i] = _prnt[i + 1] = j;
+ i += 2;
+ j++;
+ }
+
+ _freq[T] = 0xFFFF;
+ _prnt[R] = 0;
+ }
+
+ void Reconst()
+ {
+ int i, j, k;
+
+ j = 0;
+
+ for(i = 0; i < T; i++)
+ {
+ if(_son[i] >= T)
+ {
+ _freq[j] = (ushort)((_freq[i] + 1) / 2);
+ _son[j] = _son[i];
+ j++;
+ }
+ }
+
+ for(i = 0, j = N_CHAR; j < T; i += 2, j++)
+ {
+ k = i + 1;
+ ushort f = _freq[j] = (ushort)(_freq[i] + _freq[k]);
+
+ for(k = j - 1; f < _freq[k]; k--) {}
+
+ k++;
+
+ // Shift freq[k..j-1] right by one.
+ for(int m = j; m > k; m--) _freq[m] = _freq[m - 1];
+ _freq[k] = f;
+ for(int m = j; m > k; m--) _son[m] = _son[m - 1];
+ _son[k] = (short)i;
+ }
+
+ for(i = 0; i < T; i++)
+ {
+ k = _son[i];
+
+ if(k >= T)
+ _prnt[k] = i;
+ else
+ _prnt[k] = _prnt[k + 1] = i;
+ }
+ }
+
+ void Update(int c)
+ {
+ if(_freq[R] == MAX_FREQ) Reconst();
+
+ c = _prnt[c + T];
+
+ do
+ {
+ int k = ++_freq[c];
+ int l = c + 1;
+
+ if(k > _freq[l])
+ {
+ while(k > _freq[++l]) {}
+
+ l--;
+ _freq[c] = _freq[l];
+ _freq[l] = (ushort)k;
+
+ int i = _son[c];
+ _prnt[i] = l;
+ if(i < T) _prnt[i + 1] = l;
+
+ int jj = _son[l];
+ _son[l] = (short)i;
+ _prnt[jj] = c;
+ if(jj < T) _prnt[jj + 1] = c;
+ _son[c] = (short)jj;
+
+ c = l;
+ }
+
+ c = _prnt[c];
+ } while(c != 0);
+ }
+
+ int DecodeChar()
+ {
+ int c = _son[R];
+
+ while(c < T)
+ {
+ c += GetBit();
+ c = _son[c];
+ }
+
+ c -= T;
+ Update(c);
+
+ return c;
+ }
+
+ int DecodePosition()
+ {
+ int i = GetByte();
+ int c = _dCode[i] << 6;
+ int j = _dLen[i];
+
+ j -= 2;
+ while(j-- != 0) i = (i << 1) + GetBit();
+
+ return c | i & 0x3F;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Aaru.Images/SXD/Open.cs b/Aaru.Images/SXD/Open.cs
new file mode 100644
index 000000000..ac91f3bd6
--- /dev/null
+++ b/Aaru.Images/SXD/Open.cs
@@ -0,0 +1,339 @@
+// /***************************************************************************
+// Aaru Data Preservation Suite
+// ----------------------------------------------------------------------------
+//
+// Filename : Open.cs
+// Author(s) : Natalia Portillo
+//
+// Component : Disk image plugins.
+//
+// --[ Description ] ----------------------------------------------------------
+//
+// Opens Sydex CopyQM+ Self-eXtracting Disk (SXD) images.
+//
+// Based on the work of Michal Necasek (fdimg).
+//
+// --[ License ] --------------------------------------------------------------
+//
+// This library is free software; you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, see .
+//
+// ----------------------------------------------------------------------------
+// Copyright © 2011-2026 Natalia Portillo
+// ****************************************************************************/
+
+using System;
+using System.IO;
+using System.Text;
+using Aaru.CommonTypes;
+using Aaru.CommonTypes.Enums;
+using Aaru.CommonTypes.Interfaces;
+using Aaru.Helpers;
+using Aaru.Logging;
+
+namespace Aaru.Images;
+
+public sealed partial class SXD
+{
+#region IMediaImage Members
+
+ ///
+ public ErrorNumber Open(IFilter imageFilter)
+ {
+ Stream stream = imageFilter.GetDataForkStream();
+
+ long hdrOfs = FindHeaderOffset(stream);
+
+ if(hdrOfs < 0) return ErrorNumber.InvalidArgument;
+
+ _headerOffset = hdrOfs;
+
+ var rawHeader = new byte[SXD_HEADER_SIZE];
+ stream.Seek(hdrOfs, SeekOrigin.Begin);
+
+ if(stream.EnsureRead(rawHeader, 0, SXD_HEADER_SIZE) != SXD_HEADER_SIZE) return ErrorNumber.InvalidArgument;
+
+ _header = Marshal.ByteArrayToStructureLittleEndian(rawHeader);
+
+ ushort computedHdrCrc = SxdCrc16(rawHeader.AsSpan(0, SXD_CRC_HDR_COVER));
+
+ AaruLogging.Debug(MODULE_NAME, "header.trk_len = {0}", _header.trk_len);
+ AaruLogging.Debug(MODULE_NAME, "header.n_trk_sec = {0}", _header.n_trk_sec);
+ AaruLogging.Debug(MODULE_NAME, "header.n_heads = {0}", _header.n_heads);
+ AaruLogging.Debug(MODULE_NAME, "header.n_cyl_dsk = {0}", _header.n_cyl_dsk);
+ AaruLogging.Debug(MODULE_NAME, "header.n_cyl_img = {0}", _header.n_cyl_img);
+ AaruLogging.Debug(MODULE_NAME, "header.first_sid = {0}", _header.first_sid);
+ AaruLogging.Debug(MODULE_NAME, "header.interleave = {0}", _header.interleave);
+ AaruLogging.Debug(MODULE_NAME, "header.skew = {0}", _header.skew);
+ AaruLogging.Debug(MODULE_NAME, "header.sec_sz_code = {0}", _header.sec_sz_code);
+ AaruLogging.Debug(MODULE_NAME, "header.drv_typ = {0} ({1})", _header.drv_typ, (SxdDriveType)_header.drv_typ);
+ AaruLogging.Debug(MODULE_NAME, "header.density = {0} ({1})", _header.density, DensityName(_header.density));
+ AaruLogging.Debug(MODULE_NAME, "header.pwd_crc = 0x{0:X4}", _header.pwd_crc);
+ AaruLogging.Debug(MODULE_NAME, "header.pwd_hash = 0x{0:X4}", _header.pwd_hash);
+ AaruLogging.Debug(MODULE_NAME, "header.comment_len = {0}", _header.comment_len);
+ AaruLogging.Debug(MODULE_NAME, "header.crc_comment = 0x{0:X4}", _header.crc_comment);
+
+ AaruLogging.Debug(MODULE_NAME,
+ "header.unk0 = {0:X2} {1:X2} {2:X2} {3:X2}",
+ _header.unk0[0],
+ _header.unk0[1],
+ _header.unk0[2],
+ _header.unk0[3]);
+
+ AaruLogging.Debug(MODULE_NAME, "header.dos_time = 0x{0:X4}", _header.dos_time);
+ AaruLogging.Debug(MODULE_NAME, "header.dos_date = 0x{0:X4}", _header.dos_date);
+
+ AaruLogging.Debug(MODULE_NAME,
+ "header.crc_hdr = 0x{0:X4} (computed 0x{1:X4})",
+ _header.crc_hdr,
+ computedHdrCrc);
+
+ if(computedHdrCrc != _header.crc_hdr)
+ {
+ AaruLogging.Error(Localization.Sxd_header_CRC_mismatch);
+
+ return ErrorNumber.InvalidArgument;
+ }
+
+ // Password-protected images cannot be decoded; the scheme is not documented.
+ if(_header.pwd_crc != 0 || _header.pwd_hash != 0)
+ {
+ AaruLogging.Error(Localization.Sxd_image_is_password_protected_and_unsupported);
+
+ return ErrorNumber.NotSupported;
+ }
+
+ byte spt = _header.n_trk_sec;
+ byte heads = _header.n_heads;
+ byte cylsDsk = _header.n_cyl_dsk;
+ byte cylsImg = _header.n_cyl_img;
+
+ // Basic sanity checks matching fdimg's sxd_open().
+ if(spt == 0 || heads is 0 or > 2 || cylsDsk == 0 || cylsImg == 0 || cylsDsk > NTRK_MAX || cylsImg > cylsDsk)
+ {
+ AaruLogging.Error(Localization.Sxd_image_has_invalid_geometry);
+
+ return ErrorNumber.InvalidArgument;
+ }
+
+ if(_header.trk_len != spt * SECTOR_SIZE)
+ {
+ AaruLogging.Error(Localization.Sxd_inconsistent_track_length);
+
+ return ErrorNumber.InvalidArgument;
+ }
+
+ int trackLen = spt * SECTOR_SIZE;
+
+ // Read optional comment following the header.
+ long dataOffset = _headerOffset + SXD_HEADER_SIZE + _header.comment_len;
+
+ if(_header.comment_len > 0)
+ {
+ var commentBytes = new byte[_header.comment_len];
+ stream.Seek(_headerOffset + SXD_HEADER_SIZE, SeekOrigin.Begin);
+
+ if(stream.EnsureRead(commentBytes, 0, _header.comment_len) != _header.comment_len)
+ return ErrorNumber.InvalidArgument;
+
+ ushort commentCrc = SxdCrc16(commentBytes);
+
+ if(commentCrc == _header.crc_comment)
+ {
+ // Replace embedded nulls with newlines, mirroring fdimg.
+ for(var i = 0; i < commentBytes.Length; i++)
+ {
+ if(commentBytes[i] == 0) commentBytes[i] = (byte)'\n';
+ }
+
+ _imageInfo.Comments = Encoding.GetEncoding("ibm437").GetString(commentBytes);
+ }
+ else
+ AaruLogging.Debug(MODULE_NAME, Localization.Sxd_comment_CRC_mismatch_discarded);
+ }
+
+ // Allocate flat disk buffer sized to the full physical disk and pre-fill with the freshly-formatted byte.
+ long totalSectors = (long)cylsDsk * heads * spt;
+ _decodedDisk = new byte[totalSectors * SECTOR_SIZE];
+ Array.Fill(_decodedDisk, FMT_BYTE);
+
+ // Decompress every track stored in the image into its slot in the flat buffer.
+ int tracksInImage = cylsImg * heads;
+ Lzh lzh = null;
+ var cmprBuf = new byte[trackLen + 128];
+ var allCrcsOk = true;
+
+ stream.Seek(dataOffset, SeekOrigin.Begin);
+
+ for(var trkIdx = 0; trkIdx < tracksInImage; trkIdx++)
+ {
+ long dstOfs = (long)trkIdx * trackLen;
+
+ ErrorNumber err = DecompressTrack(stream,
+ _decodedDisk,
+ (int)dstOfs,
+ trackLen,
+ cmprBuf,
+ ref lzh,
+ out bool trackCrcOk);
+
+ if(err != ErrorNumber.NoError)
+ {
+ AaruLogging.Error(Localization.Sxd_track_decompression_failed_0, trkIdx);
+
+ return err;
+ }
+
+ if(!trackCrcOk) allCrcsOk = false;
+ }
+
+ _dataCrcOk = allCrcsOk;
+
+ // Populate ImageInfo.
+ _imageInfo.Application = "CopyQM+ / MAKESXD";
+ _imageInfo.CreationTime = DosDateTimeToUtc(_header.dos_date, _header.dos_time, imageFilter.CreationTime);
+ _imageInfo.LastModificationTime = _imageInfo.CreationTime;
+ _imageInfo.MediaTitle = imageFilter.Filename;
+ _imageInfo.ImageSize = (ulong)(stream.Length - dataOffset);
+ _imageInfo.Sectors = (ulong)totalSectors;
+ _imageInfo.SectorSize = SECTOR_SIZE;
+ _imageInfo.Cylinders = cylsDsk;
+ _imageInfo.Heads = heads;
+ _imageInfo.SectorsPerTrack = spt;
+ _imageInfo.MetadataMediaType = MetadataMediaType.BlockMedia;
+ _imageInfo.MediaType = Geometry.GetMediaType((cylsDsk, heads, spt, SECTOR_SIZE, MediaEncoding.MFM, false));
+
+ AaruLogging.Verbose(Localization.Sxd_image_contains_a_disk_of_type_0, _imageInfo.MediaType);
+
+ if(!string.IsNullOrEmpty(_imageInfo.Comments))
+ AaruLogging.Verbose(Localization.Sxd_comments_0, _imageInfo.Comments);
+
+ AaruLogging.Debug(MODULE_NAME, Localization.Sxd_all_track_CRCs_match_0, _dataCrcOk);
+
+ return ErrorNumber.NoError;
+ }
+
+#endregion
+
+ /// Read, decompress, and CRC-validate one track from into .
+ ErrorNumber DecompressTrack(Stream stream, byte[] outBuf, int outOfs, int trackLen, byte[] cmprBuf, ref Lzh lzh,
+ out bool crcOk)
+ {
+ crcOk = false;
+
+ var header = new byte[4];
+
+ if(stream.EnsureRead(header, 0, 4) != 4) return ErrorNumber.InvalidArgument;
+
+ var crcStored = (ushort)(header[0] | header[1] << 8);
+ var cmprLen = (short)(header[2] | header[3] << 8);
+
+ int cmprLenAbs = cmprLen < 0 ? -cmprLen : cmprLen;
+
+ if(cmprLenAbs > trackLen + 2)
+ {
+ AaruLogging.Error(Localization.Sxd_track_compressed_length_out_of_range_0, cmprLen);
+
+ return ErrorNumber.InvalidArgument;
+ }
+
+ if(stream.EnsureRead(cmprBuf, 0, cmprLenAbs) != cmprLenAbs) return ErrorNumber.InvalidArgument;
+
+ if(cmprLen < 0)
+ {
+ // CopyQM-style signed-RLE (inline).
+ var cmprPos = 0;
+ var bytesWritten = 0;
+
+ while(bytesWritten < trackLen)
+ {
+ if(cmprPos + 2 > cmprLenAbs) return ErrorNumber.InvalidArgument;
+
+ var repCnt = (short)(cmprBuf[cmprPos] | cmprBuf[cmprPos + 1] << 8);
+ cmprPos += 2;
+
+ if(repCnt > 0)
+ {
+ if(bytesWritten + repCnt > trackLen) return ErrorNumber.InvalidArgument;
+ if(cmprPos + repCnt > cmprLenAbs) return ErrorNumber.InvalidArgument;
+
+ Buffer.BlockCopy(cmprBuf, cmprPos, outBuf, outOfs + bytesWritten, repCnt);
+ cmprPos += repCnt;
+ bytesWritten += repCnt;
+ }
+ else if(repCnt < 0)
+ {
+ int count = -repCnt;
+
+ if(bytesWritten + count > trackLen) return ErrorNumber.InvalidArgument;
+ if(cmprPos + 1 > cmprLenAbs) return ErrorNumber.InvalidArgument;
+
+ byte b = cmprBuf[cmprPos++];
+
+ for(var i = 0; i < count; i++) outBuf[outOfs + bytesWritten + i] = b;
+
+ bytesWritten += count;
+ }
+ else
+ {
+ // Zero run-count is invalid per fdimg.
+ return ErrorNumber.InvalidArgument;
+ }
+ }
+ }
+ else
+ {
+ // LH1-variant LZHUF.
+ lzh ??= new Lzh();
+
+ int produced = lzh.Decode(cmprBuf, cmprLenAbs, outBuf, outOfs, trackLen);
+
+ if(produced != trackLen) return ErrorNumber.InvalidArgument;
+ }
+
+ ushort crcComputed = SxdCrc16(outBuf.AsSpan(outOfs, trackLen));
+
+ crcOk = crcComputed == crcStored;
+
+ return ErrorNumber.NoError;
+ }
+
+ static string DensityName(byte density) => density switch
+ {
+ 0 => "DD",
+ 1 => "HD",
+ 2 => "ED",
+ _ => "unknown"
+ };
+
+ static DateTime DosDateTimeToUtc(ushort dosDate, ushort dosTime, DateTime fallback)
+ {
+ if(dosDate == 0 && dosTime == 0) return fallback;
+
+ int year = (dosDate >> 9 & 0x7F) + 1980;
+ int month = dosDate >> 5 & 0x0F;
+ int day = dosDate & 0x1F;
+ int hour = dosTime >> 11 & 0x1F;
+ int minute = dosTime >> 5 & 0x3F;
+ int second = (dosTime & 0x1F) * 2;
+
+ try
+ {
+ return new DateTime(year, month, day, hour, minute, second, DateTimeKind.Unspecified);
+ }
+ catch(ArgumentOutOfRangeException)
+ {
+ return fallback;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Aaru.Images/SXD/Properties.cs b/Aaru.Images/SXD/Properties.cs
new file mode 100644
index 000000000..338cdef2f
--- /dev/null
+++ b/Aaru.Images/SXD/Properties.cs
@@ -0,0 +1,70 @@
+// /***************************************************************************
+// Aaru Data Preservation Suite
+// ----------------------------------------------------------------------------
+//
+// Filename : Properties.cs
+// Author(s) : Natalia Portillo
+//
+// Component : Disk image plugins.
+//
+// --[ Description ] ----------------------------------------------------------
+//
+// Contains properties for Sydex CopyQM+ Self-eXtracting Disk (SXD) images.
+//
+// Based on the work of Michal Necasek (fdimg).
+//
+// --[ License ] --------------------------------------------------------------
+//
+// This library is free software; you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, see .
+//
+// ----------------------------------------------------------------------------
+// Copyright © 2011-2026 Natalia Portillo
+// ****************************************************************************/
+
+using System;
+using System.Collections.Generic;
+using Aaru.CommonTypes.AaruMetadata;
+using Aaru.CommonTypes.Structs;
+
+namespace Aaru.Images;
+
+public sealed partial class SXD
+{
+#region IMediaImage Members
+
+ ///
+
+ // ReSharper disable once ConvertToAutoProperty
+ public ImageInfo Info => _imageInfo;
+
+ ///
+ public string Name => Localization.Sxd_Name;
+
+ ///
+ public Guid Id => new("B3C6E8F2-4A2D-4F10-9E1C-8D3A7B5F2E94");
+
+ ///
+ public string Format => "SXD";
+
+ ///
+ public string Author => Authors.NataliaPortillo;
+
+ ///
+ public List DumpHardware => null;
+
+ ///
+ public Metadata AaruMetadata => null;
+
+#endregion
+}
\ No newline at end of file
diff --git a/Aaru.Images/SXD/Read.cs b/Aaru.Images/SXD/Read.cs
new file mode 100644
index 000000000..9a0bd9433
--- /dev/null
+++ b/Aaru.Images/SXD/Read.cs
@@ -0,0 +1,79 @@
+// /***************************************************************************
+// Aaru Data Preservation Suite
+// ----------------------------------------------------------------------------
+//
+// Filename : Read.cs
+// Author(s) : Natalia Portillo
+//
+// Component : Disk image plugins.
+//
+// --[ Description ] ----------------------------------------------------------
+//
+// Reads Sydex CopyQM+ Self-eXtracting Disk (SXD) images.
+//
+// Based on the work of Michal Necasek (fdimg).
+//
+// --[ License ] --------------------------------------------------------------
+//
+// This library is free software; you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, see .
+//
+// ----------------------------------------------------------------------------
+// Copyright © 2011-2026 Natalia Portillo
+// ****************************************************************************/
+
+using System;
+using System.Linq;
+using Aaru.CommonTypes.Enums;
+
+namespace Aaru.Images;
+
+public sealed partial class SXD
+{
+#region IMediaImage Members
+
+ ///
+ public ErrorNumber ReadSector(ulong sectorAddress, bool negative, out byte[] buffer, out SectorStatus sectorStatus)
+ {
+ sectorStatus = SectorStatus.Dumped;
+
+ return ReadSectors(sectorAddress, negative, 1, out buffer, out _);
+ }
+
+ ///
+ public ErrorNumber ReadSectors(ulong sectorAddress, bool negative, uint length, out byte[] buffer,
+ out SectorStatus[] sectorStatus)
+ {
+ buffer = null;
+ sectorStatus = null;
+
+ if(negative) return ErrorNumber.NotSupported;
+
+ if(sectorAddress > _imageInfo.Sectors - 1) return ErrorNumber.OutOfRange;
+
+ if(sectorAddress + length > _imageInfo.Sectors) return ErrorNumber.OutOfRange;
+
+ buffer = new byte[length * _imageInfo.SectorSize];
+ sectorStatus = Enumerable.Repeat(SectorStatus.Dumped, (int)length).ToArray();
+
+ Array.Copy(_decodedDisk,
+ (long)sectorAddress * _imageInfo.SectorSize,
+ buffer,
+ 0,
+ length * _imageInfo.SectorSize);
+
+ return ErrorNumber.NoError;
+ }
+
+#endregion
+}
\ No newline at end of file
diff --git a/Aaru.Images/SXD/SXD.cs b/Aaru.Images/SXD/SXD.cs
new file mode 100644
index 000000000..e16d853a8
--- /dev/null
+++ b/Aaru.Images/SXD/SXD.cs
@@ -0,0 +1,74 @@
+// /***************************************************************************
+// Aaru Data Preservation Suite
+// ----------------------------------------------------------------------------
+//
+// Filename : SXD.cs
+// Author(s) : Natalia Portillo
+//
+// Component : Disk image plugins.
+//
+// --[ Description ] ----------------------------------------------------------
+//
+// Manages Sydex CopyQM+ Self-eXtracting Disk (SXD) images.
+//
+// Based on the work of Michal Necasek (fdimg).
+//
+// --[ License ] --------------------------------------------------------------
+//
+// This library is free software; you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, see .
+//
+// ----------------------------------------------------------------------------
+// Copyright © 2011-2026 Natalia Portillo
+// ****************************************************************************/
+
+using Aaru.CommonTypes.Interfaces;
+using Aaru.CommonTypes.Structs;
+
+namespace Aaru.Images;
+
+///
+/// Implements reading Sydex CopyQM+ Self-eXtracting Disk (SXD) images.
+public sealed partial class SXD : IMediaImage, IVerifiableImage
+{
+ const string MODULE_NAME = "SXD plugin";
+ bool _dataCrcOk;
+ byte[] _decodedDisk;
+ Header _header;
+ long _headerOffset;
+ ImageInfo _imageInfo;
+
+ public SXD() => _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/SXD/Structs.cs b/Aaru.Images/SXD/Structs.cs
new file mode 100644
index 000000000..db908baf9
--- /dev/null
+++ b/Aaru.Images/SXD/Structs.cs
@@ -0,0 +1,92 @@
+// /***************************************************************************
+// Aaru Data Preservation Suite
+// ----------------------------------------------------------------------------
+//
+// Filename : Structs.cs
+// Author(s) : Natalia Portillo
+//
+// Component : Disk image plugins.
+//
+// --[ Description ] ----------------------------------------------------------
+//
+// Contains structures for Sydex CopyQM+ Self-eXtracting Disk (SXD) images.
+//
+// Based on the work of Michal Necasek (fdimg).
+//
+// --[ License ] --------------------------------------------------------------
+//
+// This library is free software; you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, see .
+//
+// ----------------------------------------------------------------------------
+// Copyright © 2011-2026 Natalia Portillo
+// ****************************************************************************/
+
+using System.Runtime.InteropServices;
+
+namespace Aaru.Images;
+
+public sealed partial class SXD
+{
+#region Nested type: Header
+
+ /// SXD image header, exactly 33 bytes long. The first 31 bytes are protected by .
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ readonly struct Header
+ {
+ /// 0x00 'SXD' signature.
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
+ public readonly byte[] signature;
+ /// 0x03 Track length in bytes.
+ public readonly ushort trk_len;
+ /// 0x05 Number of sectors per track.
+ public readonly byte n_trk_sec;
+ /// 0x06 Number of heads (1 or 2 valid).
+ public readonly byte n_heads;
+ /// 0x07 Number of cylinders on disk.
+ public readonly byte n_cyl_dsk;
+ /// 0x08 Number of cylinders stored in the image.
+ public readonly byte n_cyl_img;
+ /// 0x09 ID of first sector minus 1 (usually 0).
+ public readonly byte first_sid;
+ /// 0x0A Interleave factor (usually 1).
+ public readonly byte interleave;
+ /// 0x0B Skew factor, probably (usually 0).
+ public readonly byte skew;
+ /// 0x0C Sector size code (2 = 512B).
+ public readonly byte sec_sz_code;
+ /// 0x0D Drive type, 1-6 (see ).
+ public readonly byte drv_typ;
+ /// 0x0E Density: 0/1/2 for DD/HD/ED.
+ public readonly byte density;
+ /// 0x0F Password CRC (nonzero if image is encrypted).
+ public readonly ushort pwd_crc;
+ /// 0x11 Password hash.
+ public readonly ushort pwd_hash;
+ /// 0x13 Length of optional comment following the header.
+ public readonly ushort comment_len;
+ /// 0x15 CRC of the comment block.
+ public readonly ushort crc_comment;
+ /// 0x17 Unknown 4 bytes.
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
+ public readonly byte[] unk0;
+ /// 0x1B Image timestamp in DOS time format.
+ public readonly ushort dos_time;
+ /// 0x1D Image date in DOS date format.
+ public readonly ushort dos_date;
+ /// 0x1F CRC of header bytes 0x00..0x1E.
+ public readonly ushort crc_hdr;
+ }
+
+#endregion
+}
\ No newline at end of file
diff --git a/Aaru.Images/SXD/Unsupported.cs b/Aaru.Images/SXD/Unsupported.cs
new file mode 100644
index 000000000..2cdc7538b
--- /dev/null
+++ b/Aaru.Images/SXD/Unsupported.cs
@@ -0,0 +1,89 @@
+// /***************************************************************************
+// Aaru Data Preservation Suite
+// ----------------------------------------------------------------------------
+//
+// Filename : Unsupported.cs
+// Author(s) : Natalia Portillo
+//
+// Component : Disk image plugins.
+//
+// --[ Description ] ----------------------------------------------------------
+//
+// Contains features unsupported by Sydex CopyQM+ Self-eXtracting Disk (SXD) images.
+//
+// Based on the work of Michal Necasek (fdimg).
+//
+// --[ License ] --------------------------------------------------------------
+//
+// This library is free software; you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, see .
+//
+// ----------------------------------------------------------------------------
+// Copyright © 2011-2026 Natalia Portillo
+// ****************************************************************************/
+
+using Aaru.CommonTypes.Enums;
+
+namespace Aaru.Images;
+
+public sealed partial class SXD
+{
+#region IMediaImage Members
+
+ ///
+ public ErrorNumber ReadSectorTag(ulong sectorAddress, bool negative, SectorTagType tag, out byte[] buffer)
+ {
+ buffer = null;
+
+ return ErrorNumber.NotSupported;
+ }
+
+ ///
+ public ErrorNumber ReadSectorsTag(ulong sectorAddress, bool negative, uint length, SectorTagType tag,
+ out byte[] buffer)
+ {
+ buffer = null;
+
+ return ErrorNumber.NotSupported;
+ }
+
+ ///
+ public ErrorNumber ReadSectorLong(ulong sectorAddress, bool negative, out byte[] buffer,
+ out SectorStatus sectorStatus)
+ {
+ buffer = null;
+ sectorStatus = SectorStatus.NotDumped;
+
+ return ErrorNumber.NotSupported;
+ }
+
+ ///
+ public ErrorNumber ReadSectorsLong(ulong sectorAddress, bool negative, uint length, out byte[] buffer,
+ out SectorStatus[] sectorStatus)
+ {
+ buffer = null;
+ sectorStatus = null;
+
+ return ErrorNumber.NotSupported;
+ }
+
+ ///
+ public ErrorNumber ReadMediaTag(MediaTagType tag, out byte[] buffer)
+ {
+ buffer = null;
+
+ return ErrorNumber.NotSupported;
+ }
+
+#endregion
+}
\ No newline at end of file
diff --git a/Aaru.Images/SXD/Verify.cs b/Aaru.Images/SXD/Verify.cs
new file mode 100644
index 000000000..fd61c59dc
--- /dev/null
+++ b/Aaru.Images/SXD/Verify.cs
@@ -0,0 +1,45 @@
+// /***************************************************************************
+// Aaru Data Preservation Suite
+// ----------------------------------------------------------------------------
+//
+// Filename : Verify.cs
+// Author(s) : Natalia Portillo
+//
+// Component : Disk image plugins.
+//
+// --[ Description ] ----------------------------------------------------------
+//
+// Verifies Sydex CopyQM+ Self-eXtracting Disk (SXD) images.
+//
+// Based on the work of Michal Necasek (fdimg).
+//
+// --[ License ] --------------------------------------------------------------
+//
+// This library is free software; you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, see .
+//
+// ----------------------------------------------------------------------------
+// Copyright © 2011-2026 Natalia Portillo
+// ****************************************************************************/
+
+namespace Aaru.Images;
+
+public sealed partial class SXD
+{
+#region IVerifiableImage Members
+
+ ///
+ public bool? VerifyMediaImage() => _dataCrcOk;
+
+#endregion
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 6da00e9a5..5918f410a 100644
--- a/README.md
+++ b/README.md
@@ -114,6 +114,7 @@ Media image formats we can read
* Software Pirates SNATCH-IT / Central Point COPY II-PC (.CP2)
* Spectrum floppy disk image (.FDI)
* SuperCardPro
+* Sydex CopyQM+ Self-eXtracting Disk (.SXD)
* TeleDisk
* UltraISO (.ISZ)
* WinOnCD (.C2D)