- /*******************************************************************************
-
- An unittest implementation of `Filesystem`
-
- *******************************************************************************/
-
- module dub.internal.io.mockfs;
-
- public import dub.internal.io.filesystem;
-
- static import dub.internal.vibecompat.core.file;
-
- import std.algorithm;
- import std.exception;
- import std.range;
- import std.string;
-
- /// Ditto
- public final class MockFS : Filesystem {
- ///
- private FSEntry cwd;
-
- ///
- public this () scope
- {
- this.cwd = new FSEntry();
- }
-
- public override NativePath getcwd () const scope
- {
- return this.cwd.path();
- }
-
- ///
- public override bool existsDirectory (in NativePath path) const scope
- {
- return this.cwd.existsDirectory(path);
- }
-
- /// Ditto
- public override void mkdir (in NativePath path) scope
- {
- this.cwd.mkdir(path);
- }
-
- /// Ditto
- public override bool existsFile (in NativePath path) const scope
- {
- return this.cwd.existsFile(path);
- }
-
- /// Ditto
- public override void writeFile (in NativePath path, const(ubyte)[] data)
- scope
- {
- return this.cwd.writeFile(path, data);
- }
-
- /// Ditto
- public override void writeFile (in NativePath path, const(char)[] data)
- scope
- {
- return this.cwd.writeFile(path, data);
- }
-
- /// Reads a file, returns the content as `ubyte[]`
- public override ubyte[] readFile (in NativePath path) const scope
- {
- return this.cwd.readFile(path);
- }
-
- /// Ditto
- public override string readText (in NativePath path) const scope
- {
- return this.cwd.readText(path);
- }
-
- /// Ditto
- public override IterateDirDg iterateDirectory (in NativePath path) scope
- {
- enforce(this.cwd.existsDirectory(path),
- path.toNativeString() ~ " does not exists or is not a directory");
- auto dir = this.cwd.lookup(path);
- int iterator(scope int delegate(ref dub.internal.vibecompat.core.file.FileInfo) del) {
- foreach (c; dir.children) {
- dub.internal.vibecompat.core.file.FileInfo fi;
- fi.name = c.name;
- fi.timeModified = c.attributes.modification;
- final switch (c.attributes.type) {
- case FSEntry.Type.File:
- fi.size = c.content.length;
- break;
- case FSEntry.Type.Directory:
- fi.isDirectory = true;
- break;
- }
- if (auto res = del(fi))
- return res;
- }
- return 0;
- }
- return &iterator;
- }
-
- /// Ditto
- public override void removeFile (in NativePath path, bool force = false) scope
- {
- return this.cwd.removeFile(path);
- }
-
- ///
- public override void removeDir (in NativePath path, bool force = false)
- {
- this.cwd.removeDir(path, force);
- }
-
- /// Ditto
- public override void setTimes (in NativePath path, in SysTime accessTime,
- in SysTime modificationTime)
- {
- this.cwd.setTimes(path, accessTime, modificationTime);
- }
-
- /// Ditto
- public override void setAttributes (in NativePath path, uint attributes)
- {
- this.cwd.setAttributes(path, attributes);
- }
-
- /**
- * Converts an `Filesystem` and its children to a `ZipFile`
- */
- public ubyte[] serializeToZip (string rootPath) {
- import std.path;
- import std.zip;
-
- scope z = new ZipArchive();
- void addToZip(scope string dir, scope FSEntry e) {
- auto m = new ArchiveMember();
- m.name = dir.buildPath(e.name);
- m.fileAttributes = e.attributes.attrs;
- m.time = e.attributes.modification;
-
- final switch (e.attributes.type) {
- case FSEntry.Type.Directory:
- // We need to ensure the directory entry ends with a slash
- // otherwise it will be considered as a file.
- if (m.name[$-1] != '/')
- m.name ~= '/';
- z.addMember(m);
- foreach (c; e.children)
- addToZip(m.name, c);
- break;
- case FSEntry.Type.File:
- m.expandedData = e.content;
- z.addMember(m);
- }
- }
- addToZip(rootPath, this.cwd);
- return cast(ubyte[]) z.build();
- }
- }
-
- /// The backing logic behind `MockFS`
- public class FSEntry
- {
- /// Type of file system entry
- public enum Type : ubyte {
- Directory,
- File,
- }
-
- /// List FSEntry attributes
- protected struct Attributes {
- /// The type of FSEntry, see `FSEntry.Type`
- public Type type;
- /// System-specific attributes for this `FSEntry`
- public uint attrs;
- /// Last access time
- public SysTime access;
- /// Last modification time
- public SysTime modification;
- }
- /// Ditto
- protected Attributes attributes;
-
- /// The name of this node
- protected string name;
- /// The parent of this entry (can be null for the root)
- protected FSEntry parent;
- union {
- /// Children for this FSEntry (with type == Directory)
- protected FSEntry[] children;
- /// Content for this FDEntry (with type == File)
- protected ubyte[] content;
- }
-
- /// Creates a new FSEntry
- package(dub) this (FSEntry p, Type t, string n)
- {
- // Avoid 'DOS File Times cannot hold dates prior to 1980.' exception
- import std.datetime.date;
- SysTime DefaultTime = SysTime(DateTime(2020, 01, 01));
-
- this.attributes.type = t;
- this.parent = p;
- this.name = n;
- this.attributes.access = DefaultTime;
- this.attributes.modification = DefaultTime;
- }
-
- /// Create the root of the filesystem, only usable from this module
- package(dub) this ()
- {
- import std.datetime.date;
- SysTime DefaultTime = SysTime(DateTime(2020, 01, 01));
-
- this.attributes.type = Type.Directory;
- this.attributes.access = DefaultTime;
- this.attributes.modification = DefaultTime;
- }
-
- /// Get a direct children node, returns `null` if it can't be found
- protected inout(FSEntry) lookup(string name) inout return scope
- {
- assert(!name.canFind('/'));
- foreach (c; this.children)
- if (c.name == name)
- return c;
- return null;
- }
-
- /// Get an arbitrarily nested children node
- protected inout(FSEntry) lookup(NativePath path) inout return scope
- {
- auto relp = this.relativePath(path);
- relp.normalize(); // try to get rid of `..`
- if (relp.empty)
- return this;
- auto segments = relp.bySegment;
- if (auto c = this.lookup(segments.front.name)) {
- segments.popFront();
- return !segments.empty ? c.lookup(NativePath(segments)) : c;
- }
- return null;
- }
-
- /** Get the parent `FSEntry` of a `NativePath`
- *
- * If the parent doesn't exist, an `Exception` will be thrown
- * unless `silent` is provided. If the parent path is a file,
- * an `Exception` will be thrown regardless of `silent`.
- *
- * Params:
- * path = The path to look up the parent for
- * silent = Whether to error on non-existing parent,
- * default to `false`.
- */
- protected inout(FSEntry) getParent(NativePath path, bool silent = false)
- inout return scope
- {
- // Relative path in the current directory
- if (!path.hasParentPath())
- return this;
-
- // If we're not in the right `FSEntry`, recurse
- const parentPath = path.parentPath();
- auto p = this.lookup(parentPath);
- enforce(silent || p !is null,
- "No such directory: " ~ parentPath.toNativeString());
- enforce(p is null || p.attributes.type == Type.Directory,
- "Parent path is not a directory: " ~ parentPath.toNativeString());
- return p;
- }
-
- /// Returns: A path relative to `this.path`
- protected NativePath relativePath(NativePath path) const scope
- {
- assert(!path.absolute() || path.startsWith(this.path),
- "Calling relativePath with a differently rooted path");
- return path.absolute() ? path.relativeTo(this.path) : path;
- }
-
- /*+*************************************************************************
-
- Utility function
-
- Below this banners are functions that are provided for the convenience
- of writing tests for `Dub`.
-
- ***************************************************************************/
-
- /// Prints a visual representation of the filesystem to stdout for debugging
- public void print(bool content = false) const scope
- {
- import std.range : repeat;
- static import std.stdio;
-
- size_t indent;
- for (auto p = &this.parent; (*p) !is null; p = &p.parent)
- indent++;
- // Don't print anything (even a newline) for root
- if (this.parent is null)
- std.stdio.write('/');
- else
- std.stdio.write('|', '-'.repeat(indent), ' ', this.name, ' ');
-
- final switch (this.attributes.type) {
- case Type.Directory:
- std.stdio.writeln('(', this.children.length, " entries):");
- foreach (c; this.children)
- c.print(content);
- break;
- case Type.File:
- if (!content)
- std.stdio.writeln('(', this.content.length, " bytes)");
- else if (this.name.endsWith(".json") || this.name.endsWith(".sdl"))
- std.stdio.writeln('(', this.content.length, " bytes): ",
- cast(string) this.content);
- else
- std.stdio.writeln('(', this.content.length, " bytes): ",
- this.content);
- break;
- }
- }
-
- /*+*************************************************************************
-
- Public filesystem functions
-
- Below this banners are functions which mimic the behavior of a file
- system.
-
- ***************************************************************************/
-
- /// Returns: The `path` of this FSEntry
- public NativePath path () const scope
- {
- if (this.parent is null)
- return NativePath("/");
- auto thisPath = this.parent.path ~ this.name;
- thisPath.endsWithSlash = (this.attributes.type == Type.Directory);
- return thisPath;
- }
-
- /// Implements `mkdir -p`, returns the created directory
- public FSEntry mkdir (in NativePath path) scope
- {
- auto relp = this.relativePath(path);
- // Check if the child already exists
- auto segments = relp.bySegment;
- auto child = this.lookup(segments.front.name);
- if (child is null) {
- child = new FSEntry(this, Type.Directory, segments.front.name);
- this.children ~= child;
- }
- // Recurse if needed
- segments.popFront();
- return !segments.empty ? child.mkdir(NativePath(segments)) : child;
- }
-
- /// Checks the existence of a file
- public bool existsFile (in NativePath path) const scope
- {
- auto entry = this.lookup(path);
- return entry !is null && entry.attributes.type == Type.File;
- }
-
- /// Checks the existence of a directory
- public bool existsDirectory (in NativePath path) const scope
- {
- auto entry = this.lookup(path);
- return entry !is null && entry.attributes.type == Type.Directory;
- }
-
- /// Reads a file, returns the content as `ubyte[]`
- public ubyte[] readFile (in NativePath path) const scope
- {
- auto entry = this.lookup(path);
- enforce(entry !is null, "No such file: " ~ path.toNativeString());
- enforce(entry.attributes.type == Type.File, "Trying to read a directory");
- // This is a hack to make poisoning a file possible.
- // However, it is rather crude and doesn't allow to poison directory.
- // Consider introducing a derived type to allow it.
- assert(entry.content != "poison".representation,
- "Trying to access poisoned path: " ~ path.toNativeString());
- return entry.content.dup;
- }
-
- /// Reads a file, returns the content as text
- public string readText (in NativePath path) const scope
- {
- import std.utf : validate;
-
- const content = this.readFile(path);
- // Ignore BOM: If it's needed for a test, add support for it.
- validate(cast(const(char[])) content);
- // `readFile` just `dup` the content, so it's safe to cast.
- return cast(string) content;
- }
-
- /// Write to this file
- public void writeFile (in NativePath path, const(char)[] data) scope
- {
- this.writeFile(path, data.representation);
- }
-
- /// Ditto
- public void writeFile (in NativePath path, const(ubyte)[] data) scope
- {
- enforce(!path.endsWithSlash(),
- "Cannot write to directory: " ~ path.toNativeString());
- if (auto file = this.lookup(path)) {
- // If the file already exists, override it
- enforce(file.attributes.type == Type.File,
- "Trying to write to directory: " ~ path.toNativeString());
- file.content = data.dup;
- } else {
- auto p = this.getParent(path);
- auto file = new FSEntry(p, Type.File, path.head.name());
- file.content = data.dup;
- p.children ~= file;
- }
- }
-
- /** Remove a file
- *
- * Always error if the target is a directory.
- * Does not error if the target does not exists
- * and `force` is set to `true`.
- *
- * Params:
- * path = Path to the file to remove
- * force = Whether to ignore non-existing file,
- * default to `false`.
- */
- public void removeFile (in NativePath path, bool force = false)
- {
- import std.algorithm.searching : countUntil;
-
- assert(!path.empty, "Empty path provided to `removeFile`");
- enforce(!path.endsWithSlash(),
- "Cannot remove file with directory path: " ~ path.toNativeString());
- auto p = this.getParent(path, force);
- const idx = p.children.countUntil!(e => e.name == path.head.name());
- if (idx < 0) {
- enforce(force,
- "removeFile: No such file: " ~ path.toNativeString());
- } else {
- enforce(p.children[idx].attributes.type == Type.File,
- "removeFile called on a directory: " ~ path.toNativeString());
- p.children = p.children[0 .. idx] ~ p.children[idx + 1 .. $];
- }
- }
-
- /** Remove a directory
- *
- * Remove an existing empty directory.
- * If `force` is set to `true`, no error will be thrown
- * if the directory is empty or non-existing.
- *
- * Params:
- * path = Path to the directory to remove
- * force = Whether to ignore non-existing / non-empty directories,
- * default to `false`.
- */
- public void removeDir (in NativePath path, bool force = false)
- {
- import std.algorithm.searching : countUntil;
-
- assert(!path.empty, "Empty path provided to `removeFile`");
- auto p = this.getParent(path, force);
- const idx = p.children.countUntil!(e => e.name == path.head.name());
- if (idx < 0) {
- enforce(force,
- "removeDir: No such directory: " ~ path.toNativeString());
- } else {
- enforce(p.children[idx].attributes.type == Type.Directory,
- "removeDir called on a file: " ~ path.toNativeString());
- enforce(force || p.children[idx].children.length == 0,
- "removeDir called on non-empty directory: " ~ path.toNativeString());
- p.children = p.children[0 .. idx] ~ p.children[idx + 1 .. $];
- }
- }
-
- /// Implement `std.file.setTimes`
- public void setTimes (in NativePath path, in SysTime accessTime,
- in SysTime modificationTime)
- {
- auto e = this.lookup(path);
- enforce(e !is null,
- "setTimes: No such file or directory: " ~ path.toNativeString());
- e.attributes.access = accessTime;
- e.attributes.modification = modificationTime;
- }
-
- /// Implement `std.file.setAttributes`
- public void setAttributes (in NativePath path, uint attributes)
- {
- auto e = this.lookup(path);
- enforce(e !is null,
- "setTimes: No such file or directory: " ~ path.toNativeString());
- e.attributes.attrs = attributes;
- }
- }