[QRST] Add support for QRST V5 (PKWARE-compressed) images with native decompression

This commit is contained in:
2026-04-21 22:48:13 +01:00
parent 1aafe2f23f
commit ee859a293a
6 changed files with 1199 additions and 1110 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -3153,6 +3153,12 @@
<data name="Qrst_V5_images_are_not_supported" xml:space="preserve">
<value>Las imágenes QRST V5 (comprimidas con PKWARE) no están soportadas.</value>
</data>
<data name="Qrst_V5_requires_native_decompressor" xml:space="preserve">
<value>Las imágenes QRST V5 (comprimidas con PKWARE) requieren la biblioteca nativa de descompresión.</value>
</data>
<data name="Qrst_V5_decompression_yielded_incomplete_data" xml:space="preserve">
<value>La descompresión de la imagen QRST V5 produjo datos incompletos.</value>
</data>
<data name="Qrst_track_decompression_yielded_incomplete_data" xml:space="preserve">
<value>La descompresión de la pista QRST produjo datos incompletos.</value>
</data>

View File

@@ -3172,6 +3172,12 @@
<data name="Qrst_V5_images_are_not_supported" xml:space="preserve">
<value>QRST V5 (PKWARE-compressed) images are not supported.</value>
</data>
<data name="Qrst_V5_requires_native_decompressor" xml:space="preserve">
<value>QRST V5 (PKWARE-compressed) images require the native decompression library.</value>
</data>
<data name="Qrst_V5_decompression_yielded_incomplete_data" xml:space="preserve">
<value>QRST V5 decompression yielded incomplete data.</value>
</data>
<data name="Qrst_track_decompression_yielded_incomplete_data" xml:space="preserve">
<value>QRST track decompression yielded incomplete data.</value>
</data>

View File

