// // 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 BIOS (MBR) Partition Table. /// public sealed class BiosPartitionTable : PartitionTable { private Stream _diskData; private Geometry _diskGeometry; /// /// Initializes a new instance of the BiosPartitionTable class. /// /// The disk containing the partition table. public BiosPartitionTable(VirtualDisk disk) { Init(disk.Content, disk.BiosGeometry); } /// /// Initializes a new instance of the BiosPartitionTable class. /// /// The stream containing the disk data. /// The geometry of the disk. public BiosPartitionTable(Stream disk, Geometry diskGeometry) { Init(disk, diskGeometry); } /// /// Gets a collection of the partitions for storing Operating System file-systems. /// public ReadOnlyCollection BiosUserPartitions { get { List result = new List(); foreach (BiosPartitionRecord r in GetAllRecords()) { if (r.IsValid) { result.Add(new BiosPartitionInfo(this, r)); } } return new ReadOnlyCollection(result); } } /// /// Gets the GUID that uniquely identifies this disk, if supported (else returns null). /// public override Guid DiskGuid { get { return Guid.Empty; } } /// /// Gets a collection of the partitions for storing Operating System file-systems. /// public override ReadOnlyCollection Partitions { get { List result = new List(); foreach (BiosPartitionRecord r in GetAllRecords()) { if (r.IsValid) { result.Add(new BiosPartitionInfo(this, r)); } } return new ReadOnlyCollection(result); } } /// /// Makes a best guess at the geometry of a disk. /// /// String containing the disk image to detect the geometry from. /// The detected geometry. public static Geometry DetectGeometry(Stream disk) { if (disk.Length >= Sizes.Sector) { disk.Position = 0; byte[] bootSector = StreamUtilities.ReadExact(disk, Sizes.Sector); if (bootSector[510] == 0x55 && bootSector[511] == 0xAA) { byte maxHead = 0; byte maxSector = 0; foreach (BiosPartitionRecord record in ReadPrimaryRecords(bootSector)) { maxHead = Math.Max(maxHead, record.EndHead); maxSector = Math.Max(maxSector, record.EndSector); } if (maxHead > 0 && maxSector > 0) { int cylSize = (maxHead + 1) * maxSector * 512; return new Geometry((int)MathUtilities.Ceil(disk.Length, cylSize), maxHead + 1, maxSector); } } } return Geometry.FromCapacity(disk.Length); } /// /// Indicates if a stream contains a valid partition table. /// /// The stream to inspect. /// true if the partition table is valid, else false. public static bool IsValid(Stream disk) { if (disk.Length < Sizes.Sector) { return false; } disk.Position = 0; byte[] bootSector = StreamUtilities.ReadExact(disk, Sizes.Sector); // Check for the 'bootable sector' marker if (bootSector[510] != 0x55 || bootSector[511] != 0xAA) { return false; } List knownPartitions = new List(); foreach (BiosPartitionRecord record in ReadPrimaryRecords(bootSector)) { // If the partition extends beyond the end of the disk, this is probably an invalid partition table if (record.LBALength != 0xFFFFFFFF && (record.LBAStart + (long)record.LBALength) * Sizes.Sector > disk.Length) { return false; } if (record.LBALength > 0) { StreamExtent[] thisPartitionExtents = { new StreamExtent(record.LBAStart, record.LBALength) }; // If the partition intersects another partition, this is probably an invalid partition table foreach (StreamExtent overlap in StreamExtent.Intersect(knownPartitions, thisPartitionExtents)) { return false; } knownPartitions = new List(StreamExtent.Union(knownPartitions, thisPartitionExtents)); } } return true; } /// /// Creates a new partition table on a disk. /// /// The disk to initialize. /// An object to access the newly created partition table. public static BiosPartitionTable Initialize(VirtualDisk disk) { return Initialize(disk.Content, disk.BiosGeometry); } /// /// 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 BiosPartitionTable Initialize(VirtualDisk disk, WellKnownPartitionType type) { BiosPartitionTable table = Initialize(disk.Content, disk.BiosGeometry); table.Create(type, true); return table; } /// /// 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 BiosPartitionTable Initialize(Stream disk, Geometry diskGeometry) { Stream data = disk; byte[] bootSector; if (data.Length >= Sizes.Sector) { data.Position = 0; bootSector = StreamUtilities.ReadExact(data, Sizes.Sector); } else { bootSector = new byte[Sizes.Sector]; } // Wipe all four 16-byte partition table entries Array.Clear(bootSector, 0x01BE, 16 * 4); // Marker bytes bootSector[510] = 0x55; bootSector[511] = 0xAA; data.Position = 0; data.Write(bootSector, 0, bootSector.Length); return new BiosPartitionTable(disk, diskGeometry); } /// /// 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) { Geometry allocationGeometry = new Geometry(_diskData.Length, _diskGeometry.HeadsPerCylinder, _diskGeometry.SectorsPerTrack, _diskGeometry.BytesPerSector); ChsAddress start = new ChsAddress(0, 1, 1); ChsAddress last = allocationGeometry.LastSector; long startLba = allocationGeometry.ToLogicalBlockAddress(start); long lastLba = allocationGeometry.ToLogicalBlockAddress(last); return CreatePrimaryByCylinder(0, allocationGeometry.Cylinders - 1, ConvertType(type, (lastLba - startLba) * Sizes.Sector), active); } /// /// 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) { int cylinderCapacity = _diskGeometry.SectorsPerTrack * _diskGeometry.HeadsPerCylinder * _diskGeometry.BytesPerSector; int numCylinders = (int)(size / cylinderCapacity); int startCylinder = FindCylinderGap(numCylinders); return CreatePrimaryByCylinder(startCylinder, startCylinder + numCylinders - 1, ConvertType(type, size), active); } /// /// 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) { Geometry allocationGeometry = new Geometry(_diskData.Length, _diskGeometry.HeadsPerCylinder, _diskGeometry.SectorsPerTrack, _diskGeometry.BytesPerSector); ChsAddress start = new ChsAddress(0, 1, 1); long startLba = MathUtilities.RoundUp(allocationGeometry.ToLogicalBlockAddress(start), alignment / _diskGeometry.BytesPerSector); long lastLba = MathUtilities.RoundDown(_diskData.Length / _diskGeometry.BytesPerSector, alignment / _diskGeometry.BytesPerSector); return CreatePrimaryBySector(startLba, lastLba - 1, ConvertType(type, (lastLba - startLba) * _diskGeometry.BytesPerSector), active); } /// /// 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 CreatePrimaryBySector(start, start + sectorLength - 1, ConvertType(type, sectorLength * Sizes.Sector), active); } /// /// Deletes a partition at a given index. /// /// The index of the partition. public override void Delete(int index) { WriteRecord(index, new BiosPartitionRecord()); } /// /// Creates a new Primary Partition that occupies whole cylinders, for best compatibility. /// /// The first cylinder to include in the partition (inclusive). /// The last cylinder to include in the partition (inclusive). /// The BIOS (MBR) type of the new partition. /// Whether to mark the partition active (bootable). /// The index of the new partition. /// If the cylinder 0 is given, the first track will not be used, to reserve space /// for the meta-data at the start of the disk. public int CreatePrimaryByCylinder(int first, int last, byte type, bool markActive) { if (first < 0) { throw new ArgumentOutOfRangeException(nameof(first), first, "First cylinder must be Zero or greater"); } if (last <= first) { throw new ArgumentException("Last cylinder must be greater than first"); } long lbaStart = first == 0 ? _diskGeometry.ToLogicalBlockAddress(0, 1, 1) : _diskGeometry.ToLogicalBlockAddress(first, 0, 1); long lbaLast = _diskGeometry.ToLogicalBlockAddress(last, _diskGeometry.HeadsPerCylinder - 1, _diskGeometry.SectorsPerTrack); return CreatePrimaryBySector(lbaStart, lbaLast, type, markActive); } /// /// Creates a new Primary Partition, specified by Logical Block Addresses. /// /// The LBA address of the first sector (inclusive). /// The LBA address of the last sector (inclusive). /// The BIOS (MBR) type of the new partition. /// Whether to mark the partition active (bootable). /// The index of the new partition. public int CreatePrimaryBySector(long first, long last, byte type, bool markActive) { if (first >= last) { throw new ArgumentException("The first sector in a partition must be before the last"); } if ((last + 1) * _diskGeometry.BytesPerSector > _diskData.Length) { throw new ArgumentOutOfRangeException(nameof(last), last, "The last sector extends beyond the end of the disk"); } BiosPartitionRecord[] existing = GetPrimaryRecords(); BiosPartitionRecord newRecord = new BiosPartitionRecord(); ChsAddress startAddr = _diskGeometry.ToChsAddress(first); ChsAddress endAddr = _diskGeometry.ToChsAddress(last); // Because C/H/S addresses can max out at lower values than the LBA values, // the special tuple (1023, 254, 63) is used. if (startAddr.Cylinder > 1023) { startAddr = new ChsAddress(1023, 254, 63); } if (endAddr.Cylinder > 1023) { endAddr = new ChsAddress(1023, 254, 63); } newRecord.StartCylinder = (ushort)startAddr.Cylinder; newRecord.StartHead = (byte)startAddr.Head; newRecord.StartSector = (byte)startAddr.Sector; newRecord.EndCylinder = (ushort)endAddr.Cylinder; newRecord.EndHead = (byte)endAddr.Head; newRecord.EndSector = (byte)endAddr.Sector; newRecord.LBAStart = (uint)first; newRecord.LBALength = (uint)(last - first + 1); newRecord.PartitionType = type; newRecord.Status = (byte)(markActive ? 0x80 : 0x00); // First check for overlap with existing partition... foreach (BiosPartitionRecord r in existing) { if (Utilities.RangesOverlap((uint)first, (uint)last + 1, r.LBAStartAbsolute, r.LBAStartAbsolute + r.LBALength)) { throw new IOException("New partition overlaps with existing partition"); } } // Now look for empty partition for (int i = 0; i < 4; ++i) { if (!existing[i].IsValid) { WriteRecord(i, newRecord); return i; } } throw new IOException("No primary partition slots available"); } /// /// Sets the active partition. /// /// The index of the primary partition to mark bootable, or -1 for none. /// The supplied index is the index within the primary partition, see PrimaryIndex on BiosPartitionInfo. public void SetActivePartition(int index) { List records = new List(GetPrimaryRecords()); for (int i = 0; i < records.Count; ++i) { records[i].Status = i == index ? (byte)0x80 : (byte)0x00; WriteRecord(i, records[i]); } } /// /// Gets all of the disk ranges containing partition table metadata. /// /// Set of stream extents, indicated as byte offset from the start of the disk. public IEnumerable GetMetadataDiskExtents() { List extents = new List(); extents.Add(new StreamExtent(0, Sizes.Sector)); foreach (BiosPartitionRecord primaryRecord in GetPrimaryRecords()) { if (primaryRecord.IsValid) { if (IsExtendedPartition(primaryRecord)) { extents.AddRange( new BiosExtendedPartitionTable(_diskData, primaryRecord.LBAStart).GetMetadataDiskExtents()); } } } return extents; } /// /// Updates the CHS fields in partition records to reflect a new BIOS geometry. /// /// The disk's new BIOS geometry. /// The partitions are not relocated to a cylinder boundary, just the CHS fields are updated on the /// assumption the LBA fields are definitive. public void UpdateBiosGeometry(Geometry geometry) { _diskData.Position = 0; byte[] bootSector = StreamUtilities.ReadExact(_diskData, Sizes.Sector); BiosPartitionRecord[] records = ReadPrimaryRecords(bootSector); for (int i = 0; i < records.Length; ++i) { BiosPartitionRecord record = records[i]; if (record.IsValid) { ChsAddress newStartAddress = geometry.ToChsAddress(record.LBAStartAbsolute); if (newStartAddress.Cylinder > 1023) { newStartAddress = new ChsAddress(1023, geometry.HeadsPerCylinder - 1, geometry.SectorsPerTrack); } ChsAddress newEndAddress = geometry.ToChsAddress(record.LBAStartAbsolute + record.LBALength - 1); if (newEndAddress.Cylinder > 1023) { newEndAddress = new ChsAddress(1023, geometry.HeadsPerCylinder - 1, geometry.SectorsPerTrack); } record.StartCylinder = (ushort)newStartAddress.Cylinder; record.StartHead = (byte)newStartAddress.Head; record.StartSector = (byte)newStartAddress.Sector; record.EndCylinder = (ushort)newEndAddress.Cylinder; record.EndHead = (byte)newEndAddress.Head; record.EndSector = (byte)newEndAddress.Sector; WriteRecord(i, record); } } _diskGeometry = geometry; } internal SparseStream Open(BiosPartitionRecord record) { return new SubStream(_diskData, Ownership.None, record.LBAStartAbsolute * _diskGeometry.BytesPerSector, record.LBALength * _diskGeometry.BytesPerSector); } private static BiosPartitionRecord[] ReadPrimaryRecords(byte[] bootSector) { BiosPartitionRecord[] records = new BiosPartitionRecord[4]; for (int i = 0; i < 4; ++i) { records[i] = new BiosPartitionRecord(bootSector, 0x01BE + i * 0x10, 0, i); } return records; } private static bool IsExtendedPartition(BiosPartitionRecord r) { return r.PartitionType == BiosPartitionTypes.Extended || r.PartitionType == BiosPartitionTypes.ExtendedLba; } private static byte ConvertType(WellKnownPartitionType type, long size) { switch (type) { case WellKnownPartitionType.WindowsFat: if (size < 512 * Sizes.OneMiB) { return BiosPartitionTypes.Fat16; } if (size < 1023 * (long)254 * 63 * 512) { // Max BIOS size return BiosPartitionTypes.Fat32; } return BiosPartitionTypes.Fat32Lba; case WellKnownPartitionType.WindowsNtfs: return BiosPartitionTypes.Ntfs; case WellKnownPartitionType.Linux: return BiosPartitionTypes.LinuxNative; case WellKnownPartitionType.LinuxSwap: return BiosPartitionTypes.LinuxSwap; case WellKnownPartitionType.LinuxLvm: return BiosPartitionTypes.LinuxLvm; default: throw new ArgumentException( string.Format(CultureInfo.InvariantCulture, "Unrecognized partition type: '{0}'", type), nameof(type)); } } private BiosPartitionRecord[] GetAllRecords() { List newList = new List(); foreach (BiosPartitionRecord primaryRecord in GetPrimaryRecords()) { if (primaryRecord.IsValid) { if (IsExtendedPartition(primaryRecord)) { newList.AddRange(GetExtendedRecords(primaryRecord)); } else { newList.Add(primaryRecord); } } } return newList.ToArray(); } private BiosPartitionRecord[] GetPrimaryRecords() { _diskData.Position = 0; byte[] bootSector = StreamUtilities.ReadExact(_diskData, Sizes.Sector); return ReadPrimaryRecords(bootSector); } private BiosPartitionRecord[] GetExtendedRecords(BiosPartitionRecord r) { return new BiosExtendedPartitionTable(_diskData, r.LBAStart).GetPartitions(); } private void WriteRecord(int i, BiosPartitionRecord newRecord) { _diskData.Position = 0; byte[] bootSector = StreamUtilities.ReadExact(_diskData, Sizes.Sector); newRecord.WriteTo(bootSector, 0x01BE + i * 16); _diskData.Position = 0; _diskData.Write(bootSector, 0, bootSector.Length); } private int FindCylinderGap(int numCylinders) { List list = Utilities.Filter, BiosPartitionRecord>(GetPrimaryRecords(), r => r.IsValid); list.Sort(); int startCylinder = 0; foreach (BiosPartitionRecord r in list) { int existingStart = r.StartCylinder; int existingEnd = r.EndCylinder; // LBA can represent bigger disk locations than CHS, so assume the LBA to be definitive in the case where it // appears the CHS address has been truncated. if (r.LBAStart > _diskGeometry.ToLogicalBlockAddress(r.StartCylinder, r.StartHead, r.StartSector)) { existingStart = _diskGeometry.ToChsAddress((int)r.LBAStart).Cylinder; } if (r.LBAStart + r.LBALength > _diskGeometry.ToLogicalBlockAddress(r.EndCylinder, r.EndHead, r.EndSector)) { existingEnd = _diskGeometry.ToChsAddress((int)(r.LBAStart + r.LBALength)).Cylinder; } if ( !Utilities.RangesOverlap(startCylinder, startCylinder + numCylinders - 1, existingStart, existingEnd)) { break; } startCylinder = existingEnd + 1; } return startCylinder; } private long FindGap(long numSectors, long alignmentSectors) { List list = Utilities.Filter, BiosPartitionRecord>(GetPrimaryRecords(), r => r.IsValid); list.Sort(); long startSector = MathUtilities.RoundUp(_diskGeometry.ToLogicalBlockAddress(0, 1, 1), alignmentSectors); int idx = 0; while (idx < list.Count) { BiosPartitionRecord entry = list[idx]; while (idx < list.Count && startSector >= entry.LBAStartAbsolute + entry.LBALength) { idx++; entry = list[idx]; } if (Utilities.RangesOverlap(startSector, startSector + numSectors, entry.LBAStartAbsolute, entry.LBAStartAbsolute + entry.LBALength)) { startSector = MathUtilities.RoundUp(entry.LBAStartAbsolute + entry.LBALength, alignmentSectors); } idx++; } if (_diskGeometry.TotalSectorsLong - startSector < numSectors) { throw new IOException(string.Format(CultureInfo.InvariantCulture, "Unable to find free space of {0} sectors", numSectors)); } return startSector; } private void Init(Stream disk, Geometry diskGeometry) { _diskData = disk; _diskGeometry = diskGeometry; _diskData.Position = 0; byte[] bootSector = StreamUtilities.ReadExact(_diskData, Sizes.Sector); if (bootSector[510] != 0x55 || bootSector[511] != 0xAA) { throw new IOException("Invalid boot sector - no magic number 0xAA55"); } } } }