diff --git a/DiscImageChef.DiscImages/CPCDSK.cs b/DiscImageChef.DiscImages/CPCDSK.cs index f9f56d8d..8fc6e1f1 100644 --- a/DiscImageChef.DiscImages/CPCDSK.cs +++ b/DiscImageChef.DiscImages/CPCDSK.cs @@ -29,14 +29,732 @@ // ---------------------------------------------------------------------------- // Copyright © 2011-2016 Natalia Portillo // ****************************************************************************/ + using System; -namespace DiscImageChef.DiscImages +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using DiscImageChef.Checksums; +using DiscImageChef.CommonTypes; +using DiscImageChef.Console; +using DiscImageChef.Decoders.Floppy; + +namespace DiscImageChef.ImagePlugins { - public class CPCDSK + // Checked using several images and strings inside Apple's DiskImages.framework + class CPCDSK : ImagePlugin { + #region Internal constants + /// + /// Identifier for CPCEMU disk images, "MV - CPCEMU Disk-File" + /// + readonly byte[] CPCDSKId = { 0x4D, 0x56, 0x20, 0x2D, 0x20, 0x43, 0x50, 0x43, 0x45, 0x4D, 0x55, 0x20, 0x44, 0x69, 0x73, 0x6B, 0x2D, 0x46, 0x69, 0x6C, 0x65 }; + /// + /// Identifier for Extended CPCEMU disk images, "EXTENDED CPC DSK File" + /// + readonly byte[] EDSKId = { 0x45, 0x58, 0x54, 0x45, 0x4E, 0x44, 0x45, 0x44, 0x20, 0x43, 0x50, 0x43, 0x20, 0x44, 0x53, 0x4B, 0x20, 0x46, 0x69, 0x6C, 0x65 }; + /// + /// Identifier for track information, "Track-Info\r\n" + /// + readonly byte[] TrackId = { 0x54, 0x72, 0x61, 0x63, 0x6B, 0x2D, 0x49, 0x6E, 0x66, 0x6F }; + #endregion + + #region Internal structures + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct CPCDiskInfo + { + /// + /// Magic number, "MV - CPCEMU Disk-File" in old files, "EXTENDED CPC DSK File" in extended ones + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 21)] + public byte[] magic; + /// + /// Second part of magic, should be "\r\nDisk-Info\r\n" in all, but some emulators write spaces instead. + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 13)] + public byte[] magic2; + /// + /// Creator application (can be null) + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 14)] + public byte[] creator; + /// + /// Tracks + /// + public byte tracks; + /// + /// Sides + /// + public byte sides; + /// + /// Size of a track including the 256 bytes header. Unused by extended format, as this format includes a table in the next field + /// + public ushort tracksize; + /// + /// Size of each track in the extended format. 0 indicates track is not formatted and not present in image. + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 204)] + public byte[] tracksizeTable; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct CPCTrackInfo + { + /// + /// Magic number, "Track-Info\r\n" + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)] + public byte[] magic; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] + public byte[] carriageReturn; + /// + /// Padding + /// + public uint padding; + /// + /// Track number + /// + public byte track; + /// + /// Side number + /// + public byte side; + /// + /// Controller data rate + /// + public byte dataRate; + /// + /// Recording mode + /// + public byte recordingMode; + /// + /// Bytes per sector + /// + public IBMSectorSizeCode bps; + /// + /// How many sectors in this track + /// + public byte sectors; + /// + /// GAP#3 + /// + public byte gap3; + /// + /// Filler + /// + public byte filler; + /// + /// Informatino for up to 32 sectors + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] + public CPCSectorInfo[] sectorsInfo; + } + + /// + /// Sector information + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct CPCSectorInfo + { + /// + /// Track number from address mark + /// + public byte track; + /// + /// Side number from address mark + /// + public byte side; + /// + /// Sector ID from address mark + /// + public byte id; + /// + /// Sector size from address mark + /// + public IBMSectorSizeCode size; + /// + /// ST1 register from controller + /// + public byte st1; + /// + /// ST2 register from controller + /// + public byte st2; + /// + /// Length in bytes of this sector size. If it is bigger than expected sector size, it's a weak sector read several times. + /// + public ushort len; + } + #endregion + + #region Internal variables + bool extended; + Dictionary sectors; + Dictionary addressMarks; + #endregion + public CPCDSK() { + Name = "CPCEMU Disk-File and Extended CPC Disk-File"; + PluginUUID = new Guid("724B16CC-ADB9-492E-BA07-CAEEC1012B16"); + 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(string imagePath) + { + FileStream stream = new FileStream(imagePath, FileMode.Open, FileAccess.Read); + stream.Seek(0, SeekOrigin.Begin); + + if(stream.Length < 512) + return false; + + byte[] header_b = new byte[256]; + stream.Read(header_b, 0, 256); + CPCDiskInfo header = new CPCDiskInfo(); + IntPtr headerPtr = Marshal.AllocHGlobal(256); + Marshal.Copy(header_b, 0, headerPtr, 256); + header = (CPCDiskInfo)Marshal.PtrToStructure(headerPtr, typeof(CPCDiskInfo)); + Marshal.FreeHGlobal(headerPtr); + + DicConsole.DebugWriteLine("CPCDSK plugin", "header.magic = \"{0}\"", StringHandlers.CToString(header.magic)); + + return CPCDSKId.SequenceEqual(header.magic) || EDSKId.SequenceEqual(header.magic); + } + + public override bool OpenImage(string imagePath) + { + FileStream stream = new FileStream(imagePath, FileMode.Open, FileAccess.Read); + stream.Seek(0, SeekOrigin.Begin); + + if(stream.Length < 512) + return false; + + byte[] header_b = new byte[256]; + stream.Read(header_b, 0, 256); + CPCDiskInfo header = new CPCDiskInfo(); + IntPtr headerPtr = Marshal.AllocHGlobal(256); + Marshal.Copy(header_b, 0, headerPtr, 256); + header = (CPCDiskInfo)Marshal.PtrToStructure(headerPtr, typeof(CPCDiskInfo)); + Marshal.FreeHGlobal(headerPtr); + + if(!CPCDSKId.SequenceEqual(header.magic) && !EDSKId.SequenceEqual(header.magic)) + return false; + + extended = EDSKId.SequenceEqual(header.magic); + DicConsole.DebugWriteLine("CPCDSK plugin", "Extended = {0}", extended); + DicConsole.DebugWriteLine("CPCDSK plugin", "header.magic = \"{0}\"", StringHandlers.CToString(header.magic)); + DicConsole.DebugWriteLine("CPCDSK plugin", "header.magic2 = \"{0}\"", StringHandlers.CToString(header.magic2)); + DicConsole.DebugWriteLine("CPCDSK plugin", "header.creator = \"{0}\"", StringHandlers.CToString(header.creator)); + DicConsole.DebugWriteLine("CPCDSK plugin", "header.tracks = {0}", header.tracks); + DicConsole.DebugWriteLine("CPCDSK plugin", "header.sides = {0}", header.sides); + if(!extended) + DicConsole.DebugWriteLine("CPCDSK plugin", "header.tracksize = {0}", header.tracksize); + else + { + for(int i = 0; i < header.tracks; i++) + { + for(int j = 0; j < header.sides; j++) + { + DicConsole.DebugWriteLine("CPCDSK plugin", "Track {0} Side {1} size = {2}", i, j, header.tracksizeTable[i*header.sides + j] * 256); + } + } + } + + ulong currentSector = 0; + sectors = new Dictionary(); + addressMarks = new Dictionary(); + ulong readtracks = 0; + bool allTracksSameSize = true; + ulong sectorsPerTrack = 0; + + // Seek to first track descriptor + stream.Seek(256, SeekOrigin.Begin); + for(int i = 0; i < header.tracks; i++) + { + for(int j = 0; j < header.sides; j++) + { + // Track not stored in image + if(extended && header.tracksizeTable[i * header.sides + j] == 0) + continue; + + long trackPos = stream.Position; + + byte[] track_b = new byte[256]; + stream.Read(track_b, 0, 256); + CPCTrackInfo trackInfo = new CPCTrackInfo(); + IntPtr trackPtr = Marshal.AllocHGlobal(256); + Marshal.Copy(track_b, 0, trackPtr, 256); + trackInfo = (CPCTrackInfo)Marshal.PtrToStructure(trackPtr, typeof(CPCTrackInfo)); + Marshal.FreeHGlobal(trackPtr); + + if(!TrackId.SequenceEqual(trackInfo.magic)) + { + DicConsole.ErrorWriteLine("Not the expected track info."); + return false; + } + + DicConsole.DebugWriteLine("CPCDSK plugin", "trackInfo[{1}:{2}].magic = \"{0}\"", StringHandlers.CToString(trackInfo.magic), i, j); + DicConsole.DebugWriteLine("CPCDSK plugin", "trackInfo[{1}:{2}].bps = {0}", SizeCodeToBytes(trackInfo.bps), i, j); + DicConsole.DebugWriteLine("CPCDSK plugin", "trackInfo[{1}:{2}].dataRate = {0}", trackInfo.dataRate, i, j); + DicConsole.DebugWriteLine("CPCDSK plugin", "trackInfo[{1}:{2}].filler = 0x{0:X2}", trackInfo.filler, i, j); + DicConsole.DebugWriteLine("CPCDSK plugin", "trackInfo[{1}:{2}].gap3 = 0x{0:X2}", trackInfo.gap3, i, j); + DicConsole.DebugWriteLine("CPCDSK plugin", "trackInfo[{1}:{2}].padding = {0}", trackInfo.padding, i, j); + DicConsole.DebugWriteLine("CPCDSK plugin", "trackInfo[{1}:{2}].recordingMode = {0}", trackInfo.recordingMode, i, j); + DicConsole.DebugWriteLine("CPCDSK plugin", "trackInfo[{1}:{2}].sectors = {0}", trackInfo.sectors, i, j); + DicConsole.DebugWriteLine("CPCDSK plugin", "trackInfo[{1}:{2}].side = {0}", trackInfo.side, i, j); + DicConsole.DebugWriteLine("CPCDSK plugin", "trackInfo[{1}:{2}].track = {0}", trackInfo.track, i, j); + + if(trackInfo.sectors != sectorsPerTrack) + { + if(sectorsPerTrack == 0) + sectorsPerTrack = trackInfo.sectors; + else + allTracksSameSize = false; + } + + byte[][] thisTrackSectors = new byte[trackInfo.sectors][]; + byte[][] thisTrackAddressMarks = new byte[trackInfo.sectors][]; + + for(int k = 1; k <= trackInfo.sectors; k++) + { + DicConsole.DebugWriteLine("CPCDSK plugin", "trackInfo[{1}:{2}].sector[{3}].id = 0x{0:X2}", trackInfo.sectorsInfo[k - 1].id, i, j, k); + DicConsole.DebugWriteLine("CPCDSK plugin", "trackInfo[{1}:{2}].sector[{3}].len = {0}", trackInfo.sectorsInfo[k - 1].len, i, j, k); + DicConsole.DebugWriteLine("CPCDSK plugin", "trackInfo[{1}:{2}].sector[{3}].side = {0}", trackInfo.sectorsInfo[k - 1].side, i, j, k); + DicConsole.DebugWriteLine("CPCDSK plugin", "trackInfo[{1}:{2}].sector[{3}].size = {0}", SizeCodeToBytes(trackInfo.sectorsInfo[k - 1].size), i, j, k); + DicConsole.DebugWriteLine("CPCDSK plugin", "trackInfo[{1}:{2}].sector[{3}].st1 = 0x{0:X2}", trackInfo.sectorsInfo[k - 1].st1, i, j, k); + DicConsole.DebugWriteLine("CPCDSK plugin", "trackInfo[{1}:{2}].sector[{3}].st2 = 0x{0:X2}", trackInfo.sectorsInfo[k - 1].st2, i, j, k); + DicConsole.DebugWriteLine("CPCDSK plugin", "trackInfo[{1}:{2}].sector[{3}].track = {0}", trackInfo.sectorsInfo[k - 1].track, i, j, k); + + int sectLen; + if(extended) + sectLen = trackInfo.sectorsInfo[k - 1].len; + else + sectLen = SizeCodeToBytes(trackInfo.sectorsInfo[k - 1].size); + + byte[] sector = new byte[sectLen]; + stream.Read(sector, 0, sectLen); + + if(sectLen < SizeCodeToBytes(trackInfo.sectorsInfo[k - 1].size)) + { + byte[] temp = new byte[SizeCodeToBytes(trackInfo.sectorsInfo[k - 1].size)]; + Array.Copy(sector, 0, temp, 0, sector.Length); + sector = temp; + } + else if(sectLen > SizeCodeToBytes(trackInfo.sectorsInfo[k - 1].size)) + { + byte[] temp = new byte[SizeCodeToBytes(trackInfo.sectorsInfo[k - 1].size)]; + Array.Copy(sector, 0, temp, 0, temp.Length); + sector = temp; + } + + thisTrackSectors[(trackInfo.sectorsInfo[k - 1].id & 0x3F) - 1] = sector; + + byte[] amForCrc = new byte[8]; + amForCrc[0] = 0xA1; + amForCrc[1] = 0xA1; + amForCrc[2] = 0xA1; + amForCrc[3] = (byte)IBMIdType.AddressMark; + amForCrc[4] = trackInfo.sectorsInfo[k - 1].track; + amForCrc[5] = trackInfo.sectorsInfo[k - 1].side; + amForCrc[6] = trackInfo.sectorsInfo[k - 1].id; + amForCrc[7] = (byte)trackInfo.sectorsInfo[k - 1].size; + byte[] amCrc; + + CRC16Context.Data(amForCrc, 8, out amCrc); + + byte[] addressMark = new byte[22]; + Array.Copy(amForCrc, 0, addressMark, 12, 8); + Array.Copy(amCrc, 0, addressMark, 20, 2); + + thisTrackAddressMarks[(trackInfo.sectorsInfo[k - 1].id & 0x3F) - 1] = addressMark; + } + + for(int s = 0; s < thisTrackSectors.Length; s++) + { + sectors.Add(currentSector, thisTrackSectors[s]); + addressMarks.Add(currentSector, thisTrackAddressMarks[s]); + currentSector++; + if(thisTrackSectors[s].Length > ImageInfo.sectorSize) + ImageInfo.sectorSize = (uint)thisTrackSectors[s].Length; + } + + stream.Seek(trackPos, SeekOrigin.Begin); + if(extended) + { + stream.Seek(header.tracksizeTable[i * header.sides + j] * 256, SeekOrigin.Current); + ImageInfo.imageSize += (ulong)(header.tracksizeTable[i * header.sides + j] * 256) - 256; + } + else + { + stream.Seek(header.tracksize, SeekOrigin.Current); + ImageInfo.imageSize += (ulong)header.tracksize - 256; + } + + readtracks++; + } + } + + DicConsole.DebugWriteLine("CPCDSK plugin", "Read {0} sectors", sectors.Count); + DicConsole.DebugWriteLine("CPCDSK plugin", "Read {0} tracks", readtracks); + DicConsole.DebugWriteLine("CPCDSK plugin", "All tracks are same size? {0}", allTracksSameSize); + + ImageInfo.imageApplication = StringHandlers.CToString(header.creator); + FileInfo fi = new FileInfo(imagePath); + ImageInfo.imageCreationTime = fi.CreationTimeUtc; + ImageInfo.imageLastModificationTime = fi.LastWriteTimeUtc; + ImageInfo.imageName = Path.GetFileNameWithoutExtension(imagePath); + ImageInfo.sectors = (ulong)sectors.Count; + ImageInfo.xmlMediaType = XmlMediaType.BlockMedia; + ImageInfo.mediaType = MediaType.CompactFloppy; + ImageInfo.readableSectorTags.Add(SectorTagType.FloppyAddressMark); + + // Debug writing full disk as raw + + FileStream foo = new FileStream(Path.GetFileNameWithoutExtension(imagePath) + ".bin", FileMode.Create); + for(ulong i = 0; i < (ulong)sectors.Count; i++) + { + byte[] foob; + sectors.TryGetValue(i, out foob); + foo.Write(foob, 0, foob.Length); + } + foo.Close(); + + + stream.Close(); + + return true; + } + + static int SizeCodeToBytes(IBMSectorSizeCode code) + { + switch(code) + { + case IBMSectorSizeCode.EighthKilo: + return 128; + case IBMSectorSizeCode.QuarterKilo: + return 256; + case IBMSectorSizeCode.HalfKilo: + return 512; + case IBMSectorSizeCode.Kilo: + return 1024; + case IBMSectorSizeCode.TwiceKilo: + return 2048; + case IBMSectorSizeCode.FriceKilo: + return 4096; + case IBMSectorSizeCode.TwiceFriceKilo: + return 8192; + case IBMSectorSizeCode.FricelyFriceKilo: + return 16384; + default: + return 0; + } + } + + 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 extended ? "CPCEMU Extended disk image" : "CPCEMU disk 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() + { + return ImageInfo.mediaType; + } + + public override byte[] ReadSector(ulong sectorAddress) + { + byte[] sector; + if(sectors.TryGetValue(sectorAddress, out sector)) + return sector; + + throw new ArgumentOutOfRangeException(nameof(sectorAddress), string.Format("Sector address {0} not found", sectorAddress)); + } + + public override byte[] ReadSectors(ulong sectorAddress, uint length) + { + if(sectorAddress > ImageInfo.sectors - 1) + throw new ArgumentOutOfRangeException(nameof(sectorAddress), string.Format("Sector address {0} not found", sectorAddress)); + + if(sectorAddress + length > ImageInfo.sectors) + throw new ArgumentOutOfRangeException(nameof(length), "Requested more sectors than available"); + + MemoryStream ms = new MemoryStream(); + + for(uint i = 0; i < length; i++) + { + byte[] sector = ReadSector(sectorAddress + i); + ms.Write(sector, 0, sector.Length); + } + + return ms.ToArray(); + } + + public override byte[] ReadSectorTag(ulong sectorAddress, SectorTagType tag) + { + if(tag != SectorTagType.FloppyAddressMark) + throw new FeatureUnsupportedImageException(string.Format("Tag {0} not supported by image format", tag)); + + byte[] addressMark; + if(addressMarks.TryGetValue(sectorAddress, out addressMark)) + return addressMark; + + throw new ArgumentOutOfRangeException(nameof(sectorAddress), "Sector address not found"); + } + + public override byte[] ReadSectorsTag(ulong sectorAddress, uint length, SectorTagType tag) + { + if(tag != SectorTagType.FloppyAddressMark) + throw new FeatureUnsupportedImageException(string.Format("Tag {0} not supported by image format", tag)); + + if(sectorAddress > ImageInfo.sectors - 1) + throw new ArgumentOutOfRangeException(nameof(sectorAddress), string.Format("Sector address {0} not found", sectorAddress)); + + if(sectorAddress + length > ImageInfo.sectors) + throw new ArgumentOutOfRangeException(nameof(length), "Requested more sectors than available"); + + MemoryStream ms = new MemoryStream(); + + for(uint i = 0; i < length; i++) + { + byte[] adddressMark = ReadSector(sectorAddress + i); + ms.Write(adddressMark, 0, adddressMark.Length); + } + + return ms.ToArray(); + } + + #region Unsupported features + + public override byte[] ReadDiskTag(MediaTagType 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[] 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 } } diff --git a/DiscImageChef.DiscImages/ChangeLog b/DiscImageChef.DiscImages/ChangeLog index c39de5c0..ed359709 100644 --- a/DiscImageChef.DiscImages/ChangeLog +++ b/DiscImageChef.DiscImages/ChangeLog @@ -1,3 +1,9 @@ +2016-08-26 Natalia Portillo + + * CPCDSK.cs: + * DiscImageChef.DiscImages.csproj: Added CPCEMU Disk File and + Extended Disk File. + 2016-08-26 Natalia Portillo * ImagePlugin.cs: Added floppy address mark sector tag. diff --git a/DiscImageChef.DiscImages/DiscImageChef.DiscImages.csproj b/DiscImageChef.DiscImages/DiscImageChef.DiscImages.csproj index afe4b3dd..7b0a240f 100644 --- a/DiscImageChef.DiscImages/DiscImageChef.DiscImages.csproj +++ b/DiscImageChef.DiscImages/DiscImageChef.DiscImages.csproj @@ -51,6 +51,7 @@ + diff --git a/DiscImageChef.Filesystems/CPM/CPM.cs b/DiscImageChef.Filesystems/CPM/CPM.cs index 7be772d6..4d6c85e8 100644 --- a/DiscImageChef.Filesystems/CPM/CPM.cs +++ b/DiscImageChef.Filesystems/CPM/CPM.cs @@ -5,11 +5,11 @@ // Filename : CPM.cs // Author(s) : Natalia Portillo // -// Component : Component +// Component : CP/M filesystem plugin. // // --[ Description ] ---------------------------------------------------------- // -// Description +// Constructors and common variables for the CP/M filesystem plugin. // // --[ License ] -------------------------------------------------------------- // @@ -29,13 +29,99 @@ // ---------------------------------------------------------------------------- // Copyright © 2011-2016 Natalia Portillo // ****************************************************************************/ + using System; +using System.Collections.Generic; +using DiscImageChef.ImagePlugins; + namespace DiscImageChef.Filesystems.CPM { - public class CPM + partial class CPM : Filesystem { + bool mounted; + readonly ImagePlugin device; + ulong partStart; + ulong partEnd; + + /// + /// Stores all known CP/M disk definitions + /// + CpmDefinitions definitions; + /// + /// True if thinks this is a CP/M filesystem + /// + bool cpmFound; + /// + /// If thinks this is a CP/M filesystem, this is the definition for it + /// + CpmDefinition workingDefinition; + /// + /// CP/M disc parameter block (on-memory) + /// + DiscParameterBlock dpb; + /// + /// Sector deinterleaving mask + /// + int[] sectorMask; + /// + /// The volume label, if the CP/M filesystem contains one + /// + string label; + /// + /// True if there are timestamps in Z80DOS or DOS+ format + /// + bool thirdPartyTimestamps; + /// + /// True if there are CP/M 3 timestamps + /// + bool standardTimestamps; + /// + /// Timestamp in volume label for creation + /// + byte[] labelCreationDate; + /// + /// Timestamp in volume label for update + /// + byte[] labelUpdateDate; + + /// + /// Cached + /// + FileSystemInfo cpmStat; + /// + /// Cached directory listing + /// + List dirList; + /// + /// Cached file data + /// + Dictionary fileCache; + /// + /// Cached file + /// + Dictionary statCache; + /// + /// Cached file passwords + /// + Dictionary passwordCache; + /// + /// Cached file passwords, decoded + /// + Dictionary decodedPasswordCache; + public CPM() { + Name = "CP/M File System"; + PluginUUID = new Guid("AA2B8585-41DF-4E3B-8A35-D1A935E2F8A1"); + } + + public CPM(ImagePlugin imagePlugin, ulong partitionStart, ulong partitionEnd) + { + device = imagePlugin; + partStart = partitionStart; + partEnd = partitionEnd; + Name = "CP/M File System"; + PluginUUID = new Guid("AA2B8585-41DF-4E3B-8A35-D1A935E2F8A1"); } } } diff --git a/DiscImageChef.Filesystems/CPM/Consts.cs b/DiscImageChef.Filesystems/CPM/Consts.cs index c4f16df8..87a91bd8 100644 --- a/DiscImageChef.Filesystems/CPM/Consts.cs +++ b/DiscImageChef.Filesystems/CPM/Consts.cs @@ -5,11 +5,11 @@ // Filename : Consts.cs // Author(s) : Natalia Portillo // -// Component : Component +// Component : CP/M filesystem plugin. // // --[ Description ] ---------------------------------------------------------- // -// Description +// CP/M filesystem constants. // // --[ License ] -------------------------------------------------------------- // @@ -29,13 +29,56 @@ // ---------------------------------------------------------------------------- // Copyright © 2011-2016 Natalia Portillo // ****************************************************************************/ -using System; + namespace DiscImageChef.Filesystems.CPM { - public class Consts + partial class CPM : Filesystem { - public Consts() + /// + /// Enumerates the format identification byte used by CP/M-86 + /// + enum FormatByte : byte { + /// + /// 5.25" double-density single-side 8 sectors/track + /// + k160 = 0, + /// + /// 5.25" double-density double-side 8 sectors/track + /// + k320 = 1, + /// + /// 5.25" double-density double-side 9 sectors/track + /// + k360 = 0x10, + /// + /// 5.25" double-density double-side 9 sectors/track + /// + k360Alt = 0x40, + /// + /// 3.5" double-density double-side 9 sectors/track + /// + k720 = 0x11, + /// + /// 3.5" double-density double-side 9 sectors/track using FEAT144 + /// + f720 = 0x48, + /// + /// 5.25" high-density double-side 15 sectors/track using FEAT144 + /// + f1200 = 0x0C, + /// + /// 3.5" high-density double-side 18 sectors/track using FEAT144 + /// + f1440 = 0x90, + /// + /// 5.25" double-density double-side 9 sectors/track + /// + k360Alt2 = 0x26, + /// + /// 3.5" double-density double-side 9 sectors/track + /// + k720Alt = 0x94 } } } diff --git a/DiscImageChef.Filesystems/CPM/Definitions.cs b/DiscImageChef.Filesystems/CPM/Definitions.cs index 3aa4f8fb..d77134cb 100644 --- a/DiscImageChef.Filesystems/CPM/Definitions.cs +++ b/DiscImageChef.Filesystems/CPM/Definitions.cs @@ -5,11 +5,11 @@ // Filename : Definitions.cs // Author(s) : Natalia Portillo // -// Component : Component +// Component : CP/M filesystem plugin. // // --[ Description ] ---------------------------------------------------------- // -// Description +// Handles definitions of known CP/M disks. // // --[ License ] -------------------------------------------------------------- // @@ -29,14 +29,187 @@ // ---------------------------------------------------------------------------- // Copyright © 2011-2016 Natalia Portillo // ****************************************************************************/ + using System; +using System.Collections.Generic; +using System.Reflection; +using System.Xml; +using System.Xml.Serialization; + namespace DiscImageChef.Filesystems.CPM { - public class Definitions + partial class CPM : Filesystem { - public Definitions() + /// + /// Loads all the known CP/M disk definitions from an XML stored as an embedded resource. + /// + /// The definitions. + public bool LoadDefinitions() { + try + { + XmlReader defsReader = XmlReader.Create(Assembly.GetExecutingAssembly().GetManifestResourceStream("DiscImageChef.Filesystems.CPM.cpmdefs.xml")); + XmlSerializer defsSerializer = new XmlSerializer(typeof(CpmDefinitions)); + definitions = (CpmDefinitions)defsSerializer.Deserialize(defsReader); + + // Patch definitions + foreach(CpmDefinition def in definitions.definitions) + { + if(def.side1 == null) + { + def.side1 = new Side(); + def.side1.sideId = 0; + def.side1.sectorIds = new int[def.sectorsPerTrack]; + for(int i = 0; i < def.sectorsPerTrack; i++) + def.side1.sectorIds[i] = i + 1; + } + + if(def.sides == 2 && def.side2 == null) + { + def.side2 = new Side(); + def.side2.sideId = 1; + def.side2.sectorIds = new int[def.sectorsPerTrack]; + for(int i = 0; i < def.sectorsPerTrack; i++) + def.side2.sectorIds[i] = i + 1; + } + } + + return true; + } + catch + { + return false; + } } } + + /// + /// CP/M disk definitions + /// + public class CpmDefinitions + { + /// + /// List of all CP/M disk definitions + /// + public List definitions; + /// + /// Timestamp of creation of the CP/M disk definitions list + /// + public DateTime creation; + } + + /// + /// CP/M disk definition + /// + public class CpmDefinition + { + /// + /// Comment and description + /// + public string comment; + /// + /// Encoding, "FM", "MFM", "GCR" + /// + public string encoding; + /// + /// Controller bitrate + /// + public string bitrate; + /// + /// Total cylinders + /// + public int cylinders; + /// + /// Total sides + /// + public int sides; + /// + /// Physical sectors per side + /// + public int sectorsPerTrack; + /// + /// Physical bytes per sector + /// + public int bytesPerSector; + /// + /// Physical sector interleaving + /// + public int skew; + /// + /// Description of controller's side 0 (usually, upper side) + /// + public Side side1; + /// + /// Description of controller's side 1 (usually, lower side) + /// + public Side side2; + /// + /// Cylinder/side ordering. SIDES = change side after each track, CYLINDERS = change side after whole side, EAGLE and COLUMBIA unknown + /// + public string order; + /// + /// Disk definition label + /// + public string label; + /// + /// Left shifts needed to translate allocation block number to lba + /// + public int bsh; + /// + /// Block mask for + /// + public int blm; + /// + /// Extent mask + /// + public int exm; + /// + /// Total number of 128 byte records on disk + /// + public int dsm; + /// + /// Total number of available directory entries + /// + public int drm; + /// + /// Maps the first 16 allocation blocks for reservation, high byte + /// + public int al0; + /// + /// Maps the first 16 allocation blocks for reservation, low byte + /// + public int al1; + /// + /// Tracks at the beginning of disk reserved for BIOS/BDOS + /// + public int ofs; + /// + /// Sectors at the beginning of disk reserved for BIOS/BDOS + /// + public int sofs; + /// + /// If true, all bytes written on disk are negated + /// + public bool complement; + /// + /// Absolutely unknown? + /// + public bool evenOdd; + } + + /// + /// Side descriptions + /// + public class Side + { + /// + /// Side ID as found in each sector address mark + /// + public int sideId; + /// + /// Software interleaving mask, [1,3,0,2] means CP/M LBA 0 is physical sector 1, LBA 1 = 3, so on + /// + public int[] sectorIds; + } } diff --git a/DiscImageChef.Filesystems/CPM/Dir.cs b/DiscImageChef.Filesystems/CPM/Dir.cs index fb23e49d..3cab3477 100644 --- a/DiscImageChef.Filesystems/CPM/Dir.cs +++ b/DiscImageChef.Filesystems/CPM/Dir.cs @@ -5,11 +5,11 @@ // Filename : Dir.cs // Author(s) : Natalia Portillo // -// Component : Component +// Component : CP/M filesystem plugin. // // --[ Description ] ---------------------------------------------------------- // -// Description +// Methods to show the CP/M filesystem directory. // // --[ License ] -------------------------------------------------------------- // @@ -29,13 +29,107 @@ // ---------------------------------------------------------------------------- // Copyright © 2011-2016 Natalia Portillo // ****************************************************************************/ + using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; + namespace DiscImageChef.Filesystems.CPM { - public class Dir + partial class CPM : Filesystem { - public Dir() + public override Errno ReadDir(string path, ref List contents) { + if(!mounted) + return Errno.AccessDenied; + + if(!string.IsNullOrEmpty(path) && string.Compare(path, "/", StringComparison.OrdinalIgnoreCase) != 0) + return Errno.NotSupported; + + contents = dirList; + + return Errno.NoError; + } + + /// + /// Checks that the given directory blocks follow the CP/M filesystem directory specification + /// Corrupted directories will fail. + /// FAT firectories will false positive if all files start with 0x05, and do not use full extentions, for example: + /// "σAFILE.GZ" (using code page 437) + /// + /// False if the directory does not follow the directory specification + /// Directory blocks. + bool CheckDir(byte[] directory) + { + try + { + if(directory == null) + return false; + + int fileCount = 0; + + for(int off = 0; off < directory.Length; off += 32) + { + DirectoryEntry entry = new DirectoryEntry(); + IntPtr dirPtr = Marshal.AllocHGlobal(32); + Marshal.Copy(directory, off, dirPtr, 32); + entry = (DirectoryEntry)Marshal.PtrToStructure(dirPtr, typeof(DirectoryEntry)); + Marshal.FreeHGlobal(dirPtr); + + if((entry.statusUser & 0x7F) < 0x20) + { + for(int f = 0; f < 8; f++) + { + if(entry.filename[f] < 0x20 && entry.filename[f] != 0x00) + return false; + } + + for(int e = 0; e < 3; e++) + { + if(entry.extension[e] < 0x20 && entry.extension[e] != 0x00) + return false; + } + + if(!ArrayHelpers.ArrayIsNullOrWhiteSpace(entry.filename)) + fileCount++; + } + else if(entry.statusUser == 0x20) + { + for(int f = 0; f < 8; f++) + { + if(entry.filename[f] < 0x20 && entry.filename[f] != 0x00) + return false; + } + + for(int e = 0; e < 3; e++) + { + if(entry.extension[e] < 0x20 && entry.extension[e] != 0x00) + return false; + } + + label = Encoding.ASCII.GetString(directory, off + 1, 11).Trim(); + labelCreationDate = new byte[4]; + labelUpdateDate = new byte[4]; + Array.Copy(directory, off + 24, labelCreationDate, 0, 4); + Array.Copy(directory, off + 28, labelUpdateDate, 0, 4); + } + else if(entry.statusUser == 0x21) + { + if(directory[off + 1] == 0x00) + { + thirdPartyTimestamps = true; + } + else standardTimestamps |= (directory[off + 21] == 0x00 && directory[off + 31] == 0x00); + } + } + + return fileCount > 0; + } + catch + { + return false; + } } } } diff --git a/DiscImageChef.Filesystems/CPM/File.cs b/DiscImageChef.Filesystems/CPM/File.cs index e81f048f..143e4931 100644 --- a/DiscImageChef.Filesystems/CPM/File.cs +++ b/DiscImageChef.Filesystems/CPM/File.cs @@ -5,11 +5,11 @@ // Filename : File.cs // Author(s) : Natalia Portillo // -// Component : Component +// Component : CP/M filesystem plugin. // // --[ Description ] ---------------------------------------------------------- // -// Description +// Methods to handle files. // // --[ License ] -------------------------------------------------------------- // @@ -29,14 +29,117 @@ // ---------------------------------------------------------------------------- // Copyright © 2011-2016 Natalia Portillo // ****************************************************************************/ + using System; + namespace DiscImageChef.Filesystems.CPM { - public class File + partial class CPM : Filesystem { - public File() + public override Errno GetAttributes(string path, ref FileAttributes attributes) { + if(!mounted) + return Errno.AccessDenied; + + string[] pathElements = path.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + if(pathElements.Length != 1) + return Errno.NotSupported; + + FileEntryInfo fInfo; + + if(string.IsNullOrEmpty(pathElements[0]) || string.Compare(pathElements[0], "/", StringComparison.OrdinalIgnoreCase) == 0) + { + attributes = new FileAttributes(); + attributes = FileAttributes.Directory; + return Errno.NoError; + } + + if(statCache.TryGetValue(pathElements[0].ToUpperInvariant(), out fInfo)) + { + attributes = fInfo.Attributes; + return Errno.NoError; + } + + return Errno.NoSuchFile; } + + public override Errno MapBlock(string path, long fileBlock, ref long deviceBlock) + { + if(!mounted) + return Errno.AccessDenied; + + // TODO: Implementing this would require storing the interleaving + return Errno.NotImplemented; + } + + public override Errno Read(string path, long offset, long size, ref byte[] buf) + { + if(!mounted) + return Errno.AccessDenied; + + if(size == 0) + { + buf = new byte[0]; + return Errno.NoError; + } + + if(offset < 0) + return Errno.InvalidArgument; + + string[] pathElements = path.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + if(pathElements.Length != 1) + return Errno.NotSupported; + + byte[] file; + + if(!fileCache.TryGetValue(pathElements[0].ToUpperInvariant(), out file)) + return Errno.NoSuchFile; + + if(offset >= file.Length) + return Errno.EINVAL; + + if(size + offset >= file.Length) + size = file.Length - offset; + + buf = new byte[size]; + Array.Copy(file, offset, buf, 0, size); + return Errno.NoError; + } + + public override Errno ReadLink(string path, ref string dest) + { + if(!mounted) + return Errno.AccessDenied; + + return Errno.NotSupported; + } + + public override Errno Stat(string path, ref FileEntryInfo stat) + { + if(!mounted) + return Errno.AccessDenied; + + string[] pathElements = path.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + if(pathElements.Length != 1) + return Errno.NotSupported; + + if(string.IsNullOrEmpty(path) || string.Compare(path, "/", StringComparison.OrdinalIgnoreCase) == 0) + { + if(labelCreationDate != null) + stat.CreationTime = DateHandlers.CPMToDateTime(labelCreationDate); + if(labelUpdateDate != null) + stat.StatusChangeTime = DateHandlers.CPMToDateTime(labelUpdateDate); + stat.Attributes = FileAttributes.Directory; + stat.BlockSize = xmlFSType.ClusterSize; + return Errno.NoError; + } + + if(statCache.TryGetValue(pathElements[0].ToUpperInvariant(), out stat)) + return Errno.NoError; + + return Errno.NoSuchFile; + } + } } diff --git a/DiscImageChef.Filesystems/CPM/Info.cs b/DiscImageChef.Filesystems/CPM/Info.cs index 0aec2753..19a23886 100644 --- a/DiscImageChef.Filesystems/CPM/Info.cs +++ b/DiscImageChef.Filesystems/CPM/Info.cs @@ -5,11 +5,11 @@ // Filename : Info.cs // Author(s) : Natalia Portillo // -// Component : Component +// Component : CP/M filesystem plugin. // // --[ Description ] ---------------------------------------------------------- // -// Description +// Identifies the CP/M filesystem and shows information. // // --[ License ] -------------------------------------------------------------- // @@ -30,12 +30,907 @@ // Copyright © 2011-2016 Natalia Portillo // ****************************************************************************/ using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using DiscImageChef.Console; +using DiscImageChef.ImagePlugins; + namespace DiscImageChef.Filesystems.CPM { - public class Info + partial class CPM : Filesystem { - public Info() + public override bool Identify(ImagePlugin imagePlugin, ulong partitionStart, ulong partitionEnd) { + // This will try to identify a CP/M filesystem + // However as it contains no identification marks whatsoever it's more something of trial-and-error + // As anything can happen, better try{}catch{} than sorry ;) + try + { + byte[] sector; + ulong sectorCount; + ulong sectorSize; + ulong sides; + ulong firstDirectorySector; + byte[] directory = null; + workingDefinition = null; + label = null; + + // Try Amstrad superblock + if(!cpmFound) + { + // Read CHS = {0,0,1} + sector = imagePlugin.ReadSector(0 + partitionStart); + int amsSbOffset = 0; + + uint sig1, sig2, sig3; + sig1 = BitConverter.ToUInt32(sector, 0x2B); + sig2 = BitConverter.ToUInt32(sector, 0x33) & 0x00FFFFFF; + sig3 = BitConverter.ToUInt32(sector, 0x7C); + + // PCW16 extended boot record + if(sig1 == 0x4D2F5043 && sig2 == 0x004B5344 && sig3 == sig1) + amsSbOffset = 0x80; + + // Read the superblock + AmstradSuperBlock amsSb = new AmstradSuperBlock(); + IntPtr amsPtr = Marshal.AllocHGlobal(16); + Marshal.Copy(sector, amsSbOffset, amsPtr, 16); + amsSb = (AmstradSuperBlock)Marshal.PtrToStructure(amsPtr, typeof(AmstradSuperBlock)); + Marshal.FreeHGlobal(amsPtr); + + // Check that format byte and sidedness indicate the same number of sizes + if((amsSb.format == 0 && (amsSb.sidedness & 0x02) == 0) || + (amsSb.format == 2 && (amsSb.sidedness & 0x02) == 1) || + (amsSb.format == 2 && (amsSb.sidedness & 0x02) == 2)) + { + // Calculate device limits + sides = (ulong)(amsSb.format == 0 ? 1 : 2); + sectorCount = (ulong)(amsSb.tps * amsSb.spt * (byte)sides); + sectorSize = (ulong)(128 << amsSb.psh); + + // Compare device limits from superblock to real limits + if(sectorSize == imagePlugin.GetSectorSize() && + sectorCount == imagePlugin.GetSectors()) + { + cpmFound = true; + firstDirectorySector = (ulong)((amsSb.off * amsSb.spt)); + + // Build a DiscParameterBlock + dpb = new DiscParameterBlock(); + dpb.al0 = sectorCount == 1440 ? (byte)0xF0 : (byte)0xC0; + dpb.spt = amsSb.spt; + dpb.bsh = amsSb.bsh; + for(int i = 0; i < dpb.bsh; i++) + dpb.blm += (byte)Math.Pow(2, i); + if(sectorCount >= 1440) + { + dpb.cks = 0x40; + dpb.drm = 0xFF; + } + else + { + dpb.cks = 0x10; + dpb.drm = 0x3F; + } + dpb.dsm = 0; // I don't care + dpb.exm = sectorCount == 2880 ? (byte)1 : (byte)0; + dpb.off = amsSb.off; + dpb.psh = amsSb.psh; + for(int i = 0; i < dpb.psh; i++) + dpb.phm += (byte)Math.Pow(2, i); + dpb.spt = (ushort)(amsSb.spt * (sectorSize / 128)); + uint directoryLength = (uint)((((ulong)dpb.drm + 1) * 32) / sectorSize); + directory = imagePlugin.ReadSector(firstDirectorySector + partitionStart, directoryLength); + + // Build a CP/M disk definition + workingDefinition = new CpmDefinition(); + workingDefinition.al0 = dpb.al0; + workingDefinition.al1 = dpb.al1; + workingDefinition.bitrate = "LOW"; + workingDefinition.blm = dpb.blm; + workingDefinition.bsh = dpb.bsh; + workingDefinition.bytesPerSector = 512; + workingDefinition.cylinders = amsSb.tps; + workingDefinition.drm = dpb.drm; + workingDefinition.dsm = dpb.dsm; + workingDefinition.encoding = "MFM"; + workingDefinition.evenOdd = false; + workingDefinition.exm = dpb.exm; + workingDefinition.label = null; + workingDefinition.comment = "Amstrad PCW superblock"; + workingDefinition.ofs = dpb.off; + workingDefinition.sectorsPerTrack = amsSb.spt; + + workingDefinition.side1 = new Side(); + workingDefinition.side1.sideId = 0; + workingDefinition.side1.sectorIds = new int[amsSb.spt]; + for(int si = 0; si < amsSb.spt; si++) + workingDefinition.side1.sectorIds[si] = si + 1; + + if(amsSb.format == 2) + { + if((amsSb.sidedness & 0x02) == 1) + workingDefinition.order = "SIDES"; + else if((amsSb.sidedness & 0x02) == 2) + workingDefinition.order = "CYLINDERS"; + else + workingDefinition.order = null; + + workingDefinition.side2 = new Side(); + workingDefinition.side2.sideId = 1; + workingDefinition.side2.sectorIds = new int[amsSb.spt]; + for(int si = 0; si < amsSb.spt; si++) + workingDefinition.side2.sectorIds[si] = si + 1; + } + else + workingDefinition.order = null; + + workingDefinition.skew = 2; + workingDefinition.sofs = 0; + + DicConsole.DebugWriteLine("CP/M Plugin", "Found Amstrad superblock."); + } + } + } + + // Try CP/M-86 superblock for hard disks + if(!cpmFound) + { + // Read CHS = {0,0,4} + sector = imagePlugin.ReadSector(3 + partitionStart); + ushort sum = 0; + + // Sum of all 16-bit words that make this sector must be 0 + for(int i = 0; i < sector.Length; i += 2) + sum += BitConverter.ToUInt16(sector, i); + + // It may happen that there is a corrupted superblock + // Better to ignore corrupted than to false positive the rest + if(sum == 0) + { + // Read the superblock + HardDiskSuperBlock hddSb = new HardDiskSuperBlock(); + IntPtr hddPtr = Marshal.AllocHGlobal(16); + Marshal.Copy(sector, 0, hddPtr, Marshal.SizeOf(hddSb)); + hddSb = (HardDiskSuperBlock)Marshal.PtrToStructure(hddPtr, typeof(HardDiskSuperBlock)); + Marshal.FreeHGlobal(hddPtr); + + // Calculate volume size + sectorSize = (ulong)(hddSb.recordsPerSector * 128); + ulong sectorsInPartition = (ulong)(hddSb.cylinders * hddSb.heads * hddSb.sectorsPerTrack); + ulong startingSector = (ulong)((hddSb.firstCylinder * hddSb.heads + hddSb.heads) * hddSb.sectorsPerTrack); + + // If volume size corresponds with working partition (this variant will be inside MBR partitioning) + if(sectorSize == imagePlugin.GetSectorSize() && + startingSector == partitionStart && + sectorsInPartition + partitionStart <= partitionEnd) + { + cpmFound = true; + firstDirectorySector = (ulong)((hddSb.off * hddSb.sectorsPerTrack)); + + // Build a DiscParameterBlock + dpb = new DiscParameterBlock(); + dpb.al0 = (byte)hddSb.al0; + dpb.al1 = (byte)hddSb.al1; + dpb.blm = hddSb.blm; + dpb.bsh = hddSb.bsh; + dpb.cks = hddSb.cks; + dpb.drm = hddSb.drm; + dpb.dsm = hddSb.dsm; + dpb.exm = hddSb.exm; + dpb.off = hddSb.off; + dpb.phm = 0; // Needed? + dpb.psh = 0; // Needed? + dpb.spt = hddSb.spt; + uint directoryLength = (uint)((((ulong)dpb.drm + 1) * 32) / sectorSize); + directory = imagePlugin.ReadSector(firstDirectorySector + partitionStart, directoryLength); + DicConsole.DebugWriteLine("CP/M Plugin", "Found CP/M-86 hard disk superblock."); + + // Build a CP/M disk definition + workingDefinition = new CpmDefinition(); + workingDefinition.al0 = dpb.al0; + workingDefinition.al1 = dpb.al1; + workingDefinition.bitrate = "HIGH"; + workingDefinition.blm = dpb.blm; + workingDefinition.bsh = dpb.bsh; + workingDefinition.bytesPerSector = 512; + workingDefinition.cylinders = hddSb.cylinders; + workingDefinition.drm = dpb.drm; + workingDefinition.dsm = dpb.dsm; + workingDefinition.encoding = "MFM"; + workingDefinition.evenOdd = false; + workingDefinition.exm = dpb.exm; + workingDefinition.label = null; + workingDefinition.comment = "CP/M-86 hard disk superblock"; + workingDefinition.ofs = dpb.off; + workingDefinition.sectorsPerTrack = hddSb.sectorsPerTrack; + workingDefinition.side1 = new Side(); + workingDefinition.side1.sideId = 0; + workingDefinition.side1.sectorIds = new int[hddSb.sectorsPerTrack]; + for(int si = 0; si < hddSb.sectorsPerTrack; si++) + workingDefinition.side1.sectorIds[si] = si + 1; + workingDefinition.order = "SIDES"; + workingDefinition.side2 = new Side(); + workingDefinition.side2.sideId = 1; + workingDefinition.side2.sectorIds = new int[hddSb.sectorsPerTrack]; + for(int si = 0; si < hddSb.spt; si++) + workingDefinition.side2.sectorIds[si] = si + 1; + workingDefinition.skew = 0; + workingDefinition.sofs = 0; + } + } + } + + // Try CP/M-86 format ID for floppies + if(!cpmFound) + { + // Read CHS = {0,0,1} + sector = imagePlugin.ReadSector(0 + partitionStart); + byte formatByte; + + // Check for alternate location of format ID + if(sector.Last() == 0x00 || sector.Last() == 0xFF) + { + if(sector[0x40] == 0x94 || sector[0x40] == 0x26) + formatByte = sector[0x40]; + else + formatByte = sector.Last(); + } + else + formatByte = sector.Last(); + + uint firstDirectorySector86 = 0; + + // Check format ID + // If it is one of the known IDs, check disk size corresponds to the one we expect + // If so, build a DiscParameterBlock and a CP/M disk definition + // Will not work on over-formatted disks (40 cylinder volume on an 80 cylinder disk, + // something that happens a lot in IBM PC 5.25" disks) + switch((FormatByte)formatByte) + { + case FormatByte.k160: + if(imagePlugin.GetSectorSize() == 512 && imagePlugin.GetSectors() == 320) + { + cpmFound = true; + firstDirectorySector86 = 8; + dpb = new DiscParameterBlock(); + dpb.al0 = 0xC0; + dpb.al1 = 0; + dpb.blm = 7; + dpb.bsh = 3; + dpb.cks = 0x10; + dpb.drm = 0x3F; + dpb.dsm = 0x9B; + dpb.exm = 0; + dpb.off = 1; + dpb.phm = 3; + dpb.psh = 2; + dpb.spt = 8 * 4; + + workingDefinition = new CpmDefinition(); + workingDefinition.al0 = dpb.al0; + workingDefinition.al1 = dpb.al1; + workingDefinition.bitrate = "LOW"; + workingDefinition.blm = dpb.blm; + workingDefinition.bsh = dpb.bsh; + workingDefinition.bytesPerSector = 512; + workingDefinition.cylinders = 40; + workingDefinition.drm = dpb.drm; + workingDefinition.dsm = dpb.dsm; + workingDefinition.encoding = "MFM"; + workingDefinition.evenOdd = false; + workingDefinition.exm = dpb.exm; + workingDefinition.label = null; + workingDefinition.comment = "CP/M-86 floppy identifier"; + workingDefinition.ofs = dpb.off; + workingDefinition.sectorsPerTrack = 8; + workingDefinition.side1 = new Side(); + workingDefinition.side1.sideId = 0; + workingDefinition.side1.sectorIds = new int[8]; + for(int si = 0; si < 8; si++) + workingDefinition.side1.sectorIds[si] = si + 1; + workingDefinition.skew = 0; + workingDefinition.sofs = 0; + } + break; + case FormatByte.k320: + if(imagePlugin.GetSectorSize() == 512 && imagePlugin.GetSectors() == 640) + { + cpmFound = true; + firstDirectorySector86 = 8; + dpb = new DiscParameterBlock(); + dpb.al0 = 0x80; + dpb.al1 = 0; + dpb.blm = 0x0F; + dpb.bsh = 4; + dpb.cks = 0x10; + dpb.drm = 0x3F; + dpb.dsm = 0x9D; + dpb.exm = 1; + dpb.off = 1; + dpb.phm = 3; + dpb.psh = 2; + dpb.spt = 8 * 4; + + workingDefinition = new CpmDefinition(); + workingDefinition.al0 = dpb.al0; + workingDefinition.al1 = dpb.al1; + workingDefinition.bitrate = "LOW"; + workingDefinition.blm = dpb.blm; + workingDefinition.bsh = dpb.bsh; + workingDefinition.bytesPerSector = 512; + workingDefinition.cylinders = 40; + workingDefinition.drm = dpb.drm; + workingDefinition.dsm = dpb.dsm; + workingDefinition.encoding = "MFM"; + workingDefinition.evenOdd = false; + workingDefinition.exm = dpb.exm; + workingDefinition.label = null; + workingDefinition.comment = "CP/M-86 floppy identifier"; + workingDefinition.ofs = dpb.off; + workingDefinition.sectorsPerTrack = 8; + workingDefinition.side1 = new Side(); + workingDefinition.side1.sideId = 0; + workingDefinition.side1.sectorIds = new int[8]; + for(int si = 0; si < 8; si++) + workingDefinition.side1.sectorIds[si] = si + 1; + workingDefinition.order = "SIDES"; + workingDefinition.side2 = new Side(); + workingDefinition.side2.sideId = 1; + workingDefinition.side2.sectorIds = new int[8]; + for(int si = 0; si < 8; si++) + workingDefinition.side2.sectorIds[si] = si + 1; + workingDefinition.skew = 0; + workingDefinition.sofs = 0; + } + break; + case FormatByte.k360: + case FormatByte.k360Alt: + case FormatByte.k360Alt2: + if(imagePlugin.GetSectorSize() == 512 && imagePlugin.GetSectors() == 720) + { + cpmFound = true; + firstDirectorySector86 = 36; + dpb = new DiscParameterBlock(); + dpb.al0 = 0x80; + dpb.al1 = 0; + dpb.blm = 0x0F; + dpb.bsh = 4; + dpb.cks = 0x10; + dpb.drm = 0x3F; + dpb.dsm = 0; // Unknown. Needed? + dpb.exm = 1; + dpb.off = 4; + dpb.phm = 3; + dpb.psh = 2; + dpb.spt = 9 * 4; + + workingDefinition = new CpmDefinition(); + workingDefinition.al0 = dpb.al0; + workingDefinition.al1 = dpb.al1; + workingDefinition.bitrate = "LOW"; + workingDefinition.blm = dpb.blm; + workingDefinition.bsh = dpb.bsh; + workingDefinition.bytesPerSector = 512; + workingDefinition.cylinders = 40; + workingDefinition.drm = dpb.drm; + workingDefinition.dsm = dpb.dsm; + workingDefinition.encoding = "MFM"; + workingDefinition.evenOdd = false; + workingDefinition.exm = dpb.exm; + workingDefinition.label = null; + workingDefinition.comment = "CP/M-86 floppy identifier"; + workingDefinition.ofs = dpb.off; + workingDefinition.sectorsPerTrack = 9; + workingDefinition.side1 = new Side(); + workingDefinition.side1.sideId = 0; + workingDefinition.side1.sectorIds = new int[9]; + for(int si = 0; si < 9; si++) + workingDefinition.side1.sectorIds[si] = si + 1; + workingDefinition.order = "SIDES"; + workingDefinition.side2 = new Side(); + workingDefinition.side2.sideId = 1; + workingDefinition.side2.sectorIds = new int[9]; + for(int si = 0; si < 9; si++) + workingDefinition.side2.sectorIds[si] = si + 1; + workingDefinition.skew = 0; + workingDefinition.sofs = 0; + } + break; + case FormatByte.k720: + case FormatByte.k720Alt: + if(imagePlugin.GetSectorSize() == 512 && imagePlugin.GetSectors() == 1440) + { + cpmFound = true; + firstDirectorySector86 = 36; + dpb = new DiscParameterBlock(); + dpb.al0 = 0xF0; + dpb.al1 = 0; + dpb.blm = 0x0F; + dpb.bsh = 4; + dpb.cks = 0x40; + dpb.drm = 0xFF; + dpb.dsm = 0x15E; + dpb.exm = 0; + dpb.off = 4; + dpb.phm = 3; + dpb.psh = 2; + dpb.spt = 9 * 4; + + workingDefinition = new CpmDefinition(); + workingDefinition.al0 = dpb.al0; + workingDefinition.al1 = dpb.al1; + workingDefinition.bitrate = "LOW"; + workingDefinition.blm = dpb.blm; + workingDefinition.bsh = dpb.bsh; + workingDefinition.bytesPerSector = 512; + workingDefinition.cylinders = 80; + workingDefinition.drm = dpb.drm; + workingDefinition.dsm = dpb.dsm; + workingDefinition.encoding = "MFM"; + workingDefinition.evenOdd = false; + workingDefinition.exm = dpb.exm; + workingDefinition.label = null; + workingDefinition.comment = "CP/M-86 floppy identifier"; + workingDefinition.ofs = dpb.off; + workingDefinition.sectorsPerTrack = 9; + workingDefinition.side1 = new Side(); + workingDefinition.side1.sideId = 0; + workingDefinition.side1.sectorIds = new int[9]; + for(int si = 0; si < 9; si++) + workingDefinition.side1.sectorIds[si] = si + 1; + workingDefinition.order = "SIDES"; + workingDefinition.side2 = new Side(); + workingDefinition.side2.sideId = 1; + workingDefinition.side2.sectorIds = new int[9]; + for(int si = 0; si < 9; si++) + workingDefinition.side2.sectorIds[si] = si + 1; + workingDefinition.skew = 0; + workingDefinition.sofs = 0; + } + break; + case FormatByte.f720: + if(imagePlugin.GetSectorSize() == 512 && imagePlugin.GetSectors() == 1440) + { + cpmFound = true; + firstDirectorySector86 = 18; + dpb = new DiscParameterBlock(); + dpb.al0 = 0xF0; + dpb.al1 = 0; + dpb.blm = 0x0F; + dpb.bsh = 4; + dpb.cks = 0x40; + dpb.drm = 0xFF; + dpb.dsm = 0x162; + dpb.exm = 0; + dpb.off = 2; + dpb.phm = 3; + dpb.psh = 2; + dpb.spt = 9 * 4; + + workingDefinition = new CpmDefinition(); + workingDefinition.al0 = dpb.al0; + workingDefinition.al1 = dpb.al1; + workingDefinition.bitrate = "LOW"; + workingDefinition.blm = dpb.blm; + workingDefinition.bsh = dpb.bsh; + workingDefinition.bytesPerSector = 512; + workingDefinition.cylinders = 80; + workingDefinition.drm = dpb.drm; + workingDefinition.dsm = dpb.dsm; + workingDefinition.encoding = "MFM"; + workingDefinition.evenOdd = false; + workingDefinition.exm = dpb.exm; + workingDefinition.label = null; + workingDefinition.comment = "CP/M-86 floppy identifier"; + workingDefinition.ofs = dpb.off; + workingDefinition.sectorsPerTrack = 9; + workingDefinition.side1 = new Side(); + workingDefinition.side1.sideId = 0; + workingDefinition.side1.sectorIds = new int[9]; + for(int si = 0; si < 9; si++) + workingDefinition.side1.sectorIds[si] = si + 1; + workingDefinition.order = "CYLINDERS"; + workingDefinition.side2 = new Side(); + workingDefinition.side2.sideId = 1; + workingDefinition.side2.sectorIds = new int[9]; + for(int si = 0; si < 9; si++) + workingDefinition.side2.sectorIds[si] = si + 1; + workingDefinition.skew = 0; + workingDefinition.sofs = 0; + } + break; + case FormatByte.f1200: + if(imagePlugin.GetSectorSize() == 512 && imagePlugin.GetSectors() == 2400) + { + cpmFound = true; + firstDirectorySector86 = 30; + dpb = new DiscParameterBlock(); + dpb.al0 = 0xC0; + dpb.al1 = 0; + dpb.blm = 0x1F; + dpb.bsh = 5; + dpb.cks = 0x40; + dpb.drm = 0xFF; + dpb.dsm = 0x127; + dpb.exm = 1; + dpb.off = 2; + dpb.phm = 3; + dpb.psh = 2; + dpb.spt = 15 * 4; + + workingDefinition = new CpmDefinition(); + workingDefinition.al0 = dpb.al0; + workingDefinition.al1 = dpb.al1; + workingDefinition.bitrate = "HIGH"; + workingDefinition.blm = dpb.blm; + workingDefinition.bsh = dpb.bsh; + workingDefinition.bytesPerSector = 512; + workingDefinition.cylinders = 80; + workingDefinition.drm = dpb.drm; + workingDefinition.dsm = dpb.dsm; + workingDefinition.encoding = "MFM"; + workingDefinition.evenOdd = false; + workingDefinition.exm = dpb.exm; + workingDefinition.label = null; + workingDefinition.comment = "CP/M-86 floppy identifier"; + workingDefinition.ofs = dpb.off; + workingDefinition.sectorsPerTrack = 15; + workingDefinition.side1 = new Side(); + workingDefinition.side1.sideId = 0; + workingDefinition.side1.sectorIds = new int[15]; + for(int si = 0; si < 15; si++) + workingDefinition.side1.sectorIds[si] = si + 1; + workingDefinition.order = "CYLINDERS"; + workingDefinition.side2 = new Side(); + workingDefinition.side2.sideId = 1; + workingDefinition.side2.sectorIds = new int[15]; + for(int si = 0; si < 15; si++) + workingDefinition.side2.sectorIds[si] = si + 1; + workingDefinition.skew = 0; + workingDefinition.sofs = 0; + } + break; + case FormatByte.f1440: + if(imagePlugin.GetSectorSize() == 512 && imagePlugin.GetSectors() == 2880) + { + cpmFound = true; + firstDirectorySector86 = 36; + dpb = new DiscParameterBlock(); + dpb.al0 = 0xC0; + dpb.al1 = 0; + dpb.blm = 0x1F; + dpb.bsh = 5; + dpb.cks = 0x40; + dpb.drm = 0xFF; + dpb.dsm = 0x162; + dpb.exm = 1; + dpb.off = 2; + dpb.phm = 3; + dpb.psh = 2; + dpb.spt = 18 * 4; + + workingDefinition = new CpmDefinition(); + workingDefinition.al0 = dpb.al0; + workingDefinition.al1 = dpb.al1; + workingDefinition.bitrate = "LOW"; + workingDefinition.blm = dpb.blm; + workingDefinition.bsh = dpb.bsh; + workingDefinition.bytesPerSector = 512; + workingDefinition.cylinders = 80; + workingDefinition.drm = dpb.drm; + workingDefinition.dsm = dpb.dsm; + workingDefinition.encoding = "MFM"; + workingDefinition.evenOdd = false; + workingDefinition.exm = dpb.exm; + workingDefinition.label = null; + workingDefinition.comment = "CP/M-86 floppy identifier"; + workingDefinition.ofs = dpb.off; + workingDefinition.sectorsPerTrack = 18; + workingDefinition.side1 = new Side(); + workingDefinition.side1.sideId = 0; + workingDefinition.side1.sectorIds = new int[18]; + for(int si = 0; si < 18; si++) + workingDefinition.side1.sectorIds[si] = si + 1; + workingDefinition.order = "CYLINDERS"; + workingDefinition.side2 = new Side(); + workingDefinition.side2.sideId = 1; + workingDefinition.side2.sectorIds = new int[18]; + for(int si = 0; si < 18; si++) + workingDefinition.side2.sectorIds[si] = si + 1; + workingDefinition.skew = 0; + workingDefinition.sofs = 0; + } + break; + } + + if(cpmFound) + { + uint directoryLength = (uint)((((ulong)dpb.drm + 1) * 32) / imagePlugin.GetSectorSize()); + directory = imagePlugin.ReadSector(firstDirectorySector86 + partitionStart, directoryLength); + DicConsole.DebugWriteLine("CP/M Plugin", "Found CP/M-86 floppy identifier."); + } + } + + // One of the few CP/M filesystem marks has been found, try for correcteness checking the whole directory + if(cpmFound) + { + if(CheckDir(directory)) + { + DicConsole.DebugWriteLine("CP/M Plugin", "First directory block seems correct."); + return true; + } + + cpmFound = false; + } + + // Try all definitions + if(!cpmFound) + { + // Load all definitions + DicConsole.DebugWriteLine("CP/M Plugin", "Trying to load definitions."); + if(LoadDefinitions() && definitions != null && definitions.definitions != null && definitions.definitions.Count > 0) + { + DicConsole.DebugWriteLine("CP/M Plugin", "Trying all known definitions."); + foreach(CpmDefinition def in definitions.definitions) + { + ulong sectors = (ulong)(def.cylinders * def.sides * def.sectorsPerTrack); + + // Definition seems to describe current disk, at least, same number of volume sectors and bytes per sector + if(sectors == imagePlugin.GetSectors() && def.bytesPerSector == imagePlugin.GetSectorSize()) + { + DicConsole.DebugWriteLine("CP/M Plugin", "Trying definition \"{0}\"", def.comment); + ulong offset; + if(def.sofs != 0) + offset = (ulong)def.sofs; + else + offset = (ulong)(def.ofs * def.sectorsPerTrack); + + int dirLen = ((def.drm + 1) * 32) / def.bytesPerSector; + + if(def.sides == 1) + { + sectorMask = new int[def.side1.sectorIds.Length]; + for(int m = 0; m < sectorMask.Length; m++) + sectorMask[m] = def.side1.sectorIds[m] - def.side1.sectorIds[0]; + } + else + { + // Head changes after every track + if(string.Compare(def.order, "SIDES", StringComparison.InvariantCultureIgnoreCase) == 0) + { + sectorMask = new int[def.side1.sectorIds.Length + def.side2.sectorIds.Length]; + for(int m = 0; m < def.side1.sectorIds.Length; m++) + sectorMask[m] = def.side1.sectorIds[m] - def.side1.sectorIds[0]; + // Skip first track (first side) + for(int m = 0; m < def.side2.sectorIds.Length; m++) + sectorMask[m + def.side1.sectorIds.Length] = (def.side2.sectorIds[m] - def.side2.sectorIds[0]) + def.side1.sectorIds.Length; + } + // Head changes after whole side + else if(string.Compare(def.order, "CYLINDERS", StringComparison.InvariantCultureIgnoreCase) == 0) + { + for(int m = 0; m < def.side1.sectorIds.Length; m++) + sectorMask[m] = def.side1.sectorIds[m] - def.side1.sectorIds[0]; + // Skip first track (first side) and first track (second side) + for(int m = 0; m < def.side1.sectorIds.Length; m++) + sectorMask[m + def.side1.sectorIds.Length] = (def.side1.sectorIds[m] - def.side1.sectorIds[0]) + def.side1.sectorIds.Length + def.side2.sectorIds.Length; + } + // TODO: Implement COLUMBIA ordering + else if(string.Compare(def.order, "COLUMBIA", StringComparison.InvariantCultureIgnoreCase) == 0) + { + DicConsole.DebugWriteLine("CP/M Plugin", "Don't know how to handle COLUMBIA ordering, not proceeding with this definition."); + continue; + } + // TODO: Implement EAGLE ordering + else if(string.Compare(def.order, "EAGLE", StringComparison.InvariantCultureIgnoreCase) == 0) + { + DicConsole.DebugWriteLine("CP/M Plugin", "Don't know how to handle EAGLE ordering, not proceeding with this definition."); + continue; + } + else + { + DicConsole.DebugWriteLine("CP/M Plugin", "Unknown order type \"{0}\", not proceeding with this definition.", def.order); + continue; + } + } + + // Read the directory marked by this definition + MemoryStream ms = new MemoryStream(); + for(int p = 0; p < dirLen; p++) + { + byte[] dirSector = imagePlugin.ReadSector((ulong)((int)offset + (int)partitionStart + (p / sectorMask.Length) * sectorMask.Length + sectorMask[p % sectorMask.Length])); + ms.Write(dirSector, 0, dirSector.Length); + } + directory = ms.ToArray(); + + if(def.evenOdd) + DicConsole.DebugWriteLine("CP/M Plugin", "Definition contains EVEN-ODD field, with unknown meaning, detection may be wrong."); + + // Complement of the directory bytes if needed + if(def.complement) + { + for(int b = 0; b < directory.Length; b++) + directory[b] = (byte)(~directory[b] & 0xFF); + } + + // Check the directory + if(CheckDir(directory)) + { + DicConsole.DebugWriteLine("CP/M Plugin", "Definition \"{0}\" has a correct directory", def.comment); + + // Build a Disc Parameter Block + workingDefinition = def; + dpb = new DiscParameterBlock(); + dpb.al0 = (byte)def.al0; + dpb.al1 = (byte)def.al1; + dpb.blm = (byte)def.blm; + dpb.bsh = (byte)def.bsh; + dpb.cks = 0; // Needed? + dpb.drm = (ushort)def.drm; + dpb.dsm = (ushort)def.dsm; + dpb.exm = (byte)def.exm; + dpb.off = (ushort)def.ofs; + switch(def.bytesPerSector) + { + case 128: + dpb.psh = 0; + dpb.phm = 0; + break; + case 256: + dpb.psh = 1; + dpb.phm = 1; + break; + case 512: + dpb.psh = 2; + dpb.phm = 3; + break; + case 1024: + dpb.psh = 3; + dpb.phm = 7; + break; + case 2048: + dpb.psh = 4; + dpb.phm = 15; + break; + case 4096: + dpb.psh = 5; + dpb.phm = 31; + break; + case 8192: + dpb.psh = 6; + dpb.phm = 63; + break; + case 16384: + dpb.psh = 7; + dpb.phm = 127; + break; + case 32768: + dpb.psh = 8; + dpb.phm = 255; + break; + } + dpb.spt = (ushort)((def.sectorsPerTrack * def.bytesPerSector) / 128); + cpmFound = true; + workingDefinition = def; + + return true; + } + + label = null; + labelCreationDate = null; + labelUpdateDate = null; + } + } + } + } + + // Clear class variables + cpmFound = false; + workingDefinition = null; + dpb = null; + label = null; + standardTimestamps = false; + thirdPartyTimestamps = false; + return false; + } + catch(Exception ex) + { + throw ex; +// return false; + } + } + + public override void GetInformation(ImagePlugin imagePlugin, ulong partitionStart, ulong partitionEnd, out string information) + { + information = ""; + // As the identification is so complex, just call Identify() and relay on its findings + if(!Identify(imagePlugin, partitionStart, partitionEnd) || !cpmFound || workingDefinition == null || dpb == null) + return; + + StringBuilder sb = new StringBuilder(); + sb.AppendLine("CP/M filesystem"); + if(!string.IsNullOrEmpty(workingDefinition.comment)) + sb.AppendFormat("Identified as {0}", workingDefinition.comment).AppendLine(); + sb.AppendFormat("Volume block is {0} bytes", 128 << dpb.bsh).AppendLine(); + if(dpb.dsm > 0) + sb.AppendFormat("Volume contains {0} blocks ({1} bytes)", dpb.dsm, (dpb.dsm) * (128 << dpb.bsh)).AppendLine(); + sb.AppendFormat("Volume contains {0} directory entries", dpb.drm + 1).AppendLine(); + if(workingDefinition.sofs > 0) + sb.AppendFormat("Volume reserves {0} sectors for system", workingDefinition.sofs).AppendLine(); + else + sb.AppendFormat("Volume reserves {1} tracks ({0} sectors) for system", workingDefinition.ofs * workingDefinition.sectorsPerTrack, workingDefinition.ofs).AppendLine(); + + int interleaveSide1; + int interleaveSide2 = 1; + + interleaveSide1 = workingDefinition.side1.sectorIds[1] - workingDefinition.side1.sectorIds[0]; + if(interleaveSide1 > 1) + sb.AppendFormat("Side 0 uses {0}:1 software interleaving", interleaveSide1).AppendLine(); + + if(workingDefinition.sides == 2) + { + interleaveSide2 = workingDefinition.side2.sectorIds[1] - workingDefinition.side2.sectorIds[0]; + if(interleaveSide2 > 1) + sb.AppendFormat("Side 1 uses {0}:1 software interleaving", interleaveSide2).AppendLine(); + switch(workingDefinition.order) + { + case "SIDES": + sb.AppendLine("Head changes after each whole track"); + break; + case "CYLINDERS": + sb.AppendLine("Head changes after whole side"); + break; + default: + sb.AppendFormat("Unknown how {0} side ordering works", workingDefinition.order).AppendLine(); + break; + } + } + + if(workingDefinition.skew > 0) + sb.AppendFormat("Device uses {0}:1 hardware interleaving", workingDefinition.skew).AppendLine(); + + if(workingDefinition.sofs > 0) + sb.AppendFormat("BSH {0} BLM {1} EXM {2} DSM {3} DRM {4} AL0 {5:X2}H AL1 {6:X2}H SOFS {7}", dpb.bsh, dpb.blm, dpb.exm, dpb.dsm, dpb.drm, dpb.al0, dpb.al1, workingDefinition.sofs).AppendLine(); + else + sb.AppendFormat("BSH {0} BLM {1} EXM {2} DSM {3} DRM {4} AL0 {5:X2}H AL1 {6:X2}H OFS {7}", dpb.bsh, dpb.blm, dpb.exm, dpb.dsm, dpb.drm, dpb.al0, dpb.al1, workingDefinition.ofs).AppendLine(); + + if(label != null) + sb.AppendFormat("Volume label {0}", label).AppendLine(); + + if(standardTimestamps) + sb.AppendLine("Volume uses standard CP/M timestamps"); + + if(thirdPartyTimestamps) + sb.AppendLine("Volume uses third party timestamps"); + + if(labelCreationDate != null) + sb.AppendFormat("Volume created on {0}", DateHandlers.CPMToDateTime(labelCreationDate)).AppendLine(); + if(labelUpdateDate != null) + sb.AppendFormat("Volume updated on {0}", DateHandlers.CPMToDateTime(labelUpdateDate)).AppendLine(); + + xmlFSType = new Schemas.FileSystemType(); + xmlFSType.Bootable |= (workingDefinition.sofs > 0 || workingDefinition.ofs > 0); + xmlFSType.ClusterSize = 128 << dpb.bsh; + if(dpb.dsm > 0) + xmlFSType.Clusters = ((dpb.dsm + 1) * 128) / (128 << dpb.bsh); + else + xmlFSType.Clusters = (long)(partitionEnd - partitionStart); + if(labelCreationDate != null) + { + xmlFSType.CreationDate = DateHandlers.CPMToDateTime(labelCreationDate); + xmlFSType.CreationDateSpecified = true; + } + if(labelUpdateDate != null) + { + xmlFSType.ModificationDate = DateHandlers.CPMToDateTime(labelUpdateDate); + xmlFSType.ModificationDateSpecified = true; + } + xmlFSType.Type = "CP/M"; + xmlFSType.VolumeName = label; + + information = sb.ToString(); } } } diff --git a/DiscImageChef.Filesystems/CPM/Structs.cs b/DiscImageChef.Filesystems/CPM/Structs.cs index 2f781ae0..332dc6d1 100644 --- a/DiscImageChef.Filesystems/CPM/Structs.cs +++ b/DiscImageChef.Filesystems/CPM/Structs.cs @@ -5,11 +5,11 @@ // Filename : Structs.cs // Author(s) : Natalia Portillo // -// Component : Component +// Component : CP/M filesystem plugin. // // --[ Description ] ---------------------------------------------------------- // -// Description +// CP/M filesystem structures. // // --[ License ] -------------------------------------------------------------- // @@ -29,13 +29,537 @@ // ---------------------------------------------------------------------------- // Copyright © 2011-2016 Natalia Portillo // ****************************************************************************/ -using System; + +using System.Runtime.InteropServices; + namespace DiscImageChef.Filesystems.CPM { - public class Structs + partial class CPM : Filesystem { - public Structs() + /// + /// Most of the times this structure is hard wired or generated by CP/M, not stored on disk + /// + class DiscParameterBlock { + /// + /// Sectors per track + /// + public ushort spt; + /// + /// Block shift + /// + public byte bsh; + /// + /// Block mask + /// + public byte blm; + /// + /// Extent mask + /// + public byte exm; + /// + /// Blocks on disk - 1 + /// + public ushort dsm; + /// + /// Directory entries - 1 + /// + public ushort drm; + /// + /// First byte of allocation bitmap + /// + public byte al0; + /// + /// Second byte of allocation bitmap + /// + public byte al1; + /// + /// Checksum vector size + /// + public ushort cks; + /// + /// Reserved tracks + /// + public ushort off; + /// + /// Physical sector shift + /// + public byte psh; + /// + /// Physical sector mask + /// + public byte phm; + } + + /// + /// Amstrad superblock, for PCW + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct AmstradSuperBlock + { + /// + /// Format ID. 0 single-side, 3 double-side. 1 and 2 are for CPC but they don't use the superblock + /// + public byte format; + /// + /// Gives information about side ordering + /// + public byte sidedness; + /// + /// Tracks per side, aka, cylinders + /// + public byte tps; + /// + /// Sectors per track + /// + public byte spt; + /// + /// Physical sector shift + /// + public byte psh; + /// + /// Reserved tracks + /// + public byte off; + /// + /// Block size shift + /// + public byte bsh; + /// + /// How many blocks does the directory take + /// + public byte dirBlocks; + /// + /// GAP#3 length (intersector) + /// + public byte gapLen; + /// + /// GAP#4 length (end-of-track) + /// + public byte formatGap; + /// + /// Must be 0 + /// + public byte zero1; + /// + /// Must be 0 + /// + public byte zero2; + /// + /// Must be 0 + /// + public byte zero3; + /// + /// Must be 0 + /// + public byte zero4; + /// + /// Must be 0 + /// + public byte zero5; + /// + /// Indicates machine the boot code following the superblock is designed to boot + /// + public byte fiddle; + } + + /// + /// Superblock found on CP/M-86 hard disk volumes + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct HardDiskSuperBlock + { + /// + /// Value so the sum of all the superblock's sector bytes taken as 16-bit values gives 0 + /// + public ushort checksum; + /// + /// Copyright string + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x1F)] + public byte[] copyright; + /// + /// First cylinder of disk where this volume resides + /// + public ushort firstCylinder; + /// + /// How many cylinders does this volume span + /// + public ushort cylinders; + /// + /// Heads on hard disk + /// + public byte heads; + /// + /// Sectors per track + /// + public byte sectorsPerTrack; + /// + /// Flags, only use by CCP/M where bit 0 equals verify on write + /// + public byte flags; + /// + /// Sector size / 128 + /// + public byte recordsPerSector; + /// + /// + /// + public ushort spt; + /// + /// + /// + public byte bsh; + /// + /// + /// + public byte blm; + /// + /// + /// + public byte exm; + /// + /// + /// + public ushort dsm; + /// + /// + /// + public ushort drm; + /// + /// + /// + public ushort al0; + /// + /// + /// + public ushort al1; + /// + /// + /// + public ushort cks; + /// + /// + /// + public ushort off; + /// + /// Must be zero + /// + public ushort zero1; + /// + /// Must be zero + /// + public ushort zero2; + /// + /// Must be zero + /// + public ushort zero3; + /// + /// Must be zero + /// + public ushort zero4; + /// + /// How many 128 bytes are in a block + /// + public ushort recordsPerBlock; + /// + /// Maximum number of bad blocks in the bad block list + /// + public ushort badBlockWordsMax; + /// + /// Used number of bad blocks in the bad block list + /// + public ushort badBlockWords; + /// + /// First block after the blocks reserved for bad block substitution + /// + public ushort firstSub; + } + + /// + /// Volume label entry + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct LabelEntry + { + /// + /// Must be 0x20 + /// + public byte signature; + /// + /// Label in ASCII + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 11)] + public byte[] label; + /// + /// Label flags. Bit 0 = label exists, bit 4 = creation timestamp, bit 5 = modification timestamp, bit 6 = access timestamp, bit 7 = password enabled + /// + public byte flags; + /// + /// Password decoder byte + /// + public byte passwordDecoder; + /// + /// Must be 0 + /// + public ushort reserved; + /// + /// Password XOR'ed with + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public byte[] password; + /// + /// Label creation time + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] ctime; + /// + /// Label modification time + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] mtime; + } + + /// + /// CP/M 3 timestamp entry + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct DateEntry + { + /// + /// Must be 0x21 + /// + public byte signature; + /// + /// File 1 create/access timestamp + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] date1; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + /// + /// File 1 modification timestamp + /// + public byte[] date2; + /// + /// File 1 password mode + /// + public byte mode1; + public byte zero1; + /// + /// File 2 create/access timestamp + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] date3; + /// + /// File 2 modification timestamp + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] date4; + /// + /// File 2 password mode + /// + public byte mode2; + public byte zero2; + /// + /// File 3 create/access timestamp + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] date5; + /// + /// File 3 modification timestamp + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] date6; + /// + /// File 3 password mode + /// + public byte mode3; + public ushort zero3; + } + + /// + /// Password entry + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct PasswordEntry + { + /// + /// 16 + user number + /// + public byte userNumber; + /// + /// Filename + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public byte[] filename; + /// + /// Extension + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] + public byte[] extension; + /// + /// Password mode. Bit 7 = required for read, bit 6 = required for write, bit 5 = required for delete + /// + public byte mode; + /// + /// Password decoder byte + /// + public byte passwordDecoder; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] + public byte[] reserved; + /// + /// Password XOR'ed with + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public byte[] password; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public byte[] reserved2; + } + + /// + /// Timestamp for Z80DOS or DOS+ + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct TrdPartyDateEntry + { + /// + /// Must be 0x21 + /// + public byte signature; + public byte zero; + /// + /// Creation year for file 1 + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] + public byte[] create1; + /// + /// Modification time for file 1 + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] modify1; + /// + /// Access time for file 1 + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] access1; + /// + /// Creation year for file 2 + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] + public byte[] create2; + /// + /// Modification time for file 2 + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] modify2; + /// + /// Access time for file 2 + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] access2; + /// + /// Creation year for file 3 + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] + public byte[] create3; + /// + /// Modification time for file 3 + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] modify3; + /// + /// Access time for file 3 + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] access3; + } + + /// + /// Directory entry for <256 allocation blocks + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct DirectoryEntry + { + /// + /// User number. Bit 7 set in CP/M 1 means hidden + /// + public byte statusUser; + /// + /// Filename and bit 7 as flags + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public byte[] filename; + /// + /// Extension and bit 7 as flags + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] + public byte[] extension; + /// + /// Low byte of extent number + /// + public byte extentCounter; + /// + /// Last record bytes. In some implementations it means how many bytes are used in the last record, in others how many bytes are free. + /// It always refer to 128 byte records even if blocks are way bigger, so it's mostly useless. + /// + public byte lastRecordBytes; + /// + /// High byte of extent number + /// + public byte extentCounterHigh; + /// + /// How many records are used in this entry. 0x80 if all are used. + /// + public byte records; + /// + /// Allocation blocks + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public byte[] allocations; + } + + /// + /// Directory entry for &bt;256 allocation blocks + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct DirectoryEntry16 + { + /// + /// User number. Bit 7 set in CP/M 1 means hidden + /// + public byte statusUser; + /// + /// Filename and bit 7 as flags + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public byte[] filename; + /// + /// Extension and bit 7 as flags + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] + public byte[] extension; + /// + /// Low byte of extent number + /// + public byte extentCounter; + /// + /// Last record bytes. In some implementations it means how many bytes are used in the last record, in others how many bytes are free. + /// It always refer to 128 byte records even if blocks are way bigger, so it's mostly useless. + /// + public byte lastRecordBytes; + /// + /// High byte of extent number + /// + public byte extentCounterHigh; + /// + /// How many records are used in this entry. 0x80 if all are used. + /// + public byte records; + /// + /// Allocation blocks + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public ushort[] allocations; } } } diff --git a/DiscImageChef.Filesystems/CPM/Super.cs b/DiscImageChef.Filesystems/CPM/Super.cs index 91bbab3b..ded93a26 100644 --- a/DiscImageChef.Filesystems/CPM/Super.cs +++ b/DiscImageChef.Filesystems/CPM/Super.cs @@ -5,11 +5,13 @@ // Filename : Super.cs // Author(s) : Natalia Portillo // -// Component : Component +// Component : CP/M filesystem plugin. // // --[ Description ] ---------------------------------------------------------- // -// Description +// Handles mounting and umounting the CP/M filesystem. +// Caches the whole volume on mounting (shouldn't be a problem, maximum +// volume size for CP/M is 8 MiB). // // --[ License ] -------------------------------------------------------------- // @@ -29,13 +31,756 @@ // ---------------------------------------------------------------------------- // Copyright © 2011-2016 Natalia Portillo // ****************************************************************************/ + using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using DiscImageChef.Console; + namespace DiscImageChef.Filesystems.CPM { - public class Super + partial class CPM : Filesystem { - public Super() + public override Errno Mount() { + return Mount(false); + } + + public override Errno Mount(bool debug) + { + // As the identification is so complex, just call Identify() and relay on its findings + if(!Identify(device, partStart, partEnd) || !cpmFound || workingDefinition == null || dpb == null) + return Errno.InvalidArgument; + + // Build the software interleaving sector mask + if(workingDefinition.sides == 1) + { + sectorMask = new int[workingDefinition.side1.sectorIds.Length]; + for(int m = 0; m < sectorMask.Length; m++) + sectorMask[m] = workingDefinition.side1.sectorIds[m] - workingDefinition.side1.sectorIds[0]; + } + else + { + // Head changes after every track + if(string.Compare(workingDefinition.order, "SIDES", StringComparison.InvariantCultureIgnoreCase) == 0) + { + sectorMask = new int[workingDefinition.side1.sectorIds.Length + workingDefinition.side2.sectorIds.Length]; + for(int m = 0; m < workingDefinition.side1.sectorIds.Length; m++) + sectorMask[m] = workingDefinition.side1.sectorIds[m] - workingDefinition.side1.sectorIds[0]; + // Skip first track (first side) + for(int m = 0; m < workingDefinition.side2.sectorIds.Length; m++) + sectorMask[m + workingDefinition.side1.sectorIds.Length] = (workingDefinition.side2.sectorIds[m] - workingDefinition.side2.sectorIds[0]) + workingDefinition.side1.sectorIds.Length; + } + // Head changes after whole side + else if(string.Compare(workingDefinition.order, "CYLINDERS", StringComparison.InvariantCultureIgnoreCase) == 0) + { + for(int m = 0; m < workingDefinition.side1.sectorIds.Length; m++) + sectorMask[m] = workingDefinition.side1.sectorIds[m] - workingDefinition.side1.sectorIds[0]; + // Skip first track (first side) and first track (second side) + for(int m = 0; m < workingDefinition.side1.sectorIds.Length; m++) + sectorMask[m + workingDefinition.side1.sectorIds.Length] = (workingDefinition.side1.sectorIds[m] - workingDefinition.side1.sectorIds[0]) + workingDefinition.side1.sectorIds.Length + workingDefinition.side2.sectorIds.Length; + + // TODO: Implement CYLINDERS ordering + DicConsole.DebugWriteLine("CP/M Plugin", "CYLINDERS ordering not yet implemented."); + return Errno.NotImplemented; + } + // TODO: Implement COLUMBIA ordering + else if(string.Compare(workingDefinition.order, "COLUMBIA", StringComparison.InvariantCultureIgnoreCase) == 0) + { + DicConsole.DebugWriteLine("CP/M Plugin", "Don't know how to handle COLUMBIA ordering, not proceeding with this definition."); + return Errno.NotImplemented; + } + // TODO: Implement EAGLE ordering + else if(string.Compare(workingDefinition.order, "EAGLE", StringComparison.InvariantCultureIgnoreCase) == 0) + { + DicConsole.DebugWriteLine("CP/M Plugin", "Don't know how to handle EAGLE ordering, not proceeding with this definition."); + return Errno.NotImplemented; + } + else + { + DicConsole.DebugWriteLine("CP/M Plugin", "Unknown order type \"{0}\", not proceeding with this definition.", workingDefinition.order); + return Errno.NotSupported; + } + } + + // Deinterleave whole volume + Dictionary deinterleavedSectors = new Dictionary(); + if(workingDefinition.sides == 1 || string.Compare(workingDefinition.order, "SIDES", StringComparison.InvariantCultureIgnoreCase) == 0) + { + DicConsole.DebugWriteLine("CP/M Plugin", "Deinterleaving whole volume."); + + for(int p = 0; p <= (int)(partEnd - partStart); p++) + { + byte[] readSector = device.ReadSector((ulong)((int)partStart + (p / sectorMask.Length) * sectorMask.Length + sectorMask[p % sectorMask.Length])); + if(workingDefinition.complement) + { + for(int b = 0; b < readSector.Length; b++) + readSector[b] = (byte)(~readSector[b] & 0xFF); + } + deinterleavedSectors.Add((ulong)p, readSector); + } + } + + int blockSize = 128 << dpb.bsh; + MemoryStream blockMs = new MemoryStream(); + ulong blockNo = 0; + int sectorsPerBlock = 0; + Dictionary allocationBlocks = new Dictionary(); + + DicConsole.DebugWriteLine("CP/M Plugin", "Creating allocation blocks."); + + // For each volume sector + for(ulong a = 0; a < (ulong)deinterleavedSectors.Count; a++) + { + byte[] sector; + deinterleavedSectors.TryGetValue(a, out sector); + + // May it happen? Just in case, CP/M blocks are smaller than physical sectors + if(sector.Length > blockSize) + { + for(int i = 0; i < (sector.Length / blockSize); i++) + { + byte[] tmp = new byte[blockSize]; + Array.Copy(sector, blockSize * i, tmp, 0, blockSize); + allocationBlocks.Add(blockNo++, tmp); + } + } + // CP/M blocks are larger than physical sectors + else if(sector.Length < blockSize) + { + blockMs.Write(sector, 0, sector.Length); + sectorsPerBlock++; + + if(sectorsPerBlock == blockSize / sector.Length) + { + allocationBlocks.Add(blockNo++, blockMs.ToArray()); + sectorsPerBlock = 0; + blockMs = new MemoryStream(); + } + } + // CP/M blocks are same size than physical sectors + else + allocationBlocks.Add(blockNo++, sector); + } + + DicConsole.DebugWriteLine("CP/M Plugin", "Reading directory."); + + int dirOff; + int dirSectors = ((dpb.drm + 1) * 32) / workingDefinition.bytesPerSector; + if(workingDefinition.sofs > 0) + dirOff = workingDefinition.sofs; + else + dirOff = workingDefinition.ofs * workingDefinition.sectorsPerTrack; + + // Read the whole directory blocks + MemoryStream dirMs = new MemoryStream(); + for(int d = 0; d < dirSectors; d++) + { + byte[] sector; + deinterleavedSectors.TryGetValue((ulong)(d + dirOff), out sector); + dirMs.Write(sector, 0, sector.Length); + } + byte[] directory = dirMs.ToArray(); + + if(directory == null) + return Errno.InvalidArgument; + + int dirCnt = 0; + string file1 = null; + string file2 = null; + string file3 = null; + Dictionary>> fileExtents = new Dictionary>>(); + statCache = new Dictionary(); + cpmStat = new FileSystemInfo(); + bool atime = false; + dirList = new List(); + labelCreationDate = null; + labelUpdateDate = null; + passwordCache = new Dictionary(); + + DicConsole.DebugWriteLine("CP/M Plugin", "Traversing directory."); + + // For each directory entry + for(int dOff = 0; dOff < directory.Length; dOff += 32) + { + // Describes a file (does not support PDOS entries with user >= 16, because they're identical to password entries + if((directory[dOff] & 0x7F) < 0x10) + { + // If there are more than 256 allocation blocks, the allocation block becomes a 16-bit value + if(allocationBlocks.Count > 256) + { + DirectoryEntry16 entry = new DirectoryEntry16(); + IntPtr dirPtr = Marshal.AllocHGlobal(32); + Marshal.Copy(directory, dOff, dirPtr, 32); + entry = (DirectoryEntry16)Marshal.PtrToStructure(dirPtr, typeof(DirectoryEntry16)); + Marshal.FreeHGlobal(dirPtr); + + bool hidden = (entry.statusUser & 0x80) == 0x80; + bool rdOnly = (entry.filename[0] & 0x80) == 0x80 || (entry.extension[0] & 0x80) == 0x80; + bool system = (entry.filename[1] & 0x80) == 0x80 || (entry.extension[2] & 0x80) == 0x80; + //bool backed = (entry.filename[3] & 0x80) == 0x80 || (entry.extension[3] & 0x80) == 0x80; + int user = (entry.statusUser & 0x0F); + + bool validEntry = true; + + for(int i = 0; i < 8; i++) + { + entry.filename[i] &= 0x7F; + validEntry &= entry.filename[i] >= 0x20; + } + for(int i = 0; i < 3; i++) + { + entry.extension[i] &= 0x7F; + validEntry &= entry.extension[i] >= 0x20; + } + + if(!validEntry) + continue; + + string filename = Encoding.ASCII.GetString(entry.filename).Trim(); + string extension = Encoding.ASCII.GetString(entry.extension).Trim(); + + // If user is != 0, append user to name to have identical filenames + if(user > 0) + filename = string.Format("{0:X1}:{1}", user, filename); + if(!string.IsNullOrEmpty(extension)) + filename = filename + "." + extension; + + int entryNo = ((32 * entry.extentCounter) + entry.extentCounterHigh) / (dpb.exm + 1); + List blocks; + Dictionary> extentBlocks; + FileEntryInfo fInfo; + + // Do we have a stat for the file already? + if(statCache.TryGetValue(filename, out fInfo)) + statCache.Remove(filename); + else + { + fInfo = new FileEntryInfo(); + fInfo.Attributes = new FileAttributes(); + } + + // And any extent? + if(fileExtents.TryGetValue(filename, out extentBlocks)) + fileExtents.Remove(filename); + else + extentBlocks = new Dictionary>(); + + // Do we already have this extent? Should never happen + if(extentBlocks.TryGetValue(entryNo, out blocks)) + extentBlocks.Remove(entryNo); + else + blocks = new List(); + + // Attributes + if(hidden) + fInfo.Attributes |= FileAttributes.Hidden; + if(rdOnly) + fInfo.Attributes |= FileAttributes.ReadOnly; + if(system) + fInfo.Attributes |= FileAttributes.System; + + // Supposedly there is a value in the directory entry telling how many blocks are designated in this entry + // However some implementations tend to do whatever they wish, but none will ever allocate block 0 for a file + // because that's where the directory resides. + // There is also a field telling how many bytes are used in the last block, but its meaning is non-standard so + // we must ignore it. + foreach(ushort blk in entry.allocations) + { + if(!blocks.Contains(blk) && blk != 0) + blocks.Add(blk); + } + + // Save the file + fInfo.UID = (ulong)user; + extentBlocks.Add(entryNo, blocks); + fileExtents.Add(filename, extentBlocks); + statCache.Add(filename, fInfo); + + // Add the file to the directory listing + if(!dirList.Contains(filename)) + dirList.Add(filename); + + // Count entries 3 by 3 for timestamps + switch(dirCnt % 3) + { + case 0: + file1 = filename; + break; + case 1: + file2 = filename; + break; + case 2: + file3 = filename; + break; + } + dirCnt++; + } + else + { + DirectoryEntry entry = new DirectoryEntry(); + IntPtr dirPtr = Marshal.AllocHGlobal(32); + Marshal.Copy(directory, dOff, dirPtr, 32); + entry = (DirectoryEntry)Marshal.PtrToStructure(dirPtr, typeof(DirectoryEntry)); + Marshal.FreeHGlobal(dirPtr); + + bool hidden = (entry.statusUser & 0x80) == 0x80; + bool rdOnly = (entry.filename[0] & 0x80) == 0x80 || (entry.extension[0] & 0x80) == 0x80; + bool system = (entry.filename[1] & 0x80) == 0x80 || (entry.extension[2] & 0x80) == 0x80; + //bool backed = (entry.filename[3] & 0x80) == 0x80 || (entry.extension[3] & 0x80) == 0x80; + int user = (entry.statusUser & 0x0F); + + bool validEntry = true; + + for(int i = 0; i < 8; i++) + { + entry.filename[i] &= 0x7F; + validEntry &= entry.filename[i] >= 0x20; + } + for(int i = 0; i < 3; i++) + { + entry.extension[i] &= 0x7F; + validEntry &= entry.extension[i] >= 0x20; + } + + if(!validEntry) + continue; + + string filename = Encoding.ASCII.GetString(entry.filename).Trim(); + string extension = Encoding.ASCII.GetString(entry.extension).Trim(); + + // If user is != 0, append user to name to have identical filenames + if(user > 0) + filename = string.Format("{0:X1}:{1}", user, filename); + if(!string.IsNullOrEmpty(extension)) + filename = filename + "." + extension; + + int entryNo = ((32 * entry.extentCounterHigh) + entry.extentCounter) / (dpb.exm + 1); + List blocks; + Dictionary> extentBlocks; + FileEntryInfo fInfo; + + // Do we have a stat for the file already? + if(statCache.TryGetValue(filename, out fInfo)) + statCache.Remove(filename); + else + { + fInfo = new FileEntryInfo(); + fInfo.Attributes = new FileAttributes(); + } + + // And any extent? + if(fileExtents.TryGetValue(filename, out extentBlocks)) + fileExtents.Remove(filename); + else + extentBlocks = new Dictionary>(); + + // Do we already have this extent? Should never happen + if(extentBlocks.TryGetValue(entryNo, out blocks)) + extentBlocks.Remove(entryNo); + else + blocks = new List(); + + // Attributes + if(hidden) + fInfo.Attributes |= FileAttributes.Hidden; + if(rdOnly) + fInfo.Attributes |= FileAttributes.ReadOnly; + if(system) + fInfo.Attributes |= FileAttributes.System; + + // Supposedly there is a value in the directory entry telling how many blocks are designated in this entry + // However some implementations tend to do whatever they wish, but none will ever allocate block 0 for a file + // because that's where the directory resides. + // There is also a field telling how many bytes are used in the last block, but its meaning is non-standard so + // we must ignore it. + foreach(ushort blk in entry.allocations) + { + if(!blocks.Contains(blk) && blk != 0) + blocks.Add(blk); + } + + // Save the file + fInfo.UID = (ulong)user; + extentBlocks.Add(entryNo, blocks); + fileExtents.Add(filename, extentBlocks); + statCache.Add(filename, fInfo); + + // Add the file to the directory listing + if(!dirList.Contains(filename)) + dirList.Add(filename); + + // Count entries 3 by 3 for timestamps + switch(dirCnt % 3) + { + case 0: + file1 = filename; + break; + case 1: + file2 = filename; + break; + case 2: + file3 = filename; + break; + } + dirCnt++; + } + } + // A password entry (or a file entry in PDOS, but this does not handle that case) + else if((directory[dOff] & 0x7F) >= 0x10 && (directory[dOff] & 0x7F) < 0x20) + { + PasswordEntry entry = new PasswordEntry(); + IntPtr dirPtr = Marshal.AllocHGlobal(32); + Marshal.Copy(directory, dOff, dirPtr, 32); + entry = (PasswordEntry)Marshal.PtrToStructure(dirPtr, typeof(PasswordEntry)); + Marshal.FreeHGlobal(dirPtr); + + int user = (entry.userNumber & 0x0F); + + for(int i = 0; i < 8; i++) + entry.filename[i] &= 0x7F; + for(int i = 0; i < 3; i++) + entry.extension[i] &= 0x7F; + + string filename = Encoding.ASCII.GetString(entry.filename).Trim(); + string extension = Encoding.ASCII.GetString(entry.extension).Trim(); + + // If user is != 0, append user to name to have identical filenames + if(user > 0) + filename = string.Format("{0:X1}:{1}", user, filename); + if(!string.IsNullOrEmpty(extension)) + filename = filename + "." + extension; + + // Do not repeat passwords + if(passwordCache.ContainsKey(filename)) + passwordCache.Remove(filename); + + // Copy whole password entry + byte[] tmp = new byte[32]; + Array.Copy(directory, dOff, tmp, 0, 32); + passwordCache.Add(filename, tmp); + + // Count entries 3 by 3 for timestamps + switch(dirCnt % 3) + { + case 0: + file1 = filename; + break; + case 1: + file2 = filename; + break; + case 2: + file3 = filename; + break; + } + dirCnt++; + } + // Volume label and password entry. Volume password is ignored. + else if((directory[dOff] & 0x7F) == 0x20) + { + LabelEntry entry = new LabelEntry(); + IntPtr dirPtr = Marshal.AllocHGlobal(32); + Marshal.Copy(directory, dOff, dirPtr, 32); + entry = (LabelEntry)Marshal.PtrToStructure(dirPtr, typeof(LabelEntry)); + Marshal.FreeHGlobal(dirPtr); + + // The volume label defines if one of the fields in CP/M 3 timestamp is a creation or an access time + atime |= (entry.flags & 0x40) == 0x40; + + label = Encoding.ASCII.GetString(directory, dOff + 1, 11).Trim(); + labelCreationDate = new byte[4]; + labelUpdateDate = new byte[4]; + Array.Copy(directory, dOff + 24, labelCreationDate, 0, 4); + Array.Copy(directory, dOff + 28, labelUpdateDate, 0, 4); + + // Count entries 3 by 3 for timestamps + switch(dirCnt % 3) + { + case 0: + file1 = null; + break; + case 1: + file2 = null; + break; + case 2: + file3 = null; + break; + } + dirCnt++; + } + // Timestamp entry + else if((directory[dOff] & 0x7F) == 0x21) + { + // These places must be zero on CP/M 3 timestamp + if(directory[dOff + 10] == 0x00 && + directory[dOff + 20] == 0x00 && + directory[dOff + 30] == 0x00 && + directory[dOff + 31] == 0x00) + { + DateEntry entry = new DateEntry(); + IntPtr dirPtr = Marshal.AllocHGlobal(32); + Marshal.Copy(directory, dOff, dirPtr, 32); + entry = (DateEntry)Marshal.PtrToStructure(dirPtr, typeof(DateEntry)); + Marshal.FreeHGlobal(dirPtr); + + FileEntryInfo fInfo; + + // Entry contains timestamps for last 3 entries, whatever the kind they are. + if(!string.IsNullOrEmpty(file1)) + { + if(statCache.TryGetValue(file1, out fInfo)) + statCache.Remove(file1); + else + fInfo = new FileEntryInfo(); + + if(atime) + fInfo.AccessTime = DateHandlers.CPMToDateTime(entry.date1); + else + fInfo.CreationTime = DateHandlers.CPMToDateTime(entry.date1); + + fInfo.LastWriteTime = DateHandlers.CPMToDateTime(entry.date2); + + statCache.Add(file1, fInfo); + } + + if(!string.IsNullOrEmpty(file2)) + { + if(statCache.TryGetValue(file2, out fInfo)) + statCache.Remove(file2); + else + fInfo = new FileEntryInfo(); + + if(atime) + fInfo.AccessTime = DateHandlers.CPMToDateTime(entry.date3); + else + fInfo.CreationTime = DateHandlers.CPMToDateTime(entry.date3); + + fInfo.LastWriteTime = DateHandlers.CPMToDateTime(entry.date4); + + statCache.Add(file2, fInfo); + } + + if(!string.IsNullOrEmpty(file3)) + { + if(statCache.TryGetValue(file3, out fInfo)) + statCache.Remove(file3); + else + fInfo = new FileEntryInfo(); + + if(atime) + fInfo.AccessTime = DateHandlers.CPMToDateTime(entry.date5); + else + fInfo.CreationTime = DateHandlers.CPMToDateTime(entry.date5); + + fInfo.LastWriteTime = DateHandlers.CPMToDateTime(entry.date6); + + statCache.Add(file3, fInfo); + } + + file1 = null; + file2 = null; + file3 = null; + dirCnt = 0; + } + // However, if this byte is 0, timestamp is in Z80DOS or DOS+ format + else if(directory[dOff + 1] == 0x00) + { + TrdPartyDateEntry entry = new TrdPartyDateEntry(); + IntPtr dirPtr = Marshal.AllocHGlobal(32); + Marshal.Copy(directory, dOff, dirPtr, 32); + entry = (TrdPartyDateEntry)Marshal.PtrToStructure(dirPtr, typeof(TrdPartyDateEntry)); + Marshal.FreeHGlobal(dirPtr); + + FileEntryInfo fInfo; + + // Entry contains timestamps for last 3 entries, whatever the kind they are. + if(!string.IsNullOrEmpty(file1)) + { + if(statCache.TryGetValue(file1, out fInfo)) + statCache.Remove(file1); + else + fInfo = new FileEntryInfo(); + + byte[] ctime = new byte[4]; + ctime[0] = entry.create1[0]; + ctime[1] = entry.create1[1]; + + fInfo.AccessTime = DateHandlers.CPMToDateTime(entry.access1); + fInfo.CreationTime = DateHandlers.CPMToDateTime(ctime); + fInfo.LastWriteTime = DateHandlers.CPMToDateTime(entry.modify1); + + statCache.Add(file1, fInfo); + } + + if(!string.IsNullOrEmpty(file2)) + { + if(statCache.TryGetValue(file2, out fInfo)) + statCache.Remove(file2); + else + fInfo = new FileEntryInfo(); + + byte[] ctime = new byte[4]; + ctime[0] = entry.create2[0]; + ctime[1] = entry.create2[1]; + + fInfo.AccessTime = DateHandlers.CPMToDateTime(entry.access2); + fInfo.CreationTime = DateHandlers.CPMToDateTime(ctime); + fInfo.LastWriteTime = DateHandlers.CPMToDateTime(entry.modify2); + + statCache.Add(file2, fInfo); + } + + if(!string.IsNullOrEmpty(file3)) + { + if(statCache.TryGetValue(file1, out fInfo)) + statCache.Remove(file3); + else + fInfo = new FileEntryInfo(); + + byte[] ctime = new byte[4]; + ctime[0] = entry.create3[0]; + ctime[1] = entry.create3[1]; + + fInfo.AccessTime = DateHandlers.CPMToDateTime(entry.access3); + fInfo.CreationTime = DateHandlers.CPMToDateTime(ctime); + fInfo.LastWriteTime = DateHandlers.CPMToDateTime(entry.modify3); + + statCache.Add(file3, fInfo); + } + + file1 = null; + file2 = null; + file3 = null; + dirCnt = 0; + } + } + } + + // Cache all files. As CP/M maximum volume size is 8 Mib + // this should not be a problem + DicConsole.DebugWriteLine("CP/M Plugin", "Reading files."); + long usedBlocks = 0; + fileCache = new Dictionary(); + foreach(string filename in dirList) + { + MemoryStream fileMs = new MemoryStream(); + FileEntryInfo fInfo = new FileEntryInfo(); + + if(statCache.TryGetValue(filename, out fInfo)) + statCache.Remove(filename); + + fInfo.Blocks = 0; + + Dictionary> extents; + if(fileExtents.TryGetValue(filename, out extents)) + { + for(int ex = 0; ex < extents.Count; ex++) + { + List alBlks; + + if(extents.TryGetValue(ex, out alBlks)) + { + foreach(ushort alBlk in alBlks) + { + byte[] blk = new byte[blockSize]; + allocationBlocks.TryGetValue(alBlk, out blk); + fileMs.Write(blk, 0, blk.Length); + fInfo.Blocks++; + } + } + } + } + + // If you insist to call CP/M "extent based" + fInfo.Attributes |= FileAttributes.Extents; + fInfo.BlockSize = blockSize; + fInfo.Length = fileMs.Length; + cpmStat.Files++; + usedBlocks += fInfo.Blocks; + + statCache.Add(filename, fInfo); + fileCache.Add(filename, fileMs.ToArray()); + } + + decodedPasswordCache = new Dictionary(); + // For each stored password, store a decoded version of it + if(passwordCache.Count > 0) + { + foreach(KeyValuePair kvp in passwordCache) + { + byte[] tmp = new byte[8]; + Array.Copy(kvp.Value, 16, tmp, 0, 8); + for(int t = 0; t < 8; t++) + tmp[t] ^= kvp.Value[13]; + + decodedPasswordCache.Add(kvp.Key, tmp); + } + } + + // Generate statfs. + cpmStat.Blocks = dpb.dsm + 1; + cpmStat.FilenameLength = 11; + cpmStat.Files = (ulong)fileCache.Count; + cpmStat.FreeBlocks = cpmStat.Blocks - usedBlocks; + cpmStat.PluginId = PluginUUID; + cpmStat.Type = "CP/M filesystem"; + + // Generate XML info + xmlFSType = new Schemas.FileSystemType(); + xmlFSType.Clusters = cpmStat.Blocks; + xmlFSType.ClusterSize = blockSize; + if(labelCreationDate != null) + { + xmlFSType.CreationDate = DateHandlers.CPMToDateTime(labelCreationDate); + xmlFSType.CreationDateSpecified = true; + } + if(labelUpdateDate != null) + { + xmlFSType.ModificationDate = DateHandlers.CPMToDateTime(labelUpdateDate); + xmlFSType.ModificationDateSpecified = true; + } + xmlFSType.Files = fileCache.Count; + xmlFSType.FilesSpecified = true; + xmlFSType.FreeClusters = cpmStat.FreeBlocks; + xmlFSType.FreeClustersSpecified = true; + xmlFSType.Type = "CP/M filesystem"; + if(!string.IsNullOrEmpty(label)) + xmlFSType.VolumeName = label; + + mounted = true; + return Errno.NoError; + } + + /// + /// Gets information about the mounted volume. + /// + /// Information about the mounted volume. + public override Errno StatFs(ref FileSystemInfo stat) + { + if(!mounted) + return Errno.AccessDenied; + + stat = cpmStat; + + return Errno.NoError; + } + + public override Errno Unmount() + { + mounted = false; + definitions = null; + cpmFound = false; + workingDefinition = null; + dpb = null; + sectorMask = null; + label = null; + thirdPartyTimestamps = false; + standardTimestamps = false; + labelCreationDate = null; + labelUpdateDate = null; + return Errno.NoError; } } } diff --git a/DiscImageChef.Filesystems/CPM/Xattr.cs b/DiscImageChef.Filesystems/CPM/Xattr.cs index ed50182a..927fa3c5 100644 --- a/DiscImageChef.Filesystems/CPM/Xattr.cs +++ b/DiscImageChef.Filesystems/CPM/Xattr.cs @@ -5,11 +5,11 @@ // Filename : Xattr.cs // Author(s) : Natalia Portillo // -// Component : Component +// Component : CP/M filesystem plugin. // // --[ Description ] ---------------------------------------------------------- // -// Description +// Methods to handle CP/M extended attributes (password). // // --[ License ] -------------------------------------------------------------- // @@ -29,13 +29,74 @@ // ---------------------------------------------------------------------------- // Copyright © 2011-2016 Natalia Portillo // ****************************************************************************/ + using System; +using System.Collections.Generic; + namespace DiscImageChef.Filesystems.CPM { - public class Xattr + partial class CPM : Filesystem { - public Xattr() + /// + /// Reads an extended attribute, alternate data stream or fork from the given file. + /// + /// Error number. + /// File path. + /// Extendad attribute, alternate data stream or fork name. + /// Buffer. + public override Errno GetXattr(string path, string xattr, ref byte[] buf) { + if(!mounted) + return Errno.AccessDenied; + + string[] pathElements = path.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + if(pathElements.Length != 1) + return Errno.NotSupported; + + if(!fileCache.ContainsKey(pathElements[0].ToUpperInvariant())) + return Errno.NoSuchFile; + + if(string.Compare(xattr, "com.caldera.cpm.password", StringComparison.InvariantCulture) == 0) + { + if(!passwordCache.TryGetValue(pathElements[0].ToUpperInvariant(), out buf)) + return Errno.NoError; + } + + if(string.Compare(xattr, "com.caldera.cpm.password.text", StringComparison.InvariantCulture) == 0) + { + if(!passwordCache.TryGetValue(pathElements[0].ToUpperInvariant(), out buf)) + return Errno.NoError; + } + + return Errno.NoSuchExtendedAttribute; + } + + /// + /// Lists all extended attributes, alternate data streams and forks of the given file. + /// + /// Error number. + /// Path. + /// List of extended attributes, alternate data streams and forks. + public override Errno ListXAttr(string path, ref List xattrs) + { + if(!mounted) + return Errno.AccessDenied; + + string[] pathElements = path.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + if(pathElements.Length != 1) + return Errno.NotSupported; + + if(!fileCache.ContainsKey(pathElements[0].ToUpperInvariant())) + return Errno.NoSuchFile; + + xattrs = new List(); + if(passwordCache.ContainsKey(pathElements[0].ToUpperInvariant())) + xattrs.Add("com.caldera.cpm.password"); + + if(decodedPasswordCache.ContainsKey(pathElements[0].ToUpperInvariant())) + xattrs.Add("com.caldera.cpm.password.text"); + + return Errno.NoError; } } } diff --git a/DiscImageChef.Filesystems/CPM/cpmdefs.xml b/DiscImageChef.Filesystems/CPM/cpmdefs.xml index a67693a2..61b0bc22 100644 --- a/DiscImageChef.Filesystems/CPM/cpmdefs.xml +++ b/DiscImageChef.Filesystems/CPM/cpmdefs.xml @@ -4255,10 +4255,10 @@ Columbia Commander 964 - DSDD 48 tpi 5.25" - 512 x 10 MFM LOW - 0 - 0 - 0 - 0 + 40 + 2 + 10 + 512 0 0 @@ -4308,10 +4308,10 @@ Columbia 1600 - DSDD 96 tpi 5.25" - 512 x 10 MFM LOW - 0 - 0 - 0 - 0 + 80 + 2 + 10 + 512 0 0 @@ -4539,59 +4539,6 @@ true false - - Compustar 30, Super IOS - DSDD 48 tpi 5.25" - 512 x 10 - MFM - LOW - 35 - 2 - 10 - 512 - 0 - - 0 - - 1 - 3 - 5 - 7 - 9 - 2 - 4 - 6 - 8 - 10 - - - - 1 - - 1 - 3 - 5 - 7 - 9 - 2 - 4 - 6 - 8 - 10 - - - CYLINDERS - - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - true - false - Compupro (Viasyn) - DSDD 96 tpi 5.25" - 1024 x 5 MFM @@ -4923,10 +4870,10 @@ Cromemco CDOS - SSSD 48 tpi 5.25" - 128 x 18 FM LOW - 0 - 0 - 0 - 0 + 40 + 1 + 18 + 128 0 0 @@ -4968,10 +4915,10 @@ Cromemco CDOS - DSSD 48 tpi 5.25" - 128 x 18 FM LOW - 0 - 0 - 0 - 0 + 40 + 2 + 128 + 18 0 0 @@ -5037,10 +4984,10 @@ Cromemco CDOS - SSDD 48 tpi 5.25" - 512 x 10 MFM LOW - 0 - 0 - 0 - 0 + 40 + 1 + 10 + 512 0 0 @@ -5074,10 +5021,10 @@ Cromemco CDOS - DSDD 48 tpi 5.25" - 512 x 10 MFM LOW - 0 - 0 - 0 - 0 + 40 + 2 + 10 + 512 0 0 @@ -5127,10 +5074,10 @@ Cromemco CDOS - DSDD 8" - 512 x 16 MFM HIGH - 0 - 0 - 0 - 0 + 77 + 2 + 16 + 512 0 0 @@ -5192,10 +5139,10 @@ Cromemco CP/M - SSDD 48 tpi 5.25" - 512 x 10 MFM LOW - 0 - 0 - 0 - 0 + 40 + 1 + 10 + 512 0 0 @@ -5229,10 +5176,10 @@ Cromemco CP/M - DSDD 48 tpi 5.25" - 512 x 10 MFM LOW - 0 - 0 - 0 - 0 + 40 + 2 + 10 + 512 0 0 @@ -13617,13 +13564,13 @@ false - People's World Computer - SSDD 48 tpi 5.25" - 1024 x 5 + People's World Computer - DSDD 48 tpi 5.25" - 1024 x 5 MFM LOW - 0 - 0 - 0 - 0 + 40 + 2 + 5 + 1024 0 0 @@ -14115,98 +14062,6 @@ false false - - QDP-500 - DSHD 96 tpi 5.25" - 1024 x 8 - MFM - HIGH - 77 - 2 - 8 - 1024 - 0 - - 0 - - 1 - 3 - 5 - 7 - 2 - 4 - 6 - 8 - - - - 1 - - 1 - 3 - 5 - 7 - 2 - 4 - 6 - 8 - - - CYLINDERS - - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - false - false - - - Quay - DSDD 48 tpi 5.25" - 1024 x 5 - MFM - LOW - 40 - 2 - 5 - 1024 - 0 - - 0 - - 1 - 3 - 5 - 2 - 4 - - - - 1 - - 1 - 3 - 5 - 2 - 4 - - - CYLINDERS - - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - false - false - Research Machines Limited 380Z - 128 x 16 FM @@ -14636,7 +14491,7 @@ 1 9 512 - 0 + 2 0 @@ -16875,10 +16730,10 @@ TeleVideo 806 TurboDOS - DSDD 48 tpi 5.25" - 1024 x 5 MFM LOW - 0 - 0 - 0 - 0 + 40 + 2 + 5 + 1024 0 0 @@ -18624,10 +18479,10 @@ Xerox 820, S/W Publishers DD - SSDD 48 tpi 5.25" - 256 x 18 MFM LOW - 0 - 0 - 0 - 0 + 40 + 1 + 18 + 256 0 0 @@ -19849,12 +19704,30 @@ 0 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 - 0 + 1 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 CYLINDERS @@ -19871,34 +19744,6 @@ false false - - CPC-STD 178K Data 40trk 9sct 64dir 1Kpb Side 1 - MFM - LOW - 40 - 1 - 9 - 512 - 5 - - 0 - - 0 - - - - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - false - false - CPC 178K Data 40trk 9sct 64dir 1Kpb Side 2 MFM @@ -19934,34 +19779,6 @@ false false - - CPC 187K Data 42trk 9sct 64dir 1Kpb Side 1 - MFM - LOW - 42 - 1 - 9 - 512 - 5 - - 0 - - 0 - - - - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - false - false - CPC 187K Data 42trk 9sct 64dir 1Kpb Side 2 MFM @@ -19997,69 +19814,6 @@ false false - - CPC 253K Data 60trk 9sct 64dir 1Kpb Side 1 - MFM - LOW - 60 - 1 - 9 - 512 - 5 - - 0 - - 0 - - - - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - false - false - - - CPC 253K Data 60trk 9sct 64dir 1Kpb Side 2 - MFM - LOW - 60 - 2 - 9 - 512 - 5 - - 0 - - 0 - - - - 0 - - 0 - - - EAGLE - - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - false - false - CPC-B360 358K Data 80trk 9sct 64dir 2Kpb Side 1 MFM @@ -20689,6 +20443,14 @@ 0 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 @@ -20717,12 +20479,28 @@ 0 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 0 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 EAGLE @@ -20752,12 +20530,28 @@ 0 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 1 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 SIDES @@ -20776,21 +20570,37 @@ PCW 706K Format 80trk 9sct 256dir 2Kpb Two Sides - 0 - 0 - 0 - 0 + 80 + 2 + 9 + 512 0 0 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 1 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 @@ -20806,132 +20616,6 @@ false false - - CPC 398K Data 80trk 10sct 64dir 2Kpb Side 1 - MFM - LOW - 80 - 1 - 10 - 512 - 2 - - 0 - - 0 - - - - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - false - false - - - CPC 398K Data 80trk 10sct 64dir 2Kpb Side 2 - MFM - LOW - 80 - 2 - 10 - 512 - 2 - - 0 - - 0 - - - - 0 - - 0 - - - EAGLE - - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - false - false - - - MAGIC-DOS 408K Data 82trk 10sct 64dir 2Kpb Side 1 - MFM - LOW - 82 - 1 - 10 - 512 - 2 - - 0 - - 0 - - - - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - false - false - - - MAGIC-DOS 408K Data 82trk 10sct 64dir 2Kpb Side 2 - MFM - LOW - 82 - 2 - 10 - 512 - 2 - - 0 - - 0 - - - - 0 - - 0 - - - EAGLE - - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - false - false - PCW 784K Format 80trk 10sct 256dir 4Kpb Two Sides MFM @@ -20945,12 +20629,30 @@ 0 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 1 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 SIDES @@ -21958,6 +21660,42 @@ false false + + Extended CPC 3" 43-track + MFM + LOW + 43 + 1 + 9 + 512 + 2 + + 0 + + 193 + 194 + 195 + 196 + 197 + 198 + 199 + 200 + 201 + + + + 3 + 7 + 0 + 193 + 63 + 192 + 0 + 0 + 0 + false + false + Apple // CPM card 13-sector GCR diff --git a/DiscImageChef.Filesystems/ChangeLog b/DiscImageChef.Filesystems/ChangeLog index d7f578a9..d5f402aa 100644 --- a/DiscImageChef.Filesystems/ChangeLog +++ b/DiscImageChef.Filesystems/ChangeLog @@ -1,3 +1,18 @@ +2016-08-26 Natalia Portillo + + * CPM.cs: + * Dir.cs: + * File.cs: + * Info.cs: + * Xattr.cs: + * Super.cs: + * Consts.cs: + * Structs.cs: + * cpmdefs.xml: + * Definitions.cs: + * DiscImageChef.Filesystems.csproj: Added CP/M filesystem, + closes #29. + 2016-08-26 Natalia Portillo * Dir.cs: Typo. diff --git a/DiscImageChef.Filesystems/DiscImageChef.Filesystems.csproj b/DiscImageChef.Filesystems/DiscImageChef.Filesystems.csproj index 54b850e9..c8379470 100644 --- a/DiscImageChef.Filesystems/DiscImageChef.Filesystems.csproj +++ b/DiscImageChef.Filesystems/DiscImageChef.Filesystems.csproj @@ -31,6 +31,7 @@ + @@ -87,6 +88,15 @@ + + + + + + + + + @@ -127,11 +137,13 @@ + LICENSE.LGPL + diff --git a/README.md b/README.md index 59a4bbcf..16bdebeb 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Supported disk image formats * BlindWrite 4 TOC files (.BWT/.BWI/.BWS) * BlindWrite 5/6 TOC files (.B5T/.B5I and .B6T/.B6I) * X68k DIM disk image files (.DIM) +* CPCEMU Disk file and Extended Disk File Supported partitioning schemes ============================== @@ -77,6 +78,7 @@ Supported file systems for read-only operations * Apple Lisa file system * Apple Macintosh File System (MFS) * U.C.S.D Pascal file system +* CP/M file system Supported file systems for identification and information only ============================================================== diff --git a/TODO b/TODO index a7907313..ba09aa8e 100644 --- a/TODO +++ b/TODO @@ -19,8 +19,6 @@ Filesystem plugins: --- Add support for SFS filesystem --- Add support for PFS3 filesystem --- Add support for Apple DOS filesystems ---- Add support for AMSDOS filesystem ---- Add support for CP/M filesystem --- Add support for CBM filesystem --- Add support for ZFS --- Add support for UDF