// // Copyright (c) 2008-2011, Kenneth Bell // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. // using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.IO; using DiscUtils.Internal; using DiscUtils.Streams; namespace DiscUtils.Partitions { /// /// Represents a GUID Partition Table. /// public sealed class GuidPartitionTable : PartitionTable { private Stream _diskData; private Geometry _diskGeometry; private byte[] _entryBuffer; private GptHeader _primaryHeader; private GptHeader _secondaryHeader; /// /// Initializes a new instance of the GuidPartitionTable class. /// /// The disk containing the partition table. public GuidPartitionTable(VirtualDisk disk) { Init(disk.Content, disk.Geometry); } /// /// Initializes a new instance of the GuidPartitionTable class. /// /// The stream containing the disk data. /// The geometry of the disk. public GuidPartitionTable(Stream disk, Geometry diskGeometry) { Init(disk, diskGeometry); } /// /// Gets the unique GPT identifier for this disk. /// public override Guid DiskGuid { get { return _primaryHeader.DiskGuid; } } /// /// Gets the first sector of the disk available to hold partitions. /// public long FirstUsableSector { get { return _primaryHeader.FirstUsable; } } /// /// Gets the last sector of the disk available to hold partitions. /// public long LastUsableSector { get { return _primaryHeader.LastUsable; } } /// /// Gets a collection of the partitions for storing Operating System file-systems. /// public override ReadOnlyCollection Partitions { get { return new ReadOnlyCollection(Utilities.Map(GetAllEntries(), e => new GuidPartitionInfo(this, e))); } } /// /// Creates a new partition table on a disk. /// /// The disk to initialize. /// An object to access the newly created partition table. public static GuidPartitionTable Initialize(VirtualDisk disk) { return Initialize(disk.Content, disk.Geometry); } /// /// Creates a new partition table on a disk. /// /// The stream containing the disk data. /// The geometry of the disk. /// An object to access the newly created partition table. public static GuidPartitionTable Initialize(Stream disk, Geometry diskGeometry) { // Create the protective MBR partition record. BiosPartitionTable pt = BiosPartitionTable.Initialize(disk, diskGeometry); pt.CreatePrimaryByCylinder(0, diskGeometry.Cylinders - 1, BiosPartitionTypes.GptProtective, false); // Create the GPT headers, and blank-out the entry areas const int EntryCount = 128; const int EntrySize = 128; int entrySectors = (EntryCount * EntrySize + diskGeometry.BytesPerSector - 1) / diskGeometry.BytesPerSector; byte[] entriesBuffer = new byte[EntryCount * EntrySize]; // Prepare primary header GptHeader header = new GptHeader(diskGeometry.BytesPerSector); header.HeaderLba = 1; header.AlternateHeaderLba = disk.Length / diskGeometry.BytesPerSector - 1; header.FirstUsable = header.HeaderLba + entrySectors + 1; header.LastUsable = header.AlternateHeaderLba - entrySectors - 1; header.DiskGuid = Guid.NewGuid(); header.PartitionEntriesLba = 2; header.PartitionEntryCount = EntryCount; header.PartitionEntrySize = EntrySize; header.EntriesCrc = CalcEntriesCrc(entriesBuffer); // Write the primary header byte[] headerBuffer = new byte[diskGeometry.BytesPerSector]; header.WriteTo(headerBuffer, 0); disk.Position = header.HeaderLba * diskGeometry.BytesPerSector; disk.Write(headerBuffer, 0, headerBuffer.Length); // Calc alternate header header.HeaderLba = header.AlternateHeaderLba; header.AlternateHeaderLba = 1; header.PartitionEntriesLba = header.HeaderLba - entrySectors; // Write the alternate header header.WriteTo(headerBuffer, 0); disk.Position = header.HeaderLba * diskGeometry.BytesPerSector; disk.Write(headerBuffer, 0, headerBuffer.Length); return new GuidPartitionTable(disk, diskGeometry); } /// /// Creates a new partition table on a disk containing a single partition. /// /// The disk to initialize. /// The partition type for the single partition. /// An object to access the newly created partition table. public static GuidPartitionTable Initialize(VirtualDisk disk, WellKnownPartitionType type) { GuidPartitionTable pt = Initialize(disk); pt.Create(type, true); return pt; } /// /// Creates a new partition that encompasses the entire disk. /// /// The partition type. /// Whether the partition is active (bootable). /// The index of the partition. /// The partition table must be empty before this method is called, /// otherwise IOException is thrown. public override int Create(WellKnownPartitionType type, bool active) { List allEntries = new List(GetAllEntries()); EstablishReservedPartition(allEntries); // Fill the rest of the disk with the requested partition long start = FirstAvailableSector(allEntries); long end = FindLastFreeSector(start, allEntries); return Create(start, end, GuidPartitionTypes.Convert(type), 0, "Data Partition"); } /// /// Creates a new primary partition with a target size. /// /// The target size (in bytes). /// The partition type. /// Whether the partition is active (bootable). /// The index of the new partition. public override int Create(long size, WellKnownPartitionType type, bool active) { if (size < _diskGeometry.BytesPerSector) { throw new ArgumentOutOfRangeException(nameof(size), size, "size must be at least one sector"); } long sectorLength = size / _diskGeometry.BytesPerSector; long start = FindGap(size / _diskGeometry.BytesPerSector, 1); return Create(start, start + sectorLength - 1, GuidPartitionTypes.Convert(type), 0, "Data Partition"); } /// /// Creates a new aligned partition that encompasses the entire disk. /// /// The partition type. /// Whether the partition is active (bootable). /// The alignment (in bytes). /// The index of the partition. /// The partition table must be empty before this method is called, /// otherwise IOException is thrown. /// /// Traditionally partitions were aligned to the physical structure of the underlying disk, /// however with modern storage greater efficiency is acheived by aligning partitions on /// large values that are a power of two. /// public override int CreateAligned(WellKnownPartitionType type, bool active, int alignment) { if (alignment % _diskGeometry.BytesPerSector != 0) { throw new ArgumentException("Alignment is not a multiple of the sector size"); } List allEntries = new List(GetAllEntries()); EstablishReservedPartition(allEntries); // Fill the rest of the disk with the requested partition long start = MathUtilities.RoundUp(FirstAvailableSector(allEntries), alignment / _diskGeometry.BytesPerSector); long end = MathUtilities.RoundDown(FindLastFreeSector(start, allEntries) + 1, alignment / _diskGeometry.BytesPerSector); if (end <= start) { throw new IOException("No available space"); } return Create(start, end - 1, GuidPartitionTypes.Convert(type), 0, "Data Partition"); } /// /// Creates a new aligned partition with a target size. /// /// The target size (in bytes). /// The partition type. /// Whether the partition is active (bootable). /// The alignment (in bytes). /// The index of the new partition. /// /// Traditionally partitions were aligned to the physical structure of the underlying disk, /// however with modern storage greater efficiency is achieved by aligning partitions on /// large values that are a power of two. /// public override int CreateAligned(long size, WellKnownPartitionType type, bool active, int alignment) { if (size < _diskGeometry.BytesPerSector) { throw new ArgumentOutOfRangeException(nameof(size), size, "size must be at least one sector"); } if (alignment % _diskGeometry.BytesPerSector != 0) { throw new ArgumentException("Alignment is not a multiple of the sector size"); } if (size % alignment != 0) { throw new ArgumentException("Size is not a multiple of the alignment"); } long sectorLength = size / _diskGeometry.BytesPerSector; long start = FindGap(size / _diskGeometry.BytesPerSector, alignment / _diskGeometry.BytesPerSector); return Create(start, start + sectorLength - 1, GuidPartitionTypes.Convert(type), 0, "Data Partition"); } /// /// Creates a new GUID partition on the disk. /// /// The first sector of the partition. /// The last sector of the partition. /// The partition type. /// The partition attributes. /// The name of the partition. /// The index of the new partition. /// No checking is performed on the parameters, the caller is /// responsible for ensuring that the partition does not overlap other partitions. public int Create(long startSector, long endSector, Guid type, long attributes, string name) { GptEntry newEntry = CreateEntry(startSector, endSector, type, attributes, name); return GetEntryIndex(newEntry.Identity); } /// /// Deletes a partition at a given index. /// /// The index of the partition. public override void Delete(int index) { int offset = GetPartitionOffset(index); Array.Clear(_entryBuffer, offset, _primaryHeader.PartitionEntrySize); Write(); } internal SparseStream Open(GptEntry entry) { long start = entry.FirstUsedLogicalBlock * _diskGeometry.BytesPerSector; long end = (entry.LastUsedLogicalBlock + 1) * _diskGeometry.BytesPerSector; return new SubStream(_diskData, start, end - start); } private static uint CalcEntriesCrc(byte[] buffer) { return Crc32LittleEndian.Compute(Crc32Algorithm.Common, buffer, 0, buffer.Length); } private static int CountEntries(ICollection values, Func pred) { int count = 0; foreach (T val in values) { if (pred(val)) { ++count; } } return count; } private void Init(Stream disk, Geometry diskGeometry) { BiosPartitionTable bpt; try { bpt = new BiosPartitionTable(disk, diskGeometry); } catch (IOException ioe) { throw new IOException("Invalid GPT disk, protective MBR table not present or invalid", ioe); } if (bpt.Count != 1 || bpt[0].BiosType != BiosPartitionTypes.GptProtective) { throw new IOException("Invalid GPT disk, protective MBR table is not valid"); } _diskData = disk; _diskGeometry = diskGeometry; disk.Position = diskGeometry.BytesPerSector; byte[] sector = StreamUtilities.ReadExact(disk, diskGeometry.BytesPerSector); _primaryHeader = new GptHeader(diskGeometry.BytesPerSector); if (!_primaryHeader.ReadFrom(sector, 0) || !ReadEntries(_primaryHeader)) { disk.Position = disk.Length - diskGeometry.BytesPerSector; disk.Read(sector, 0, sector.Length); _secondaryHeader = new GptHeader(diskGeometry.BytesPerSector); if (!_secondaryHeader.ReadFrom(sector, 0) || !ReadEntries(_secondaryHeader)) { throw new IOException("No valid GUID Partition Table found"); } // Generate from the primary table from the secondary one _primaryHeader = new GptHeader(_secondaryHeader); _primaryHeader.HeaderLba = _secondaryHeader.AlternateHeaderLba; _primaryHeader.AlternateHeaderLba = _secondaryHeader.HeaderLba; _primaryHeader.PartitionEntriesLba = 2; // If the disk is writeable, fix up the primary partition table based on the // (valid) secondary table. if (disk.CanWrite) { WritePrimaryHeader(); } } if (_secondaryHeader == null) { _secondaryHeader = new GptHeader(diskGeometry.BytesPerSector); disk.Position = disk.Length - diskGeometry.BytesPerSector; disk.Read(sector, 0, sector.Length); if (!_secondaryHeader.ReadFrom(sector, 0) || !ReadEntries(_secondaryHeader)) { // Generate from the secondary table from the primary one _secondaryHeader = new GptHeader(_primaryHeader); _secondaryHeader.HeaderLba = _secondaryHeader.AlternateHeaderLba; _secondaryHeader.AlternateHeaderLba = _secondaryHeader.HeaderLba; _secondaryHeader.PartitionEntriesLba = _secondaryHeader.HeaderLba - MathUtilities.RoundUp( _secondaryHeader.PartitionEntryCount * _secondaryHeader.PartitionEntrySize, diskGeometry.BytesPerSector); // If the disk is writeable, fix up the secondary partition table based on the // (valid) primary table. if (disk.CanWrite) { WriteSecondaryHeader(); } } } } private void EstablishReservedPartition(List allEntries) { // If no MicrosoftReserved partition, and no Microsoft Data partitions, and the disk // has a 'reasonable' size free, create a Microsoft Reserved partition. if (CountEntries(allEntries, e => e.PartitionType == GuidPartitionTypes.MicrosoftReserved) == 0 && CountEntries(allEntries, e => e.PartitionType == GuidPartitionTypes.WindowsBasicData) == 0 && _diskGeometry.Capacity > 512 * 1024 * 1024) { long reservedStart = FirstAvailableSector(allEntries); long reservedEnd = FindLastFreeSector(reservedStart, allEntries); if ((reservedEnd - reservedStart + 1) * _diskGeometry.BytesPerSector > 512 * 1024 * 1024) { long size = (_diskGeometry.Capacity < 16 * 1024L * 1024 * 1024 ? 32 : 128) * 1024 * 1024; reservedEnd = reservedStart + size / _diskGeometry.BytesPerSector - 1; int reservedOffset = GetFreeEntryOffset(); GptEntry newReservedEntry = new GptEntry(); newReservedEntry.PartitionType = GuidPartitionTypes.MicrosoftReserved; newReservedEntry.Identity = Guid.NewGuid(); newReservedEntry.FirstUsedLogicalBlock = reservedStart; newReservedEntry.LastUsedLogicalBlock = reservedEnd; newReservedEntry.Attributes = 0; newReservedEntry.Name = "Microsoft reserved partition"; newReservedEntry.WriteTo(_entryBuffer, reservedOffset); allEntries.Add(newReservedEntry); } } } private GptEntry CreateEntry(long startSector, long endSector, Guid type, long attributes, string name) { if (endSector < startSector) { throw new ArgumentException("The end sector is before the start sector"); } int offset = GetFreeEntryOffset(); GptEntry newEntry = new GptEntry(); newEntry.PartitionType = type; newEntry.Identity = Guid.NewGuid(); newEntry.FirstUsedLogicalBlock = startSector; newEntry.LastUsedLogicalBlock = endSector; newEntry.Attributes = (ulong)attributes; newEntry.Name = name; newEntry.WriteTo(_entryBuffer, offset); // Commit changes to disk Write(); return newEntry; } private long FindGap(long numSectors, long alignmentSectors) { List list = new List(GetAllEntries()); list.Sort(); long startSector = MathUtilities.RoundUp(_primaryHeader.FirstUsable, alignmentSectors); foreach (GptEntry entry in list) { if ( !Utilities.RangesOverlap(startSector, startSector + numSectors - 1, entry.FirstUsedLogicalBlock, entry.LastUsedLogicalBlock)) { break; } startSector = MathUtilities.RoundUp(entry.LastUsedLogicalBlock + 1, alignmentSectors); } if (_diskGeometry.TotalSectorsLong - startSector < numSectors) { throw new IOException(string.Format(CultureInfo.InvariantCulture, "Unable to find free space of {0} sectors", numSectors)); } return startSector; } private long FirstAvailableSector(List allEntries) { long start = _primaryHeader.FirstUsable; foreach (GptEntry entry in allEntries) { if (entry.LastUsedLogicalBlock >= start) { start = entry.LastUsedLogicalBlock + 1; } } return start; } private long FindLastFreeSector(long start, List allEntries) { long end = _primaryHeader.LastUsable; foreach (GptEntry entry in allEntries) { if (entry.LastUsedLogicalBlock > start && entry.FirstUsedLogicalBlock <= end) { end = entry.FirstUsedLogicalBlock - 1; } } return end; } private void Write() { WritePrimaryHeader(); WriteSecondaryHeader(); } private void WritePrimaryHeader() { byte[] buffer = new byte[_diskGeometry.BytesPerSector]; _primaryHeader.EntriesCrc = CalcEntriesCrc(); _primaryHeader.WriteTo(buffer, 0); _diskData.Position = _diskGeometry.BytesPerSector; _diskData.Write(buffer, 0, buffer.Length); _diskData.Position = 2 * _diskGeometry.BytesPerSector; _diskData.Write(_entryBuffer, 0, _entryBuffer.Length); } private void WriteSecondaryHeader() { byte[] buffer = new byte[_diskGeometry.BytesPerSector]; _secondaryHeader.EntriesCrc = CalcEntriesCrc(); _secondaryHeader.WriteTo(buffer, 0); _diskData.Position = _diskData.Length - _diskGeometry.BytesPerSector; _diskData.Write(buffer, 0, buffer.Length); _diskData.Position = _secondaryHeader.PartitionEntriesLba * _diskGeometry.BytesPerSector; _diskData.Write(_entryBuffer, 0, _entryBuffer.Length); } private bool ReadEntries(GptHeader header) { _diskData.Position = header.PartitionEntriesLba * _diskGeometry.BytesPerSector; _entryBuffer = StreamUtilities.ReadExact(_diskData, (int)(header.PartitionEntrySize * header.PartitionEntryCount)); if (header.EntriesCrc != CalcEntriesCrc()) { return false; } return true; } private uint CalcEntriesCrc() { return Crc32LittleEndian.Compute(Crc32Algorithm.Common, _entryBuffer, 0, _entryBuffer.Length); } private IEnumerable GetAllEntries() { for (int i = 0; i < _primaryHeader.PartitionEntryCount; ++i) { GptEntry entry = new GptEntry(); entry.ReadFrom(_entryBuffer, i * _primaryHeader.PartitionEntrySize); if (entry.PartitionType != Guid.Empty) { yield return entry; } } } private int GetPartitionOffset(int index) { bool found = false; int entriesSoFar = 0; int position = 0; while (!found && position < _primaryHeader.PartitionEntryCount) { GptEntry entry = new GptEntry(); entry.ReadFrom(_entryBuffer, position * _primaryHeader.PartitionEntrySize); if (entry.PartitionType != Guid.Empty) { if (index == entriesSoFar) { found = true; break; } entriesSoFar++; } position++; } if (found) { return position * _primaryHeader.PartitionEntrySize; } throw new IOException(string.Format(CultureInfo.InvariantCulture, "No such partition: {0}", index)); } private int GetEntryIndex(Guid identity) { int index = 0; for (int i = 0; i < _primaryHeader.PartitionEntryCount; ++i) { GptEntry entry = new GptEntry(); entry.ReadFrom(_entryBuffer, i * _primaryHeader.PartitionEntrySize); if (entry.Identity == identity) { return index; } if (entry.PartitionType != Guid.Empty) { index++; } } throw new IOException("No such partition"); } private int GetFreeEntryOffset() { for (int i = 0; i < _primaryHeader.PartitionEntryCount; ++i) { GptEntry entry = new GptEntry(); entry.ReadFrom(_entryBuffer, i * _primaryHeader.PartitionEntrySize); if (entry.PartitionType == Guid.Empty) { return i * _primaryHeader.PartitionEntrySize; } } throw new IOException("No free partition entries available"); } } }