diff --git a/DiscImageChef.DiscImages/DiscImageChef.DiscImages.csproj b/DiscImageChef.DiscImages/DiscImageChef.DiscImages.csproj index e8b81c95..10fd3cb9 100644 --- a/DiscImageChef.DiscImages/DiscImageChef.DiscImages.csproj +++ b/DiscImageChef.DiscImages/DiscImageChef.DiscImages.csproj @@ -52,6 +52,7 @@ + diff --git a/DiscImageChef.DiscImages/HDCopy.cs b/DiscImageChef.DiscImages/HDCopy.cs new file mode 100644 index 00000000..d46ed25c --- /dev/null +++ b/DiscImageChef.DiscImages/HDCopy.cs @@ -0,0 +1,657 @@ +// /*************************************************************************** +// The Disc Image Chef +// ---------------------------------------------------------------------------- +// +// Filename : HDCopy.cs +// Author(s) : Michael Drüing +// +// Component : Disc image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Manages floppy disk images created with HD-Copy +// +// --[ 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 © 2017 Michael Drüing +// ****************************************************************************/ + +/* Some information on the file format from Michal Necasek (www.os2museum.com): + * + * The HD-Copy diskette image format was used by the eponymous DOS utility, + * written by Oliver Fromme around 1995. The HD-Copy format is relatively + * straightforward, supporting images with 512-byte sector size and uniform + * sectors per track count. A basic form of run-length compression is also + * supported, and empty/unused tracks aren't stored in the image. Images + * with up to 82 cylinders are supported. + * + * No provision appears to be made for single-sided images. The disk image + * is stored as a sequence of compressed tracks (where a track refers to only + * one side of the disk), and individual tracks may be left out. + * + * The HD-Copy RLE compression works as follows. The image is divided into a + * number of independent blocks, one per track. Each compressed block starts + * with a header which contains the size of compressed data (16-bit little + * endian) and the escape byte. Whenever the escape byte is encountered in the + * byte stream, it is followed by a data byte and a count byte. + * + * Note that HD-Copy uses RLE compression for sequences of as few as three + * bytes, even though that provides no benefit. + * + * It would be tempting to perform in-place decompression to save memory. + * Unfortunately the simplistic RLE algorithm means the encoded data may be + * larger than the decoded version, with unknown worst case behavior. Hence + * the compressed data for a sector may not fit into a buffer the size of the + * uncompressed sector. + * + * There is no signature, hence heuristics must be used to identify a HD-Copy + * diskette image. Fortunately, the HD-Copy header is highly recognizable. + */ + +using System; +using System.IO; +using System.Collections.Generic; +using DiscImageChef.Console; +using DiscImageChef.CommonTypes; +using DiscImageChef.Filters; +using System.Runtime.InteropServices; + +namespace DiscImageChef.ImagePlugins +{ + public class HDCopy : ImagePlugin + { + #region Internal structures + /// + /// The global header of a HDCP image file + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct HDCPFileHeader + { + /// + /// Last cylinder (zero-based) + /// + public byte lastCylinder; + + /// + /// Sectors per track + /// + public byte sectorsPerTrack; + + /// + /// The track map. It contains one byte for each track. + /// Up to 82 tracks (41 tracks * 2 sides) are supported. + /// 0 means track is not present, 1 means it is present. + /// The first 2 tracks are always present. + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2 * 82)] + public byte[] trackMap; + } + + /// + /// The header for a RLE-compressed block + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct HDCPBlockHeader + { + /// + /// The length of the compressed block, in bytes. Little-endian. + /// + public UInt16 length; + + /// + /// The byte value used as RLE escape sequence + /// + public byte escape; + } + + struct MediaTypeTableEntry + { + public byte tracks; + public byte sectorsPerTrack; + public MediaType mediaType; + + public MediaTypeTableEntry(byte _tracks, byte _sectorsPerTrack, MediaType _mediaType) + { + tracks = _tracks; + sectorsPerTrack = _sectorsPerTrack; + mediaType = _mediaType; + } + } + + #endregion + + #region Internal variables + /// + /// The HDCP file header after the image has been opened + /// + private HDCPFileHeader fileHeader; + + /// + /// Every track that has been read is cached here + /// + private Dictionary trackCache = new Dictionary(); + + /// + /// The offset in the file where each track starts, or -1 if the track is not present + /// + private Dictionary trackOffset = new Dictionary(); + + /// + /// The ImageFilter we're reading from, after the file has been opened + /// + Filter hdcpImageFilter = null; + #endregion + + #region Internal constants + private readonly MediaTypeTableEntry[] mediaTypes = + { + new MediaTypeTableEntry(80, 8, MediaType.DOS_35_DS_DD_8), + new MediaTypeTableEntry(80, 9, MediaType.DOS_35_DS_DD_9), + new MediaTypeTableEntry(80, 18, MediaType.DOS_35_HD), + new MediaTypeTableEntry(80, 36, MediaType.DOS_35_ED), + new MediaTypeTableEntry(40, 8, MediaType.DOS_525_DS_DD_8), + new MediaTypeTableEntry(40, 9, MediaType.DOS_525_DS_DD_9), + new MediaTypeTableEntry(80, 15, MediaType.DOS_525_HD), + }; + #endregion + + public HDCopy() + { + Name = "HD-Copy disk image"; + PluginUUID = new Guid("8D57483F-71A5-42EC-9B87-66AEC439C792"); + ImageInfo = new ImageInfo(); + ImageInfo.readableSectorTags = new List(); + ImageInfo.readableMediaTags = new List(); + ImageInfo.imageHasPartitions = false; + ImageInfo.imageHasSessions = false; + ImageInfo.imageVersion = null; + ImageInfo.imageApplication = null; + ImageInfo.imageApplicationVersion = null; + ImageInfo.imageCreator = null; + ImageInfo.imageComments = null; + ImageInfo.mediaManufacturer = null; + ImageInfo.mediaModel = null; + ImageInfo.mediaSerialNumber = null; + ImageInfo.mediaBarcode = null; + ImageInfo.mediaPartNumber = null; + ImageInfo.mediaSequence = 0; + ImageInfo.lastMediaSequence = 0; + ImageInfo.driveManufacturer = null; + ImageInfo.driveModel = null; + ImageInfo.driveSerialNumber = null; + ImageInfo.driveFirmwareRevision = null; + } + + public override bool IdentifyImage(Filter imageFilter) + { + HDCPFileHeader fheader; + + Stream stream = imageFilter.GetDataForkStream(); + stream.Seek(0, SeekOrigin.Begin); + + if (stream.Length < 2 + 2 * 82) + return false; + + byte[] header = new byte[2 + 2 * 82]; + stream.Read(header, 0, 2 + 2 * 82); + + IntPtr hdrPtr = Marshal.AllocHGlobal(2 + 2 * 82); + Marshal.Copy(header, 0, hdrPtr, 2 + 2 * 82); + fheader = (HDCPFileHeader)Marshal.PtrToStructure(hdrPtr, typeof(HDCPFileHeader)); + Marshal.FreeHGlobal(hdrPtr); + + /* Some sanity checks on the values we just read. + * We know the image is from a DOS floppy disk, so assume + * some sane cylinder and sectors-per-track count. + */ + if ((fheader.sectorsPerTrack < 8) || (fheader.sectorsPerTrack > 40)) + return false; + + if ((fheader.lastCylinder < 37) || (fheader.lastCylinder >= 82)) + return false; + + // Validate the trackmap. First two tracks need to be present + if ((fheader.trackMap[0] != 1) || (fheader.trackMap[1] != 1)) + return false; + + // all other tracks must be either present (=1) or absent (=0) + for (int i = 0; i < 2 * 82; i++) + { + if (fheader.trackMap[i] > 1) + return false; + } + + // TODO: validate the tracks + // For now, having a valid header should be sufficient. + return true; + } + + public override bool OpenImage(Filter imageFilter) + { + HDCPFileHeader fheader; + long currentOffset; + + Stream stream = imageFilter.GetDataForkStream(); + stream.Seek(0, SeekOrigin.Begin); + + byte[] header = new byte[2 + 2 * 82]; + stream.Read(header, 0, 2 + 2 * 82); + + IntPtr hdrPtr = Marshal.AllocHGlobal(2 + 2 * 82); + Marshal.Copy(header, 0, hdrPtr, 2 + 2 * 82); + fheader = (HDCPFileHeader)Marshal.PtrToStructure(hdrPtr, typeof(HDCPFileHeader)); + Marshal.FreeHGlobal(hdrPtr); + DicConsole.DebugWriteLine("HDCP plugin", "Detected HD-Copy image with {0} tracks and {1} sectors per track.", fheader.lastCylinder + 1, fheader.sectorsPerTrack); + + ImageInfo.cylinders = (uint)fheader.lastCylinder + 1; + ImageInfo.sectorsPerTrack = fheader.sectorsPerTrack; + ImageInfo.sectorSize = 512; // only 512 bytes per sector supported + ImageInfo.heads = 2; // only 2-sided floppies are supported + ImageInfo.sectors = 2 * ImageInfo.cylinders * ImageInfo.sectorsPerTrack; + ImageInfo.imageSize = ImageInfo.sectors * ImageInfo.sectorSize; + + ImageInfo.xmlMediaType = XmlMediaType.BlockMedia; + + ImageInfo.imageCreationTime = imageFilter.GetCreationTime(); + ImageInfo.imageLastModificationTime = imageFilter.GetLastWriteTime(); + ImageInfo.imageName = Path.GetFileNameWithoutExtension(imageFilter.GetFilename()); + ImageInfo.mediaType = GetMediaType(); + + // the start offset of the track data + currentOffset = 2 + 2 * 82; + + // build table of track offsets + for (int i = 0; i < ImageInfo.cylinders * 2; i++) + { + if (fheader.trackMap[i] == 0) + { + // track is not present in image + trackOffset[i] = -1; + } + else + { + // track is present, read the block header + if (currentOffset + 3 >= stream.Length) + return false; + + byte[] blkHeader = new byte[2]; + short blkLength; + stream.Read(blkHeader, 0, 2); + blkLength = BitConverter.ToInt16(blkHeader, 0); + + // assume block sizes are positive + if (blkLength < 0) + return false; + + DicConsole.DebugWriteLine("HDCP plugin", "Track {0} offset 0x{1:x8}, size={2:x4}", i, currentOffset, blkLength); + trackOffset[i] = currentOffset; + + currentOffset += 2 + blkLength; + // skip the block data + stream.Seek(blkLength, SeekOrigin.Current); + } + } + + // ensure that the last track is present completely + if (currentOffset > stream.Length) + return false; + + // save some variables for later use + fileHeader = fheader; + hdcpImageFilter = imageFilter; + return true; + } + + public override bool ImageHasPartitions() + { + return false; + } + + public override ulong GetImageSize() + { + return ImageInfo.imageSize; + } + + public override ulong GetSectors() + { + return ImageInfo.sectors; + } + + public override uint GetSectorSize() + { + return ImageInfo.sectorSize; + } + + public override string GetImageFormat() + { + return "HD-Copy image"; + } + + public override string GetImageVersion() + { + return ImageInfo.imageVersion; + } + + public override string GetImageApplication() + { + return ImageInfo.imageApplication; + } + + public override string GetImageApplicationVersion() + { + return ImageInfo.imageApplicationVersion; + } + + public override string GetImageCreator() + { + return ImageInfo.imageCreator; + } + + public override DateTime GetImageCreationTime() + { + return ImageInfo.imageCreationTime; + } + + public override DateTime GetImageLastModificationTime() + { + return ImageInfo.imageLastModificationTime; + } + + public override string GetImageName() + { + return ImageInfo.imageName; + } + + public override string GetImageComments() + { + return ImageInfo.imageComments; + } + + public override MediaType GetMediaType() + { + foreach (MediaTypeTableEntry ent in mediaTypes) + { + if ((ent.tracks == ImageInfo.cylinders) && (ent.sectorsPerTrack == ImageInfo.sectorsPerTrack)) + return ent.mediaType; + } + + return MediaType.Unknown; + } + + private void ReadTrackIntoCache(Stream stream, int tracknum) + { + byte[] trackData = new byte[ImageInfo.sectorSize * ImageInfo.sectorsPerTrack]; + byte[] blkHeader = new byte[3]; + byte escapeByte; + byte fillByte; + byte fillCount; + byte[] cBuffer; + short compressedLength; + + // check that track is present + if (trackOffset[tracknum] == -1) + throw new InvalidDataException("Tried reading a track that is not present in image"); + + stream.Seek(trackOffset[tracknum], SeekOrigin.Begin); + + // read the compressed track data + stream.Read(blkHeader, 0, 3); + compressedLength = (short)(BitConverter.ToInt16(blkHeader, 0) - 1); + escapeByte = blkHeader[2]; + + cBuffer = new byte[compressedLength]; + stream.Read(cBuffer, 0, compressedLength); + + // decompress the data + int sIndex = 0; // source buffer position + int dIndex = 0; // destination buffer position + while (sIndex < compressedLength) + { + if (cBuffer[sIndex] == escapeByte) + { + sIndex++; // skip over escape byte + fillByte = cBuffer[sIndex++]; // read fill byte + fillCount = cBuffer[sIndex++]; // read fill count + // fill destination buffer + for (int i = 0; i < fillCount; i++) + { + trackData[dIndex++] = fillByte; + } + } + else + { + trackData[dIndex++] = cBuffer[sIndex++]; + } + } + + // check that the number of bytes decompressed matches a whole track + if (dIndex != ImageInfo.sectorSize * ImageInfo.sectorsPerTrack) + throw new InvalidDataException("Track decompression yielded incomplete data"); + + // store track in cache + trackCache[tracknum] = trackData; + } + + public override byte[] ReadSector(ulong sectorAddress) + { + int trackNum = (int)(sectorAddress / ImageInfo.sectorsPerTrack); + int sectorOffset = (int)(sectorAddress % (ImageInfo.sectorsPerTrack * ImageInfo.sectorSize)); + byte[] result; + + if (sectorAddress > ImageInfo.sectors - 1) + throw new ArgumentOutOfRangeException(nameof(sectorAddress), "Sector address not found"); + + if (trackNum > 2 * ImageInfo.cylinders) + throw new ArgumentOutOfRangeException(nameof(sectorAddress), "Sector address not found"); + + result = new byte[ImageInfo.sectorSize]; + if (trackOffset[trackNum] == -1) + { + // track is not present. Fill with zeroes. + Array.Clear(result, 0, (int)ImageInfo.sectorSize); + } + else + { + // track is present in file, make sure it has been loaded + if (!trackCache.ContainsKey(trackNum)) + ReadTrackIntoCache(hdcpImageFilter.GetDataForkStream(), trackNum); + + Array.Copy(trackCache[trackNum], sectorOffset, result, 0, ImageInfo.sectorSize); + } + + return result; + } + + public override byte[] ReadSectors(ulong sectorAddress, uint length) + { + byte[] result = new byte[length * ImageInfo.sectorSize]; + + if (sectorAddress + length > ImageInfo.sectors) + throw new ArgumentOutOfRangeException(nameof(length), "Requested more sectors than available"); + + for (int i = 0; i < length; i++) + { + ReadSector(sectorAddress + (ulong)i).CopyTo(result, i * ImageInfo.sectorSize); + } + + return result; + } + + #region Unsupported features + + public override byte[] ReadDiskTag(MediaTagType tag) + { + throw new FeatureUnsupportedImageException("Feature not supported by image format"); + } + + public override byte[] ReadSectorTag(ulong sectorAddress, SectorTagType tag) + { + throw new FeatureUnsupportedImageException("Feature not supported by image format"); + } + + public override byte[] ReadSector(ulong sectorAddress, uint track) + { + throw new FeatureUnsupportedImageException("Feature not supported by image format"); + } + + public override byte[] ReadSectorTag(ulong sectorAddress, uint track, SectorTagType tag) + { + throw new FeatureUnsupportedImageException("Feature not supported by image format"); + } + + public override byte[] ReadSectorsTag(ulong sectorAddress, uint length, SectorTagType tag) + { + throw new FeatureUnsupportedImageException("Feature not supported by image format"); + } + + public override byte[] ReadSectors(ulong sectorAddress, uint length, uint track) + { + throw new FeatureUnsupportedImageException("Feature not supported by image format"); + } + + public override byte[] ReadSectorsTag(ulong sectorAddress, uint length, uint track, SectorTagType tag) + { + throw new FeatureUnsupportedImageException("Feature not supported by image format"); + } + + public override byte[] ReadSectorLong(ulong sectorAddress) + { + throw new FeatureUnsupportedImageException("Feature not supported by image format"); + } + + public override byte[] ReadSectorLong(ulong sectorAddress, uint track) + { + throw new FeatureUnsupportedImageException("Feature not supported by image format"); + } + + public override byte[] ReadSectorsLong(ulong sectorAddress, uint length) + { + throw new FeatureUnsupportedImageException("Feature not supported by image format"); + } + + public override byte[] ReadSectorsLong(ulong sectorAddress, uint length, uint track) + { + throw new FeatureUnsupportedImageException("Feature not supported by image format"); + } + + public override string GetMediaManufacturer() + { + return null; + } + + public override string GetMediaModel() + { + return null; + } + + public override string GetMediaSerialNumber() + { + return null; + } + + public override string GetMediaBarcode() + { + return null; + } + + public override string GetMediaPartNumber() + { + return null; + } + + public override int GetMediaSequence() + { + return 0; + } + + public override int GetLastDiskSequence() + { + return 0; + } + + public override string GetDriveManufacturer() + { + return null; + } + + public override string GetDriveModel() + { + return null; + } + + public override string GetDriveSerialNumber() + { + return null; + } + + public override List GetPartitions() + { + throw new FeatureUnsupportedImageException("Feature not supported by image format"); + } + + public override List GetTracks() + { + throw new FeatureUnsupportedImageException("Feature not supported by image format"); + } + + public override List GetSessionTracks(Session session) + { + throw new FeatureUnsupportedImageException("Feature not supported by image format"); + } + + public override List GetSessionTracks(ushort session) + { + throw new FeatureUnsupportedImageException("Feature not supported by image format"); + } + + public override List GetSessions() + { + throw new FeatureUnsupportedImageException("Feature not supported by image format"); + } + + public override bool? VerifySector(ulong sectorAddress) + { + return null; + } + + public override bool? VerifySector(ulong sectorAddress, uint track) + { + throw new FeatureUnsupportedImageException("Feature not supported by image format"); + } + + public override bool? VerifySectors(ulong sectorAddress, uint length, out List FailingLBAs, out List UnknownLBAs) + { + FailingLBAs = new List(); + UnknownLBAs = new List(); + for (ulong i = 0; i < ImageInfo.sectors; i++) + UnknownLBAs.Add(i); + return null; + } + + public override bool? VerifySectors(ulong sectorAddress, uint length, uint track, out List FailingLBAs, out List UnknownLBAs) + { + throw new FeatureUnsupportedImageException("Feature not supported by image format"); + } + + public override bool? VerifyMediaImage() + { + return null; + } + + #endregion + } +}