@@ -35,6 +35,7 @@
using System;
using System.IO;
using Aaru.CommonTypes.Enums;
using Aaru.Compression.Zip;
using Aaru.Helpers;
using Aaru.Logging;
@@ -42,6 +43,94 @@ namespace Aaru.Images;
public sealed partial class Qrst
{
ErrorNumber WalkPreV5Tracks(Stream stream, int headerSize, int totalTracks)
{
long curOfs = headerSize;
var trkHdrBuf = new byte[Marshal.SizeOf<QrstTrackHeader>()];
var blkLenBuf = new byte[sizeof(ushort)];
for(var i = 0; i < totalTracks; i++)
{
stream.Seek(curOfs, SeekOrigin.Begin);
if(stream.EnsureRead(trkHdrBuf, 0, trkHdrBuf.Length) != trkHdrBuf.Length)
return ErrorNumber.InvalidArgument;
QrstTrackHeader trkHdr = Marshal.ByteArrayToStructureLittleEndian<QrstTrackHeader>(trkHdrBuf);
if(trkHdr.cyl > _cyls || trkHdr.head > _heads || trkHdr.type > TRK_CMPRSD)
return ErrorNumber.InvalidArgument;
int trkIdx = trkHdr.cyl * _heads + trkHdr.head;
if(_trackOffset.ContainsKey(trkIdx)) return ErrorNumber.InvalidArgument;
_trackOffset[trkIdx] = curOfs;
curOfs += trkHdrBuf.Length;
switch(trkHdr.type)
{
case TRK_NORMAL:
curOfs += _trackLen;
break;
case TRK_BLANK:
curOfs += 1;
break;
case TRK_CMPRSD:
if(stream.EnsureRead(blkLenBuf, 0, blkLenBuf.Length) != blkLenBuf.Length)
return ErrorNumber.InvalidArgument;
var blkLen = BitConverter.ToUInt16(blkLenBuf, 0);
curOfs += blkLenBuf.Length + blkLen;
break;
default:
return ErrorNumber.InvalidArgument;
}
}
if(stream.Length < curOfs) return ErrorNumber.InvalidArgument;
if(_trackOffset.Count != totalTracks) return ErrorNumber.InvalidArgument;
return ErrorNumber.NoError;
}
ErrorNumber DecompressV5(Stream stream, int headerSize, long totalBytes)
{
if(!Blast.IsSupported)
{
AaruLogging.Error(MODULE_NAME, Localization.Qrst_V5_requires_native_decompressor);
return ErrorNumber.NotSupported;
}
long payloadLen = stream.Length - headerSize;
if(payloadLen <= 0 || payloadLen > int.MaxValue) return ErrorNumber.InvalidArgument;
var compressed = new byte[payloadLen];
stream.Seek(headerSize, SeekOrigin.Begin);
if(stream.EnsureRead(compressed, 0, compressed.Length) != compressed.Length) return ErrorNumber.InOutError;
var decompressed = new byte[totalBytes];
int actual = Blast.DecodeBuffer(compressed, decompressed);
if(actual != totalBytes)
{
AaruLogging.Error(MODULE_NAME, Localization.Qrst_V5_decompression_yielded_incomplete_data);
return ErrorNumber.InOutError;
}
_flatImage = decompressed;
return ErrorNumber.NoError;
}
ErrorNumber ReadTrackIntoCache(Stream stream, int trackNum)
{
if(!_trackOffset.TryGetValue(trackNum, out long offset)) return ErrorNumber.SectorNotFound;

View File

@@ -37,7 +37,8 @@
* original (pre-V5) version and version 5 (V5).
*
* QRST V5 contains a disk image compressed with the PKWARE Data Compression
* Library and is not supported.
* Library (DCL) Implode algorithm, stored as a single compressed stream after
* the header that decompresses to a flat sector-sequential raw disk image.
*
* QRST pre-V5 contains a collection of tracks. Each track may be uncompressed,
* blank (with a filler byte), or run-length encoded. Only standard DOS disk
@@ -65,6 +66,9 @@ public sealed partial class Qrst : IMediaImage
readonly Dictionary<int, long> _trackOffset = new();
byte _cyls;
/// <summary>When non-null, the image is a V5 (PKWARE-compressed) QRST and this holds the decompressed flat disk image.</summary>
byte[] _flatImage;
QrstHeader _header;
byte _heads;

View File

@@ -63,14 +63,6 @@ public sealed partial class Qrst
if(!hdr.signature.SequenceEqual(_signature)) return ErrorNumber.InvalidArgument;
// V5 (PKWARE-compressed) images are not supported.
if(hdr.type != 0)
{
AaruLogging.Error(MODULE_NAME, Localization.Qrst_V5_images_are_not_supported);
return ErrorNumber.NotSupported;
}
if(hdr.disk_fmt == 0 || hdr.disk_fmt >= _dskDesc.Length) return ErrorNumber.InvalidArgument;
(byte cyls, byte heads, byte spt) = _dskDesc[hdr.disk_fmt];
@@ -81,60 +73,30 @@ public sealed partial class Qrst
_trackLen = spt * SECTOR_SIZE;
_header = hdr;
int totalTracks = _cyls * _heads;
int totalTracks = _cyls * _heads;
long totalBytes = (long)totalTracks * _trackLen;
// Walk the track headers to build a lookup table of file offsets.
long curOfs = headerSize;
var trkHdrBuf = new byte[Marshal.SizeOf<QrstTrackHeader>()];
var blkLenBuf = new byte[sizeof(ushort)];
for(var i = 0; i < totalTracks; i++)
switch(hdr.type)
{
stream.Seek(curOfs, SeekOrigin.Begin);
case 0:
// Pre-V5: walk the track headers to build a lookup table of file offsets.
ErrorNumber walkErr = WalkPreV5Tracks(stream, headerSize, totalTracks);
if(stream.EnsureRead(trkHdrBuf, 0, trkHdrBuf.Length) != trkHdrBuf.Length)
if(walkErr != ErrorNumber.NoError) return walkErr;
break;
case 2:
// V5: the remainder of the file is a single PKWARE DCL Implode stream that
// decompresses to a raw, sector-sequential flat disk image.
ErrorNumber v5Err = DecompressV5(stream, headerSize, totalBytes);
if(v5Err != ErrorNumber.NoError) return v5Err;
break;
default:
return ErrorNumber.InvalidArgument;
QrstTrackHeader trkHdr = Marshal.ByteArrayToStructureLittleEndian<QrstTrackHeader>(trkHdrBuf);
if(trkHdr.cyl > _cyls || trkHdr.head > _heads || trkHdr.type > TRK_CMPRSD)
return ErrorNumber.InvalidArgument;
int trkIdx = trkHdr.cyl * _heads + trkHdr.head;
// Reject duplicates.
if(_trackOffset.ContainsKey(trkIdx)) return ErrorNumber.InvalidArgument;
_trackOffset[trkIdx] = curOfs;
curOfs += trkHdrBuf.Length;
switch(trkHdr.type)
{
case TRK_NORMAL:
curOfs += _trackLen;
break;
case TRK_BLANK:
curOfs += 1;
break;
case TRK_CMPRSD:
if(stream.EnsureRead(blkLenBuf, 0, blkLenBuf.Length) != blkLenBuf.Length)
return ErrorNumber.InvalidArgument;
var blkLen = BitConverter.ToUInt16(blkLenBuf, 0);
curOfs += blkLenBuf.Length + blkLen;
break;
default:
return ErrorNumber.InvalidArgument;
}
}
if(stream.Length < curOfs) return ErrorNumber.InvalidArgument;
if(_trackOffset.Count != totalTracks) return ErrorNumber.InvalidArgument;
_imageInfo.Cylinders = _cyls;
_imageInfo.Heads = _heads;
_imageInfo.SectorsPerTrack = _spt;
@@ -170,6 +132,16 @@ public sealed partial class Qrst
if(sectorAddress >= _imageInfo.Sectors) return ErrorNumber.OutOfRange;
buffer = new byte[SECTOR_SIZE];
if(_flatImage != null)
{
Array.Copy(_flatImage, (long)sectorAddress * SECTOR_SIZE, buffer, 0, SECTOR_SIZE);
sectorStatus = SectorStatus.Dumped;
return ErrorNumber.NoError;
}
var trackNum = (int)(sectorAddress / _spt);
var sectorInTrk = (int)(sectorAddress % _spt);
@@ -182,7 +154,6 @@ public sealed partial class Qrst
trackData = _trackCache[trackNum];
}
buffer = new byte[SECTOR_SIZE];
Array.Copy(trackData, sectorInTrk * SECTOR_SIZE, buffer, 0, SECTOR_SIZE);
sectorStatus = SectorStatus.Dumped;