// // 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.Globalization; using System.IO; using DiscUtils.Internal; using DiscUtils.Partitions; using DiscUtils.Streams; namespace DiscUtils { /// /// Base class representing virtual hard disks. /// public abstract class VirtualDisk : #if !NETCORE MarshalByRefObject, #endif IDisposable { private VirtualDiskTransport _transport; /// /// Finalizes an instance of the VirtualDisk class. /// ~VirtualDisk() { Dispose(false); } /// /// Gets the set of disk formats supported as an array of file extensions. /// [Obsolete("Use VirtualDiskManager.SupportedDiskFormats")] public static ICollection SupportedDiskFormats { get { return VirtualDiskManager.SupportedDiskFormats; } } /// /// Gets the set of disk types supported, as an array of identifiers. /// [Obsolete("Use VirtualDiskManager.SupportedDiskTypes")] public static ICollection SupportedDiskTypes { get { return VirtualDiskManager.SupportedDiskTypes; } } /// /// Gets the geometry of the disk. /// public abstract Geometry Geometry { get; } /// /// Gets the geometry of the disk as it is anticipated a hypervisor BIOS will represent it. /// public virtual Geometry BiosGeometry { get { return Geometry.MakeBiosSafe(Geometry, Capacity); } } /// /// Gets the type of disk represented by this object. /// public abstract VirtualDiskClass DiskClass { get; } /// /// Gets the capacity of the disk (in bytes). /// public abstract long Capacity { get; } /// /// Gets the size of the disk's logical blocks (aka sector size), in bytes. /// public virtual int BlockSize { get { return Sizes.Sector; } } /// /// Gets the logical sector size of the disk, in bytes. /// /// This is an alias for the BlockSize property. public int SectorSize { get { return BlockSize; } } /// /// Gets the content of the disk as a stream. /// /// Note the returned stream is not guaranteed to be at any particular position. The actual position /// will depend on the last partition table/file system activity, since all access to the disk contents pass /// through a single stream instance. Set the stream position before accessing the stream. public abstract SparseStream Content { get; } /// /// Gets the layers that make up the disk. /// public abstract IEnumerable Layers { get; } /// /// Gets or sets the Windows disk signature of the disk, which uniquely identifies the disk. /// public virtual int Signature { get { return EndianUtilities.ToInt32LittleEndian(GetMasterBootRecord(), 0x01B8); } set { byte[] mbr = GetMasterBootRecord(); EndianUtilities.WriteBytesLittleEndian(value, mbr, 0x01B8); SetMasterBootRecord(mbr); } } /// /// Gets a value indicating whether the disk appears to have a valid partition table. /// /// There is no reliable way to determine whether a disk has a valid partition /// table. The 'guess' consists of checking for basic indicators and looking for obviously /// invalid data, such as overlapping partitions. public virtual bool IsPartitioned { get { return PartitionTable.IsPartitioned(Content); } } /// /// Gets the object that interprets the partition structure. /// /// It is theoretically possible for a disk to contain two independent partition structures - a /// BIOS/GPT one and an Apple one, for example. This method will return in order of preference, /// a GUID partition table, a BIOS partition table, then in undefined preference one of any other partition /// tables found. See PartitionTable.GetPartitionTables to gain access to all the discovered partition /// tables on a disk. public virtual PartitionTable Partitions { get { IList tables = PartitionTable.GetPartitionTables(this); if (tables == null || tables.Count == 0) { return null; } if (tables.Count == 1) { return tables[0]; } PartitionTable best = null; int bestScore = -1; for (int i = 0; i < tables.Count; ++i) { int newScore = 0; if (tables[i] is GuidPartitionTable) { newScore = 2; } else if (tables[i] is BiosPartitionTable) { newScore = 1; } if (newScore > bestScore) { bestScore = newScore; best = tables[i]; } } return best; } } /// /// Gets the parameters of the disk. /// /// Most of the parameters are also available individually, such as DiskType and Capacity. public virtual VirtualDiskParameters Parameters { get { return new VirtualDiskParameters { DiskType = DiskClass, Capacity = Capacity, Geometry = Geometry, BiosGeometry = BiosGeometry, AdapterType = GenericDiskAdapterType.Ide }; } } /// /// Gets information about the type of disk. /// /// This property provides access to meta-data about the disk format, for example whether the /// BIOS geometry is preserved in the disk file. public abstract VirtualDiskTypeInfo DiskTypeInfo { get; } /// /// Gets the set of supported variants of a type of virtual disk. /// /// A type, as returned by . /// A collection of identifiers, or empty if there is no variant concept for this type of disk. public static ICollection GetSupportedDiskVariants(string type) { return VirtualDiskManager.TypeMap[type].Variants; } /// /// Gets information about disk type. /// /// The disk type, as returned by . /// The variant of the disk type. /// Information about the disk type. public static VirtualDiskTypeInfo GetDiskType(string type, string variant) { return VirtualDiskManager.TypeMap[type].GetDiskTypeInformation(variant); } /// /// Create a new virtual disk, possibly within an existing disk. /// /// The file system to create the disk on. /// The type of disk to create (see ). /// The variant of the type to create (see ). /// The path (or URI) for the disk to create. /// The capacity of the new disk. /// The geometry of the new disk (or null). /// Untyped parameters controlling the creation process (TBD). /// The newly created disk. public static VirtualDisk CreateDisk(DiscFileSystem fileSystem, string type, string variant, string path, long capacity, Geometry geometry, Dictionary parameters) { VirtualDiskFactory factory = VirtualDiskManager.TypeMap[type]; VirtualDiskParameters diskParams = new VirtualDiskParameters { AdapterType = GenericDiskAdapterType.Scsi, Capacity = capacity, Geometry = geometry }; if (parameters != null) { foreach (string key in parameters.Keys) { diskParams.ExtendedParameters[key] = parameters[key]; } } return factory.CreateDisk(new DiscFileLocator(fileSystem, Utilities.GetDirectoryFromPath(path)), variant.ToLowerInvariant(), Utilities.GetFileFromPath(path), diskParams); } /// /// Create a new virtual disk. /// /// The type of disk to create (see ). /// The variant of the type to create (see ). /// The path (or URI) for the disk to create. /// The capacity of the new disk. /// The geometry of the new disk (or null). /// Untyped parameters controlling the creation process (TBD). /// The newly created disk. public static VirtualDisk CreateDisk(string type, string variant, string path, long capacity, Geometry geometry, Dictionary parameters) { return CreateDisk(type, variant, path, capacity, geometry, null, null, parameters); } /// /// Create a new virtual disk. /// /// The type of disk to create (see ). /// The variant of the type to create (see ). /// The path (or URI) for the disk to create. /// The capacity of the new disk. /// The geometry of the new disk (or null). /// The user identity to use when accessing the path (or null). /// The password to use when accessing the path (or null). /// Untyped parameters controlling the creation process (TBD). /// The newly created disk. public static VirtualDisk CreateDisk(string type, string variant, string path, long capacity, Geometry geometry, string user, string password, Dictionary parameters) { VirtualDiskParameters diskParams = new VirtualDiskParameters { AdapterType = GenericDiskAdapterType.Scsi, Capacity = capacity, Geometry = geometry }; if (parameters != null) { foreach (string key in parameters.Keys) { diskParams.ExtendedParameters[key] = parameters[key]; } } return CreateDisk(type, variant, path, diskParams, user, password); } /// /// Create a new virtual disk. /// /// The type of disk to create (see ). /// The variant of the type to create (see ). /// The path (or URI) for the disk to create. /// Parameters controlling the capacity, geometry, etc of the new disk. /// The user identity to use when accessing the path (or null). /// The password to use when accessing the path (or null). /// The newly created disk. public static VirtualDisk CreateDisk(string type, string variant, string path, VirtualDiskParameters diskParameters, string user, string password) { Uri uri = PathToUri(path); VirtualDisk result = null; Type transportType; if (!VirtualDiskManager.DiskTransports.TryGetValue(uri.Scheme.ToUpperInvariant(), out transportType)) { throw new FileNotFoundException(string.Format(CultureInfo.InvariantCulture, "Unable to parse path '{0}'", path), path); } VirtualDiskTransport transport = (VirtualDiskTransport)Activator.CreateInstance(transportType); try { transport.Connect(uri, user, password); if (transport.IsRawDisk) { result = transport.OpenDisk(FileAccess.ReadWrite); } else { VirtualDiskFactory factory = VirtualDiskManager.TypeMap[type]; result = factory.CreateDisk(transport.GetFileLocator(), variant.ToLowerInvariant(), Utilities.GetFileFromPath(path), diskParameters); } if (result != null) { result._transport = transport; transport = null; } return result; } finally { if (transport != null) { transport.Dispose(); } } } /// /// Opens an existing virtual disk. /// /// The path of the virtual disk to open, can be a URI. /// The desired access to the disk. /// The Virtual Disk, or null if an unknown disk format. public static VirtualDisk OpenDisk(string path, FileAccess access) { return OpenDisk(path, null, access, null, null); } /// /// Opens an existing virtual disk. /// /// The path of the virtual disk to open, can be a URI. /// The desired access to the disk. /// The user name to use for authentication (if necessary). /// The password to use for authentication (if necessary). /// The Virtual Disk, or null if an unknown disk format. public static VirtualDisk OpenDisk(string path, FileAccess access, string user, string password) { return OpenDisk(path, null, access, user, password); } /// /// Opens an existing virtual disk. /// /// The path of the virtual disk to open, can be a URI. /// Force the detected disk type (null to detect). /// The desired access to the disk. /// The user name to use for authentication (if necessary). /// The password to use for authentication (if necessary). /// The Virtual Disk, or null if an unknown disk format. /// /// The detected disk type can be forced by specifying a known disk type: /// RAW, VHD, VMDK, etc. /// public static VirtualDisk OpenDisk(string path, string forceType, FileAccess access, string user, string password) { Uri uri = PathToUri(path); VirtualDisk result = null; Type transportType; if (!VirtualDiskManager.DiskTransports.TryGetValue(uri.Scheme.ToUpperInvariant(), out transportType)) { throw new FileNotFoundException(string.Format(CultureInfo.InvariantCulture, "Unable to parse path '{0}'", path), path); } VirtualDiskTransport transport = (VirtualDiskTransport)Activator.CreateInstance(transportType); try { transport.Connect(uri, user, password); if (transport.IsRawDisk) { result = transport.OpenDisk(access); } else { bool foundFactory; VirtualDiskFactory factory; if (!string.IsNullOrEmpty(forceType)) { foundFactory = VirtualDiskManager.TypeMap.TryGetValue(forceType, out factory); } else { string extension = Path.GetExtension(uri.AbsolutePath).ToUpperInvariant(); if (extension.StartsWith(".", StringComparison.Ordinal)) { extension = extension.Substring(1); } foundFactory = VirtualDiskManager.ExtensionMap.TryGetValue(extension, out factory); } if (foundFactory) { result = factory.OpenDisk(transport.GetFileLocator(), transport.GetFileName(), access); } } if (result != null) { result._transport = transport; transport = null; } return result; } finally { if (transport != null) { transport.Dispose(); } } } /// /// Opens an existing virtual disk, possibly from within an existing disk. /// /// The file system to open the disk on. /// The path of the virtual disk to open. /// The desired access to the disk. /// The Virtual Disk, or null if an unknown disk format. public static VirtualDisk OpenDisk(DiscFileSystem fs, string path, FileAccess access) { if (fs == null) { return OpenDisk(path, access); } string extension = Path.GetExtension(path).ToUpperInvariant(); if (extension.StartsWith(".", StringComparison.Ordinal)) { extension = extension.Substring(1); } VirtualDiskFactory factory; if (VirtualDiskManager.ExtensionMap.TryGetValue(extension, out factory)) { return factory.OpenDisk(fs, path, access); } return null; } /// /// Disposes of this instance, freeing underlying resources. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Reads the first sector of the disk, known as the Master Boot Record. /// /// The MBR as a byte array. public virtual byte[] GetMasterBootRecord() { byte[] sector = new byte[Sizes.Sector]; long oldPos = Content.Position; Content.Position = 0; StreamUtilities.ReadExact(Content, sector, 0, Sizes.Sector); Content.Position = oldPos; return sector; } /// /// Overwrites the first sector of the disk, known as the Master Boot Record. /// /// The master boot record, must be 512 bytes in length. public virtual void SetMasterBootRecord(byte[] data) { if (data == null) { throw new ArgumentNullException(nameof(data)); } if (data.Length != Sizes.Sector) { throw new ArgumentException("The Master Boot Record must be exactly 512 bytes in length", nameof(data)); } long oldPos = Content.Position; Content.Position = 0; Content.Write(data, 0, Sizes.Sector); Content.Position = oldPos; } /// /// Create a new differencing disk, possibly within an existing disk. /// /// The file system to create the disk on. /// The path (or URI) for the disk to create. /// The newly created disk. public abstract VirtualDisk CreateDifferencingDisk(DiscFileSystem fileSystem, string path); /// /// Create a new differencing disk. /// /// The path (or URI) for the disk to create. /// The newly created disk. public abstract VirtualDisk CreateDifferencingDisk(string path); internal static VirtualDiskLayer OpenDiskLayer(FileLocator locator, string path, FileAccess access) { string extension = Path.GetExtension(path).ToUpperInvariant(); if (extension.StartsWith(".", StringComparison.Ordinal)) { extension = extension.Substring(1); } VirtualDiskFactory factory; if (VirtualDiskManager.ExtensionMap.TryGetValue(extension, out factory)) { return factory.OpenDiskLayer(locator, path, access); } return null; } /// /// Disposes of underlying resources. /// /// true if running inside Dispose(), indicating /// graceful cleanup of all managed objects should be performed, or false /// if running inside destructor. protected virtual void Dispose(bool disposing) { if (disposing) { if (_transport != null) { _transport.Dispose(); } _transport = null; } } private static Uri PathToUri(string path) { if (string.IsNullOrEmpty(path)) { throw new ArgumentException("Path must not be null or empty", nameof(path)); } if (path.Contains("://")) { return new Uri(path); } if (!Path.IsPathRooted(path)) { path = Path.GetFullPath(path); } // Built-in Uri class does cope well with query params on file Uris, so do some // parsing ourselves... if (path.Length >= 1 && path[0] == '\\') { UriBuilder builder = new UriBuilder("file:" + path.Replace('\\', '/')); return builder.Uri; } if (path.StartsWith("//", StringComparison.OrdinalIgnoreCase)) { UriBuilder builder = new UriBuilder("file:" + path); return builder.Uri; } if (path.Length >= 2 && path[1] == ':') { UriBuilder builder = new UriBuilder("file:///" + path.Replace('\\', '/')); return builder.Uri; } return new Uri(path); } } }