public static bool TryGetMbrInfo(DiskImage hdd, out MbrPartitioningInfo info)
        {
            info = default;

            Debug.Assert(Unsafe.SizeOf <MbrHeader>() == MBR_SIZE);

            var       headerBytes = hdd.ReadBytes(0, MBR_SIZE);
            MbrHeader header      = MemoryMarshal.Cast <byte, MbrHeader>(new Span <byte>(headerBytes))[0];

            if (header.BootSig != MbrHeader.MbrMagic)
            {
                return(false);
            }

            bool hasGpt = false;

            for (int i = 0; i < 4; i++)
            {
                var part = header.GetPartition(i);
                hasGpt |= part.Type == MbrPartitionType.GptProtective;
            }

            //MBR paritioned disks often contain boot loaders in non-paritioned space.
            //So to be safe copy all the data.
            List <FileExtent> partitions = new List <FileExtent>()
            {
                new FileExtent(MBR_SIZE, hdd.Length - MBR_SIZE),
            };

            info = new MbrPartitioningInfo(headerBytes, partitions, hasGpt);
            return(true);
        }
        private static GptHeader LoadHeader(DiskImage hdd, long offset)
        {
            Debug.Assert(offset % SectorSize == 0);
            var headerBytes = hdd.ReadBytes(offset, Unsafe.SizeOf <GptHeader>());

            ref GptHeader ret = ref MemoryMarshal.Cast <byte, GptHeader>(new Span <byte>(headerBytes))[0];
        public static bool TryGetGptInfo(DiskImage hdd, out GptPartitioningInfo info)
        {
            Debug.Assert(Unsafe.SizeOf <GptHeader>() == CurrentHeaderSize);
            Debug.Assert(Unsafe.SizeOf <PartitionEntry>() == PartitionEntrySize);

            info = null;

            MbrPartitioningInfo mbr;

            if (!MbrPartitioningInfo.TryGetMbrInfo(hdd, out mbr))
            {
                return(false);
            }
            if (!mbr.HasGpt)
            {
                return(false);
            }

            var primaryHeader = LoadHeader(hdd, SectorSize);
            var backupHeader  = LoadHeader(hdd, hdd.Length - SectorSize);

            //Make sure the headers agree on each other's locations
            if (primaryHeader.BackupLba != (hdd.Length / SectorSize) - 1)
            {
                throw new Exception("Location of backup LBA in primary header is wrong.");
            }
            if (backupHeader.BackupLba != 1)
            {
                throw new Exception("Location of primary LBA in backup header is wrong.");
            }

            //make sure the headers match
            if (primaryHeader.NumberOfPartitions != backupHeader.NumberOfPartitions)
            {
                throw new Exception("Primary and backup GPT headers do not agree on the number of partitions.");
            }
            if (primaryHeader.FirstUsableLba != backupHeader.FirstUsableLba || primaryHeader.LastUsableLba != backupHeader.LastUsableLba)
            {
                throw new Exception("Primary and backup GPT headers do not agree on the usable area of the disk.");
            }
            if (primaryHeader.DiskGuid != backupHeader.DiskGuid)
            {
                throw new Exception("Primary and backup GPT headers do not agree on the disk GUID.");
            }

            //TODO: Make sure the partition entries do not intersect with the usable area of the disk or the GptHeader.

            //at this point we have varified that the header for both the primary and backup
            //GPT are correct. We are only going to load paritions from the primary header.

            //Make sure the minimum space for partition entries was reserved
            if (primaryHeader.FirstUsableLba < (Program.RoundUp(MinimumSizeForParitionEntriesArea, SectorSize) / SectorSize + 2)) //extra two sectors for protective MBR and GPT header
            {
                throw new Exception("Not enough space for primary parition entries was reserved.");
            }
            if (primaryHeader.LastUsableLba >= Program.RoundDown(hdd.Length - MinimumSizeForParitionEntriesArea - SectorSize, SectorSize) / SectorSize)
            {
                throw new Exception("Not enough space for backup parition entries was reserved.");
            }


            byte[] partitionEntriesBytes = new byte[primaryHeader.NumberOfPartitions * PartitionEntrySize];
            hdd.ReadBytes(new Span <byte>(partitionEntriesBytes), primaryHeader.StartingLbaOfPartitionEntries * SectorSize);
            uint actualEntriesCrc = Crc32Algorithm.Compute(partitionEntriesBytes);

            if (primaryHeader.CrcOfPartitionEntry != actualEntriesCrc)
            {
                throw new Exception("CRC of partition entries not correct.");
            }

            var partitions = new List <PartitionEntry>();

            foreach (PartitionEntry part in MemoryMarshal.Cast <byte, PartitionEntry>(new ReadOnlySpan <byte>(partitionEntriesBytes)))
            {
                if (part.Type == Guid.Empty)
                {
                    continue;
                }
                if (part.FirstLba < primaryHeader.FirstUsableLba || part.LastLba > primaryHeader.LastUsableLba)
                {
                    throw new Exception($"Parition '{part.Name}' is outside the usable space.");
                }
                partitions.Add(part);
            }

            partitions.Sort((a, b) => a.FirstLba.CompareTo(b.FirstLba));

            //TODO: make sure the paritions do not intersect with each other

            info = new GptPartitioningInfo(mbr, partitions);
            return(true);
        }
        /// <summary>
        /// Copies the contents of the disk image to the disk.
        /// </summary>
        /// <param name="disk"></param>
        /// <param name="image"></param>
        /// <param name="progress">Recievies the percent complete progress.</param>
        /// <returns></returns>
        public static async Task WriteImageToDisk(Disk disk, DiskImage image, IProgress <int> progress)
        {
            if (image.Length > disk.Capacity)
            {
                throw new InvalidOperationException("Disk image is larger than the size of the selected disk.");
            }
            if (disk.Capacity % Program.SECTOR_SIZE != 0)
            {
                throw new InvalidOperationException("Disk capacity is not a multiple of sector size.");
            }

            IPartitioningInfo partInfo;
            bool imageLengthOk = image.Length % Program.SECTOR_SIZE == 0;
            bool hasPartitions = getParitionInfo(image, out partInfo);

            if (!imageLengthOk)
            {
                if (hasPartitions)
                {
                    throw new InvalidOperationException($"Found partition info, but image file length is not a multiple of sector size ({SECTOR_SIZE} bytes). Maybe the file got partially truncated?");
                }
                else
                {
                    throw new PartitionInformationMissingException();
                }
            }
            else if (!hasPartitions)
            {
                throw new PartitionInformationMissingException();
            }

            var copyPlan = CreateCopyPlan(image, partInfo);

            await disk.Clean().ConfigureAwait(false);

            int access = Interop.Kernel32.GenericOperations.GENERIC_WRITE;

            using (var hDisk = Interop.Kernel32.CreateFile(disk.DiskPath, access, FileShare.ReadWrite, FileMode.Open, 0))
            {
                if (hDisk.IsInvalid)
                {
                    throw new Exception("Failed to open disk handle.");
                }

                using (var fs = new FileStream(hDisk, FileAccess.Write))
                {
                    var buffer = new byte[MAX_CHUNK];

                    for (int i = 0; i < copyPlan.Count; i++)
                    {
                        var chunk = copyPlan[i];
                        image.ReadBytes(new Span <byte>(buffer, 0, (int)chunk.Length), chunk.Offset);
                        fs.Seek(chunk.Offset, SeekOrigin.Begin);
                        await fs.WriteAsync(buffer, 0, (int)chunk.Length);

                        progress.Report(i * 100 / copyPlan.Count);
                    }

                    if (partInfo != null)
                    {
                        await partInfo.WriteParitionTableAsync(fs, disk.Capacity);
                    }

                    await fs.FlushAsync();

                    fs.Flush(flushToDisk: true);
                }

                progress.Report(100);
            }

            disk.Eject();
        }