// // Copyright (c) 2008-2011, Kenneth Bell // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. // using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text.RegularExpressions; using DiscUtils.Internal; using DiscUtils.Streams; namespace DiscUtils.Vfs { /// /// Base class for VFS file systems. /// /// The concrete type representing directory entries. /// The concrete type representing files. /// The concrete type representing directories. /// The concrete type holding global state. public abstract class VfsFileSystem : DiscFileSystem where TDirEntry : VfsDirEntry where TFile : IVfsFile where TDirectory : class, IVfsDirectory, TFile where TContext : VfsContext { private readonly ObjectCache _fileCache; /// /// Initializes a new instance of the VfsFileSystem class. /// /// The default file system options. protected VfsFileSystem(DiscFileSystemOptions defaultOptions) : base(defaultOptions) { _fileCache = new ObjectCache(); } /// /// Gets or sets the global shared state. /// protected TContext Context { get; set; } /// /// Gets or sets the object representing the root directory. /// protected TDirectory RootDirectory { get; set; } /// /// Gets the volume label. /// public abstract override string VolumeLabel { get; } /// /// Copies a file - not supported on read-only file systems. /// /// 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) { throw new NotImplementedException(); } /// /// Creates a directory - not supported on read-only file systems. /// /// The path of the new directory. public override void CreateDirectory(string path) { throw new NotImplementedException(); } /// /// Deletes a directory - not supported on read-only file systems. /// /// The path of the directory to delete. public override void DeleteDirectory(string path) { throw new NotImplementedException(); } /// /// Deletes a file - not supported on read-only file systems. /// /// The path of the file to delete. public override void DeleteFile(string path) { throw new NotImplementedException(); } /// /// Indicates if a directory exists. /// /// The path to test. /// true if the directory exists. public override bool DirectoryExists(string path) { if (IsRoot(path)) { return true; } TDirEntry dirEntry = GetDirectoryEntry(path); if (dirEntry != null) { return dirEntry.IsDirectory; } return false; } /// /// Indicates if a file exists. /// /// The path to test. /// true if the file exists. public override bool FileExists(string path) { TDirEntry dirEntry = GetDirectoryEntry(path); if (dirEntry != null) { return !dirEntry.IsDirectory; } return false; } /// /// 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 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) { string fullPath = path; if (!fullPath.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) { fullPath = @"\" + fullPath; } TDirectory parentDir = GetDirectory(fullPath); return Utilities.Map(parentDir.AllEntries, m => Utilities.CombinePaths(fullPath, FormatFileName(m.FileName))); } /// /// 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); TDirectory parentDir = GetDirectory(path); List result = new List(); foreach (TDirEntry dirEntry in parentDir.AllEntries) { if (re.IsMatch(dirEntry.SearchName)) { result.Add(Utilities.CombinePaths(path, dirEntry.FileName)); } } return result.ToArray(); } /// /// Moves a directory. /// /// The directory to move. /// The target directory name. public override void MoveDirectory(string sourceDirectoryName, string destinationDirectoryName) { throw new NotImplementedException(); } /// /// Moves a file. /// /// The file to move. /// The target file name. /// Overwrite any existing file. public override void MoveFile(string sourceName, string destinationName, bool overwrite) { throw new NotImplementedException(); } /// /// Opens the specified file. /// /// The full path of the file to open. /// The file mode for the created stream. /// The access permissions for the created stream. /// The new stream. public override SparseStream OpenFile(string path, FileMode mode, FileAccess access) { if (!CanWrite) { if (mode != FileMode.Open) { throw new NotSupportedException("Only existing files can be opened"); } if (access != FileAccess.Read) { throw new NotSupportedException("Files cannot be opened for write"); } } string fileName = Utilities.GetFileFromPath(path); string attributeName = null; int streamSepPos = fileName.IndexOf(':'); if (streamSepPos >= 0) { attributeName = fileName.Substring(streamSepPos + 1); } string dirName; try { dirName = Utilities.GetDirectoryFromPath(path); } catch (ArgumentException) { throw new IOException("Invalid path: " + path); } string entryPath = Utilities.CombinePaths(dirName, fileName); TDirEntry entry = GetDirectoryEntry(entryPath); if (entry == null) { if (mode == FileMode.Open) { throw new FileNotFoundException("No such file", path); } TDirectory parentDir = GetDirectory(Utilities.GetDirectoryFromPath(path)); entry = parentDir.CreateNewFile(Utilities.GetFileFromPath(path)); } else if (mode == FileMode.CreateNew) { throw new IOException("File already exists"); } if (entry.IsSymlink) { entry = ResolveSymlink(entry, entryPath); } if (entry.IsDirectory) { throw new IOException("Attempt to open directory as a file"); } TFile file = GetFile(entry); SparseStream stream = null; if (string.IsNullOrEmpty(attributeName)) { stream = new BufferStream(file.FileContent, access); } else { IVfsFileWithStreams fileStreams = file as IVfsFileWithStreams; if (fileStreams != null) { stream = fileStreams.OpenExistingStream(attributeName); if (stream == null) { if (mode == FileMode.Create || mode == FileMode.OpenOrCreate) { stream = fileStreams.CreateStream(attributeName); } else { throw new FileNotFoundException("No such attribute on file", path); } } } else { throw new NotSupportedException( "Attempt to open a file stream on a file system that doesn't support them"); } } if (mode == FileMode.Create || mode == FileMode.Truncate) { stream.SetLength(0); } return stream; } /// /// 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) { if (IsRoot(path)) { return RootDirectory.FileAttributes; } TDirEntry dirEntry = GetDirectoryEntry(path); if (dirEntry == null) { throw new FileNotFoundException("File not found", path); } if (dirEntry.HasVfsFileAttributes) { return dirEntry.FileAttributes; } return GetFile(dirEntry).FileAttributes; } /// /// 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) { throw new NotImplementedException(); } /// /// 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 (IsRoot(path)) { return RootDirectory.CreationTimeUtc; } TDirEntry dirEntry = GetDirectoryEntry(path); if (dirEntry == null) { throw new FileNotFoundException("No such file or directory", path); } if (dirEntry.HasVfsTimeInfo) { return dirEntry.CreationTimeUtc; } return GetFile(dirEntry).CreationTimeUtc; } /// /// 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) { throw new NotImplementedException(); } /// /// Gets the last access time (in UTC) of a file or directory. /// /// The path of the file or directory. /// The last access time. public override DateTime GetLastAccessTimeUtc(string path) { if (IsRoot(path)) { return RootDirectory.LastAccessTimeUtc; } TDirEntry dirEntry = GetDirectoryEntry(path); if (dirEntry == null) { throw new FileNotFoundException("No such file or directory", path); } if (dirEntry.HasVfsTimeInfo) { return dirEntry.LastAccessTimeUtc; } return GetFile(dirEntry).LastAccessTimeUtc; } /// /// 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) { throw new NotImplementedException(); } /// /// Gets the last modification time (in UTC) of a file or directory. /// /// The path of the file or directory. /// The last write time. public override DateTime GetLastWriteTimeUtc(string path) { if (IsRoot(path)) { return RootDirectory.LastWriteTimeUtc; } TDirEntry dirEntry = GetDirectoryEntry(path); if (dirEntry == null) { throw new FileNotFoundException("No such file or directory", path); } if (dirEntry.HasVfsTimeInfo) { return dirEntry.LastWriteTimeUtc; } return GetFile(dirEntry).LastWriteTimeUtc; } /// /// 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) { throw new NotImplementedException(); } /// /// Gets the length of a file. /// /// The path to the file. /// The length in bytes. public override long GetFileLength(string path) { TFile file = GetFile(path); if (file == null || (file.FileAttributes & FileAttributes.Directory) != 0) { throw new FileNotFoundException("No such file", path); } return file.FileLength; } internal TFile GetFile(TDirEntry dirEntry) { long cacheKey = dirEntry.UniqueCacheId; TFile file = _fileCache[cacheKey]; if (file == null) { file = ConvertDirEntryToFile(dirEntry); _fileCache[cacheKey] = file; } return file; } internal TDirectory GetDirectory(string path) { if (IsRoot(path)) { return RootDirectory; } TDirEntry dirEntry = GetDirectoryEntry(path); if (dirEntry != null && dirEntry.IsSymlink) { dirEntry = ResolveSymlink(dirEntry, path); } if (dirEntry == null || !dirEntry.IsDirectory) { throw new DirectoryNotFoundException("No such directory: " + path); } return (TDirectory)GetFile(dirEntry); } internal TDirEntry GetDirectoryEntry(string path) { return GetDirectoryEntry(RootDirectory, path); } /// /// Gets all directory entries in the specified directory and sub-directories. /// /// The path to inspect. /// Delegate invoked for each directory entry. protected void ForAllDirEntries(string path, DirEntryHandler handler) { TDirectory dir = null; TDirEntry self = GetDirectoryEntry(path); if (self != null) { handler(path, self); if (self.IsDirectory) { dir = GetFile(self) as TDirectory; } } else { dir = GetFile(path) as TDirectory; } if (dir != null) { foreach (TDirEntry subentry in dir.AllEntries) { ForAllDirEntries(Utilities.CombinePaths(path, subentry.FileName), handler); } } } /// /// Gets the file object for a given path. /// /// The path to query. /// The file object corresponding to the path. protected TFile GetFile(string path) { if (IsRoot(path)) { return RootDirectory; } if (path == null) { return default(TFile); } TDirEntry dirEntry = GetDirectoryEntry(path); if (dirEntry == null) { throw new FileNotFoundException("No such file or directory", path); } return GetFile(dirEntry); } /// /// Converts a directory entry to an object representing a file. /// /// The directory entry to convert. /// The corresponding file object. protected abstract TFile ConvertDirEntryToFile(TDirEntry dirEntry); /// /// Converts an internal directory entry name into an external one. /// /// The name to convert. /// The external name. /// /// This method is called on a single path element (i.e. name contains no path /// separators). /// protected virtual string FormatFileName(string name) { return name; } private static bool IsRoot(string path) { return string.IsNullOrEmpty(path) || path == @"\"; } private TDirEntry GetDirectoryEntry(TDirectory dir, string path) { string[] pathElements = path.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); return GetDirectoryEntry(dir, pathElements, 0); } private TDirEntry GetDirectoryEntry(TDirectory dir, string[] pathEntries, int pathOffset) { TDirEntry entry; if (pathEntries.Length == 0) { return dir.Self; } entry = dir.GetEntryByName(pathEntries[pathOffset]); if (entry != null) { if (pathOffset == pathEntries.Length - 1) { return entry; } if (entry.IsDirectory) { return GetDirectoryEntry((TDirectory)ConvertDirEntryToFile(entry), pathEntries, pathOffset + 1); } throw new IOException(string.Format(CultureInfo.InvariantCulture, "{0} is a file, not a directory", pathEntries[pathOffset])); } return null; } private void DoSearch(List results, string path, Regex regex, bool subFolders, bool dirs, bool files) { TDirectory parentDir = GetDirectory(path); if (parentDir == null) { throw new DirectoryNotFoundException(string.Format(CultureInfo.InvariantCulture, "The directory '{0}' was not found", path)); } string resultPrefixPath = path; if (IsRoot(path)) { resultPrefixPath = @"\"; } foreach (TDirEntry de in parentDir.AllEntries) { TDirEntry entry = de; if (entry.IsSymlink) { entry = ResolveSymlink(entry, path + "\\" + entry.FileName); } bool isDir = entry.IsDirectory; if ((isDir && dirs) || (!isDir && files)) { if (regex.IsMatch(de.SearchName)) { results.Add(Utilities.CombinePaths(resultPrefixPath, FormatFileName(entry.FileName))); } } if (subFolders && isDir) { DoSearch(results, Utilities.CombinePaths(resultPrefixPath, FormatFileName(entry.FileName)), regex, subFolders, dirs, files); } } } private TDirEntry ResolveSymlink(TDirEntry entry, string path) { TDirEntry currentEntry = entry; if (path.Length > 0 && path[0] != '\\') { path = '\\' + path; } string currentPath = path; int resolvesLeft = 20; while (currentEntry.IsSymlink && resolvesLeft > 0) { IVfsSymlink symlink = GetFile(currentEntry) as IVfsSymlink; if (symlink == null) { throw new FileNotFoundException("Unable to resolve symlink", path); } currentPath = Utilities.ResolvePath(currentPath.TrimEnd('\\'), symlink.TargetPath); currentEntry = GetDirectoryEntry(currentPath); if (currentEntry == null) { throw new FileNotFoundException("Unable to resolve symlink", path); } --resolvesLeft; } if (currentEntry.IsSymlink) { throw new FileNotFoundException("Unable to resolve symlink - too many links", path); } return currentEntry; } /// /// Delegate for processing directory entries. /// /// Full path to the directory entry. /// The directory entry itself. protected delegate void DirEntryHandler(string path, TDirEntry dirEntry); } }