Created
December 11, 2012 22:52
-
-
Save shilrobot/4263088 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Text; | |
using System.IO; | |
using System.Linq; | |
using Microsoft.Xna.Framework.Content; | |
namespace Platformer | |
{ | |
/// <summary> | |
/// The different types a VFS entry can have. | |
/// </summary> | |
public enum VfsEntryType | |
{ | |
/// <summary> | |
/// The VFS entry is a file. | |
/// </summary> | |
File, | |
/// <summary> | |
/// The VFS entry is a directory. | |
/// </summary> | |
Directory, | |
} | |
/// <summary> | |
/// Information about a VFS entry. | |
/// </summary> | |
public class VfsEntryInfo | |
{ | |
/// <summary> | |
/// Type of this entry (directory or file.) | |
/// </summary> | |
public VfsEntryType Type { get; private set; } | |
/// <summary> | |
/// If a file, the length of the file in bytes. Zero otherwise. | |
/// </summary> | |
public long Length { get; private set; } | |
/// <summary> | |
/// Date this entry was last modified. | |
/// </summary> | |
public DateTime ModificationDate { get; private set; } | |
/// <summary> | |
/// Constructor. | |
/// </summary> | |
/// <param name="type">Type of this entry (file or directory.)</param> | |
/// <param name="modificationDate">The date this entry was last modified.</param> | |
/// <param name="length">For files, the length of the file; for directories, zero.</param> | |
public VfsEntryInfo(VfsEntryType type, DateTime modificationDate, long length=0) | |
{ | |
if (length < 0) | |
throw new ArgumentOutOfRangeException("length"); | |
if (type == VfsEntryType.Directory && length != 0) | |
throw new ArgumentException("length must be zero for directories", "length"); | |
Type = type; | |
ModificationDate = modificationDate; | |
Length = length; | |
} | |
} | |
/// <summary> | |
/// A plugin that supplies backend FS operations to the VFS. | |
/// </summary> | |
/// <remarks> | |
/// A plugin might use as its backing store the normal host filesystem, | |
/// a zip file, resources embedded in the .EXE, etc. | |
/// This interface is intentionally minimal to minimize the work needed to support | |
/// different backends. | |
/// </remarks> | |
public interface IVfsPlugin | |
{ | |
/// <summary> | |
/// Looks up information about a VFS entry. | |
/// </summary> | |
/// <param name="path">Path to look up information for.</param> | |
/// <param name="info">Information about the entry at that path, if it exists. Null otherwise.</param> | |
/// <returns>True if the entry exists, false otherwise.</returns> | |
bool GetEntryInfo(string path, out VfsEntryInfo info); | |
/// <summary> | |
/// Returns a list of the names of entries in a directory. | |
/// </summary> | |
/// <param name="path">Path to list the contents of.</param> | |
/// <returns>An array of names of entries in this directory. Names do not include any directory information.</returns> | |
string[] ListDirectory(string path); | |
/// <summary> | |
/// Opens a readable stream from a file path. | |
/// </summary> | |
/// <param name="path">Path of the file to open.</param> | |
/// <returns>Readable stream.</returns> | |
Stream OpenStream(string path); | |
/// <summary> | |
/// Attempts to get a real filesystem path for an entry. | |
/// </summary> | |
/// <param name="path">Path to get the real filesystem equivalent of.</param> | |
/// <returns>An absolute filesystem path if it exists; otherwise, null.</returns> | |
String GetRealPath(string path); | |
} | |
/// <summary> | |
/// A plugin that uses the normal host filesystem as its backing store. | |
/// </summary> | |
public class NormalFSPlugin : IVfsPlugin | |
{ | |
private string root; | |
/// <summary> | |
/// Constructor. | |
/// </summary> | |
/// <param name="root">Root directory in the normal filesystem.</param> | |
public NormalFSPlugin(string root) | |
{ | |
if (root == null) | |
throw new ArgumentNullException("root"); | |
this.root = Path.GetFullPath(root); | |
} | |
public bool GetEntryInfo(string path, out VfsEntryInfo info) | |
{ | |
info = null; | |
string full = Path.Combine(root, path); | |
FileInfo fi = new FileInfo(full); | |
if (fi.Exists) | |
{ | |
WarnPathCase(path); | |
info = new VfsEntryInfo(VfsEntryType.File, fi.LastWriteTime, fi.Length); | |
return true; | |
} | |
DirectoryInfo di = new DirectoryInfo(full); | |
if (di.Exists) | |
{ | |
WarnPathCase(path); | |
info = new VfsEntryInfo(VfsEntryType.Directory, fi.LastWriteTime, 0); | |
return true; | |
} | |
return false; | |
} | |
public string[] ListDirectory(string path) | |
{ | |
List<string> list = new List<string>(Directory.EnumerateFileSystemEntries(Path.Combine(root, path))); | |
WarnPathCase(path); | |
for (int i = 0; i < list.Count; ++i) | |
list[i] = Path.GetFileName(list[i]); | |
return list.ToArray(); | |
} | |
public Stream OpenStream(string path) | |
{ | |
var fs = new FileStream(Path.Combine(root, path), FileMode.Open, FileAccess.Read, FileShare.Read); | |
WarnPathCase(path); | |
return fs; | |
} | |
public string GetRealPath(string path) | |
{ | |
return Path.Combine(root, path); | |
} | |
private void WarnPathCase(string path) | |
{ | |
/* | |
var parts = path.Split(new char[] { '/' }); | |
var currDir = root; | |
foreach (var part in parts) | |
{ | |
var entries = from x in Directory.EnumerateFileSystemEntries(currDir) | |
select Path.GetFileName(x); | |
if (!entries.Contains(part)) | |
{ | |
bool found = false; | |
foreach (var entry in entries) | |
{ | |
if (StringComparer.OrdinalIgnoreCase.Equals(entry, part)) | |
{ | |
Log.Warning("Case sensitivity issue: {0}: \"{1}\" is actually \"{2}\" on disk", path, part, entry); | |
found = true; | |
break; | |
} | |
} | |
if (!found) | |
{ | |
// path doesn't even exist...? | |
Log.Warning("Case sensitivity issue??? {0}: path element \"{1}\" cannot be found", path, part); | |
return; | |
} | |
} | |
currDir = Path.Combine(currDir, part); | |
} | |
*/ | |
} | |
} | |
/// <summary> | |
/// A global virtual filesystem for accessing read-only game content. | |
/// </summary> | |
/// <remarks> | |
/// The VFS is extensible with plugins to allow loading resources from arbitrary sources (filesystem, pack files, etc.) | |
/// Plugins' filesystems are "overlaid" on top of each other, with the first plugin to be added taking priority. | |
/// Paths provided to VFS functions are always "absolute" VFS paths -- there is no concept of a current VFS directory. | |
/// </remarks> | |
public class Vfs | |
{ | |
private List<IVfsPlugin> plugins = new List<IVfsPlugin>(); | |
private static char[] splits = new char[] { '/', '\\' }; | |
public Vfs() | |
{ | |
} | |
/// <summary> | |
/// Converts a path that may contain ".", "..", backslashes, redundant slashes, etc. into a forward path using only forward slashes. | |
/// </summary> | |
/// <param name="path">Path to clean.</param> | |
/// <returns>Cleaned path.</returns> | |
/// <exception cref="System.InvalidOperationException">Path has ".." elements that try to go below root level, or contains drive specifiers.</exception> | |
public static string CleanPath(string path) | |
{ | |
if (path == null) | |
throw new ArgumentNullException("path"); | |
string[] parts = path.Split(splits); | |
List<string> actualParts = new List<string>(); | |
foreach (string part in parts) | |
{ | |
if (part == "..") | |
{ | |
if (actualParts.Count >= 1) | |
actualParts.RemoveAt(actualParts.Count - 1); | |
else | |
throw new InvalidOperationException("Path tries to go below root"); | |
} | |
else if (part == "." || part == "") | |
continue; | |
else if (part.EndsWith(":")) | |
throw new InvalidOperationException("Path appears to have a drive specifier"); | |
else | |
actualParts.Add(part); | |
} | |
return string.Join("/", actualParts); | |
} | |
/// <summary> | |
/// Combines two VFS path components, so that there is always a slash between them. | |
/// </summary> | |
/// <param name="a">First part of path.</param> | |
/// <param name="b">Second part of path.</param> | |
/// <returns>Combined path.</returns> | |
public static string Combine(string a, string b) | |
{ | |
// "" + "" = "" | |
// "" + "a" = "a" | |
// "a" + "" = "a" | |
// "a/" + "b" = "a/b" | |
if (a == "") | |
return b; | |
else if (b == "") | |
return a; | |
else if (a.EndsWith("/") || a.EndsWith("\\")) | |
return a + b; | |
else | |
return String.Format("{0}/{1}", a, b); | |
} | |
/// <summary> | |
/// Registers a VFS plugin with the VFS. | |
/// </summary> | |
/// <param name="plugin">Plugin to add.</param> | |
public void AddPlugin(IVfsPlugin plugin) | |
{ | |
if (plugin == null) | |
throw new ArgumentNullException("plugin"); | |
if (plugins.Contains(plugin)) | |
throw new InvalidOperationException("This plugin is already in use."); | |
plugins.Add(plugin); | |
} | |
/// <summary> | |
/// Unregisters a VFS plugin from the VFS. | |
/// </summary> | |
/// <param name="plugin">Plugin to remove.</param> | |
public void RemovePlugin(IVfsPlugin plugin) | |
{ | |
plugins.Remove(plugin); | |
} | |
/// <summary> | |
/// Returns information about a VFS entry if it is available. | |
/// </summary> | |
/// <param name="path">Path to get information about.</param> | |
/// <param name="info">Receives information regarding the entry if it is present; null otherwise.</param> | |
/// <returns>True if the entry was found, false otherwise.</returns> | |
public bool GetEntryInfo(string path, out VfsEntryInfo info) | |
{ | |
path = CleanPath(path); | |
info = null; | |
foreach (var plugin in plugins) | |
if (plugin.GetEntryInfo(path, out info)) | |
return true; | |
return false; | |
} | |
/// <summary> | |
/// Checks if a VFS path is a valid file. | |
/// </summary> | |
/// <param name="path">Path to check.</param> | |
/// <returns>True if path is a file, false otherwise.</returns> | |
public bool IsFile(string path) | |
{ | |
VfsEntryInfo info; | |
if (GetEntryInfo(path, out info)) | |
return info.Type == VfsEntryType.File; | |
else | |
return false; | |
} | |
/// <summary> | |
/// Checks if a VFS path is a valid directory. | |
/// </summary> | |
/// <param name="path">Path to check.</param> | |
/// <returns>True if path is a directory, false otherwise.</returns> | |
public bool IsDirectory(string path) | |
{ | |
VfsEntryInfo info; | |
if (GetEntryInfo(path, out info)) | |
return info.Type == VfsEntryType.Directory; | |
else | |
return false; | |
} | |
/// <summary> | |
/// Checks if an entry exists in the VFS. | |
/// </summary> | |
/// <param name="path">Path to verify the existence of.</param> | |
/// <returns>True if the file exists, false otherwise.</returns> | |
public bool Exists(string path) | |
{ | |
VfsEntryInfo info; | |
return GetEntryInfo(path, out info); | |
} | |
/// <summary> | |
/// Opens a readable stream for the given VFS path. | |
/// </summary> | |
/// <param name="path">Path of the file to open.</param> | |
/// <returns>Readable stream.</returns> | |
public Stream OpenStream(string path) | |
{ | |
path = CleanPath(path); | |
VfsEntryInfo info; | |
foreach (var plugin in plugins) | |
if (plugin.GetEntryInfo(path, out info)) | |
return plugin.OpenStream(path); | |
throw new FileNotFoundException("Cannot find file", path); | |
} | |
/// <summary> | |
/// Returns a list of file names in a VFS directory. | |
/// </summary> | |
/// <param name="path">The path of the directory to list.</param> | |
/// <param name="type">Whether to look for files or directories. If null, returns both.</param> | |
/// <param name="extension">An optional extension to match against. Should include the dot (e.g. ".xml")</param> | |
/// <returns>List of file names matching the filtering criteria.</returns> | |
public string[] ListDirectory(string path, VfsEntryType? type = null, string extension = null) | |
{ | |
path = CleanPath(path); | |
HashSet<string> found = new HashSet<string>(); | |
VfsEntryInfo info; | |
foreach (var plugin in plugins) | |
{ | |
if (plugin.GetEntryInfo(path, out info) && info.Type == VfsEntryType.Directory) | |
{ | |
string[] results = plugin.ListDirectory(path); | |
foreach (string result in results) | |
{ | |
if (extension == null || result.EndsWith(extension, StringComparison.OrdinalIgnoreCase)) | |
found.Add(result); | |
} | |
} | |
} | |
List<string> filtered = new List<string>(); | |
foreach (string f in found) | |
{ | |
if (type != null) | |
{ | |
if (GetEntryInfo(Combine(path, f), out info) && info.Type == type) | |
filtered.Add(f); | |
} | |
else | |
filtered.Add(f); | |
} | |
filtered.Sort(StringComparer.OrdinalIgnoreCase); | |
return filtered.ToArray(); | |
} | |
/// <summary> | |
/// Attempts to get a real filesystem path to the given VFS path. | |
/// </summary> | |
/// <param name="path">Path to get the real equivalent of.</param> | |
/// <returns>Absolute filesystem path if available; null otherwise.</returns> | |
public string GetRealPath(string path) | |
{ | |
path = CleanPath(path); | |
VfsEntryInfo info; | |
foreach (var plugin in plugins) | |
if (plugin.GetEntryInfo(path, out info)) | |
{ | |
return plugin.GetRealPath(path); | |
} | |
return null; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment