[SaveDskF] Add LZMW decompression support. Fixes #108

This commit is contained in:
2026-04-21 00:53:21 +01:00
parent cb5faaa674
commit b1b0b3b262
4 changed files with 644 additions and 47 deletions

View File

@@ -64,6 +64,7 @@
<Compile Include="Lh5Stream.cs"/>
<Compile Include="LhaStream.cs"/>
<Compile Include="LzdStream.cs"/>
<Compile Include="Lzmw.cs"/>
<Compile Include="LZ4.cs"/>
<Compile Include="LZFSE.cs"/>
<Compile Include="LZIP.cs"/>

283
Aaru.Compression/Lzmw.cs Normal file
View File

@@ -0,0 +1,283 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Lzmw.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : Compression algorithms.
//
// --[ Description ] ----------------------------------------------------------
//
// Decompresses LZMW (Lempel-Ziv-Miller-Wegman) as used by IBM's SaveDskF
// compressed ('Z' variant) disk images.
//
// Based on code from fdimg (img_dskf / dsk_lzmw) by Michal Necasek,
// Copyright (c) 2013-2026 Michal Necasek, distributed under the MIT
// license. Translated from C to C# for the Aaru Data Preservation Suite.
//
// --[ License ] --------------------------------------------------------------
//
// This library is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation; either version 2.1 of the
// License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// Copyright © 2013-2026 Michal Necasek (original C implementation)
// ****************************************************************************/
using System;
namespace Aaru.Compression;
/// <summary>
/// Implements LZMW (Lempel-Ziv-Miller-Wegman) decompression as used by IBM's SaveDskF compressed disk images.
/// Based on the reference C implementation in fdimg (<c>dsk_lzmw.c</c>) by Michal Necasek.
/// </summary>
public static class Lzmw
{
const int DICT_SIZE = 4096;
/// <summary>Set to <c>true</c> if this algorithm is supported, <c>false</c> otherwise.</summary>
public static bool IsSupported => true;
/// <summary>Decodes a buffer compressed with LZMW as used by SaveDskF.</summary>
/// <param name="source">Encoded buffer.</param>
/// <param name="destination">Buffer where to write the decoded data.</param>
/// <returns>The number of decoded bytes, or <c>-1</c> on decompression failure.</returns>
public static int DecodeBuffer(byte[] source, byte[] destination)
{
if(source == null || destination == null) return -1;
var dict = new DictEntry[DICT_SIZE];
// Initialize the dictionary (equivalent to lzmw_dict_init).
dict[0].Str = null;
dict[0].Len = 0;
dict[0].Next = 0;
dict[0].Prev = 0;
dict[0].Ancestor = 0;
for(var i = 1; i <= 256; i++)
{
dict[i].Str = new byte[1];
dict[i].Str[0] = (byte)(i - 1);
dict[i].Len = 1;
dict[i].Next = -1;
dict[i].Prev = 0;
dict[i].Ancestor = 0;
}
for(var i = 257; i < DICT_SIZE - 1; i++) dict[i].Next = (short)(i + 1);
dict[DICT_SIZE - 1].Next = -1;
var avail = 257;
var readTwo = true;
var nibble = 0;
var inPos = 0;
var outPos = 0;
// The first code is output but not added to the dictionary.
int prevCode = ReadNextCode(source, ref inPos, ref readTwo, ref nibble);
if(prevCode < 0) return -1;
int produced = DictOut(dict, prevCode, destination, outPos);
if(produced < 0) return -1;
outPos += produced;
// Process the remaining codes.
while(true)
{
int code = ReadNextCode(source, ref inPos, ref readTwo, ref nibble);
if(code < 0) break; // End of input.
if(code == 0) break; // End marker.
if(!DictAdd(dict, ref avail, code, prevCode)) return -1;
produced = DictOut(dict, code, destination, outPos);
if(produced < 0) return -1;
prevCode = code;
outPos += produced;
}
return outPos;
}
/// <summary>Read the next 12-bit code from the compressed stream.</summary>
static int ReadNextCode(byte[] src, ref int pos, ref bool readTwo, ref int nibble)
{
int code;
if(readTwo)
{
// Read the first 16 bits of a code tuple (big-endian in the stream).
if(pos + 2 > src.Length) return -1;
int tw = src[pos] << 8 | src[pos + 1];
pos += 2;
nibble = tw & 0x0F;
code = tw >> 4;
}
else
{
// Read the final 8 bits of a code tuple.
if(pos + 1 > src.Length) return -1;
int tb = src[pos++];
code = nibble << 8 | tb;
}
readTwo = !readTwo;
return code;
}
/// <summary>Emit the string associated with <paramref name="code" /> to the destination buffer.</summary>
static int DictOut(DictEntry[] dict, int code, byte[] dst, int dstPos)
{
if(code is <= 0 or >= DICT_SIZE) return -1;
byte[] str = dict[code].Str;
int len = dict[code].Len;
if(str == null || len == 0) return -1;
if(dstPos + len > dst.Length) return -1;
Buffer.BlockCopy(str, 0, dst, dstPos, len);
return len;
}
/// <summary>Raise the reference count for a dictionary entry.</summary>
static void DictAddRef(DictEntry[] dict, int entry)
{
if(entry == 0) return;
// For (Next < 0), reference count is -Next.
if(dict[entry].Next < 0)
{
// Increase reference count.
dict[entry].Next--;
}
else
{
// Remove entry from the unreferenced (LRU) list.
int k = dict[entry].Next;
int l = dict[entry].Prev;
dict[k].Prev = (short)l;
dict[l].Next = (short)k;
dict[entry].Next = -1; // Set reference count to one.
}
}
/// <summary>Move an entry to the unreferenced (LRU) queue.</summary>
static void DictMoveToUnref(DictEntry[] dict, int entry)
{
int k = dict[0].Prev;
dict[entry].Prev = (short)k;
dict[k].Next = (short)entry;
dict[entry].Next = 0;
dict[0].Prev = (short)entry;
}
/// <summary>Lower the reference count for a dictionary entry.</summary>
static void DictRemoveRef(DictEntry[] dict, int entry)
{
if(entry == 0) return;
// Decrease reference count.
dict[entry].Next++;
// If no longer referenced, move to the LRU queue.
if(dict[entry].Next == 0) DictMoveToUnref(dict, entry);
}
/// <summary>Obtain the next available slot in the dictionary.</summary>
static int DictGetAvailSlot(DictEntry[] dict, ref int avail)
{
int k;
if(avail != -1)
{
// An unused entry is available.
k = avail;
avail = dict[k].Next;
}
else
{
// Recycle the least recently used entry.
if(dict[0].Prev == dict[0].Next) return -1;
k = dict[0].Next;
int l = dict[k].Next;
if(k <= 256 || l <= 256) return -1;
dict[0].Next = (short)l;
dict[l].Prev = 0;
DictRemoveRef(dict, dict[k].Ancestor);
dict[k].Str = null;
}
return k;
}
/// <summary>Add a new dictionary entry formed by <paramref name="prevCode" /> + first byte of <paramref name="code" />.</summary>
static bool DictAdd(DictEntry[] dict, ref int avail, int code, int prevCode)
{
if(code is 0 or >= DICT_SIZE) return false;
if(prevCode is 0 or >= DICT_SIZE) return false;
int j = DictGetAvailSlot(dict, ref avail);
if(j is <= 256 or >= DICT_SIZE) return false;
int pLen = dict[prevCode].Len;
if(pLen == 0) return false;
int srcCode = code;
// The "cScSc" case: the newly-allocated slot is the same as the current code.
if(j == srcCode) srcCode = prevCode;
if(dict[srcCode].Str == null || dict[srcCode].Len == 0) return false;
dict[j].Len = pLen + 1;
dict[j].Str = new byte[dict[j].Len];
DictAddRef(dict, prevCode);
DictMoveToUnref(dict, j);
dict[j].Ancestor = (short)prevCode;
Buffer.BlockCopy(dict[prevCode].Str, 0, dict[j].Str, 0, pLen);
dict[j].Str[pLen] = dict[srcCode].Str[0];
return true;
}
struct DictEntry
{
public byte[] Str; // String associated with this code.
public int Len; // Length of the string.
public short Next; // Next LRU queue entry (or negated refcount when < 0).
public short Prev; // Previous LRU queue entry.
public short Ancestor; // Parent entry (sans last character).
}
}

