diff --git a/CommandLine.cs b/CommandLine.cs index b45b274..ee4cabf 100644 --- a/CommandLine.cs +++ b/CommandLine.cs @@ -909,7 +909,7 @@ namespace WPinternals // Save supported mobilestartup.efi LogFile.Log("Taking mobilestartup.efi from donor-FFU"); - Stream MobileStartupStream = UnlockedEFIESPFileSystem.OpenFile(@"Windows\System32\Boot\mobilestartup.efi", FileMode.Create, FileAccess.Write); + Stream MobileStartupStream = UnlockedEFIESPFileSystem.OpenFile(@"\Windows\System32\Boot\mobilestartup.efi", FileMode.Create, FileAccess.Write); MobileStartupStream.Write(SupportedMobileStartup, 0, SupportedMobileStartup.Length); MobileStartupStream.Close(); } @@ -923,7 +923,7 @@ namespace WPinternals // Edit BCD LogFile.Log("Edit BCD"); - using Stream BCDFileStream = UnlockedEFIESPFileSystem.OpenFile(@"efi\Microsoft\Boot\BCD", FileMode.Open, FileAccess.ReadWrite); + using Stream BCDFileStream = UnlockedEFIESPFileSystem.OpenFile(@"\efi\Microsoft\Boot\BCD", FileMode.Open, FileAccess.ReadWrite); using DiscUtils.Registry.RegistryHive BCDHive = new(BCDFileStream); DiscUtils.BootConfig.Store BCDStore = new(BCDHive.Root); DiscUtils.BootConfig.BcdObject MobileStartupObject = BCDStore.GetObject(new Guid("{01de5a27-8705-40db-bad6-96fa5187d4a6}")); @@ -997,7 +997,7 @@ namespace WPinternals // Save supported mobilestartup.efi LogFile.Log("Taking mobilestartup.efi from donor-FFU"); - Stream MobileStartupStream = UnlockedEFIESPFileSystem.OpenFile(@"Windows\System32\Boot\mobilestartup.efi", FileMode.Create, FileAccess.Write); + Stream MobileStartupStream = UnlockedEFIESPFileSystem.OpenFile(@"\Windows\System32\Boot\mobilestartup.efi", FileMode.Create, FileAccess.Write); MobileStartupStream.Write(SupportedMobileStartup, 0, SupportedMobileStartup.Length); MobileStartupStream.Close(); } @@ -1011,7 +1011,7 @@ namespace WPinternals // Edit BCD LogFile.Log("Edit BCD"); - using Stream BCDFileStream = UnlockedEFIESPFileSystem.OpenFile(@"efi\Microsoft\Boot\BCD", FileMode.Open, FileAccess.ReadWrite); + using Stream BCDFileStream = UnlockedEFIESPFileSystem.OpenFile(@"\efi\Microsoft\Boot\BCD", FileMode.Open, FileAccess.ReadWrite); using DiscUtils.Registry.RegistryHive BCDHive = new(BCDFileStream); DiscUtils.BootConfig.Store BCDStore = new(BCDHive.Root); DiscUtils.BootConfig.BcdObject MobileStartupObject = BCDStore.GetObject(new Guid("{01de5a27-8705-40db-bad6-96fa5187d4a6}")); diff --git a/DiscUtils/DiscUtils.Core/CoreCompat/EncodingHelper.cs b/DiscUtils/DiscUtils.Core/CoreCompat/EncodingHelper.cs new file mode 100644 index 0000000..4060ca2 --- /dev/null +++ b/DiscUtils/DiscUtils.Core/CoreCompat/EncodingHelper.cs @@ -0,0 +1,23 @@ +#if !NET40 && !NET45 +using System.Text; +#endif + +namespace DiscUtils.CoreCompat +{ + internal static class EncodingHelper + { + private static bool _registered; + + public static void RegisterEncodings() + { + if (_registered) + return; + + _registered = true; + +#if !NET40 && !NET45 + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); +#endif + } + } +} \ No newline at end of file diff --git a/DiscUtils/DiscUtils.Core/Internal/Utilities.cs b/DiscUtils/DiscUtils.Core/Internal/Utilities.cs new file mode 100644 index 0000000..dc042bc --- /dev/null +++ b/DiscUtils/DiscUtils.Core/Internal/Utilities.cs @@ -0,0 +1,467 @@ +// +// 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.IO; +using System.Text; +using System.Text.RegularExpressions; + +namespace DiscUtils.Internal +{ + internal static class Utilities + { + /// + /// Converts between two arrays. + /// + /// The type of the elements of the source array. + /// The type of the elements of the destination array. + /// The source array. + /// The function to map from source type to destination type. + /// The resultant array. + public static U[] Map(ICollection source, Func func) + { + U[] result = new U[source.Count]; + int i = 0; + + foreach (T sVal in source) + { + result[i++] = func(sVal); + } + + return result; + } + + /// + /// Converts between two arrays. + /// + /// The type of the elements of the source array. + /// The type of the elements of the destination array. + /// The source array. + /// The function to map from source type to destination type. + /// The resultant array. + public static U[] Map(IEnumerable source, Func func) + { + List result = new List(); + + foreach (T sVal in source) + { + result.Add(func(sVal)); + } + + return result.ToArray(); + } + + /// + /// Filters a collection into a new collection. + /// + /// The type of the new collection. + /// The type of the collection entries. + /// The collection to filter. + /// The predicate to select which entries are carried over. + /// The new collection, containing all entries where the predicate returns true. + public static C Filter(ICollection source, Func predicate) where C : ICollection, new() + { + C result = new C(); + foreach (T val in source) + { + if (predicate(val)) + { + result.Add(val); + } + } + + return result; + } + + /// + /// Indicates if two ranges overlap. + /// + /// The type of the ordinals. + /// The lowest ordinal of the first range (inclusive). + /// The highest ordinal of the first range (exclusive). + /// The lowest ordinal of the second range (inclusive). + /// The highest ordinal of the second range (exclusive). + /// true if the ranges overlap, else false. + public static bool RangesOverlap(T xFirst, T xLast, T yFirst, T yLast) where T : IComparable + { + return !((xLast.CompareTo(yFirst) <= 0) || (xFirst.CompareTo(yLast) >= 0)); + } + + #region Bit Twiddling + + public static bool IsAllZeros(byte[] buffer, int offset, int count) + { + int end = offset + count; + for (int i = offset; i < end; ++i) + { + if (buffer[i] != 0) + { + return false; + } + } + + return true; + } + + public static bool IsPowerOfTwo(uint val) + { + if (val == 0) + { + return false; + } + + while ((val & 1) != 1) + { + val >>= 1; + } + + return val == 1; + } + + public static bool IsPowerOfTwo(long val) + { + if (val == 0) + { + return false; + } + + while ((val & 1) != 1) + { + val >>= 1; + } + + return val == 1; + } + + public static bool AreEqual(byte[] a, byte[] b) + { + if (a.Length != b.Length) + { + return false; + } + + for (int i = 0; i < a.Length; ++i) + { + if (a[i] != b[i]) + { + return false; + } + } + + return true; + } + + public static ushort BitSwap(ushort value) + { + return (ushort)(((value & 0x00FF) << 8) | ((value & 0xFF00) >> 8)); + } + + public static uint BitSwap(uint value) + { + return ((value & 0xFF) << 24) | ((value & 0xFF00) << 8) | ((value & 0x00FF0000) >> 8) | + ((value & 0xFF000000) >> 24); + } + + public static ulong BitSwap(ulong value) + { + return ((ulong)BitSwap((uint)(value & 0xFFFFFFFF)) << 32) | BitSwap((uint)(value >> 32)); + } + + public static short BitSwap(short value) + { + return (short)BitSwap((ushort)value); + } + + public static int BitSwap(int value) + { + return (int)BitSwap((uint)value); + } + + public static long BitSwap(long value) + { + return (long)BitSwap((ulong)value); + } + + #endregion + + #region Path Manipulation + + /// + /// Extracts the directory part of a path. + /// + /// The path to process. + /// The directory part. + public static string GetDirectoryFromPath(string path) + { + string trimmed = path.TrimEnd('\\'); + + int index = trimmed.LastIndexOf('\\'); + if (index < 0) + { + return string.Empty; // No directory, just a file name + } + + return trimmed.Substring(0, index); + } + + /// + /// Extracts the file part of a path. + /// + /// The path to process. + /// The file part of the path. + public static string GetFileFromPath(string path) + { + string trimmed = path.Trim('\\'); + + int index = trimmed.LastIndexOf('\\'); + if (index < 0) + { + return trimmed; // No directory, just a file name + } + + return trimmed.Substring(index + 1); + } + + /// + /// Combines two paths. + /// + /// The first part of the path. + /// The second part of the path. + /// The combined path. + public static string CombinePaths(string a, string b) + { + if (string.IsNullOrEmpty(a) || (b.Length > 0 && b[0] == '\\')) + { + return b; + } + if (string.IsNullOrEmpty(b)) + { + return a; + } + return a.TrimEnd('\\') + '\\' + b.TrimStart('\\'); + } + + /// + /// Resolves a relative path into an absolute one. + /// + /// The base path to resolve from. + /// The relative path. + /// The absolute path. If no is specified + /// then relativePath is returned as-is. If + /// contains more '..' characters than the base path contains levels of + /// directory, the resultant string be the root drive followed by the file name. + /// If no the basePath starts with '\' (no drive specified) then the returned + /// path will also start with '\'. + /// For example: (\TEMP\Foo.txt, ..\..\Bar.txt) gives (\Bar.txt). + /// + public static string ResolveRelativePath(string basePath, string relativePath) + { + if (string.IsNullOrEmpty(basePath)) + { + return relativePath; + } + + if (!basePath.EndsWith(@"\")) + basePath = Path.GetDirectoryName(basePath); + + string merged = Path.GetFullPath(Path.Combine(basePath, relativePath)); + + if (basePath.StartsWith(@"\") && merged.Length > 2 && merged[1].Equals(':')) + { + return merged.Substring(2); + } + + return merged; + } + + public static string ResolvePath(string basePath, string path) + { + if (!path.StartsWith("\\", StringComparison.OrdinalIgnoreCase)) + { + return ResolveRelativePath(basePath, path); + } + return path; + } + + public static string MakeRelativePath(string path, string basePath) + { + List pathElements = + new List(path.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries)); + List basePathElements = + new List(basePath.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries)); + + if (!basePath.EndsWith("\\", StringComparison.Ordinal) && basePathElements.Count > 0) + { + basePathElements.RemoveAt(basePathElements.Count - 1); + } + + // Find first part of paths that don't match + int i = 0; + while (i < Math.Min(pathElements.Count - 1, basePathElements.Count)) + { + if (pathElements[i].ToUpperInvariant() != basePathElements[i].ToUpperInvariant()) + { + break; + } + + ++i; + } + + // For each remaining part of the base path, insert '..' + StringBuilder result = new StringBuilder(); + if (i == basePathElements.Count) + { + result.Append(@".\"); + } + else if (i < basePathElements.Count) + { + for (int j = 0; j < basePathElements.Count - i; ++j) + { + result.Append(@"..\"); + } + } + + // For each remaining part of the path, add the path element + for (int j = i; j < pathElements.Count - 1; ++j) + { + result.Append(pathElements[j]); + result.Append(@"\"); + } + + result.Append(pathElements[pathElements.Count - 1]); + + // If the target was a directory, put the terminator back + if (path.EndsWith(@"\", StringComparison.Ordinal)) + { + result.Append(@"\"); + } + + return result.ToString(); + } + + #endregion + + #region Filesystem Support + + /// + /// Indicates if a file name matches the 8.3 pattern. + /// + /// The name to test. + /// true if the name is 8.3, otherwise false. + public static bool Is8Dot3(string name) + { + if (name.Length > 12) + { + return false; + } + + string[] split = name.Split('.'); + + if (split.Length > 2 || split.Length < 1) + { + return false; + } + + if (split[0].Length > 8) + { + return false; + } + + foreach (char ch in split[0]) + { + if (!Is8Dot3Char(ch)) + { + return false; + } + } + + if (split.Length > 1) + { + if (split[1].Length > 3) + { + return false; + } + + foreach (char ch in split[1]) + { + if (!Is8Dot3Char(ch)) + { + return false; + } + } + } + + return true; + } + + public static bool Is8Dot3Char(char ch) + { + return (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || "_^$~!#%£-{}()@'`&".IndexOf(ch) != -1; + } + + /// + /// Converts a 'standard' wildcard file/path specification into a regular expression. + /// + /// The wildcard pattern to convert. + /// The resultant regular expression. + /// + /// The wildcard * (star) matches zero or more characters (including '.'), and ? + /// (question mark) matches precisely one character (except '.'). + /// + public static Regex ConvertWildcardsToRegEx(string pattern) + { + if (!pattern.Contains(".")) + { + pattern += "."; + } + + string query = "^" + Regex.Escape(pattern).Replace(@"\*", ".*").Replace(@"\?", "[^.]") + "$"; + return new Regex(query, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + } + + public static FileAttributes FileAttributesFromUnixFileType(UnixFileType fileType) + { + switch (fileType) + { + case UnixFileType.Fifo: + return FileAttributes.Device | FileAttributes.System; + case UnixFileType.Character: + return FileAttributes.Device | FileAttributes.System; + case UnixFileType.Directory: + return FileAttributes.Directory; + case UnixFileType.Block: + return FileAttributes.Device | FileAttributes.System; + case UnixFileType.Regular: + return FileAttributes.Normal; + case UnixFileType.Link: + return FileAttributes.ReparsePoint; + case UnixFileType.Socket: + return FileAttributes.Device | FileAttributes.System; + default: + return 0; + } + } + + #endregion + } +} \ No newline at end of file diff --git a/DiscUtils/DiscUtils.Fat/ClusterReader.cs b/DiscUtils/DiscUtils.Fat/ClusterReader.cs new file mode 100644 index 0000000..7831056 --- /dev/null +++ b/DiscUtils/DiscUtils.Fat/ClusterReader.cs @@ -0,0 +1,95 @@ +// +// 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.IO; +using DiscUtils.Streams; + +namespace DiscUtils.Fat +{ + internal sealed class ClusterReader + { + private readonly int _bytesPerSector; + + /// + /// Pre-calculated value because of number of uses of this externally. + /// + private readonly int _clusterSize; + + private readonly int _firstDataSector; + private readonly int _sectorsPerCluster; + private readonly Stream _stream; + + public ClusterReader(Stream stream, int firstDataSector, int sectorsPerCluster, int bytesPerSector) + { + _stream = stream; + _firstDataSector = firstDataSector; + _sectorsPerCluster = sectorsPerCluster; + _bytesPerSector = bytesPerSector; + + _clusterSize = _sectorsPerCluster * _bytesPerSector; + } + + public int ClusterSize + { + get { return _clusterSize; } + } + + public void ReadCluster(uint cluster, byte[] buffer, int offset) + { + if (offset + ClusterSize > buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(offset), + "buffer is too small - cluster would overflow buffer"); + } + + uint firstSector = (uint)((cluster - 2) * _sectorsPerCluster + _firstDataSector); + + _stream.Position = firstSector * _bytesPerSector; + StreamUtilities.ReadExact(_stream, buffer, offset, _clusterSize); + } + + internal void WriteCluster(uint cluster, byte[] buffer, int offset) + { + if (offset + ClusterSize > buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(offset), + "buffer is too small - cluster would overflow buffer"); + } + + uint firstSector = (uint)((cluster - 2) * _sectorsPerCluster + _firstDataSector); + + _stream.Position = firstSector * _bytesPerSector; + + _stream.Write(buffer, offset, _clusterSize); + } + + internal void WipeCluster(uint cluster) + { + uint firstSector = (uint)((cluster - 2) * _sectorsPerCluster + _firstDataSector); + + _stream.Position = firstSector * _bytesPerSector; + + _stream.Write(new byte[_clusterSize], 0, _clusterSize); + } + } +} \ No newline at end of file diff --git a/DiscUtils/DiscUtils.Fat/ClusterStream.cs b/DiscUtils/DiscUtils.Fat/ClusterStream.cs new file mode 100644 index 0000000..310b291 --- /dev/null +++ b/DiscUtils/DiscUtils.Fat/ClusterStream.cs @@ -0,0 +1,464 @@ +// +// 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.IO; + +namespace DiscUtils.Fat +{ + internal class ClusterStream : Stream + { + private readonly FileAccess _access; + private readonly byte[] _clusterBuffer; + private readonly FileAllocationTable _fat; + + private readonly List _knownClusters; + private readonly ClusterReader _reader; + + private bool _atEOF; + + private uint _currentCluster; + private uint _length; + private long _position; + + internal ClusterStream(FatFileSystem fileSystem, FileAccess access, uint firstCluster, uint length) + { + _access = access; + _reader = fileSystem.ClusterReader; + _fat = fileSystem.Fat; + _length = length; + + _knownClusters = new List(); + if (firstCluster != 0) + { + _knownClusters.Add(firstCluster); + } + else + { + _knownClusters.Add(FatBuffer.EndOfChain); + } + + if (_length == uint.MaxValue) + { + _length = DetectLength(); + } + + _currentCluster = uint.MaxValue; + _clusterBuffer = new byte[_reader.ClusterSize]; + } + + public override bool CanRead + { + get { return _access == FileAccess.Read || _access == FileAccess.ReadWrite; } + } + + public override bool CanSeek + { + get { return true; } + } + + public override bool CanWrite + { + get { return _access == FileAccess.ReadWrite || _access == FileAccess.Write; } + } + + public override long Length + { + get { return _length; } + } + + public override long Position + { + get { return _position; } + + set + { + if (value >= 0) + { + _position = value; + _atEOF = false; + } + else + { + throw new ArgumentOutOfRangeException(nameof(value), "Attempt to move before beginning of stream"); + } + } + } + + public event FirstClusterChangedDelegate FirstClusterChanged; + + public override void Flush() { } + + public override int Read(byte[] buffer, int offset, int count) + { + if (!CanRead) + { + throw new IOException("Attempt to read from file not opened for read"); + } + + if (_position > _length) + { + throw new IOException("Attempt to read beyond end of file"); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "Attempt to read negative number of bytes"); + } + + int target = count; + if (_length - _position < count) + { + target = (int)(_length - _position); + } + + if (!TryLoadCurrentCluster()) + { + if ((_position == _length || _position == DetectLength()) && !_atEOF) + { + _atEOF = true; + return 0; + } + throw new IOException("Attempt to read beyond known clusters"); + } + + int numRead = 0; + while (numRead < target) + { + int clusterOffset = (int)(_position % _reader.ClusterSize); + int toCopy = Math.Min(_reader.ClusterSize - clusterOffset, target - numRead); + Array.Copy(_clusterBuffer, clusterOffset, buffer, offset + numRead, toCopy); + + // Remember how many we've read in total + numRead += toCopy; + + // Increment the position + _position += toCopy; + + // Abort if we've hit the end of the file + if (!TryLoadCurrentCluster()) + { + break; + } + } + + if (numRead == 0) + { + _atEOF = true; + } + + return numRead; + } + + public override long Seek(long offset, SeekOrigin origin) + { + long newPos = offset; + if (origin == SeekOrigin.Current) + { + newPos += _position; + } + else if (origin == SeekOrigin.End) + { + newPos += Length; + } + + _position = newPos; + _atEOF = false; + return newPos; + } + + public override void SetLength(long value) + { + long desiredNumClusters = (value + _reader.ClusterSize - 1) / _reader.ClusterSize; + long actualNumClusters = (_length + _reader.ClusterSize - 1) / _reader.ClusterSize; + + if (desiredNumClusters < actualNumClusters) + { + uint cluster; + if (!TryGetClusterByPosition(value, out cluster)) + { + throw new IOException("Internal state corrupt - unable to find cluster"); + } + + uint firstToFree = _fat.GetNext(cluster); + _fat.SetEndOfChain(cluster); + _fat.FreeChain(firstToFree); + + while (_knownClusters.Count > desiredNumClusters) + { + _knownClusters.RemoveAt(_knownClusters.Count - 1); + } + + _knownClusters.Add(FatBuffer.EndOfChain); + + if (desiredNumClusters == 0) + { + FireFirstClusterAllocated(0); + } + } + else if (desiredNumClusters > actualNumClusters) + { + uint cluster; + while (!TryGetClusterByPosition(value, out cluster)) + { + cluster = ExtendChain(); + _reader.WipeCluster(cluster); + } + } + + if (_length != value) + { + _length = (uint)value; + if (_position > _length) + { + _position = _length; + } + } + } + + public override void Write(byte[] buffer, int offset, int count) + { + int bytesRemaining = count; + + if (!CanWrite) + { + throw new IOException("Attempting to write to file not opened for writing"); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), count, + "Attempting to write negative number of bytes"); + } + + if (offset > buffer.Length || offset + count > buffer.Length) + { + throw new ArgumentException("Attempt to write bytes outside of the buffer"); + } + + // TODO: Free space check... + try + { + while (bytesRemaining > 0) + { + // Extend the stream until it encompasses _position + uint cluster; + while (!TryGetClusterByPosition(_position, out cluster)) + { + cluster = ExtendChain(); + _reader.WipeCluster(cluster); + } + + // Fill this cluster with as much data as we can (WriteToCluster preserves existing cluster + // data, if necessary) + int numWritten = WriteToCluster(cluster, (int)(_position % _reader.ClusterSize), buffer, offset, + bytesRemaining); + offset += numWritten; + bytesRemaining -= numWritten; + _position += numWritten; + } + + _length = (uint)Math.Max(_length, _position); + } + finally + { + _fat.Flush(); + } + + _atEOF = false; + } + + /// + /// Writes up to the next cluster boundary, making sure to preserve existing data in the cluster + /// that falls outside of the updated range. + /// + /// The cluster to write to. + /// The file position of the write (within the cluster). + /// The buffer with the new data. + /// Offset into buffer of the first byte to write. + /// The maximum number of bytes to write. + /// The number of bytes written - either count, or the number that fit up to + /// the cluster boundary. + private int WriteToCluster(uint cluster, int pos, byte[] buffer, int offset, int count) + { + if (pos == 0 && count >= _reader.ClusterSize) + { + _currentCluster = cluster; + Array.Copy(buffer, offset, _clusterBuffer, 0, _reader.ClusterSize); + + WriteCurrentCluster(); + + return _reader.ClusterSize; + } + + // Partial cluster, so need to read existing cluster data first + LoadCluster(cluster); + + int copyLength = Math.Min(count, _reader.ClusterSize - pos % _reader.ClusterSize); + Array.Copy(buffer, offset, _clusterBuffer, pos, copyLength); + + WriteCurrentCluster(); + + return copyLength; + } + + /// + /// Adds a new cluster to the end of the existing chain, by allocating a free cluster. + /// + /// The cluster allocated. + /// This method does not initialize the data in the cluster, the caller should + /// perform a write to ensure the cluster data is in known state. + private uint ExtendChain() + { + // Sanity check - make sure the final known cluster is the EOC marker + if (!_fat.IsEndOfChain(_knownClusters[_knownClusters.Count - 1])) + { + throw new IOException("Corrupt file system: final cluster isn't End-of-Chain"); + } + + uint cluster; + if (!_fat.TryGetFreeCluster(out cluster)) + { + throw new IOException("Out of disk space"); + } + + _fat.SetEndOfChain(cluster); + if (_knownClusters.Count == 1) + { + FireFirstClusterAllocated(cluster); + } + else + { + _fat.SetNext(_knownClusters[_knownClusters.Count - 2], cluster); + } + + _knownClusters[_knownClusters.Count - 1] = cluster; + _knownClusters.Add(_fat.GetNext(cluster)); + + return cluster; + } + + private void FireFirstClusterAllocated(uint cluster) + { + if (FirstClusterChanged != null) + { + FirstClusterChanged(cluster); + } + } + + private bool TryLoadCurrentCluster() + { + return TryLoadClusterByPosition(_position); + } + + private bool TryLoadClusterByPosition(long pos) + { + uint cluster; + if (!TryGetClusterByPosition(pos, out cluster)) + { + return false; + } + + // Read the cluster, it's different to the one currently loaded + if (cluster != _currentCluster) + { + _reader.ReadCluster(cluster, _clusterBuffer, 0); + _currentCluster = cluster; + } + + return true; + } + + private void LoadCluster(uint cluster) + { + // Read the cluster, it's different to the one currently loaded + if (cluster != _currentCluster) + { + _reader.ReadCluster(cluster, _clusterBuffer, 0); + _currentCluster = cluster; + } + } + + private void WriteCurrentCluster() + { + _reader.WriteCluster(_currentCluster, _clusterBuffer, 0); + } + + private bool TryGetClusterByPosition(long pos, out uint cluster) + { + int index = (int)(pos / _reader.ClusterSize); + + if (_knownClusters.Count <= index) + { + if (!TryPopulateKnownClusters(index)) + { + cluster = uint.MaxValue; + return false; + } + } + + // Chain is shorter than the current stream position + if (_knownClusters.Count <= index) + { + cluster = uint.MaxValue; + return false; + } + + cluster = _knownClusters[index]; + + // This is the 'special' End-of-chain cluster identifer, so the stream position + // is greater than the actual file length. + if (_fat.IsEndOfChain(cluster)) + { + return false; + } + + return true; + } + + private bool TryPopulateKnownClusters(int index) + { + uint lastKnown = _knownClusters[_knownClusters.Count - 1]; + while (!_fat.IsEndOfChain(lastKnown) && _knownClusters.Count <= index) + { + lastKnown = _fat.GetNext(lastKnown); + _knownClusters.Add(lastKnown); + } + + return _knownClusters.Count > index; + } + + private uint DetectLength() + { + while (!_fat.IsEndOfChain(_knownClusters[_knownClusters.Count - 1])) + { + if (!TryPopulateKnownClusters(_knownClusters.Count)) + { + throw new IOException("Corrupt file stream - unable to discover end of cluster chain"); + } + } + + return (uint)((_knownClusters.Count - 1) * (long)_reader.ClusterSize); + } + } +} \ No newline at end of file diff --git a/DiscUtils/DiscUtils.Fat/Directory.cs b/DiscUtils/DiscUtils.Fat/Directory.cs new file mode 100644 index 0000000..b0869d0 --- /dev/null +++ b/DiscUtils/DiscUtils.Fat/Directory.cs @@ -0,0 +1,569 @@ +// +// 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. +// + +namespace DiscUtils.Fat +{ + using DiscUtils.Streams; + using System; + using System.Collections.Generic; + using System.IO; + + internal class Directory : IDisposable + { + private FatFileSystem _fileSystem; + private Directory _parent; + private long _parentId; + private Stream _dirStream; + + private Dictionary _entries; + private List _freeEntries; + private long _endOfEntries; + + private DirectoryEntry _selfEntry; + private long _selfEntryLocation; + private DirectoryEntry _parentEntry; + private long _parentEntryLocation; + + internal Dictionary LongFileNames_ShortKey = new Dictionary(StringComparer.OrdinalIgnoreCase); + internal Dictionary LongFileNames_LongKey = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Initializes a new instance of the Directory class. Use this constructor to represent non-root directories. + /// + /// The parent directory. + /// The identity of the entry representing this directory in the parent. + internal Directory(Directory parent, long parentId) + { + _fileSystem = parent._fileSystem; + _parent = parent; + _parentId = parentId; + + DirectoryEntry dirEntry = ParentsChildEntry; + _dirStream = new ClusterStream(_fileSystem, FileAccess.ReadWrite, dirEntry.FirstCluster, uint.MaxValue); + + LoadEntries(); + } + + /// + /// Initializes a new instance of the Directory class. Use this constructor to represent the root directory. + /// + /// The file system. + /// The stream containing the directory info. + internal Directory(FatFileSystem fileSystem, Stream dirStream) + { + _fileSystem = fileSystem; + _dirStream = dirStream; + + LoadEntries(); + } + + private static string GetLfnChunk(byte[] buffer) + { + // see http://home.teleport.com/~brainy/lfn.htm + // NOTE: we assume ordinals are ok here. + char[] chars = new char[13]; + chars[0] = (char)(256 * buffer[2] + buffer[1]); + chars[1] = (char)(256 * buffer[4] + buffer[3]); + chars[2] = (char)(256 * buffer[6] + buffer[5]); + chars[3] = (char)(256 * buffer[8] + buffer[7]); + chars[4] = (char)(256 * buffer[10] + buffer[9]); + + chars[5] = (char)(256 * buffer[15] + buffer[14]); + chars[6] = (char)(256 * buffer[17] + buffer[16]); + chars[7] = (char)(256 * buffer[19] + buffer[18]); + chars[8] = (char)(256 * buffer[21] + buffer[20]); + chars[9] = (char)(256 * buffer[23] + buffer[22]); + chars[10] = (char)(256 * buffer[25] + buffer[24]); + + chars[11] = (char)(256 * buffer[29] + buffer[28]); + chars[12] = (char)(256 * buffer[31] + buffer[30]); + string chunk = new string(chars); + int zero = chunk.IndexOf('\0'); + return zero >= 0 ? chunk.Substring(0, zero) : chunk; + } + + public FatFileSystem FileSystem + { + get { return _fileSystem; } + } + + public bool IsEmpty + { + get { return _entries.Count == 0; } + } + + public DirectoryEntry[] Entries + { + get { return new List(_entries.Values).ToArray(); } + } + + #region Convenient accessors for special entries + internal DirectoryEntry ParentsChildEntry + { + get + { + if (_parent == null) + { + return new DirectoryEntry(_fileSystem.FatOptions, FileName.ParentEntryName, FatAttributes.Directory, _fileSystem.FatVariant); + } + else + { + return _parent.GetEntry(_parentId); + } + } + + set + { + if (_parent != null) + { + _parent.UpdateEntry(_parentId, value); + } + } + } + + internal DirectoryEntry SelfEntry + { + get + { + if (_parent == null) + { + // If we're the root directory, simulate the parent entry with a dummy record + return new DirectoryEntry(_fileSystem.FatOptions, FileName.Null, FatAttributes.Directory, _fileSystem.FatVariant); + } + else + { + return _selfEntry; + } + } + + set + { + if (_selfEntryLocation >= 0) + { + _dirStream.Position = _selfEntryLocation; + value.WriteTo(_dirStream); + _selfEntry = value; + } + } + } + + internal DirectoryEntry ParentEntry + { + get + { + return _parentEntry; + } + + set + { + if (_parentEntryLocation < 0) + { + throw new IOException("No parent entry on disk to update"); + } + + _dirStream.Position = _parentEntryLocation; + value.WriteTo(_dirStream); + _parentEntry = value; + } + } + #endregion + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public DirectoryEntry[] GetDirectories() + { + List dirs = new List(_entries.Count); + foreach (DirectoryEntry dirEntry in _entries.Values) + { + if ((dirEntry.Attributes & FatAttributes.Directory) != 0) + { + dirs.Add(dirEntry); + } + } + + return dirs.ToArray(); + } + + public DirectoryEntry[] GetFiles() + { + List files = new List(_entries.Count); + foreach (DirectoryEntry dirEntry in _entries.Values) + { + if ((dirEntry.Attributes & (FatAttributes.Directory | FatAttributes.VolumeId)) == 0) + { + files.Add(dirEntry); + } + } + + return files.ToArray(); + } + + public DirectoryEntry GetEntry(long id) + { + return (id < 0) ? null : _entries[id]; + } + + public Directory GetChildDirectory(FileName name) + { + long id = FindEntry(name); + if (id < 0) + { + return null; + } + else if ((_entries[id].Attributes & FatAttributes.Directory) == 0) + { + return null; + } + else + { + return _fileSystem.GetDirectory(this, id); + } + } + + internal Directory CreateChildDirectory(FileName name) + { + long id = FindEntry(name); + if (id >= 0) + { + if ((_entries[id].Attributes & FatAttributes.Directory) == 0) + { + throw new IOException("A file exists with the same name"); + } + else + { + return _fileSystem.GetDirectory(this, id); + } + } + else + { + try + { + uint firstCluster; + if (!_fileSystem.Fat.TryGetFreeCluster(out firstCluster)) + { + throw new IOException("Failed to allocate first cluster for new directory"); + } + + _fileSystem.Fat.SetEndOfChain(firstCluster); + + DirectoryEntry newEntry = new DirectoryEntry(_fileSystem.FatOptions, name, FatAttributes.Directory, _fileSystem.FatVariant); + newEntry.FirstCluster = firstCluster; + newEntry.CreationTime = _fileSystem.ConvertFromUtc(DateTime.UtcNow); + newEntry.LastWriteTime = newEntry.CreationTime; + + id = AddEntry(newEntry); + + PopulateNewChildDirectory(newEntry); + + // Rather than just creating a new instance, pull it through the fileSystem cache + // to ensure the cache model is preserved. + return _fileSystem.GetDirectory(this, id); + } + finally + { + _fileSystem.Fat.Flush(); + } + } + } + + internal void AttachChildDirectory(FileName name, Directory newChild) + { + long id = FindEntry(name); + if (id >= 0) + { + throw new IOException("Directory entry already exists"); + } + + DirectoryEntry newEntry = new DirectoryEntry(newChild.ParentsChildEntry); + newEntry.Name = name; + AddEntry(newEntry); + + DirectoryEntry newParentEntry = new DirectoryEntry(SelfEntry); + newParentEntry.Name = FileName.ParentEntryName; + newChild.ParentEntry = newParentEntry; + } + + internal long FindVolumeId() + { + foreach (long id in _entries.Keys) + { + DirectoryEntry focus = _entries[id]; + if ((focus.Attributes & FatAttributes.VolumeId) != 0) + { + return id; + } + } + + return -1; + } + + internal long FindEntry(FileName name) + { + foreach (long id in _entries.Keys) + { + DirectoryEntry focus = _entries[id]; + if (focus.Name == name && (focus.Attributes & FatAttributes.VolumeId) == 0) + { + return id; + } + } + + return -1; + } + + internal SparseStream OpenFile(FileName name, FileMode mode, FileAccess fileAccess) + { + if (mode == FileMode.Append || mode == FileMode.Truncate) + { + throw new NotImplementedException(); + } + + long fileId = FindEntry(name); + bool exists = fileId != -1; + + if (mode == FileMode.CreateNew && exists) + { + throw new IOException("File already exists"); + } + else if (mode == FileMode.Open && !exists) + { + throw new FileNotFoundException("File not found", name.GetDisplayName(_fileSystem.FatOptions.FileNameEncoding)); + } + else if ((mode == FileMode.Open || mode == FileMode.OpenOrCreate || mode == FileMode.Create) && exists) + { + SparseStream stream = new FatFileStream(_fileSystem, this, fileId, fileAccess); + if (mode == FileMode.Create) + { + stream.SetLength(0); + } + + HandleAccessed(false); + + return stream; + } + else if ((mode == FileMode.OpenOrCreate || mode == FileMode.CreateNew || mode == FileMode.Create) && !exists) + { + // Create new file + DirectoryEntry newEntry = new DirectoryEntry(_fileSystem.FatOptions, name, FatAttributes.Archive, _fileSystem.FatVariant); + newEntry.FirstCluster = 0; // i.e. Zero-length + newEntry.CreationTime = _fileSystem.ConvertFromUtc(DateTime.UtcNow); + newEntry.LastWriteTime = newEntry.CreationTime; + + fileId = AddEntry(newEntry); + + return new FatFileStream(_fileSystem, this, fileId, fileAccess); + } + else + { + // Should never get here... + throw new NotImplementedException(); + } + } + + internal long AddEntry(DirectoryEntry newEntry) + { + // Unlink an entry from the free list (or add to the end of the existing directory) + long pos; + if (_freeEntries.Count > 0) + { + pos = _freeEntries[0]; + _freeEntries.RemoveAt(0); + } + else + { + pos = _endOfEntries; + _endOfEntries += 32; + } + + // Put the new entry into it's slot + _dirStream.Position = pos; + newEntry.WriteTo(_dirStream); + + // Update internal structures to reflect new entry (as if read from disk) + _entries.Add(pos, newEntry); + + HandleAccessed(true); + + return pos; + } + + internal void DeleteEntry(long id, bool releaseContents) + { + if (id < 0) + { + throw new IOException("Attempt to delete unknown directory entry"); + } + + try + { + DirectoryEntry entry = _entries[id]; + + DirectoryEntry copy = new DirectoryEntry(entry); + copy.Name = entry.Name.Deleted(); + _dirStream.Position = id; + copy.WriteTo(_dirStream); + + if (releaseContents) + { + _fileSystem.Fat.FreeChain(entry.FirstCluster); + } + + _entries.Remove(id); + _freeEntries.Add(id); + + HandleAccessed(true); + } + finally + { + _fileSystem.Fat.Flush(); + } + } + + internal void UpdateEntry(long id, DirectoryEntry entry) + { + if (id < 0) + { + throw new IOException("Attempt to update unknown directory entry"); + } + + _dirStream.Position = id; + entry.WriteTo(_dirStream); + _entries[id] = entry; + } + + private void LoadEntries() + { + _entries = new Dictionary(); + _freeEntries = new List(); + + _selfEntryLocation = -1; + _parentEntryLocation = -1; + + string lfn = null; //+++ + while (_dirStream.Position < _dirStream.Length) + { + long streamPos = _dirStream.Position; + DirectoryEntry entry = new DirectoryEntry(_fileSystem.FatOptions, _dirStream, _fileSystem.FatVariant); + + if (entry.Attributes == (FatAttributes.ReadOnly | FatAttributes.Hidden | FatAttributes.System | FatAttributes.VolumeId)) + { + // Long File Name entry + _dirStream.Position = streamPos; //+++ + lfn = GetLfnChunk(StreamUtilities.ReadExact(_dirStream, 32)) + lfn; //+++ + } + else if (entry.Name.IsDeleted()) + { + // E5 = Free Entry + _freeEntries.Add(streamPos); + lfn = null; //+++ + } + else if (entry.Name == FileName.SelfEntryName) + { + _selfEntry = entry; + _selfEntryLocation = streamPos; + lfn = null; //+++ + } + else if (entry.Name == FileName.ParentEntryName) + { + _parentEntry = entry; + _parentEntryLocation = streamPos; + lfn = null; //+++ + } + else if (entry.Name.IsEndMarker()) + { + // Free Entry, no more entries available + _endOfEntries = streamPos; + break; + } + else + { + if (lfn != null) //+++ + { //+++ + LongFileNames_ShortKey.Add(entry.Name.GetDisplayName(_fileSystem.FatOptions.FileNameEncoding), lfn); //+++ + LongFileNames_LongKey.Add(lfn, entry.Name.GetDisplayName(_fileSystem.FatOptions.FileNameEncoding)); //+++ + } //+++ + _entries.Add(streamPos, entry); + lfn = null; //+++ + } + } + } + + private void HandleAccessed(bool forWrite) + { + if (_fileSystem.CanWrite && _parent != null) + { + DateTime now = DateTime.Now; + DirectoryEntry entry = SelfEntry; + + DateTime oldAccessTime = entry.LastAccessTime; + DateTime oldWriteTime = entry.LastWriteTime; + + entry.LastAccessTime = now; + if (forWrite) + { + entry.LastWriteTime = now; + } + + if (entry.LastAccessTime != oldAccessTime || entry.LastWriteTime != oldWriteTime) + { + SelfEntry = entry; + + DirectoryEntry parentEntry = ParentsChildEntry; + parentEntry.LastAccessTime = entry.LastAccessTime; + parentEntry.LastWriteTime = entry.LastWriteTime; + ParentsChildEntry = parentEntry; + } + } + } + + private void PopulateNewChildDirectory(DirectoryEntry newEntry) + { + // Populate new directory with initial (special) entries. First one is easy, just change the name! + using (ClusterStream stream = new ClusterStream(_fileSystem, FileAccess.Write, newEntry.FirstCluster, uint.MaxValue)) + { + // First is the self-referencing entry... + DirectoryEntry selfEntry = new DirectoryEntry(newEntry); + selfEntry.Name = FileName.SelfEntryName; + selfEntry.WriteTo(stream); + + // Second is a clone of our self entry (i.e. parent) - though dates are odd... + DirectoryEntry parentEntry = new DirectoryEntry(SelfEntry); + parentEntry.Name = FileName.ParentEntryName; + parentEntry.CreationTime = newEntry.CreationTime; + parentEntry.LastWriteTime = newEntry.LastWriteTime; + parentEntry.WriteTo(stream); + } + } + + private void Dispose(bool disposing) + { + if (disposing) + { + _dirStream.Dispose(); + } + } + } +} diff --git a/DiscUtils/DiscUtils.Fat/DirectoryEntry.cs b/DiscUtils/DiscUtils.Fat/DirectoryEntry.cs new file mode 100644 index 0000000..7c9a618 --- /dev/null +++ b/DiscUtils/DiscUtils.Fat/DirectoryEntry.cs @@ -0,0 +1,210 @@ +// +// 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.IO; +using DiscUtils.Streams; + +namespace DiscUtils.Fat +{ + internal class DirectoryEntry + { + private readonly FatType _fatVariant; + private readonly FatFileSystemOptions _options; + private byte _attr; + private ushort _creationDate; + private ushort _creationTime; + private byte _creationTimeTenth; + private uint _fileSize; + private ushort _firstClusterHi; + private ushort _firstClusterLo; + private ushort _lastAccessDate; + private ushort _lastWriteDate; + private ushort _lastWriteTime; + + internal DirectoryEntry(FatFileSystemOptions options, Stream stream, FatType fatVariant) + { + _options = options; + _fatVariant = fatVariant; + byte[] buffer = StreamUtilities.ReadExact(stream, 32); + Load(buffer, 0); + } + + internal DirectoryEntry(FatFileSystemOptions options, FileName name, FatAttributes attrs, FatType fatVariant) + { + _options = options; + _fatVariant = fatVariant; + Name = name; + _attr = (byte)attrs; + } + + internal DirectoryEntry(DirectoryEntry toCopy) + { + _options = toCopy._options; + _fatVariant = toCopy._fatVariant; + Name = toCopy.Name; + _attr = toCopy._attr; + _creationTimeTenth = toCopy._creationTimeTenth; + _creationTime = toCopy._creationTime; + _creationDate = toCopy._creationDate; + _lastAccessDate = toCopy._lastAccessDate; + _firstClusterHi = toCopy._firstClusterHi; + _lastWriteTime = toCopy._lastWriteTime; + _firstClusterLo = toCopy._firstClusterLo; + _fileSize = toCopy._fileSize; + } + + public FatAttributes Attributes + { + get { return (FatAttributes)_attr; } + set { _attr = (byte)value; } + } + + public DateTime CreationTime + { + get { return FileTimeToDateTime(_creationDate, _creationTime, _creationTimeTenth); } + set { DateTimeToFileTime(value, out _creationDate, out _creationTime, out _creationTimeTenth); } + } + + public int FileSize + { + get { return (int)_fileSize; } + set { _fileSize = (uint)value; } + } + + public uint FirstCluster + { + get + { + if (_fatVariant == FatType.Fat32) + { + return (uint)(_firstClusterHi << 16) | _firstClusterLo; + } + return _firstClusterLo; + } + + set + { + if (_fatVariant == FatType.Fat32) + { + _firstClusterHi = (ushort)((value >> 16) & 0xFFFF); + } + + _firstClusterLo = (ushort)(value & 0xFFFF); + } + } + + public DateTime LastAccessTime + { + get { return FileTimeToDateTime(_lastAccessDate, 0, 0); } + set { DateTimeToFileTime(value, out _lastAccessDate); } + } + + public DateTime LastWriteTime + { + get { return FileTimeToDateTime(_lastWriteDate, _lastWriteTime, 0); } + set { DateTimeToFileTime(value, out _lastWriteDate, out _lastWriteTime); } + } + + public FileName Name { get; set; } + + internal void WriteTo(Stream stream) + { + byte[] buffer = new byte[32]; + + Name.GetBytes(buffer, 0); + buffer[11] = _attr; + buffer[13] = _creationTimeTenth; + EndianUtilities.WriteBytesLittleEndian(_creationTime, buffer, 14); + EndianUtilities.WriteBytesLittleEndian(_creationDate, buffer, 16); + EndianUtilities.WriteBytesLittleEndian(_lastAccessDate, buffer, 18); + EndianUtilities.WriteBytesLittleEndian(_firstClusterHi, buffer, 20); + EndianUtilities.WriteBytesLittleEndian(_lastWriteTime, buffer, 22); + EndianUtilities.WriteBytesLittleEndian(_lastWriteDate, buffer, 24); + EndianUtilities.WriteBytesLittleEndian(_firstClusterLo, buffer, 26); + EndianUtilities.WriteBytesLittleEndian(_fileSize, buffer, 28); + + stream.Write(buffer, 0, buffer.Length); + } + + private static DateTime FileTimeToDateTime(ushort date, ushort time, byte tenths) + { + if (date == 0 || date == 0xFFFF) + { + // Return Epoch - this is an invalid date + return FatFileSystem.Epoch; + } + + int year = 1980 + ((date & 0xFE00) >> 9); + int month = (date & 0x01E0) >> 5; + int day = date & 0x001F; + int hour = (time & 0xF800) >> 11; + int minute = (time & 0x07E0) >> 5; + int second = (time & 0x001F) * 2 + tenths / 100; + int millis = tenths % 100 * 10; + + return new DateTime(year, month, day, hour, minute, second, millis); + } + + private static void DateTimeToFileTime(DateTime value, out ushort date) + { + byte tenths; + ushort time; + DateTimeToFileTime(value, out date, out time, out tenths); + } + + private static void DateTimeToFileTime(DateTime value, out ushort date, out ushort time) + { + byte tenths; + DateTimeToFileTime(value, out date, out time, out tenths); + } + + private static void DateTimeToFileTime(DateTime value, out ushort date, out ushort time, out byte tenths) + { + if (value.Year < 1980) + { + value = FatFileSystem.Epoch; + } + + date = + (ushort)((((value.Year - 1980) << 9) & 0xFE00) | ((value.Month << 5) & 0x01E0) | (value.Day & 0x001F)); + time = + (ushort)(((value.Hour << 11) & 0xF800) | ((value.Minute << 5) & 0x07E0) | ((value.Second / 2) & 0x001F)); + tenths = (byte)(value.Second % 2 * 100 + value.Millisecond / 10); + } + + private void Load(byte[] data, int offset) + { + Name = new FileName(data, offset); + _attr = data[offset + 11]; + _creationTimeTenth = data[offset + 13]; + _creationTime = EndianUtilities.ToUInt16LittleEndian(data, offset + 14); + _creationDate = EndianUtilities.ToUInt16LittleEndian(data, offset + 16); + _lastAccessDate = EndianUtilities.ToUInt16LittleEndian(data, offset + 18); + _firstClusterHi = EndianUtilities.ToUInt16LittleEndian(data, offset + 20); + _lastWriteTime = EndianUtilities.ToUInt16LittleEndian(data, offset + 22); + _lastWriteDate = EndianUtilities.ToUInt16LittleEndian(data, offset + 24); + _firstClusterLo = EndianUtilities.ToUInt16LittleEndian(data, offset + 26); + _fileSize = EndianUtilities.ToUInt32LittleEndian(data, offset + 28); + } + } +} \ No newline at end of file diff --git a/DiscUtils/DiscUtils.Fat/FatAttributes.cs b/DiscUtils/DiscUtils.Fat/FatAttributes.cs new file mode 100644 index 0000000..1651b40 --- /dev/null +++ b/DiscUtils/DiscUtils.Fat/FatAttributes.cs @@ -0,0 +1,37 @@ +// +// 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; + +namespace DiscUtils.Fat +{ + [Flags] + internal enum FatAttributes : byte + { + ReadOnly = 0x01, + Hidden = 0x02, + System = 0x04, + VolumeId = 0x08, + Directory = 0x10, + Archive = 0x20 + } +} \ No newline at end of file diff --git a/DiscUtils/DiscUtils.Fat/FatBuffer.cs b/DiscUtils/DiscUtils.Fat/FatBuffer.cs new file mode 100644 index 0000000..63dbddb --- /dev/null +++ b/DiscUtils/DiscUtils.Fat/FatBuffer.cs @@ -0,0 +1,267 @@ +// +// 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.IO; +using DiscUtils.Streams; + +namespace DiscUtils.Fat +{ + internal class FatBuffer + { + /// + /// The End-of-chain marker to WRITE (SetNext). Don't use this value to test for end of chain. + /// + /// + /// The actual end-of-chain marker bits on disk vary by FAT type, and can end ...F8 through ...FF. + /// + public const uint EndOfChain = 0xFFFFFFFF; + + /// + /// The Bad-Cluster marker to WRITE (SetNext). Don't use this value to test for bad clusters. + /// + /// + /// The actual bad-cluster marker bits on disk vary by FAT type. + /// + public const uint BadCluster = 0xFFFFFFF7; + + /// + /// The Free-Cluster marker to WRITE (SetNext). Don't use this value to test for free clusters. + /// + /// + /// The actual free-cluster marker bits on disk vary by FAT type. + /// + public const uint FreeCluster = 0; + + private const uint DirtyRegionSize = 512; + private readonly byte[] _buffer; + private readonly Dictionary _dirtySectors; + + private readonly FatType _type; + private uint _nextFreeCandidate; + + public FatBuffer(FatType type, byte[] buffer) + { + _type = type; + _buffer = buffer; + _dirtySectors = new Dictionary(); + } + + internal int NumEntries + { + get + { + switch (_type) + { + case FatType.Fat12: + return _buffer.Length / 3 * 2; + case FatType.Fat16: + return _buffer.Length / 2; + default: // FAT32 + return _buffer.Length / 4; + } + } + } + + internal int Size + { + get { return _buffer.Length; } + } + + internal bool IsFree(uint val) + { + return val == 0; + } + + internal bool IsEndOfChain(uint val) + { + switch (_type) + { + case FatType.Fat12: + return (val & 0x0FFF) >= 0x0FF8; + case FatType.Fat16: + return (val & 0xFFFF) >= 0xFFF8; + case FatType.Fat32: + return (val & 0x0FFFFFF8) >= 0x0FFFFFF8; + default: + throw new ArgumentException("Unknown FAT type"); + } + } + + internal bool IsBadCluster(uint val) + { + switch (_type) + { + case FatType.Fat12: + return (val & 0x0FFF) == 0x0FF7; + case FatType.Fat16: + return (val & 0xFFFF) == 0xFFF7; + case FatType.Fat32: + return (val & 0x0FFFFFF8) == 0x0FFFFFF7; + default: + throw new ArgumentException("Unknown FAT type"); + } + } + + internal uint GetNext(uint cluster) + { + if (_type == FatType.Fat16) + { + return EndianUtilities.ToUInt16LittleEndian(_buffer, (int)(cluster * 2)); + } + if (_type == FatType.Fat32) + { + return EndianUtilities.ToUInt32LittleEndian(_buffer, (int)(cluster * 4)) & 0x0FFFFFFF; + } + + // FAT12 + if ((cluster & 1) != 0) + { + return + (uint)((EndianUtilities.ToUInt16LittleEndian(_buffer, (int)(cluster + cluster / 2)) >> 4) & 0x0FFF); + } + return (uint)(EndianUtilities.ToUInt16LittleEndian(_buffer, (int)(cluster + cluster / 2)) & 0x0FFF); + } + + internal void SetEndOfChain(uint cluster) + { + SetNext(cluster, EndOfChain); + } + + internal void SetBadCluster(uint cluster) + { + SetNext(cluster, BadCluster); + } + + internal void SetFree(uint cluster) + { + if (cluster < _nextFreeCandidate) + { + _nextFreeCandidate = cluster; + } + + SetNext(cluster, FreeCluster); + } + + internal void SetNext(uint cluster, uint next) + { + if (_type == FatType.Fat16) + { + MarkDirty(cluster * 2); + EndianUtilities.WriteBytesLittleEndian((ushort)next, _buffer, (int)(cluster * 2)); + } + else if (_type == FatType.Fat32) + { + MarkDirty(cluster * 4); + uint oldVal = EndianUtilities.ToUInt32LittleEndian(_buffer, (int)(cluster * 4)); + uint newVal = (oldVal & 0xF0000000) | (next & 0x0FFFFFFF); + EndianUtilities.WriteBytesLittleEndian(newVal, _buffer, (int)(cluster * 4)); + } + else + { + uint offset = cluster + cluster / 2; + MarkDirty(offset); + MarkDirty(offset + 1); // On alternate sector boundaries, cluster info crosses two sectors + + ushort maskedOldVal; + if ((cluster & 1) != 0) + { + next = next << 4; + maskedOldVal = (ushort)(EndianUtilities.ToUInt16LittleEndian(_buffer, (int)offset) & 0x000F); + } + else + { + next = next & 0x0FFF; + maskedOldVal = (ushort)(EndianUtilities.ToUInt16LittleEndian(_buffer, (int)offset) & 0xF000); + } + + ushort newVal = (ushort)(maskedOldVal | next); + + EndianUtilities.WriteBytesLittleEndian(newVal, _buffer, (int)offset); + } + } + + internal bool TryGetFreeCluster(out uint cluster) + { + // Simple scan - don't hold a free list... + uint numEntries = (uint)NumEntries; + for (uint i = 0; i < numEntries; i++) + { + uint candidate = (i + _nextFreeCandidate) % numEntries; + if (IsFree(GetNext(candidate))) + { + cluster = candidate; + _nextFreeCandidate = candidate + 1; + return true; + } + } + + cluster = 0; + return false; + } + + internal void FreeChain(uint head) + { + foreach (uint cluster in GetChain(head)) + { + SetFree(cluster); + } + } + + internal List GetChain(uint head) + { + List result = new List(); + + if (head != 0) + { + uint focus = head; + while (!IsEndOfChain(focus)) + { + result.Add(focus); + focus = GetNext(focus); + } + } + + return result; + } + + internal void MarkDirty(uint offset) + { + _dirtySectors[offset / DirtyRegionSize] = offset / DirtyRegionSize; + } + + internal void WriteDirtyRegions(Stream stream, long position) + { + foreach (uint val in _dirtySectors.Values) + { + stream.Position = position + val * DirtyRegionSize; + stream.Write(_buffer, (int)(val * DirtyRegionSize), (int)DirtyRegionSize); + } + } + + internal void ClearDirtyRegions() + { + _dirtySectors.Clear(); + } + } +} \ No newline at end of file diff --git a/DiscUtils/DiscUtils.Fat/FatFileStream.cs b/DiscUtils/DiscUtils.Fat/FatFileStream.cs new file mode 100644 index 0000000..6605928 --- /dev/null +++ b/DiscUtils/DiscUtils.Fat/FatFileStream.cs @@ -0,0 +1,141 @@ +// +// 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. +// + +namespace DiscUtils.Fat +{ + using DiscUtils.Streams; + using System; + using System.Collections.Generic; + using System.IO; + + internal class FatFileStream : SparseStream + { + private Directory _dir; + private long _dirId; + private ClusterStream _stream; + + private bool didWrite = false; + + public FatFileStream(FatFileSystem fileSystem, Directory dir, long fileId, FileAccess access) + { + _dir = dir; + _dirId = fileId; + + DirectoryEntry dirEntry = _dir.GetEntry(_dirId); + _stream = new ClusterStream(fileSystem, access, (uint)dirEntry.FirstCluster, (uint)dirEntry.FileSize); + _stream.FirstClusterChanged += FirstClusterAllocatedHandler; + } + + public override long Position + { + get { return _stream.Position; } + set { _stream.Position = value; } + } + + public override bool CanRead + { + get { return _stream.CanRead; } + } + + public override bool CanSeek + { + get { return _stream.CanSeek; } + } + + public override bool CanWrite + { + get { return _stream.CanWrite; } + } + + public override long Length + { + get { return _stream.Length; } + } + + public override IEnumerable Extents + { + get + { + return new StreamExtent[] { new StreamExtent(0, Length) }; + } + } + + public override void Close() + { + if (_dir.FileSystem.CanWrite) + { + try + { + DateTime now = _dir.FileSystem.ConvertFromUtc(DateTime.UtcNow); + + DirectoryEntry dirEntry = _dir.GetEntry(_dirId); + dirEntry.LastAccessTime = now; + if (didWrite) + { + dirEntry.FileSize = (int)_stream.Length; + dirEntry.LastWriteTime = now; + } + + _dir.UpdateEntry(_dirId, dirEntry); + } + finally + { + base.Close(); + } + } + } + + public override void SetLength(long value) + { + didWrite = true; + _stream.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + didWrite = true; + _stream.Write(buffer, offset, count); + } + + public override void Flush() + { + _stream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _stream.Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return _stream.Seek(offset, origin); + } + + private void FirstClusterAllocatedHandler(uint cluster) + { + DirectoryEntry dirEntry = _dir.GetEntry(_dirId); + dirEntry.FirstCluster = cluster; + _dir.UpdateEntry(_dirId, dirEntry); + } + } +} diff --git a/DiscUtils/DiscUtils.Fat/FatFileSystem.cs b/DiscUtils/DiscUtils.Fat/FatFileSystem.cs new file mode 100644 index 0000000..31ec2db --- /dev/null +++ b/DiscUtils/DiscUtils.Fat/FatFileSystem.cs @@ -0,0 +1,2048 @@ +// +// 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. +// + +namespace DiscUtils.Fat +{ + using DiscUtils.Internal; + using DiscUtils.Streams; + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Text; + using System.Text.RegularExpressions; + + /// + /// 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 TimeConverter _timeConverter; + private Stream _data; + private Ownership _ownsData; + private byte[] _bootSector; + private FileAllocationTable _fat; + private ClusterReader _clusterReader; + private Directory _rootDir; + private Dictionary _dirCache; + + private FatType _type; + private string _bpbOEMName; + private ushort _bpbBytesPerSec; + private byte _bpbSecPerClus; + private ushort _bpbRsvdSecCnt; + private byte _bpbNumFATs; + private ushort _bpbRootEntCnt; + private ushort _bpbTotSec16; + private byte _bpbMedia; + private ushort _bpbFATSz16; + private ushort _bpbSecPerTrk; + private ushort _bpbNumHeads; + private uint _bpbHiddSec; + private uint _bpbTotSec32; + + private byte _bsDrvNum; + private byte _bsBootSig; + private uint _bsVolId; + private string _bsVolLab; + private string _bsFilSysType; + + private uint _bpbFATSz32; + private ushort _bpbExtFlags; + private ushort _bpbFSVer; + private uint _bpbRootClus; + private ushort _bpbFSInfo; + private ushort _bpbBkBootSec; + + /// + /// 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; + } + + private delegate void EntryUpdateAction(DirectoryEntry entry); + + /// + /// Gets the FAT file system options, which can be modified. + /// + public FatFileSystemOptions FatOptions + { + get { return (FatFileSystemOptions)Options; } + } + + /// + /// Gets the FAT variant of the file system. + /// + public FatType FatVariant + { + get { return _type; } + } + + /// + /// Gets the friendly name for the file system, including FAT variant. + /// + public override string FriendlyName + { + get + { + switch (_type) + { + case FatType.Fat12: return "Microsoft FAT12"; + case FatType.Fat16: return "Microsoft FAT16"; + case FatType.Fat32: return "Microsoft FAT32"; + default: return "Unknown FAT"; + } + } + } + + /// + /// Gets the OEM name from the file system. + /// + public string OemName + { + get { return _bpbOEMName; } + } + + /// + /// Gets the number of bytes per sector (as stored in the file-system meta data). + /// + public int BytesPerSector + { + get { return _bpbBytesPerSec; } + } + + /// + /// Gets the number of contiguous sectors that make up one cluster. + /// + public byte SectorsPerCluster + { + get { return _bpbSecPerClus; } + } + + /// + /// Gets the number of reserved sectors at the start of the disk. + /// + public int ReservedSectorCount + { + get { return _bpbRsvdSecCnt; } + } + + /// + /// Gets the number of FATs present. + /// + public byte FatCount + { + get { return _bpbNumFATs; } + } + + /// + /// Gets the maximum number of root directory entries (on FAT variants that have a limit). + /// + public int MaxRootDirectoryEntries + { + get { return _bpbRootEntCnt; } + } + + /// + /// Gets the total number of sectors on the disk. + /// + public long TotalSectors + { + get { return (_bpbTotSec16 != 0) ? _bpbTotSec16 : _bpbTotSec32; } + } + + /// + /// Gets the Media marker byte, which indicates fixed or removable media. + /// + public byte Media + { + get { return _bpbMedia; } + } + + /// + /// Gets the size of a single FAT, in sectors. + /// + public long FatSize + { + get { return (_bpbFATSz16 != 0) ? _bpbFATSz16 : _bpbFATSz32; } + } + + /// + /// Gets the number of sectors per logical track. + /// + public int SectorsPerTrack + { + get { return _bpbSecPerTrk; } + } + + /// + /// 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 BIOS drive number for BIOS Int 13h calls. + /// + public byte BiosDriveNumber + { + get { return _bsDrvNum; } + } + + /// + /// Gets a value indicating whether the VolumeId, VolumeLabel and FileSystemType fields are valid. + /// + public bool ExtendedBootSignaturePresent + { + get { return _bsBootSig == 0x29; } + } + + /// + /// 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; + } + else + { + return _rootDir.GetEntry(volId).Name.GetRawName(FatOptions.FileNameEncoding); + } + } + } + + /// + /// Gets the (informational only) file system type recorded in the meta-data. + /// + public string FileSystemType + { + get { return _bsFilSysType; } + } + + /// + /// Gets the active FAT (zero-based index). + /// + public byte ActiveFat + { + get { return (byte)(((_bpbExtFlags & 0x08) != 0) ? _bpbExtFlags & 0x7 : 0); } + } + + /// + /// 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 file-system version (usually 0). + /// + public int Version + { + get { return _bpbFSVer; } + } + + /// + /// Gets the cluster number of the first cluster of the root directory (FAT32 only). + /// + public long RootDirectoryCluster + { + get { return _bpbRootClus; } + } + + /// + /// Gets the sector location of the FSINFO structure (FAT32 only). + /// + public int FSInfoSector + { + get { return _bpbFSInfo; } + } + + /// + /// Gets the Sector location of the backup boot sector (FAT32 only). + /// + public int BackupBootSector + { + get { return _bpbBkBootSec; } + } + + /// + /// Indicates if this file system is read-only or read-write. + /// + /// . + public override bool CanWrite + { + get { return _data.CanWrite; } + } + + internal FileAllocationTable Fat + { + get { return _fat; } + } + + internal ClusterReader ClusterReader + { + get { return _clusterReader; } + } + + #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", "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"); + } + else 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 + + /// + /// 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"); + } + else + { + 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 char[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); + + Directory focusDir = _rootDir; + + for (int i = 0; i < pathElements.Length; ++i) + { + FileName name; + try + { + name = new FileName(pathElements[i], FatOptions.FileNameEncoding); + } + 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"); + } + else 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; + } + else + { + 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; + } + else + { + 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; + } + else + { + 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("destinationDirectoryName"); + } + else + { + 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"); + } + else 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); + } + else + { + 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((ushort)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((uint)hiddenSectors, bootSector, 28); + + // Total number of sectors (large) + EndianUtilities.WriteBytesLittleEndian((uint)(sectors >= 0x10000 ? sectors : 0), bootSector, 32); + + if (fatType < FatType.Fat32) + { + WriteBS(bootSector, 36, isFloppy, volId, label, fatType); + } + else + { + // FAT size (FAT32) + EndianUtilities.WriteBytesLittleEndian((uint)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 = (uint)(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) ? (uint)bpbFATSz16 : bpbFATSz32; + uint totalSec = (bpbTotSec16 != 0) ? (uint)bpbTotSec16 : bpbTotSec32; + + uint dataSec = totalSec - (bpbResvdSecCnt + (bpbNumFATs * fatSz) + rootDirSectors); + uint countOfClusters = dataSec / bpbSecPerClus; + + if (countOfClusters < 4085) + { + return FatType.Fat12; + } + else if (countOfClusters < 65525) + { + return FatType.Fat16; + } + else + { + 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); + + _type = DetectFATType(_bootSector); + + ReadBPB(); + + LoadFAT(); + + LoadClusterReader(); + + LoadRootDirectory(); + } + + private void LoadClusterReader() + { + int rootDirSectors = ((_bpbRootEntCnt * 32) + (_bpbBytesPerSec - 1)) / _bpbBytesPerSec; + int firstDataSector = (int)(_bpbRsvdSecCnt + (_bpbNumFATs * FatSize) + rootDirSectors); + _clusterReader = new ClusterReader(_data, firstDataSector, _bpbSecPerClus, _bpbBytesPerSec); + } + + private void LoadRootDirectory() + { + Stream fatStream; + if (_type != FatType.Fat32) + { + fatStream = new SubStream(_data, (_bpbRsvdSecCnt + (_bpbNumFATs * _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(_type, _data, _bpbRsvdSecCnt, (uint)FatSize, (byte)FatCount, ActiveFat); + } + + private void ReadBPB() + { + _bpbOEMName = Encoding.ASCII.GetString(_bootSector, 3, 8).TrimEnd('\0'); + _bpbBytesPerSec = EndianUtilities.ToUInt16LittleEndian(_bootSector, 11); + _bpbSecPerClus = _bootSector[13]; + _bpbRsvdSecCnt = EndianUtilities.ToUInt16LittleEndian(_bootSector, 14); + _bpbNumFATs = _bootSector[16]; + _bpbRootEntCnt = EndianUtilities.ToUInt16LittleEndian(_bootSector, 17); + _bpbTotSec16 = EndianUtilities.ToUInt16LittleEndian(_bootSector, 19); + _bpbMedia = _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 (_type != 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) + { + _bsDrvNum = _bootSector[offset]; + _bsBootSig = _bootSector[offset + 2]; + _bsVolId = EndianUtilities.ToUInt32LittleEndian(_bootSector, offset + 3); + _bsVolLab = Encoding.ASCII.GetString(_bootSector, offset + 7, 11); + _bsFilSysType = Encoding.ASCII.GetString(_bootSector, offset + 18, 8); + } + + private long GetDirectoryEntry(Directory dir, string path, out Directory parent) + { + string[] pathElements = path.Split(new char[] { '\\' }, 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; + } + else + { + // Long filename support - translate long filename lookup to short filename + string ShortName; + if (dir.LongFileNames_LongKey.TryGetValue(pathEntries[pathOffset], out ShortName)) + pathEntries[pathOffset] = ShortName; + + entryId = dir.FindEntry(new FileName(pathEntries[pathOffset], FatOptions.FileNameEncoding)); + if (entryId >= 0) + { + if (pathOffset == pathEntries.Length - 1) + { + parent = dir; + return entryId; + } + else + { + return GetDirectoryEntry(GetDirectory(dir, entryId), pathEntries, pathOffset + 1, out parent); + } + } + else if (pathOffset == pathEntries.Length - 1) + { + parent = dir; + return -1; + } + else + { + 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; } } + + /// + /// Gets the long name of a given file. + /// + /// The short full path to the file. Input path segments must be short names. + /// The corresponding long file name. + public string GetLongFileName(string shortFullPath) + { + if (shortFullPath == null) + throw new ArgumentNullException("shortFullPath"); + + string dirPath = Path.GetDirectoryName(shortFullPath); + string fileName = Path.GetFileName(shortFullPath); + Directory dir = GetDirectory(dirPath); + if (dir == null) + return fileName; + + string lfn; + if (dir.LongFileNames_ShortKey.TryGetValue(Path.GetFileName(shortFullPath), out lfn)) + return lfn; + + return fileName; + } + + /// + /// Gets the long path to a given file. + /// + /// The short full path to the file. Input path segments must be short names. + /// The corresponding long file path to the file or null if not found. + public string GetLongFilePath(string shortFullPath) + { + if (shortFullPath == null) + throw new ArgumentNullException("shortFullPath"); + + string path = null; + string current = null; + foreach (string segment in shortFullPath.Split(Path.DirectorySeparatorChar)) + { + if (current == null) + { + current = segment; + path = GetLongFileName(current); + } + else + { + current = Path.Combine(current, segment); + path = Path.Combine(path, GetLongFileName(current)); + } + } + return path; + } + } +} diff --git a/DiscUtils/DiscUtils.Fat/FatFileSystemOptions.cs b/DiscUtils/DiscUtils.Fat/FatFileSystemOptions.cs new file mode 100644 index 0000000..10ec867 --- /dev/null +++ b/DiscUtils/DiscUtils.Fat/FatFileSystemOptions.cs @@ -0,0 +1,75 @@ +// +// 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. +// + +namespace DiscUtils.Fat +{ + using System; + using System.Text; + + /// + /// FAT file system options. + /// + public sealed class FatFileSystemOptions : DiscFileSystemOptions + { + private Encoding _encoding; + + internal FatFileSystemOptions() + { + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + FileNameEncoding = Encoding.GetEncoding(437); + } + + internal FatFileSystemOptions(FileSystemParameters parameters) + { + if (parameters != null && parameters.FileNameEncoding != null) + { + FileNameEncoding = parameters.FileNameEncoding; + } + else + { + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + FileNameEncoding = Encoding.GetEncoding(437); + } + } + + /// + /// Gets or sets the character encoding used for file names. + /// + public Encoding FileNameEncoding + { + get + { + return _encoding; + } + + set + { + if (!value.IsSingleByte) + { + throw new ArgumentException(value.EncodingName + " is not a single byte encoding"); + } + + _encoding = value; + } + } + } +} diff --git a/DiscUtils/DiscUtils.Fat/FatType.cs b/DiscUtils/DiscUtils.Fat/FatType.cs new file mode 100644 index 0000000..c85fb96 --- /dev/null +++ b/DiscUtils/DiscUtils.Fat/FatType.cs @@ -0,0 +1,50 @@ +// +// 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. +// + +namespace DiscUtils.Fat +{ + /// + /// Enumeration of known FAT types. + /// + public enum FatType + { + /// + /// Represents no known FAT type. + /// + None = 0, + + /// + /// Represents a 12-bit FAT. + /// + Fat12 = 12, + + /// + /// Represents a 16-bit FAT. + /// + Fat16 = 16, + + /// + /// Represents a 32-bit FAT. + /// + Fat32 = 32 + } +} \ No newline at end of file diff --git a/DiscUtils/DiscUtils.Fat/FileAllocationTable.cs b/DiscUtils/DiscUtils.Fat/FileAllocationTable.cs new file mode 100644 index 0000000..c034477 --- /dev/null +++ b/DiscUtils/DiscUtils.Fat/FileAllocationTable.cs @@ -0,0 +1,106 @@ +// +// 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.IO; +using DiscUtils.Streams; + +namespace DiscUtils.Fat +{ + internal class FileAllocationTable + { + private readonly FatBuffer _buffer; + private readonly ushort _firstFatSector; + private readonly byte _numFats; + private readonly Stream _stream; + + public FileAllocationTable(FatType type, Stream stream, ushort firstFatSector, uint fatSize, byte numFats, + byte activeFat) + { + _stream = stream; + _firstFatSector = firstFatSector; + _numFats = numFats; + + _stream.Position = (firstFatSector + fatSize * activeFat) * Sizes.Sector; + _buffer = new FatBuffer(type, StreamUtilities.ReadExact(_stream, (int)(fatSize * Sizes.Sector))); + } + + internal bool IsFree(uint val) + { + return _buffer.IsFree(val); + } + + internal bool IsEndOfChain(uint val) + { + return _buffer.IsEndOfChain(val); + } + + internal bool IsBadCluster(uint val) + { + return _buffer.IsBadCluster(val); + } + + internal uint GetNext(uint cluster) + { + return _buffer.GetNext(cluster); + } + + internal void SetEndOfChain(uint cluster) + { + _buffer.SetEndOfChain(cluster); + } + + internal void SetBadCluster(uint cluster) + { + _buffer.SetBadCluster(cluster); + } + + internal void SetNext(uint cluster, uint next) + { + _buffer.SetNext(cluster, next); + } + + internal void Flush() + { + for (int i = 0; i < _numFats; ++i) + { + _buffer.WriteDirtyRegions(_stream, _firstFatSector * Sizes.Sector + _buffer.Size * i); + } + + _buffer.ClearDirtyRegions(); + } + + internal bool TryGetFreeCluster(out uint cluster) + { + return _buffer.TryGetFreeCluster(out cluster); + } + + internal void FreeChain(uint head) + { + _buffer.FreeChain(head); + } + + internal int NumEntries + { + get { return _buffer.NumEntries; } + } + } +} \ No newline at end of file diff --git a/DiscUtils/DiscUtils.Fat/FileName.cs b/DiscUtils/DiscUtils.Fat/FileName.cs new file mode 100644 index 0000000..d4e4767 --- /dev/null +++ b/DiscUtils/DiscUtils.Fat/FileName.cs @@ -0,0 +1,213 @@ +// +// 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. +// + +namespace DiscUtils.Fat +{ + using DiscUtils.Internal; + using System; + using System.Text; + + internal sealed class FileName : IEquatable + { + public static readonly FileName SelfEntryName = new FileName(new byte[] { 0x2E, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 }, 0); + public static readonly FileName ParentEntryName = new FileName(new byte[] { 0x2E, 0x2E, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 }, 0); + public static readonly FileName Null = new FileName(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, 0); + + private const byte SpaceByte = 0x20; + + private static readonly byte[] InvalidBytes = new byte[] { 0x22, 0x2A, 0x2B, 0x2C, 0x2E, 0x2F, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x5B, 0x5C, 0x5D, 0x7C }; + + private byte[] _raw; + + public FileName(byte[] data, int offset) + { + _raw = new byte[11]; + Array.Copy(data, offset, _raw, 0, 11); + } + + public FileName(string name, Encoding encoding) + { + _raw = new byte[11]; + byte[] bytes = encoding.GetBytes(name.ToUpperInvariant()); + + int nameIdx = 0; + int rawIdx = 0; + while (nameIdx < bytes.Length && bytes[nameIdx] != '.' && rawIdx < _raw.Length) + { + byte b = bytes[nameIdx++]; + if (b < 0x20 || Contains(InvalidBytes, b)) + { + throw new ArgumentException("Invalid character in file name '" + (char)b + "'", "name"); + } + + _raw[rawIdx++] = b; + } + + if (rawIdx > 8) + { + throw new ArgumentException("File name too long '" + name + "'", "name"); + } + else if (rawIdx == 0) + { + throw new ArgumentException("File name too short '" + name + "'", "name"); + } + + while (rawIdx < 8) + { + _raw[rawIdx++] = SpaceByte; + } + + if (nameIdx < bytes.Length && bytes[nameIdx] == '.') + { + ++nameIdx; + } + + while (nameIdx < bytes.Length && rawIdx < _raw.Length) + { + byte b = bytes[nameIdx++]; + if (b < 0x20 || Contains(InvalidBytes, b)) + { + throw new ArgumentException("Invalid character in file extension '" + (char)b + "'", "name"); + } + + _raw[rawIdx++] = b; + } + + while (rawIdx < 11) + { + _raw[rawIdx++] = SpaceByte; + } + + if (nameIdx != bytes.Length) + { + throw new ArgumentException("File extension too long '" + name + "'", "name"); + } + } + + public static FileName FromPath(string path, Encoding encoding) + { + return new FileName(Utilities.GetFileFromPath(path), encoding); + } + + public static bool operator ==(FileName a, FileName b) + { + return CompareRawNames(a, b) == 0; + } + + public static bool operator !=(FileName a, FileName b) + { + return CompareRawNames(a, b) != 0; + } + + public string GetDisplayName(Encoding encoding) + { + return GetSearchName(encoding).TrimEnd('.'); + } + + public string GetSearchName(Encoding encoding) + { + return encoding.GetString(_raw, 0, 8).TrimEnd() + "." + encoding.GetString(_raw, 8, 3).TrimEnd(); + } + + public string GetRawName(Encoding encoding) + { + return encoding.GetString(_raw, 0, 11).TrimEnd(); + } + + public FileName Deleted() + { + byte[] data = new byte[11]; + Array.Copy(_raw, data, 11); + data[0] = 0xE5; + + return new FileName(data, 0); + } + + public bool IsDeleted() + { + return _raw[0] == 0xE5; + } + + public bool IsEndMarker() + { + return _raw[0] == 0x00; + } + + public void GetBytes(byte[] data, int offset) + { + Array.Copy(_raw, 0, data, offset, 11); + } + + public override bool Equals(object other) + { + return Equals(other as FileName); + } + + public bool Equals(FileName other) + { + if (other == null) + { + return false; + } + + return CompareRawNames(this, other) == 0; + } + + public override int GetHashCode() + { + int val = 0x1A8D3C4E; + + for (int i = 0; i < 11; ++i) + { + val = (val << 2) ^ _raw[i]; + } + + return val; + } + + private static int CompareRawNames(FileName a, FileName b) + { + for (int i = 0; i < 11; ++i) + { + if (a._raw[i] != b._raw[i]) + { + return (int)a._raw[i] - (int)b._raw[i]; + } + } + + return 0; + } + + private static bool Contains(byte[] array, byte val) + { + foreach (byte b in array) + { + if (b == val) + { + return true; + } + } + + return false; + } + } +} diff --git a/DiscUtils/DiscUtils.Fat/FileSystemFactory.cs b/DiscUtils/DiscUtils.Fat/FileSystemFactory.cs new file mode 100644 index 0000000..25982f7 --- /dev/null +++ b/DiscUtils/DiscUtils.Fat/FileSystemFactory.cs @@ -0,0 +1,47 @@ +// +// 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.IO; +using DiscUtils.Vfs; +using DiscUtils.Streams; + +namespace DiscUtils.Fat +{ + [VfsFileSystemFactory] + internal class FileSystemFactory : VfsFileSystemFactory + { + public override FileSystemInfo[] Detect(Stream stream, VolumeInfo volume) + { + if (FatFileSystem.Detect(stream)) + { + return new FileSystemInfo[] { new VfsFileSystemInfo("FAT", "Microsoft FAT", Open) }; + } + + return new FileSystemInfo[0]; + } + + private DiscFileSystem Open(Stream stream, VolumeInfo volumeInfo, FileSystemParameters parameters) + { + return new FatFileSystem(stream, Ownership.None, parameters); + } + } +} \ No newline at end of file diff --git a/DiscUtils/DiscUtils.Fat/FirstClusterChangedDelegate.cs b/DiscUtils/DiscUtils.Fat/FirstClusterChangedDelegate.cs new file mode 100644 index 0000000..a88e652 --- /dev/null +++ b/DiscUtils/DiscUtils.Fat/FirstClusterChangedDelegate.cs @@ -0,0 +1,4 @@ +namespace DiscUtils.Fat +{ + internal delegate void FirstClusterChangedDelegate(uint cluster); +} \ No newline at end of file diff --git a/Models/FFU.cs b/Models/FFU.cs index 599ebfa..0531f70 100644 --- a/Models/FFU.cs +++ b/Models/FFU.cs @@ -480,7 +480,7 @@ namespace WPinternals byte[] efiesp = GetPartition("EFIESP"); MemoryStream s = new(efiesp); DiscUtils.Fat.FatFileSystem fs = new(s); - Stream mss = fs.OpenFile(@"Windows\System32\Boot\mobilestartup.efi", FileMode.Open, FileAccess.Read); + Stream mss = fs.OpenFile(@"\Windows\System32\Boot\mobilestartup.efi", FileMode.Open, FileAccess.Read); MemoryStream msms = new(); mss.CopyTo(msms); byte[] mobilestartup = msms.ToArray(); diff --git a/ViewModels/LumiaUnlockBootloaderViewModel.cs b/ViewModels/LumiaUnlockBootloaderViewModel.cs index 4dfe134..f54ad88 100644 --- a/ViewModels/LumiaUnlockBootloaderViewModel.cs +++ b/ViewModels/LumiaUnlockBootloaderViewModel.cs @@ -2753,7 +2753,7 @@ namespace WPinternals using (DiscUtils.Fat.FatFileSystem SupportedEFIESPFileSystem = new(new MemoryStream(SupportedEFIESP))) using (DiscUtils.Streams.SparseStream SupportedMobileStartupStream = SupportedEFIESPFileSystem.OpenFile(@"\Windows\System32\Boot\mobilestartup.efi", FileMode.Open)) using (MemoryStream SupportedMobileStartupMemStream = new()) - using (Stream MobileStartupStream = EFIESPFileSystem.OpenFile(@"Windows\System32\Boot\mobilestartup.efi", FileMode.Create, FileAccess.Write)) + using (Stream MobileStartupStream = EFIESPFileSystem.OpenFile(@"\Windows\System32\Boot\mobilestartup.efi", FileMode.Create, FileAccess.Write)) { SupportedMobileStartupStream.CopyTo(SupportedMobileStartupMemStream); byte[] SupportedMobileStartup = SupportedMobileStartupMemStream.ToArray(); @@ -2771,7 +2771,7 @@ namespace WPinternals } LogFile.Log("Edit BCD"); - using Stream BCDFileStream = EFIESPFileSystem.OpenFile(@"efi\Microsoft\Boot\BCD", FileMode.Open, FileAccess.ReadWrite); + using Stream BCDFileStream = EFIESPFileSystem.OpenFile(@"\efi\Microsoft\Boot\BCD", FileMode.Open, FileAccess.ReadWrite); using DiscUtils.Registry.RegistryHive BCDHive = new(BCDFileStream); DiscUtils.BootConfig.Store BCDStore = new(BCDHive.Root); DiscUtils.BootConfig.BcdObject MobileStartupObject = BCDStore.GetObject(new Guid("{01de5a27-8705-40db-bad6-96fa5187d4a6}")); diff --git a/WPinternals.csproj b/WPinternals.csproj index af9823b..3bb0c78 100644 --- a/WPinternals.csproj +++ b/WPinternals.csproj @@ -354,7 +354,6 @@ - @@ -364,6 +363,10 @@ + + + + "C:\Program Files\PsTools\pskill.exe" XDesProc.exe 2>nul 1>nul