diff --git a/DiscImageChef/ChangeLog b/DiscImageChef/ChangeLog index 7fc0c78a7..dc7573299 100644 --- a/DiscImageChef/ChangeLog +++ b/DiscImageChef/ChangeLog @@ -1,3 +1,8 @@ +2015-04-24 Natalia Portillo + + * ImagePlugins/VHD.cs: + Add support for dynamic disk images. + 2015-04-22 Natalia Portillo * ImagePlugins/Apple2MG.cs: diff --git a/DiscImageChef/ImagePlugins/VHD.cs b/DiscImageChef/ImagePlugins/VHD.cs index fc4c65e7b..b0b1b60c5 100644 --- a/DiscImageChef/ImagePlugins/VHD.cs +++ b/DiscImageChef/ImagePlugins/VHD.cs @@ -39,6 +39,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Text; +using System.Runtime.InteropServices; namespace DiscImageChef.ImagePlugins { @@ -126,23 +127,23 @@ namespace DiscImageChef.ImagePlugins struct ParentLocatorEntry { /// - /// Describes the platform specific type this entry belongs to + /// Offset 0x00, Describes the platform specific type this entry belongs to /// public UInt32 platformCode; /// - /// Describes the number of 512 bytes sectors used by this entry + /// Offset 0x04, Describes the number of 512 bytes sectors used by this entry /// public UInt32 platformDataSpace; /// - /// Describes this entry's size in bytes + /// Offset 0x08, Describes this entry's size in bytes /// public UInt32 platformDataLength; /// - /// Reserved + /// Offset 0x0c, Reserved /// public UInt32 reserved; /// - /// Offset on disk image this entry resides on + /// Offset 0x10, Offset on disk image this entry resides on /// public UInt64 platformDataOffset; } @@ -198,41 +199,20 @@ namespace DiscImageChef.ImagePlugins /// /// Offset 0x240, Parent disk image locator entry, /// - public ParentLocatorEntry locatorEntry1; - /// - /// Offset 0x258, Parent disk image locator entry, - /// - public ParentLocatorEntry locatorEntry2; - /// - /// Offset 0x270, Parent disk image locator entry, - /// - public ParentLocatorEntry locatorEntry3; - /// - /// Offset 0x288, Parent disk image locator entry, - /// - public ParentLocatorEntry locatorEntry4; - /// - /// Offset 0x2A0, Parent disk image locator entry, - /// - public ParentLocatorEntry locatorEntry5; - /// - /// Offset 0x2B8, Parent disk image locator entry, - /// - public ParentLocatorEntry locatorEntry6; - /// - /// Offset 0x2D0, Parent disk image locator entry, - /// - public ParentLocatorEntry locatorEntry7; - /// - /// Offset 0x2E8, Parent disk image locator entry, - /// - public ParentLocatorEntry locatorEntry8; + public ParentLocatorEntry[] locatorEntries; /// /// Offset 0x300, 256 reserved bytes /// public byte[] reserved2; } + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct BATSector + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 128)] + public UInt32[] blockPointer; + } + #endregion #region Internal Constants @@ -381,6 +361,8 @@ namespace DiscImageChef.ImagePlugins DateTime parentDateTime; string thisPath; string parentPath; + UInt32[] blockAllocationTable; + UInt32 bitmapSize; #endregion @@ -659,20 +641,173 @@ namespace DiscImageChef.ImagePlugins ImageInfo.imageLastModificationTime = thisDateTime; ImageInfo.imageName = Path.GetFileNameWithoutExtension(imagePath); + if (thisFooter.diskType == typeDynamic || thisFooter.diskType == typeDifferencing) + { + imageStream.Seek((long)thisFooter.offset, SeekOrigin.Begin); + byte[] dynamicBytes = new byte[1024]; + imageStream.Read(dynamicBytes, 0, 1024); + + UInt32 dynamicChecksum = BigEndianBitConverter.ToUInt32(dynamicBytes, 0x24); + + dynamicBytes[0x24] = 0; + dynamicBytes[0x25] = 0; + dynamicBytes[0x26] = 0; + dynamicBytes[0x27] = 0; + + UInt32 dynamicChecksumCalculated = VHDChecksum(dynamicBytes); + + if (MainClass.isDebug) + Console.WriteLine("DEBUG (VirtualPC plugin): Dynamic header checksum = 0x{0:X8}, calculated = 0x{1:X8}", dynamicChecksum, dynamicChecksumCalculated); + + if (dynamicChecksum != dynamicChecksumCalculated) + throw new ImageNotSupportedException("(VirtualPC plugin): Both header and footer are corrupt, image cannot be opened."); + + thisDynamic = new DynamicDiskHeader(); + thisDynamic.locatorEntries = new ParentLocatorEntry[8]; + thisDynamic.reserved2 = new byte[256]; + + for (int i = 0; i < 8; i++) + thisDynamic.locatorEntries[i] = new ParentLocatorEntry(); + + thisDynamic.cookie = BigEndianBitConverter.ToUInt64(dynamicBytes, 0x00); + thisDynamic.dataOffset = BigEndianBitConverter.ToUInt64(dynamicBytes, 0x08); + thisDynamic.tableOffset = BigEndianBitConverter.ToUInt64(dynamicBytes, 0x10); + thisDynamic.headerVersion = BigEndianBitConverter.ToUInt32(dynamicBytes, 0x18); + thisDynamic.maxTableEntries = BigEndianBitConverter.ToUInt32(dynamicBytes, 0x1C); + thisDynamic.blockSize = BigEndianBitConverter.ToUInt32(dynamicBytes, 0x20); + thisDynamic.checksum = dynamicChecksum; + thisDynamic.parentID = BigEndianBitConverter.ToGuid(dynamicBytes, 0x28); + thisDynamic.parentTimestamp = BigEndianBitConverter.ToUInt32(dynamicBytes, 0x38); + thisDynamic.reserved = BigEndianBitConverter.ToUInt32(dynamicBytes, 0x3C); + thisDynamic.parentName = Encoding.BigEndianUnicode.GetString(dynamicBytes, 0x40, 512); + + for (int i = 0; i < 8; i++) + { + thisDynamic.locatorEntries[i].platformCode = BigEndianBitConverter.ToUInt32(dynamicBytes, 0x240 + 0x00 + 24 * i); + thisDynamic.locatorEntries[i].platformDataSpace = BigEndianBitConverter.ToUInt32(dynamicBytes, 0x240 + 0x04 + 24 * i); + thisDynamic.locatorEntries[i].platformDataLength = BigEndianBitConverter.ToUInt32(dynamicBytes, 0x240 + 0x08 + 24 * i); + thisDynamic.locatorEntries[i].reserved = BigEndianBitConverter.ToUInt32(dynamicBytes, 0x240 + 0x0C + 24 * i); + thisDynamic.locatorEntries[i].platformDataOffset = BigEndianBitConverter.ToUInt64(dynamicBytes, 0x240 + 0x10 + 24 * i); + } + + Array.Copy(dynamicBytes, 0x300, thisDynamic.reserved2, 0, 256); + + parentDateTime = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc); + parentDateTime = parentDateTime.AddSeconds(thisDynamic.parentTimestamp); + + if (MainClass.isDebug) + { + Checksums.SHA1Context sha1Ctx = new Checksums.SHA1Context(); + sha1Ctx.Init(); + sha1Ctx.Update(thisDynamic.reserved2); + + Console.WriteLine("DEBUG (VirtualPC plugin): dynamic.cookie = 0x{0:X8}", thisDynamic.cookie); + Console.WriteLine("DEBUG (VirtualPC plugin): dynamic.dataOffset = {0}", thisDynamic.dataOffset); + Console.WriteLine("DEBUG (VirtualPC plugin): dynamic.tableOffset = {0}", thisDynamic.tableOffset); + Console.WriteLine("DEBUG (VirtualPC plugin): dynamic.headerVersion = 0x{0:X8}", thisDynamic.headerVersion); + Console.WriteLine("DEBUG (VirtualPC plugin): dynamic.maxTableEntries = {0}", thisDynamic.maxTableEntries); + Console.WriteLine("DEBUG (VirtualPC plugin): dynamic.blockSize = {0}", thisDynamic.blockSize); + Console.WriteLine("DEBUG (VirtualPC plugin): dynamic.checksum = 0x{0:X8}", thisDynamic.checksum); + Console.WriteLine("DEBUG (VirtualPC plugin): dynamic.parentID = {0}", thisDynamic.parentID); + Console.WriteLine("DEBUG (VirtualPC plugin): dynamic.parentTimestamp = 0x{0:X8} ({1})", thisDynamic.parentTimestamp, parentDateTime); + Console.WriteLine("DEBUG (VirtualPC plugin): dynamic.reserved = 0x{0:X8}", thisDynamic.reserved); + for (int i = 0; i < 8; i++) + { + Console.WriteLine("DEBUG (VirtualPC plugin): dynamic.locatorEntries[{0}].platformCode = 0x{1:X8} (\"{2}\")", i, thisDynamic.locatorEntries[i].platformCode, + Encoding.ASCII.GetString(BigEndianBitConverter.GetBytes(thisDynamic.locatorEntries[i].platformCode))); + Console.WriteLine("DEBUG (VirtualPC plugin): dynamic.locatorEntries[{0}].platformDataSpace = {1}", i, thisDynamic.locatorEntries[i].platformDataSpace); + Console.WriteLine("DEBUG (VirtualPC plugin): dynamic.locatorEntries[{0}].platformDataLength = {1}", i, thisDynamic.locatorEntries[i].platformDataLength); + Console.WriteLine("DEBUG (VirtualPC plugin): dynamic.locatorEntries[{0}].reserved = 0x{1:X8}", i, thisDynamic.locatorEntries[i].reserved); + Console.WriteLine("DEBUG (VirtualPC plugin): dynamic.locatorEntries[{0}].platformDataOffset = {1}", i, thisDynamic.locatorEntries[i].platformDataOffset); + } + Console.WriteLine("DEBUG (VirtualPC plugin): dynamic.parentName = \"{0}\"", thisDynamic.parentName); + Console.WriteLine("DEBUG (VirtualPC plugin): dynamic.reserved2's SHA1 = 0x{0}", sha1Ctx.End()); + } + + if (thisDynamic.headerVersion != Version1) + throw new ImageNotSupportedException(String.Format("(VirtualPC plugin): Unknown image type {0} found. Please submit a bug with an example image.", thisFooter.diskType)); + + DateTime startTime = DateTime.UtcNow; + + blockAllocationTable = new uint[thisDynamic.maxTableEntries]; + + // Safe and slow code. It takes 76,572 ms to fill a 30720 entries BAT + /* + byte[] bat = new byte[thisDynamic.maxTableEntries * 4]; + imageStream.Seek((long)thisDynamic.tableOffset, SeekOrigin.Begin); + imageStream.Read(bat, 0, (int)(thisDynamic.maxTableEntries * 4)); + for (int i = 0; i < thisDynamic.maxTableEntries; i++) + blockAllocationTable[i] = BigEndianBitConverter.ToUInt32(bat, 4 * i); + + if (MainClass.isDebug) + { + DateTime endTime = DateTime.UtcNow; + Console.WriteLine("DEBUG (VirtualPC plugin): Filling the BAT took {0} seconds", (endTime-startTime).TotalSeconds); + } + */ + + // How many sectors uses the BAT + UInt32 batSectorCount = (uint)Math.Ceiling(((double)thisDynamic.maxTableEntries * 4) / 512); + + byte[] batSectorBytes = new byte[512]; + BATSector batSector = new BATSector(); + + // Unsafe and fast code. It takes 4 ms to fill a 30720 entries BAT + for (int i = 0; i < batSectorCount; i++) + { + imageStream.Seek((long)thisDynamic.tableOffset + i * 512, SeekOrigin.Begin); + imageStream.Read(batSectorBytes, 0, 512); + // This does the big-endian trick but reverses the order of elements also + Array.Reverse(batSectorBytes); + GCHandle handle = GCHandle.Alloc(batSectorBytes, GCHandleType.Pinned); + batSector = (BATSector)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(BATSector)); + handle.Free(); + // This restores the order of elements + Array.Reverse(batSector.blockPointer); + if (blockAllocationTable.Length >= (i * 512) / 4 + 512 / 4) + Array.Copy(batSector.blockPointer, 0, blockAllocationTable, (i * 512) / 4, 512 / 4); + else + Array.Copy(batSector.blockPointer, 0, blockAllocationTable, (i * 512) / 4, blockAllocationTable.Length - (i * 512) / 4); + } + + if (MainClass.isDebug) + { + DateTime endTime = DateTime.UtcNow; + Console.WriteLine("DEBUG (VirtualPC plugin): Filling the BAT took {0} seconds", (endTime - startTime).TotalSeconds); + } + + // Too noisy + /* + if (MainClass.isDebug) + { + for (int i = 0; i < thisDynamic.maxTableEntries; i++) + Console.WriteLine("DEBUG (VirtualPC plugin): blockAllocationTable[{0}] = {1}", i, blockAllocationTable[i]); + }*/ + + // Get the roundest number of sectors needed to store the block bitmap + bitmapSize = (uint)Math.Ceiling((double)( + // How many sectors do a block store + (thisDynamic.blockSize / 512) + // 1 bit per sector on the bitmap + / 8 + // and aligned to 512 byte boundary + / 512)); + if (MainClass.isDebug) + Console.WriteLine("DEBUG (VirtualPC plugin): Bitmap is {0} sectors", bitmapSize); + } + switch (thisFooter.diskType) { case typeFixed: + case typeDynamic: { // Nothing to do here, really. imageStream.Close(); return true; } - case typeDynamic: - { - throw new NotImplementedException("(VirtualPC plugin): Dynamic disk images not yet supported"); - } case typeDifferencing: { + throw new NotImplementedException("(VirtualPC plugin): Differencing disk images not yet supported"); } case typeDeprecated1: @@ -785,9 +920,62 @@ namespace DiscImageChef.ImagePlugins thisStream.Close(); return data; } + // Contrary to Microsoft's specifications that tell us to check the bitmap + // and in case of unused sector just return zeros, as blocks are allocated + // as a whole, this would waste time and miss cache, so we read any sector + // as long as it is in the block. case typeDynamic: { - throw new NotImplementedException("(VirtualPC plugin): Dynamic disk images not yet supported"); + // Block number for BAT searching + UInt32 blockNumber = (uint)Math.Floor((double)(sectorAddress / (thisDynamic.blockSize / 512))); + // Sector number inside of block + UInt32 sectorInBlock = (uint)(sectorAddress % (thisDynamic.blockSize / 512)); + // How many sectors before reaching end of block + UInt32 remainingInBlock = (thisDynamic.blockSize / 512) - sectorInBlock; + + // Data that can be read in this block + byte[] prefix; + // Data that needs to be read from another block + byte[] suffix = null; + + // How many sectors to read from this block + UInt32 sectorsToReadHere; + + // Asked to read more sectors than are remaining in block + if (length > remainingInBlock) + { + suffix = ReadSectors(sectorAddress + remainingInBlock, length - remainingInBlock); + sectorsToReadHere = remainingInBlock; + } + else + sectorsToReadHere = length; + + // Offset of sector in file + UInt32 sectorOffset = blockAllocationTable[blockNumber] + bitmapSize + sectorInBlock; + prefix = new byte[sectorsToReadHere * 512]; + + // 0xFFFFFFFF means unallocated + if (sectorOffset != 0xFFFFFFFF) + { + thisStream = new FileStream(thisPath, FileMode.Open, FileAccess.Read); + thisStream.Seek((long)(sectorOffset * 512), SeekOrigin.Begin); + thisStream.Read(prefix, 0, (int)(512 * sectorsToReadHere)); + thisStream.Close(); + } + // If it is unallocated, just fill with zeroes + else + Array.Clear(prefix, 0, prefix.Length); + + // If we needed to read from another block, join all the data + if (suffix != null) + { + byte[] data = new byte[512 * length]; + Array.Copy(prefix, 0, data, 0, prefix.Length); + Array.Copy(suffix, 0, data, prefix.Length, suffix.Length); + return data; + } + + return prefix; } case typeDifferencing: {