View File

@@ -37,6 +37,7 @@ using System.Text;
using Aaru.CommonTypes;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.Compression;
using Aaru.Helpers;
using Aaru.Logging;
@@ -57,25 +58,7 @@ public sealed partial class SaveDskF
stream.EnsureRead(hdr, 0, 40);
_header = Marshal.ByteArrayToStructureLittleEndian<Header>(hdr);
AaruLogging.Debug(MODULE_NAME, "header.magic = 0x{0:X4}", _header.magic);
AaruLogging.Debug(MODULE_NAME, "header.mediaType = 0x{0:X2}", _header.mediaType);
AaruLogging.Debug(MODULE_NAME, "header.sectorSize = {0}", _header.sectorSize);
AaruLogging.Debug(MODULE_NAME, "header.clusterMask = {0}", _header.clusterMask);
AaruLogging.Debug(MODULE_NAME, "header.clusterShift = {0}", _header.clusterShift);
AaruLogging.Debug(MODULE_NAME, "header.reservedSectors = {0}", _header.reservedSectors);
AaruLogging.Debug(MODULE_NAME, "header.fatCopies = {0}", _header.fatCopies);
AaruLogging.Debug(MODULE_NAME, "header.rootEntries = {0}", _header.rootEntries);
AaruLogging.Debug(MODULE_NAME, "header.firstCluster = {0}", _header.firstCluster);
AaruLogging.Debug(MODULE_NAME, "header.clustersCopied = {0}", _header.clustersCopied);
AaruLogging.Debug(MODULE_NAME, "header.sectorsPerFat = {0}", _header.sectorsPerFat);
AaruLogging.Debug(MODULE_NAME, "header.checksum = 0x{0:X8}", _header.checksum);
AaruLogging.Debug(MODULE_NAME, "header.cylinders = {0}", _header.cylinders);
AaruLogging.Debug(MODULE_NAME, "header.heads = {0}", _header.heads);
AaruLogging.Debug(MODULE_NAME, "header.sectorsPerTrack = {0}", _header.sectorsPerTrack);
AaruLogging.Debug(MODULE_NAME, "header.padding = {0}", _header.padding);
AaruLogging.Debug(MODULE_NAME, "header.sectorsCopied = {0}", _header.sectorsCopied);
AaruLogging.Debug(MODULE_NAME, "header.commentOffset = {0}", _header.commentOffset);
AaruLogging.Debug(MODULE_NAME, "header.dataOffset = {0}", _header.dataOffset);
LogHeader();
if(_header is { dataOffset: 0, magic: SDF_MAGIC_OLD }) _header.dataOffset = 512;
@@ -85,23 +68,6 @@ public sealed partial class SaveDskF
if(cmt.Length > 1) _imageInfo.Comments = StringHandlers.CToString(cmt, Encoding.GetEncoding("ibm437"));
_calculatedChk = 0;
stream.Seek(0, SeekOrigin.Begin);
int b;
do
{
b = stream.ReadByte();
if(b >= 0) _calculatedChk += (uint)b;
} while(b >= 0);
AaruLogging.Debug(MODULE_NAME,
Localization.Calculated_checksum_equals_0_X8_1,
_calculatedChk,
_calculatedChk == _header.checksum);
_imageInfo.Application = "SaveDskF";
_imageInfo.CreationTime = imageFilter.CreationTime;
_imageInfo.LastModificationTime = imageFilter.LastWriteTime;
@@ -120,18 +86,13 @@ public sealed partial class SaveDskF
if(!string.IsNullOrEmpty(_imageInfo.Comments))
AaruLogging.Verbose(Localization.SaveDskF_comments_0, _imageInfo.Comments);
// TODO: Support compressed images
if(_header.magic == SDF_MAGIC_COMPRESSED)
{
AaruLogging.Error(Localization.Compressed_SaveDskF_images_are_not_supported);
return ErrorNumber.NotSupported;
}
// SaveDskF only omits ending clusters, leaving no gaps behind, so reading all data we have...
stream.Seek(_header.dataOffset, SeekOrigin.Begin);
_decodedDisk = new byte[_imageInfo.Sectors * _imageInfo.SectorSize];
stream.EnsureRead(_decodedDisk, 0, (int)(stream.Length - _header.dataOffset));
ErrorNumber loadError = _header.magic == SDF_MAGIC_COMPRESSED
? LoadCompressed(stream)
: LoadUncompressed(stream);
if(loadError != ErrorNumber.NoError) return loadError;
_imageInfo.Cylinders = _header.cylinders;
_imageInfo.Heads = _header.heads;
@@ -140,6 +101,95 @@ public sealed partial class SaveDskF
return ErrorNumber.NoError;
}
void LogHeader()
{
AaruLogging.Debug(MODULE_NAME, "header.magic = 0x{0:X4}", _header.magic);
AaruLogging.Debug(MODULE_NAME, "header.mediaType = 0x{0:X2}", _header.mediaType);
AaruLogging.Debug(MODULE_NAME, "header.sectorSize = {0}", _header.sectorSize);
AaruLogging.Debug(MODULE_NAME, "header.clusterMask = {0}", _header.clusterMask);
AaruLogging.Debug(MODULE_NAME, "header.clusterShift = {0}", _header.clusterShift);
AaruLogging.Debug(MODULE_NAME, "header.reservedSectors = {0}", _header.reservedSectors);
AaruLogging.Debug(MODULE_NAME, "header.fatCopies = {0}", _header.fatCopies);
AaruLogging.Debug(MODULE_NAME, "header.rootEntries = {0}", _header.rootEntries);
AaruLogging.Debug(MODULE_NAME, "header.firstCluster = {0}", _header.firstCluster);
AaruLogging.Debug(MODULE_NAME, "header.clustersCopied = {0}", _header.clustersCopied);
AaruLogging.Debug(MODULE_NAME, "header.sectorsPerFat = {0}", _header.sectorsPerFat);
AaruLogging.Debug(MODULE_NAME, "header.checksum = 0x{0:X8}", _header.checksum);
AaruLogging.Debug(MODULE_NAME, "header.cylinders = {0}", _header.cylinders);
AaruLogging.Debug(MODULE_NAME, "header.heads = {0}", _header.heads);
AaruLogging.Debug(MODULE_NAME, "header.sectorsPerTrack = {0}", _header.sectorsPerTrack);
AaruLogging.Debug(MODULE_NAME, "header.padding = {0}", _header.padding);
AaruLogging.Debug(MODULE_NAME, "header.sectorsCopied = {0}", _header.sectorsCopied);
AaruLogging.Debug(MODULE_NAME, "header.commentOffset = {0}", _header.commentOffset);
AaruLogging.Debug(MODULE_NAME, "header.dataOffset = {0}", _header.dataOffset);
}
ErrorNumber LoadCompressed(Stream stream)
{
long compressedLength = stream.Length - _header.dataOffset;
if(compressedLength <= 0) return ErrorNumber.InvalidArgument;
var compressed = new byte[compressedLength];
stream.Seek(_header.dataOffset, SeekOrigin.Begin);
stream.EnsureRead(compressed, 0, compressed.Length);
// The image stores `sectorsCopied` sectors (the trailing unallocated clusters are omitted).
int storedBytes = _header.sectorsCopied * _header.sectorSize;
var decompressed = new byte[storedBytes];
int produced = Lzmw.DecodeBuffer(compressed, decompressed);
if(produced != storedBytes)
{
AaruLogging.Error(Localization.Compressed_SaveDskF_images_are_not_supported);
return ErrorNumber.InvalidArgument;
}
// Validate the decompressed data against the stored 16-bit word checksum (fdimg dskf_calc_csum).
uint cksum = 0;
for(var i = 0; i + 1 < decompressed.Length; i += 2) cksum += (uint)(decompressed[i] | decompressed[i + 1] << 8);
_calculatedChk = cksum;
AaruLogging.Debug(MODULE_NAME,
Localization.Calculated_checksum_equals_0_X8_1,
_calculatedChk,
_calculatedChk == _header.checksum);
Array.Copy(decompressed, 0, _decodedDisk, 0, storedBytes);
return ErrorNumber.NoError;
}
ErrorNumber LoadUncompressed(Stream stream)
{
_calculatedChk = 0;
stream.Seek(0, SeekOrigin.Begin);
int b;
do
{
b = stream.ReadByte();
if(b >= 0) _calculatedChk += (uint)b;
} while(b >= 0);
AaruLogging.Debug(MODULE_NAME,
Localization.Calculated_checksum_equals_0_X8_1,
_calculatedChk,
_calculatedChk == _header.checksum);
// SaveDskF only omits ending clusters, leaving no gaps behind, so reading all data we have...
stream.Seek(_header.dataOffset, SeekOrigin.Begin);
stream.EnsureRead(_decodedDisk, 0, (int)(stream.Length - _header.dataOffset));
return ErrorNumber.NoError;
}
/// <inheritdoc />
public ErrorNumber ReadSector(ulong sectorAddress, bool negative, out byte[] buffer, out SectorStatus sectorStatus)
{

View File

@@ -0,0 +1,263 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : SaveDskFCompressed.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : Aaru unit testing.
//
// --[ License ] --------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using System.IO;
using Aaru.CommonTypes;
using Aaru.CommonTypes.Interfaces;
using NUnit.Framework;
namespace Aaru.Tests.Images;
[TestFixture]
public class SaveDskFCompressed : BlockMediaImageTest
{
public override string DataFolder =>
Path.Combine(Consts.TestFilesRoot, "Media image formats", "SaveDskF", "compressed");
public override IMediaImage Plugin => new Aaru.Images.SaveDskF();
public override BlockImageTestExpected[] Tests =>
[
new()
{
TestFile = "5dd8_a.dsk",
MediaType = MediaType.DOS_525_DS_DD_8,
Sectors = 640,
SectorSize = 512,
Md5 = "4989762c82f173f9b52e0bdb8cf5becb"
},
new()
{
TestFile = "5dd8_ak.dsk",
MediaType = MediaType.DOS_525_DS_DD_8,
Sectors = 640,
SectorSize = 512,
Md5 = "4989762c82f173f9b52e0bdb8cf5becb"
},
new()
{
TestFile = "5dd8defs.dsk",
MediaType = MediaType.DOS_525_DS_DD_8,
Sectors = 640,
SectorSize = 512,
Md5 = "5a1e0a75d31d88c1ce7429fd333c268f"
},
new()
{
TestFile = "5dd_a.dsk",
MediaType = MediaType.DOS_525_DS_DD_9,
Sectors = 720,
SectorSize = 512,
Md5 = "8a4d35dd0d97e6bca8b000170a43a56f"
},
new()
{
TestFile = "5dd_ak.dsk",
MediaType = MediaType.DOS_525_DS_DD_9,
Sectors = 720,
SectorSize = 512,
Md5 = "8a4d35dd0d97e6bca8b000170a43a56f"
},
new()
{
TestFile = "5dddefs.dsk",
MediaType = MediaType.DOS_525_DS_DD_9,
Sectors = 720,
SectorSize = 512,
Md5 = "c1a67b27bc76b64d0845965501b24120"
},
new()
{
TestFile = "5hd_a.dsk",
MediaType = MediaType.DOS_525_HD,
Sectors = 2400,
SectorSize = 512,
Md5 = "2ce745ac23712d3eb03d7a11ba933b12"
},
new()
{
TestFile = "5hd_ak.dsk",
MediaType = MediaType.DOS_525_HD,
Sectors = 2400,
SectorSize = 512,
Md5 = "2ce745ac23712d3eb03d7a11ba933b12"
},
new()
{
TestFile = "5hddefs.dsk",
MediaType = MediaType.DOS_525_HD,
Sectors = 2400,
SectorSize = 512,
Md5 = "1c28b4c3cdc1dbf19c24a5eca3891a87"
},
new()
{
TestFile = "5sd8_a.dsk",
MediaType = MediaType.DOS_525_SS_DD_8,
Sectors = 320,
SectorSize = 512,
Md5 = "6f5d09c13a7b481bad9ea78042e61e00"
},
new()
{
TestFile = "5sd8_ak.dsk",
MediaType = MediaType.DOS_525_SS_DD_8,
Sectors = 320,
SectorSize = 512,
Md5 = "6f5d09c13a7b481bad9ea78042e61e00"
},
new()
{
TestFile = "5sd8defs.dsk",
MediaType = MediaType.DOS_525_SS_DD_8,
Sectors = 320,
SectorSize = 512,
Md5 = "65ce0cd08d90c882df12637c9c72c1ba"
},
new()
{
TestFile = "5sd_a.dsk",
MediaType = MediaType.DOS_525_SS_DD_9,
Sectors = 360,
SectorSize = 512,
Md5 = "fd81fceb26bda5b02053c5c729a6f67f"
},
new()
{
TestFile = "5sd_ak.dsk",
MediaType = MediaType.DOS_525_SS_DD_9,
Sectors = 360,
SectorSize = 512,
Md5 = "fd81fceb26bda5b02053c5c729a6f67f"
},
new()
{
TestFile = "5sddefs.dsk",
MediaType = MediaType.DOS_525_SS_DD_9,
Sectors = 360,
SectorSize = 512,
Md5 = "412fdc582506c0d7e76735d403b30759"
},
new()
{
TestFile = "mf2dd_a.dsk",
MediaType = MediaType.DOS_35_DS_DD_9,
Sectors = 1440,
SectorSize = 512,
Md5 = "e574be0d057f2ef775dfb685561d27cf"
},
new()
{
TestFile = "mf2dd_ak.dsk",
MediaType = MediaType.DOS_35_DS_DD_9,
Sectors = 1440,
SectorSize = 512,
Md5 = "e574be0d057f2ef775dfb685561d27cf"
},
new()
{
TestFile = "mf2dd_defs.dsk",
MediaType = MediaType.DOS_35_DS_DD_9,
Sectors = 1440,
SectorSize = 512,
Md5 = "2aefc1e97f29bf9982e0fd7091dfb9f5"
},
new()
{
TestFile = "mf2ed_a.dsk",
MediaType = MediaType.ECMA_147,
Sectors = 5760,
SectorSize = 512,
Md5 = "42e73287b23ac985c9825466cae26859"
},
new()
{
TestFile = "mf2ed_ak.dsk",
MediaType = MediaType.ECMA_147,
Sectors = 5760,
SectorSize = 512,
Md5 = "42e73287b23ac985c9825466cae26859"
},
new()
{
TestFile = "mf2ed_defs.dsk",
MediaType = MediaType.ECMA_147,
Sectors = 5760,
SectorSize = 512,
Md5 = "e4746aa9629a2325c520db1c8a641ac6"
},
new()
{
TestFile = "mf2hd_a.dsk",
MediaType = MediaType.DOS_35_HD,
Sectors = 2880,
SectorSize = 512,
Md5 = "009cc68e28b2b13814d3afbec9d9e59f"
},
new()
{
TestFile = "mf2hd_ak.dsk",
MediaType = MediaType.DOS_35_HD,
Sectors = 2880,
SectorSize = 512,
Md5 = "009cc68e28b2b13814d3afbec9d9e59f"
},
new()
{
TestFile = "mf2hd_defs.dsk",
MediaType = MediaType.DOS_35_HD,
Sectors = 2880,
SectorSize = 512,
Md5 = "003e9130d83a23018f488f9fa89cae5e"
},
new()
{
TestFile = "mf2hd_xdf_a.dsk",
MediaType = MediaType.XDF_35,
Sectors = 3680,
SectorSize = 512,
Md5 = "34b4bdab5fcc17076cceb7c1a39ea430"
},
new()
{
TestFile = "mf2hd_xdf_ak.dsk",
MediaType = MediaType.XDF_35,
Sectors = 3680,
SectorSize = 512,
Md5 = "34b4bdab5fcc17076cceb7c1a39ea430"
},
new()
{
TestFile = "mf2hd_xdf_defs.dsk",
MediaType = MediaType.XDF_35,
Sectors = 3680,
SectorSize = 512,
Md5 = "2770e5b1b7935ca6e9695a32008b936a"
}
];
}