// // 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 System.Text; using System.Text.RegularExpressions; using DiscUtils.Internal; using DiscUtils.Streams; namespace DiscUtils.Fat { /// /// Class for accessing FAT file systems. /// public sealed class FatFileSystem : DiscFileSystem { /// /// The Epoch for FAT file systems (1st Jan, 1980). /// public static readonly DateTime Epoch = new DateTime(1980, 1, 1); private readonly Dictionary _dirCache; private readonly Ownership _ownsData; private readonly TimeConverter _timeConverter; private byte[] _bootSector; private ushort _bpbBkBootSec; private ushort _bpbBytesPerSec; private ushort _bpbExtFlags; private ushort _bpbFATSz16; private uint _bpbFATSz32; private ushort _bpbFSInfo; private ushort _bpbFSVer; private uint _bpbHiddSec; private ushort _bpbNumHeads; private uint _bpbRootClus; private ushort _bpbRootEntCnt; private ushort _bpbRsvdSecCnt; private ushort _bpbSecPerTrk; private ushort _bpbTotSec16; private uint _bpbTotSec32; private byte _bsBootSig; private uint _bsVolId; private string _bsVolLab; private Stream _data; private Directory _rootDir; /// /// Initializes a new instance of the FatFileSystem class. /// /// The stream containing the file system. /// /// Local time is the effective timezone of the new instance. /// public FatFileSystem(Stream data) : base(new FatFileSystemOptions()) { _dirCache = new Dictionary(); _timeConverter = DefaultTimeConverter; Initialize(data); } /// /// Initializes a new instance of the FatFileSystem class. /// /// The stream containing the file system. /// Indicates if the new instance should take ownership /// of . /// /// Local time is the effective timezone of the new instance. /// public FatFileSystem(Stream data, Ownership ownsData) : base(new FatFileSystemOptions()) { _dirCache = new Dictionary(); _timeConverter = DefaultTimeConverter; Initialize(data); _ownsData = ownsData; } /// /// Initializes a new instance of the FatFileSystem class. /// /// The stream containing the file system. /// A delegate to convert to/from the file system's timezone. public FatFileSystem(Stream data, TimeConverter timeConverter) : base(new FatFileSystemOptions()) { _dirCache = new Dictionary(); _timeConverter = timeConverter; Initialize(data); } /// /// Initializes a new instance of the FatFileSystem class. /// /// The stream containing the file system. /// Indicates if the new instance should take ownership /// of . /// A delegate to convert to/from the file system's timezone. public FatFileSystem(Stream data, Ownership ownsData, TimeConverter timeConverter) : base(new FatFileSystemOptions()) { _dirCache = new Dictionary(); _timeConverter = timeConverter; Initialize(data); _ownsData = ownsData; } /// /// Initializes a new instance of the FatFileSystem class. /// /// The stream containing the file system. /// Indicates if the new instance should take ownership /// of . /// The parameters for the file system. public FatFileSystem(Stream data, Ownership ownsData, FileSystemParameters parameters) : base(new FatFileSystemOptions(parameters)) { _dirCache = new Dictionary(); if (parameters != null && parameters.TimeConverter != null) { _timeConverter = parameters.TimeConverter; } else { _timeConverter = DefaultTimeConverter; } Initialize(data); _ownsData = ownsData; } /// /// Gets the active FAT (zero-based index). /// public byte ActiveFat { get { return (byte)((_bpbExtFlags & 0x08) != 0 ? _bpbExtFlags & 0x7 : 0); } } /// /// Gets the Sector location of the backup boot sector (FAT32 only). /// public int BackupBootSector { get { return _bpbBkBootSec; } } /// /// Gets the BIOS drive number for BIOS Int 13h calls. /// public byte BiosDriveNumber { get; private set; } /// /// Gets the number of bytes per sector (as stored in the file-system meta data). /// public int BytesPerSector { get { return _bpbBytesPerSec; } } /// /// Indicates if this file system is read-only or read-write. /// /// . public override bool CanWrite { get { return _data.CanWrite; } } internal ClusterReader ClusterReader { get; private set; } /// /// Gets a value indicating whether the VolumeId, VolumeLabel and FileSystemType fields are valid. /// public bool ExtendedBootSignaturePresent { get { return _bsBootSig == 0x29; } } internal FileAllocationTable Fat { get; private set; } /// /// Gets the number of FATs present. /// public byte FatCount { get; private set; } /// /// Gets the FAT file system options, which can be modified. /// public FatFileSystemOptions FatOptions { get { return (FatFileSystemOptions)Options; } } /// /// Gets the size of a single FAT, in sectors. /// public long FatSize { get { return _bpbFATSz16 != 0 ? _bpbFATSz16 : _bpbFATSz32; } } /// /// Gets the FAT variant of the file system. /// public FatType FatVariant { get; private set; } /// /// Gets the (informational only) file system type recorded in the meta-data. /// public string FileSystemType { get; private set; } /// /// Gets the friendly name for the file system, including FAT variant. /// public override string FriendlyName { get { switch (FatVariant) { case FatType.Fat12: return "Microsoft FAT12"; case FatType.Fat16: return "Microsoft FAT16"; case FatType.Fat32: return "Microsoft FAT32"; default: return "Unknown FAT"; } } } /// /// Gets the sector location of the FSINFO structure (FAT32 only). /// public int FSInfoSector { get { return _bpbFSInfo; } } /// /// Gets the number of logical heads. /// public int Heads { get { return _bpbNumHeads; } } /// /// Gets the number of hidden sectors, hiding partition tables, etc. /// public long HiddenSectors { get { return _bpbHiddSec; } } /// /// Gets the maximum number of root directory entries (on FAT variants that have a limit). /// public int MaxRootDirectoryEntries { get { return _bpbRootEntCnt; } } /// /// Gets the Media marker byte, which indicates fixed or removable media. /// public byte Media { get; private set; } /// /// Gets a value indicating whether FAT changes are mirrored to all copies of the FAT. /// public bool MirrorFat { get { return (_bpbExtFlags & 0x08) == 0; } } /// /// Gets the OEM name from the file system. /// public string OemName { get; private set; } /// /// Gets the number of reserved sectors at the start of the disk. /// public int ReservedSectorCount { get { return _bpbRsvdSecCnt; } } /// /// Gets the cluster number of the first cluster of the root directory (FAT32 only). /// public long RootDirectoryCluster { get { return _bpbRootClus; } } /// /// Gets the number of contiguous sectors that make up one cluster. /// public byte SectorsPerCluster { get; private set; } /// /// Gets the number of sectors per logical track. /// public int SectorsPerTrack { get { return _bpbSecPerTrk; } } /// /// Gets the total number of sectors on the disk. /// public long TotalSectors { get { return _bpbTotSec16 != 0 ? _bpbTotSec16 : _bpbTotSec32; } } /// /// Gets the file-system version (usually 0). /// public int Version { get { return _bpbFSVer; } } /// /// Gets the volume serial number. /// public int VolumeId { get { return (int)_bsVolId; } } /// /// Gets the volume label. /// public override string VolumeLabel { get { long volId = _rootDir.FindVolumeId(); if (volId < 0) { return _bsVolLab; } return _rootDir.GetEntry(volId).Name.GetRawName(FatOptions.FileNameEncoding); } } /// /// Detects if a stream contains a FAT file system. /// /// The stream to inspect. /// true if the stream appears to be a FAT file system, else false. public static bool Detect(Stream stream) { if (stream.Length < 512) { return false; } stream.Position = 0; byte[] bytes = StreamUtilities.ReadExact(stream, 512); ushort bpbBytesPerSec = EndianUtilities.ToUInt16LittleEndian(bytes, 11); if (bpbBytesPerSec != 512) { return false; } byte bpbNumFATs = bytes[16]; if (bpbNumFATs == 0 || bpbNumFATs > 2) { return false; } ushort bpbTotSec16 = EndianUtilities.ToUInt16LittleEndian(bytes, 19); uint bpbTotSec32 = EndianUtilities.ToUInt32LittleEndian(bytes, 32); if (!((bpbTotSec16 == 0) ^ (bpbTotSec32 == 0))) { return false; } uint totalSectors = bpbTotSec16 + bpbTotSec32; return totalSectors * (long)bpbBytesPerSec <= stream.Length; } /// /// Opens a file for reading and/or writing. /// /// The full path to the file. /// The file mode. /// The desired access. /// The stream to the opened file. public override SparseStream OpenFile(string path, FileMode mode, FileAccess access) { Directory parent; long entryId; try { entryId = GetDirectoryEntry(_rootDir, path, out parent); } catch (ArgumentException) { throw new IOException("Invalid path: " + path); } if (parent == null) { throw new FileNotFoundException("Could not locate file", path); } if (entryId < 0) { return parent.OpenFile(FileName.FromPath(path, FatOptions.FileNameEncoding), mode, access); } DirectoryEntry dirEntry = parent.GetEntry(entryId); if ((dirEntry.Attributes & FatAttributes.Directory) != 0) { throw new IOException("Attempt to open directory as a file"); } return parent.OpenFile(dirEntry.Name, mode, access); } /// /// Gets the attributes of a file or directory. /// /// The file or directory to inspect. /// The attributes of the file or directory. public override FileAttributes GetAttributes(string path) { // Simulate a root directory entry - doesn't really exist though if (IsRootPath(path)) { return FileAttributes.Directory; } DirectoryEntry dirEntry = GetDirectoryEntry(path); if (dirEntry == null) { throw new FileNotFoundException("No such file", path); } // Luckily, FAT and .NET FileAttributes match, bit-for-bit return (FileAttributes)dirEntry.Attributes; } /// /// Sets the attributes of a file or directory. /// /// The file or directory to change. /// The new attributes of the file or directory. public override void SetAttributes(string path, FileAttributes newValue) { if (IsRootPath(path)) { if (newValue != FileAttributes.Directory) { throw new NotSupportedException("The attributes of the root directory cannot be modified"); } return; } Directory parent; long id = GetDirectoryEntry(path, out parent); DirectoryEntry dirEntry = parent.GetEntry(id); FatAttributes newFatAttr = (FatAttributes)newValue; if ((newFatAttr & FatAttributes.Directory) != (dirEntry.Attributes & FatAttributes.Directory)) { throw new ArgumentException("Attempted to change the directory attribute"); } dirEntry.Attributes = newFatAttr; parent.UpdateEntry(id, dirEntry); // For directories, need to update their 'self' entry also if ((dirEntry.Attributes & FatAttributes.Directory) != 0) { Directory dir = GetDirectory(path); dirEntry = dir.SelfEntry; dirEntry.Attributes = newFatAttr; dir.SelfEntry = dirEntry; } } /// /// Gets the creation time (in local time) of a file or directory. /// /// The path of the file or directory. /// The creation time. public override DateTime GetCreationTime(string path) { if (IsRootPath(path)) { return Epoch; } return GetDirectoryEntry(path).CreationTime; } /// /// Sets the creation time (in local time) of a file or directory. /// /// The path of the file or directory. /// The new time to set. public override void SetCreationTime(string path, DateTime newTime) { if (IsRootPath(path)) { if (newTime != Epoch) { throw new NotSupportedException("The creation time of the root directory cannot be modified"); } return; } UpdateDirEntry(path, e => { e.CreationTime = newTime; }); } /// /// Gets the creation time (in UTC) of a file or directory. /// /// The path of the file or directory. /// The creation time. public override DateTime GetCreationTimeUtc(string path) { if (IsRootPath(path)) { return ConvertToUtc(Epoch); } return ConvertToUtc(GetDirectoryEntry(path).CreationTime); } /// /// Sets the creation time (in UTC) of a file or directory. /// /// The path of the file or directory. /// The new time to set. public override void SetCreationTimeUtc(string path, DateTime newTime) { if (IsRootPath(path)) { if (ConvertFromUtc(newTime) != Epoch) { throw new NotSupportedException("The last write time of the root directory cannot be modified"); } return; } UpdateDirEntry(path, e => { e.CreationTime = ConvertFromUtc(newTime); }); } /// /// Gets the last access time (in local time) of a file or directory. /// /// The path of the file or directory. /// The time the file or directory was last accessed. public override DateTime GetLastAccessTime(string path) { if (IsRootPath(path)) { return Epoch; } return GetDirectoryEntry(path).LastAccessTime; } /// /// Sets the last access time (in local time) of a file or directory. /// /// The path of the file or directory. /// The new time to set. public override void SetLastAccessTime(string path, DateTime newTime) { if (IsRootPath(path)) { if (newTime != Epoch) { throw new NotSupportedException("The last access time of the root directory cannot be modified"); } return; } UpdateDirEntry(path, e => { e.LastAccessTime = newTime; }); } /// /// Gets the last access time (in UTC) of a file or directory. /// /// The path of the file or directory. /// The time the file or directory was last accessed. public override DateTime GetLastAccessTimeUtc(string path) { if (IsRootPath(path)) { return ConvertToUtc(Epoch); } return ConvertToUtc(GetDirectoryEntry(path).LastAccessTime); } /// /// Sets the last access time (in UTC) of a file or directory. /// /// The path of the file or directory. /// The new time to set. public override void SetLastAccessTimeUtc(string path, DateTime newTime) { if (IsRootPath(path)) { if (ConvertFromUtc(newTime) != Epoch) { throw new NotSupportedException("The last write time of the root directory cannot be modified"); } return; } UpdateDirEntry(path, e => { e.LastAccessTime = ConvertFromUtc(newTime); }); } /// /// Gets the last modification time (in local time) of a file or directory. /// /// The path of the file or directory. /// The time the file or directory was last modified. public override DateTime GetLastWriteTime(string path) { if (IsRootPath(path)) { return Epoch; } return GetDirectoryEntry(path).LastWriteTime; } /// /// Sets the last modification time (in local time) of a file or directory. /// /// The path of the file or directory. /// The new time to set. public override void SetLastWriteTime(string path, DateTime newTime) { if (IsRootPath(path)) { if (newTime != Epoch) { throw new NotSupportedException("The last write time of the root directory cannot be modified"); } return; } UpdateDirEntry(path, e => { e.LastWriteTime = newTime; }); } /// /// Gets the last modification time (in UTC) of a file or directory. /// /// The path of the file or directory. /// The time the file or directory was last modified. public override DateTime GetLastWriteTimeUtc(string path) { if (IsRootPath(path)) { return ConvertToUtc(Epoch); } return ConvertToUtc(GetDirectoryEntry(path).LastWriteTime); } /// /// Sets the last modification time (in UTC) of a file or directory. /// /// The path of the file or directory. /// The new time to set. public override void SetLastWriteTimeUtc(string path, DateTime newTime) { if (IsRootPath(path)) { if (ConvertFromUtc(newTime) != Epoch) { throw new NotSupportedException("The last write time of the root directory cannot be modified"); } return; } UpdateDirEntry(path, e => { e.LastWriteTime = ConvertFromUtc(newTime); }); } /// /// Gets the length of a file. /// /// The path to the file. /// The length in bytes. public override long GetFileLength(string path) { return GetDirectoryEntry(path).FileSize; } /// /// Copies an existing file to a new file, allowing overwriting of an existing file. /// /// The source file. /// The destination file. /// Whether to permit over-writing of an existing file. public override void CopyFile(string sourceFile, string destinationFile, bool overwrite) { Directory sourceDir; long sourceEntryId = GetDirectoryEntry(sourceFile, out sourceDir); if (sourceDir == null || sourceEntryId < 0) { throw new FileNotFoundException(string.Format(CultureInfo.InvariantCulture, "The source file '{0}' was not found", sourceFile)); } DirectoryEntry sourceEntry = sourceDir.GetEntry(sourceEntryId); if ((sourceEntry.Attributes & FatAttributes.Directory) != 0) { throw new IOException("The source file is a directory"); } DirectoryEntry newEntry = new DirectoryEntry(sourceEntry); newEntry.Name = FileName.FromPath(destinationFile, FatOptions.FileNameEncoding); newEntry.FirstCluster = 0; Directory destDir; long destEntryId = GetDirectoryEntry(destinationFile, out destDir); if (destDir == null) { throw new DirectoryNotFoundException(string.Format(CultureInfo.InvariantCulture, "The destination directory for '{0}' was not found", destinationFile)); } // If the destination is a directory, use the old file name to construct a full path. if (destEntryId >= 0) { DirectoryEntry destEntry = destDir.GetEntry(destEntryId); if ((destEntry.Attributes & FatAttributes.Directory) != 0) { newEntry.Name = FileName.FromPath(sourceFile, FatOptions.FileNameEncoding); destinationFile = Utilities.CombinePaths(destinationFile, Utilities.GetFileFromPath(sourceFile)); destEntryId = GetDirectoryEntry(destinationFile, out destDir); } } // If there's an existing entry... if (destEntryId >= 0) { DirectoryEntry destEntry = destDir.GetEntry(destEntryId); if ((destEntry.Attributes & FatAttributes.Directory) != 0) { throw new IOException("Destination file is an existing directory"); } if (!overwrite) { throw new IOException("Destination file already exists"); } // Remove the old file destDir.DeleteEntry(destEntryId, true); } // Add the new file's entry destEntryId = destDir.AddEntry(newEntry); // Copy the contents... using (Stream sourceStream = new FatFileStream(this, sourceDir, sourceEntryId, FileAccess.Read), destStream = new FatFileStream(this, destDir, destEntryId, FileAccess.Write)) { StreamUtilities.PumpStreams(sourceStream, destStream); } } /// /// Creates a directory. /// /// The directory to create. public override void CreateDirectory(string path) { string[] pathElements = path.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); Directory focusDir = _rootDir; for (int i = 0; i < pathElements.Length; ++i) { FileName name; try { name = new FileName(pathElements[i], FatOptions.FileNameEncoding, false); } catch (ArgumentException ae) { throw new IOException("Invalid path", ae); } Directory child = focusDir.GetChildDirectory(name); if (child == null) { child = focusDir.CreateChildDirectory(name); } focusDir = child; } } /// /// Deletes a directory, optionally with all descendants. /// /// The path of the directory to delete. public override void DeleteDirectory(string path) { Directory dir = GetDirectory(path); if (dir == null) { throw new DirectoryNotFoundException(string.Format(CultureInfo.InvariantCulture, "No such directory: {0}", path)); } if (!dir.IsEmpty) { throw new IOException("Unable to delete non-empty directory"); } Directory parent; long id = GetDirectoryEntry(path, out parent); if (parent == null && id == 0) { throw new IOException("Unable to delete root directory"); } if (parent != null && id >= 0) { DirectoryEntry deadEntry = parent.GetEntry(id); parent.DeleteEntry(id, true); ForgetDirectory(deadEntry); } else { throw new DirectoryNotFoundException("No such directory: " + path); } } /// /// Deletes a file. /// /// The path of the file to delete. public override void DeleteFile(string path) { Directory parent; long id = GetDirectoryEntry(path, out parent); if (parent == null || id < 0) { throw new FileNotFoundException("No such file", path); } DirectoryEntry entry = parent.GetEntry(id); if (entry == null || (entry.Attributes & FatAttributes.Directory) != 0) { throw new FileNotFoundException("No such file", path); } parent.DeleteEntry(id, true); } /// /// Indicates if a directory exists. /// /// The path to test. /// true if the directory exists. public override bool DirectoryExists(string path) { // Special case - root directory if (string.IsNullOrEmpty(path)) { return true; } DirectoryEntry dirEntry = GetDirectoryEntry(path); return dirEntry != null && (dirEntry.Attributes & FatAttributes.Directory) != 0; } /// /// Indicates if a file exists. /// /// The path to test. /// true if the file exists. public override bool FileExists(string path) { // Special case - root directory if (string.IsNullOrEmpty(path)) { return true; } DirectoryEntry dirEntry = GetDirectoryEntry(path); return dirEntry != null && (dirEntry.Attributes & FatAttributes.Directory) == 0; } /// /// Indicates if a file or directory exists. /// /// The path to test. /// true if the file or directory exists. public override bool Exists(string path) { // Special case - root directory if (string.IsNullOrEmpty(path)) { return true; } return GetDirectoryEntry(path) != null; } /// /// Gets the names of subdirectories in a specified directory. /// /// The path to search. /// Array of directories. public override string[] GetDirectories(string path) { Directory dir = GetDirectory(path); if (dir == null) { throw new DirectoryNotFoundException(string.Format(CultureInfo.InvariantCulture, "The directory '{0}' was not found", path)); } DirectoryEntry[] entries = dir.GetDirectories(); List dirs = new List(entries.Length); foreach (DirectoryEntry dirEntry in entries) { dirs.Add(Utilities.CombinePaths(path, dirEntry.Name.GetDisplayName(FatOptions.FileNameEncoding))); } return dirs.ToArray(); } /// /// Gets the names of subdirectories in a specified directory matching a specified /// search pattern, using a value to determine whether to search subdirectories. /// /// The path to search. /// The search string to match against. /// Indicates whether to search subdirectories. /// Array of directories matching the search pattern. public override string[] GetDirectories(string path, string searchPattern, SearchOption searchOption) { Regex re = Utilities.ConvertWildcardsToRegEx(searchPattern); List dirs = new List(); DoSearch(dirs, path, re, searchOption == SearchOption.AllDirectories, true, false); return dirs.ToArray(); } /// /// Gets the names of files in a specified directory. /// /// The path to search. /// Array of files. public override string[] GetFiles(string path) { Directory dir = GetDirectory(path); DirectoryEntry[] entries = dir.GetFiles(); List files = new List(entries.Length); foreach (DirectoryEntry dirEntry in entries) { files.Add(Utilities.CombinePaths(path, dirEntry.Name.GetDisplayName(FatOptions.FileNameEncoding))); } return files.ToArray(); } /// /// Gets the names of files in a specified directory matching a specified /// search pattern, using a value to determine whether to search subdirectories. /// /// The path to search. /// The search string to match against. /// Indicates whether to search subdirectories. /// Array of files matching the search pattern. public override string[] GetFiles(string path, string searchPattern, SearchOption searchOption) { Regex re = Utilities.ConvertWildcardsToRegEx(searchPattern); List results = new List(); DoSearch(results, path, re, searchOption == SearchOption.AllDirectories, false, true); return results.ToArray(); } /// /// Gets the names of all files and subdirectories in a specified directory. /// /// The path to search. /// Array of files and subdirectories matching the search pattern. public override string[] GetFileSystemEntries(string path) { Directory dir = GetDirectory(path); DirectoryEntry[] entries = dir.Entries; List result = new List(entries.Length); foreach (DirectoryEntry dirEntry in entries) { result.Add(Utilities.CombinePaths(path, dirEntry.Name.GetDisplayName(FatOptions.FileNameEncoding))); } return result.ToArray(); } /// /// Gets the names of files and subdirectories in a specified directory matching a specified /// search pattern. /// /// The path to search. /// The search string to match against. /// Array of files and subdirectories matching the search pattern. public override string[] GetFileSystemEntries(string path, string searchPattern) { Regex re = Utilities.ConvertWildcardsToRegEx(searchPattern); Directory dir = GetDirectory(path); DirectoryEntry[] entries = dir.Entries; List result = new List(entries.Length); foreach (DirectoryEntry dirEntry in entries) { if (re.IsMatch(dirEntry.Name.GetSearchName(FatOptions.FileNameEncoding))) { result.Add(Utilities.CombinePaths(path, dirEntry.Name.GetDisplayName(FatOptions.FileNameEncoding))); } } return result.ToArray(); } /// /// Moves a directory. /// /// The directory to move. /// The target directory name. public override void MoveDirectory(string sourceDirectoryName, string destinationDirectoryName) { if (string.IsNullOrEmpty(destinationDirectoryName)) { if (destinationDirectoryName == null) { throw new ArgumentNullException(nameof(destinationDirectoryName)); } throw new ArgumentException("Invalid destination name (empty string)"); } Directory destParent; long destId = GetDirectoryEntry(destinationDirectoryName, out destParent); if (destParent == null) { throw new DirectoryNotFoundException("Target directory doesn't exist"); } if (destId >= 0) { throw new IOException("Target directory already exists"); } Directory sourceParent; long sourceId = GetDirectoryEntry(sourceDirectoryName, out sourceParent); if (sourceParent == null || sourceId < 0) { throw new IOException("Source directory doesn't exist"); } destParent.AttachChildDirectory(FileName.FromPath(destinationDirectoryName, FatOptions.FileNameEncoding), GetDirectory(sourceDirectoryName)); sourceParent.DeleteEntry(sourceId, false); } /// /// Moves a file, allowing an existing file to be overwritten. /// /// The file to move. /// The target file name. /// Whether to permit a destination file to be overwritten. public override void MoveFile(string sourceName, string destinationName, bool overwrite) { Directory sourceDir; long sourceEntryId = GetDirectoryEntry(sourceName, out sourceDir); if (sourceDir == null || sourceEntryId < 0) { throw new FileNotFoundException(string.Format(CultureInfo.InvariantCulture, "The source file '{0}' was not found", sourceName)); } DirectoryEntry sourceEntry = sourceDir.GetEntry(sourceEntryId); if ((sourceEntry.Attributes & FatAttributes.Directory) != 0) { throw new IOException("The source file is a directory"); } DirectoryEntry newEntry = new DirectoryEntry(sourceEntry); newEntry.Name = FileName.FromPath(destinationName, FatOptions.FileNameEncoding); Directory destDir; long destEntryId = GetDirectoryEntry(destinationName, out destDir); if (destDir == null) { throw new DirectoryNotFoundException(string.Format(CultureInfo.InvariantCulture, "The destination directory for '{0}' was not found", destinationName)); } // If the destination is a directory, use the old file name to construct a full path. if (destEntryId >= 0) { DirectoryEntry destEntry = destDir.GetEntry(destEntryId); if ((destEntry.Attributes & FatAttributes.Directory) != 0) { newEntry.Name = FileName.FromPath(sourceName, FatOptions.FileNameEncoding); destinationName = Utilities.CombinePaths(destinationName, Utilities.GetFileFromPath(sourceName)); destEntryId = GetDirectoryEntry(destinationName, out destDir); } } // If there's an existing entry... if (destEntryId >= 0) { DirectoryEntry destEntry = destDir.GetEntry(destEntryId); if ((destEntry.Attributes & FatAttributes.Directory) != 0) { throw new IOException("Destination file is an existing directory"); } if (!overwrite) { throw new IOException("Destination file already exists"); } // Remove the old file destDir.DeleteEntry(destEntryId, true); } // Add the new file's entry and remove the old link to the file's contents destDir.AddEntry(newEntry); sourceDir.DeleteEntry(sourceEntryId, false); } internal DateTime ConvertToUtc(DateTime dateTime) { return _timeConverter(dateTime, true); } internal DateTime ConvertFromUtc(DateTime dateTime) { return _timeConverter(dateTime, false); } internal Directory GetDirectory(string path) { Directory parent; if (string.IsNullOrEmpty(path) || path == "\\") { return _rootDir; } long id = GetDirectoryEntry(_rootDir, path, out parent); if (id >= 0) { return GetDirectory(parent, id); } return null; } internal Directory GetDirectory(Directory parent, long parentId) { if (parent == null) { return _rootDir; } DirectoryEntry dirEntry = parent.GetEntry(parentId); if ((dirEntry.Attributes & FatAttributes.Directory) == 0) { throw new DirectoryNotFoundException(); } // If we have this one cached, return it Directory result; if (_dirCache.TryGetValue(dirEntry.FirstCluster, out result)) { return result; } // Not cached - create a new one. result = new Directory(parent, parentId); _dirCache[dirEntry.FirstCluster] = result; return result; } internal void ForgetDirectory(DirectoryEntry entry) { uint index = entry.FirstCluster; if (index != 0 && _dirCache.ContainsKey(index)) { Directory dir = _dirCache[index]; _dirCache.Remove(index); dir.Dispose(); } } internal DirectoryEntry GetDirectoryEntry(string path) { Directory parent; long id = GetDirectoryEntry(_rootDir, path, out parent); if (parent == null || id < 0) { return null; } return parent.GetEntry(id); } internal long GetDirectoryEntry(string path, out Directory parent) { return GetDirectoryEntry(_rootDir, path, out parent); } /// /// Disposes of this instance. /// /// The value true if Disposing. protected override void Dispose(bool disposing) { try { if (disposing) { foreach (Directory dir in _dirCache.Values) { dir.Dispose(); } _rootDir.Dispose(); if (_ownsData == Ownership.Dispose) { _data.Dispose(); _data = null; } } } finally { base.Dispose(disposing); } } /// /// Writes a FAT12/FAT16 BPB. /// /// The buffer to fill. /// The total capacity of the disk (in sectors). /// The number of bits in each FAT entry. /// The maximum number of root directory entries. /// The number of hidden sectors before this file system (i.e. partition offset). /// The number of reserved sectors before the FAT. /// The number of sectors per cluster. /// The geometry of the disk containing the Fat file system. /// Indicates if the disk is a removable media (a floppy disk). /// The disk's volume Id. /// The disk's label (or null). private static void WriteBPB( byte[] bootSector, uint sectors, FatType fatType, ushort maxRootEntries, uint hiddenSectors, ushort reservedSectors, byte sectorsPerCluster, Geometry diskGeometry, bool isFloppy, uint volId, string label) { uint fatSectors = CalcFatSize(sectors, fatType, sectorsPerCluster); bootSector[0] = 0xEB; bootSector[1] = 0x3C; bootSector[2] = 0x90; // OEM Name EndianUtilities.StringToBytes("DISCUTIL", bootSector, 3, 8); // Bytes Per Sector (512) bootSector[11] = 0; bootSector[12] = 2; // Sectors Per Cluster bootSector[13] = sectorsPerCluster; // Reserved Sector Count EndianUtilities.WriteBytesLittleEndian(reservedSectors, bootSector, 14); // Number of FATs bootSector[16] = 2; // Number of Entries in the root directory EndianUtilities.WriteBytesLittleEndian(maxRootEntries, bootSector, 17); // Total number of sectors (small) EndianUtilities.WriteBytesLittleEndian((ushort)(sectors < 0x10000 ? sectors : 0), bootSector, 19); // Media bootSector[21] = (byte)(isFloppy ? 0xF0 : 0xF8); // FAT size (FAT12/FAT16) EndianUtilities.WriteBytesLittleEndian((ushort)(fatType < FatType.Fat32 ? fatSectors : 0), bootSector, 22); // Sectors Per Track EndianUtilities.WriteBytesLittleEndian((ushort)diskGeometry.SectorsPerTrack, bootSector, 24); // Heads Per Cylinder EndianUtilities.WriteBytesLittleEndian((ushort)diskGeometry.HeadsPerCylinder, bootSector, 26); // Hidden Sectors EndianUtilities.WriteBytesLittleEndian(hiddenSectors, bootSector, 28); // Total number of sectors (large) EndianUtilities.WriteBytesLittleEndian(sectors >= 0x10000 ? sectors : 0, bootSector, 32); if (fatType < FatType.Fat32) { WriteBS(bootSector, 36, isFloppy, volId, label, fatType); } else { // FAT size (FAT32) EndianUtilities.WriteBytesLittleEndian(fatSectors, bootSector, 36); // Ext flags: 0x80 = FAT 1 (i.e. Zero) active, mirroring bootSector[40] = 0x00; bootSector[41] = 0x00; // Filesystem version (0.0) bootSector[42] = 0; bootSector[43] = 0; // First cluster of the root directory, always 2 since we don't do bad sectors... EndianUtilities.WriteBytesLittleEndian((uint)2, bootSector, 44); // Sector number of FSINFO EndianUtilities.WriteBytesLittleEndian((uint)1, bootSector, 48); // Sector number of the Backup Boot Sector EndianUtilities.WriteBytesLittleEndian((uint)6, bootSector, 50); // Reserved area - must be set to 0 Array.Clear(bootSector, 52, 12); WriteBS(bootSector, 64, isFloppy, volId, label, fatType); } bootSector[510] = 0x55; bootSector[511] = 0xAA; } private static uint CalcFatSize(uint sectors, FatType fatType, byte sectorsPerCluster) { uint numClusters = sectors / sectorsPerCluster; uint fatBytes = numClusters * (ushort)fatType / 8; return (fatBytes + Sizes.Sector - 1) / Sizes.Sector; } private static void WriteBS(byte[] bootSector, int offset, bool isFloppy, uint volId, string label, FatType fatType) { if (string.IsNullOrEmpty(label)) { label = "NO NAME "; } string fsType = "FAT32 "; if (fatType == FatType.Fat12) { fsType = "FAT12 "; } else if (fatType == FatType.Fat16) { fsType = "FAT16 "; } // Drive Number (for BIOS) bootSector[offset + 0] = (byte)(isFloppy ? 0x00 : 0x80); // Reserved bootSector[offset + 1] = 0; // Boot Signature (indicates next 3 fields present) bootSector[offset + 2] = 0x29; // Volume Id EndianUtilities.WriteBytesLittleEndian(volId, bootSector, offset + 3); // Volume Label EndianUtilities.StringToBytes(label + " ", bootSector, offset + 7, 11); // File System Type EndianUtilities.StringToBytes(fsType, bootSector, offset + 18, 8); } private static FatType DetectFATType(byte[] bpb) { uint bpbBytesPerSec = EndianUtilities.ToUInt16LittleEndian(bpb, 11); uint bpbRootEntCnt = EndianUtilities.ToUInt16LittleEndian(bpb, 17); uint bpbFATSz16 = EndianUtilities.ToUInt16LittleEndian(bpb, 22); uint bpbFATSz32 = EndianUtilities.ToUInt32LittleEndian(bpb, 36); uint bpbTotSec16 = EndianUtilities.ToUInt16LittleEndian(bpb, 19); uint bpbTotSec32 = EndianUtilities.ToUInt32LittleEndian(bpb, 32); uint bpbResvdSecCnt = EndianUtilities.ToUInt16LittleEndian(bpb, 14); uint bpbNumFATs = bpb[16]; uint bpbSecPerClus = bpb[13]; uint rootDirSectors = (bpbRootEntCnt * 32 + bpbBytesPerSec - 1) / bpbBytesPerSec; uint fatSz = bpbFATSz16 != 0 ? bpbFATSz16 : bpbFATSz32; uint totalSec = bpbTotSec16 != 0 ? bpbTotSec16 : bpbTotSec32; uint dataSec = totalSec - (bpbResvdSecCnt + bpbNumFATs * fatSz + rootDirSectors); uint countOfClusters = dataSec / bpbSecPerClus; if (countOfClusters < 4085) { return FatType.Fat12; } if (countOfClusters < 65525) { return FatType.Fat16; } return FatType.Fat32; } private static bool IsRootPath(string path) { return string.IsNullOrEmpty(path) || path == @"\"; } private static DateTime DefaultTimeConverter(DateTime time, bool toUtc) { return toUtc ? time.ToUniversalTime() : time.ToLocalTime(); } private void Initialize(Stream data) { _data = data; _data.Position = 0; _bootSector = StreamUtilities.ReadSector(_data); FatVariant = DetectFATType(_bootSector); ReadBPB(); LoadFAT(); LoadClusterReader(); LoadRootDirectory(); } private void LoadClusterReader() { int rootDirSectors = (_bpbRootEntCnt * 32 + (_bpbBytesPerSec - 1)) / _bpbBytesPerSec; int firstDataSector = (int)(_bpbRsvdSecCnt + FatCount * FatSize + rootDirSectors); ClusterReader = new ClusterReader(_data, firstDataSector, SectorsPerCluster, _bpbBytesPerSec); } private void LoadRootDirectory() { Stream fatStream; if (FatVariant != FatType.Fat32) { fatStream = new SubStream(_data, (_bpbRsvdSecCnt + FatCount * _bpbFATSz16) * _bpbBytesPerSec, _bpbRootEntCnt * 32); } else { fatStream = new ClusterStream(this, FileAccess.ReadWrite, _bpbRootClus, uint.MaxValue); } _rootDir = new Directory(this, fatStream); } private void LoadFAT() { Fat = new FileAllocationTable(FatVariant, _data, _bpbRsvdSecCnt, (uint)FatSize, FatCount, ActiveFat); } private void ReadBPB() { OemName = Encoding.ASCII.GetString(_bootSector, 3, 8).TrimEnd('\0'); _bpbBytesPerSec = EndianUtilities.ToUInt16LittleEndian(_bootSector, 11); SectorsPerCluster = _bootSector[13]; _bpbRsvdSecCnt = EndianUtilities.ToUInt16LittleEndian(_bootSector, 14); FatCount = _bootSector[16]; _bpbRootEntCnt = EndianUtilities.ToUInt16LittleEndian(_bootSector, 17); _bpbTotSec16 = EndianUtilities.ToUInt16LittleEndian(_bootSector, 19); Media = _bootSector[21]; _bpbFATSz16 = EndianUtilities.ToUInt16LittleEndian(_bootSector, 22); _bpbSecPerTrk = EndianUtilities.ToUInt16LittleEndian(_bootSector, 24); _bpbNumHeads = EndianUtilities.ToUInt16LittleEndian(_bootSector, 26); _bpbHiddSec = EndianUtilities.ToUInt32LittleEndian(_bootSector, 28); _bpbTotSec32 = EndianUtilities.ToUInt32LittleEndian(_bootSector, 32); if (FatVariant != FatType.Fat32) { ReadBS(36); } else { _bpbFATSz32 = EndianUtilities.ToUInt32LittleEndian(_bootSector, 36); _bpbExtFlags = EndianUtilities.ToUInt16LittleEndian(_bootSector, 40); _bpbFSVer = EndianUtilities.ToUInt16LittleEndian(_bootSector, 42); _bpbRootClus = EndianUtilities.ToUInt32LittleEndian(_bootSector, 44); _bpbFSInfo = EndianUtilities.ToUInt16LittleEndian(_bootSector, 48); _bpbBkBootSec = EndianUtilities.ToUInt16LittleEndian(_bootSector, 50); ReadBS(64); } } private void ReadBS(int offset) { BiosDriveNumber = _bootSector[offset]; _bsBootSig = _bootSector[offset + 2]; _bsVolId = EndianUtilities.ToUInt32LittleEndian(_bootSector, offset + 3); _bsVolLab = Encoding.ASCII.GetString(_bootSector, offset + 7, 11); FileSystemType = Encoding.ASCII.GetString(_bootSector, offset + 18, 8); } private long GetDirectoryEntry(Directory dir, string path, out Directory parent) { string[] pathElements = path.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); return GetDirectoryEntry(dir, pathElements, 0, out parent); } private long GetDirectoryEntry(Directory dir, string[] pathEntries, int pathOffset, out Directory parent) { long entryId; if (pathEntries.Length == 0) { // Looking for root directory, simulate the directory entry in its parent... parent = null; return 0; } entryId = dir.FindEntry(new FileName(pathEntries[pathOffset], FatOptions.FileNameEncoding, true)); if (entryId >= 0) { if (pathOffset == pathEntries.Length - 1) { parent = dir; return entryId; } return GetDirectoryEntry(GetDirectory(dir, entryId), pathEntries, pathOffset + 1, out parent); } if (pathOffset == pathEntries.Length - 1) { parent = dir; return -1; } parent = null; return -1; } private void DoSearch(List results, string path, Regex regex, bool subFolders, bool dirs, bool files) { Directory dir = GetDirectory(path); if (dir == null) { throw new DirectoryNotFoundException(string.Format(CultureInfo.InvariantCulture, "The directory '{0}' was not found", path)); } DirectoryEntry[] entries = dir.Entries; foreach (DirectoryEntry de in entries) { bool isDir = (de.Attributes & FatAttributes.Directory) != 0; if ((isDir && dirs) || (!isDir && files)) { if (regex.IsMatch(de.Name.GetSearchName(FatOptions.FileNameEncoding))) { results.Add(Utilities.CombinePaths(path, de.Name.GetDisplayName(FatOptions.FileNameEncoding))); } } if (subFolders && isDir) { DoSearch(results, Utilities.CombinePaths(path, de.Name.GetDisplayName(FatOptions.FileNameEncoding)), regex, subFolders, dirs, files); } } } private void UpdateDirEntry(string path, EntryUpdateAction action) { Directory parent; long id = GetDirectoryEntry(path, out parent); DirectoryEntry entry = parent.GetEntry(id); action(entry); parent.UpdateEntry(id, entry); if ((entry.Attributes & FatAttributes.Directory) != 0) { Directory dir = GetDirectory(path); DirectoryEntry selfEntry = dir.SelfEntry; action(selfEntry); dir.SelfEntry = selfEntry; } } /// /// Size of the Filesystem in bytes /// public override long Size { get { return ((TotalSectors - ReservedSectorCount - (FatSize * FatCount))*BytesPerSector); } } /// /// Used space of the Filesystem in bytes /// public override long UsedSpace { get { uint usedCluster = 0; for (uint i = 2; i < Fat.NumEntries; i++) { var fatValue = Fat.GetNext(i); if (!Fat.IsFree(fatValue)) { usedCluster++; } } return (usedCluster *SectorsPerCluster*BytesPerSector); } } /// /// Available space of the Filesystem in bytes /// public override long AvailableSpace { get { return Size - UsedSpace; } } private delegate void EntryUpdateAction(DirectoryEntry entry); #region Disk Formatting /// /// Creates a formatted floppy disk image in a stream. /// /// The stream to write the blank image to. /// The type of floppy to create. /// The volume label for the floppy (or null). /// An object that provides access to the newly created floppy disk image. public static FatFileSystem FormatFloppy(Stream stream, FloppyDiskType type, string label) { long pos = stream.Position; long ticks = DateTime.UtcNow.Ticks; uint volId = (uint)((ticks & 0xFFFF) | (ticks >> 32)); // Write the BIOS Parameter Block (BPB) - a single sector byte[] bpb = new byte[512]; uint sectors; if (type == FloppyDiskType.DoubleDensity) { sectors = 1440; WriteBPB(bpb, sectors, FatType.Fat12, 224, 0, 1, 1, new Geometry(80, 2, 9), true, volId, label); } else if (type == FloppyDiskType.HighDensity) { sectors = 2880; WriteBPB(bpb, sectors, FatType.Fat12, 224, 0, 1, 1, new Geometry(80, 2, 18), true, volId, label); } else if (type == FloppyDiskType.Extended) { sectors = 5760; WriteBPB(bpb, sectors, FatType.Fat12, 224, 0, 1, 1, new Geometry(80, 2, 36), true, volId, label); } else { throw new ArgumentException("Unrecognised Floppy Disk type", nameof(type)); } stream.Write(bpb, 0, bpb.Length); // Write both FAT copies uint fatSize = CalcFatSize(sectors, FatType.Fat12, 1); byte[] fat = new byte[fatSize * Sizes.Sector]; FatBuffer fatBuffer = new FatBuffer(FatType.Fat12, fat); fatBuffer.SetNext(0, 0xFFFFFFF0); fatBuffer.SetEndOfChain(1); stream.Write(fat, 0, fat.Length); stream.Write(fat, 0, fat.Length); // Write the (empty) root directory uint rootDirSectors = (224 * 32 + Sizes.Sector - 1) / Sizes.Sector; byte[] rootDir = new byte[rootDirSectors * Sizes.Sector]; stream.Write(rootDir, 0, rootDir.Length); // Write a single byte at the end of the disk to ensure the stream is at least as big // as needed for this disk image. stream.Position = pos + sectors * Sizes.Sector - 1; stream.WriteByte(0); // Give the caller access to the new file system stream.Position = pos; return new FatFileSystem(stream); } /// /// Formats a virtual hard disk partition. /// /// The disk containing the partition. /// The index of the partition on the disk. /// The volume label for the partition (or null). /// An object that provides access to the newly created partition file system. public static FatFileSystem FormatPartition(VirtualDisk disk, int partitionIndex, string label) { using (Stream partitionStream = disk.Partitions[partitionIndex].Open()) { return FormatPartition( partitionStream, label, disk.Geometry, (int)disk.Partitions[partitionIndex].FirstSector, (int)(1 + disk.Partitions[partitionIndex].LastSector - disk.Partitions[partitionIndex].FirstSector), 0); } } /// /// Creates a formatted hard disk partition in a stream. /// /// The stream to write the new file system to. /// The volume label for the partition (or null). /// The geometry of the disk containing the partition. /// The starting sector number of this partition (hide's sectors in other partitions). /// The number of sectors in this partition. /// The number of reserved sectors at the start of the partition. /// An object that provides access to the newly created partition file system. public static FatFileSystem FormatPartition( Stream stream, string label, Geometry diskGeometry, int firstSector, int sectorCount, short reservedSectors) { long pos = stream.Position; long ticks = DateTime.UtcNow.Ticks; uint volId = (uint)((ticks & 0xFFFF) | (ticks >> 32)); byte sectorsPerCluster; FatType fatType; ushort maxRootEntries; /* * Write the BIOS Parameter Block (BPB) - a single sector */ byte[] bpb = new byte[512]; if (sectorCount <= 8400) { throw new ArgumentException("Requested size is too small for a partition"); } if (sectorCount < 1024 * 1024) { fatType = FatType.Fat16; maxRootEntries = 512; if (sectorCount <= 32680) { sectorsPerCluster = 2; } else if (sectorCount <= 262144) { sectorsPerCluster = 4; } else if (sectorCount <= 524288) { sectorsPerCluster = 8; } else { sectorsPerCluster = 16; } if (reservedSectors < 1) { reservedSectors = 1; } } else { fatType = FatType.Fat32; maxRootEntries = 0; if (sectorCount <= 532480) { sectorsPerCluster = 1; } else if (sectorCount <= 16777216) { sectorsPerCluster = 8; } else if (sectorCount <= 33554432) { sectorsPerCluster = 16; } else if (sectorCount <= 67108864) { sectorsPerCluster = 32; } else { sectorsPerCluster = 64; } if (reservedSectors < 32) { reservedSectors = 32; } } WriteBPB(bpb, (uint)sectorCount, fatType, maxRootEntries, (uint)firstSector, (ushort)reservedSectors, sectorsPerCluster, diskGeometry, false, volId, label); stream.Write(bpb, 0, bpb.Length); /* * Skip the reserved sectors */ stream.Position = pos + (ushort)reservedSectors * Sizes.Sector; /* * Write both FAT copies */ byte[] fat = new byte[CalcFatSize((uint)sectorCount, fatType, sectorsPerCluster) * Sizes.Sector]; FatBuffer fatBuffer = new FatBuffer(fatType, fat); fatBuffer.SetNext(0, 0xFFFFFFF8); fatBuffer.SetEndOfChain(1); if (fatType >= FatType.Fat32) { // Mark cluster 2 as End-of-chain (i.e. root directory // is a single cluster in length) fatBuffer.SetEndOfChain(2); } stream.Write(fat, 0, fat.Length); stream.Write(fat, 0, fat.Length); /* * Write the (empty) root directory */ uint rootDirSectors; if (fatType < FatType.Fat32) { rootDirSectors = (uint)((maxRootEntries * 32 + Sizes.Sector - 1) / Sizes.Sector); } else { rootDirSectors = sectorsPerCluster; } byte[] rootDir = new byte[rootDirSectors * Sizes.Sector]; stream.Write(rootDir, 0, rootDir.Length); /* * Make sure the stream is at least as large as the partition requires. */ if (stream.Length < pos + sectorCount * Sizes.Sector) { stream.SetLength(pos + sectorCount * Sizes.Sector); } /* * Give the caller access to the new file system */ stream.Position = pos; return new FatFileSystem(stream); } #endregion } }