Skip to content

Instantly share code, notes, and snippets.

@shilrobot
Created December 11, 2012 22:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shilrobot/4263088 to your computer and use it in GitHub Desktop.
Save shilrobot/4263088 to your computer and use it in GitHub Desktop.
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