Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MockFS: Move all absolute-path-aware functions from FSEntry to MockFS #2969

Merged
merged 4 commits into from
Sep 24, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
249 changes: 127 additions & 122 deletions source/dub/internal/io/mockfs.d
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@ public final class MockFS : Filesystem {
///
private FSEntry cwd;

///
private FSEntry root;

///
public this () scope
{
this.cwd = new FSEntry();
this.root = this.cwd = new FSEntry();
}

public override NativePath getcwd () const scope
Expand All @@ -34,20 +37,23 @@ public final class MockFS : Filesystem {
///
public override bool existsDirectory (in NativePath path) const scope
{
auto entry = this.cwd.lookup(path);
auto entry = this.lookup(path);
return entry !is null && entry.isDirectory();
}

/// Ditto
public override void mkdir (in NativePath path) scope
{
this.cwd.mkdir(path);
if (path.absolute())
this.root.mkdir(path);
else
this.cwd.mkdir(path);
}

/// Ditto
public override bool existsFile (in NativePath path) const scope
{
auto entry = this.cwd.lookup(path);
auto entry = this.lookup(path);
return entry !is null && entry.isFile();
}

Expand All @@ -57,13 +63,13 @@ public final class MockFS : Filesystem {
{
enforce(!path.endsWithSlash(),
"Cannot write to directory: " ~ path.toNativeString());
if (auto file = this.cwd.lookup(path)) {
if (auto file = this.lookup(path)) {
// If the file already exists, override it
enforce(file.isFile(),
"Trying to write to directory: " ~ path.toNativeString());
file.content = data.dup;
} else {
auto p = this.cwd.getParent(path);
auto p = this.getParent(path);
auto file = new FSEntry(p, FSEntry.Type.File, path.head.name());
file.content = data.dup;
p.children ~= file;
Expand All @@ -73,7 +79,7 @@ public final class MockFS : Filesystem {
/// Reads a file, returns the content as `ubyte[]`
public override ubyte[] readFile (in NativePath path) const scope
{
auto entry = this.cwd.lookup(path);
auto entry = this.lookup(path);
enforce(entry !is null, "No such file: " ~ path.toNativeString());
enforce(entry.isFile(), "Trying to read a directory");
// This is a hack to make poisoning a file possible.
Expand Down Expand Up @@ -101,7 +107,7 @@ public final class MockFS : Filesystem {
{
enforce(this.existsDirectory(path),
path.toNativeString() ~ " does not exists or is not a directory");
auto dir = this.cwd.lookup(path);
auto dir = this.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;
Expand All @@ -123,23 +129,71 @@ public final class MockFS : Filesystem {
return &iterator;
}

/// Ditto
public override void removeFile (in NativePath path, bool force = false) scope
/** 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 override void removeFile (in NativePath path, bool force = false)
{
return this.cwd.removeFile(path);
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 == FSEntry.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 override void removeDir (in NativePath path, bool force = false)
{
this.cwd.removeDir(path, force);
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 == FSEntry.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 .. $];
}
}

/// Ditto
public override void setTimes (in NativePath path, in SysTime accessTime,
in SysTime modificationTime)
{
auto e = this.cwd.lookup(path);
auto e = this.lookup(path);
enforce(e !is null,
"setTimes: No such file or directory: " ~ path.toNativeString());
e.setTimes(accessTime, modificationTime);
Expand All @@ -148,7 +202,7 @@ public final class MockFS : Filesystem {
/// Ditto
public override void setAttributes (in NativePath path, uint attributes)
{
auto e = this.cwd.lookup(path);
auto e = this.lookup(path);
enforce(e !is null,
"setAttributes: No such file or directory: " ~ path.toNativeString());
e.setAttributes(attributes);
Expand Down Expand Up @@ -186,9 +240,54 @@ public final class MockFS : Filesystem {
addToZip(rootPath, this.cwd);
return cast(ubyte[]) z.build();
}

/** 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.cwd;

// 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 == FSEntry.Type.Directory,
"Parent path is not a directory: " ~ parentPath.toNativeString());
return p;
}

/// Get an arbitrarily nested children node
protected inout(FSEntry) lookup(NativePath path) inout return scope
{
// The following does not work due to `inout` / `const`:
// return reduce!((base, segment) => base.lookup(segment))(b, path.bySegment);
// So we have to resort to a member function added to `FSEntry`
return path.absolute ? this.root.lookup(path) : this.cwd.lookup(path);
}
}

/// The backing logic behind `MockFS`
/*******************************************************************************

Represents a node on the filesystem

This class encapsulates operations which are node specific, such as looking
up a child node, adding one, or setting properties.

*******************************************************************************/

public class FSEntry
{
/// Type of file system entry
Expand Down Expand Up @@ -251,63 +350,28 @@ public class FSEntry
protected inout(FSEntry) lookup(string name) inout return scope
{
assert(!name.canFind('/'));
if (name == ".") return this;
if (name == "..") return this.parent;
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
protected inout(FSEntry) lookup(in 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)) {
assert(!path.absolute() || this.parent is null,
`FSEntry.lookup should not be called with absolute paths`);
auto segments = path.bySegment;
if (segments.empty) return this;
if (auto next = this.lookup(segments.front.name)) {
segments.popFront();
return !segments.empty ? c.lookup(NativePath(segments)) : c;
return next.lookup(NativePath(segments));
}
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
Expand Down Expand Up @@ -373,9 +437,10 @@ public class FSEntry
/// Implements `mkdir -p`, returns the created directory
public FSEntry mkdir (in NativePath path) scope
{
auto relp = this.relativePath(path);
assert(!path.absolute() || this.parent is null,
`FSEntry.mkdir needs to be called with a relative path`);
// Check if the child already exists
auto segments = relp.bySegment;
auto segments = path.bySegment;
auto child = this.lookup(segments.front.name);
if (child is null) {
child = new FSEntry(this, Type.Directory, segments.front.name);
Expand All @@ -398,66 +463,6 @@ public class FSEntry
return this.attributes.type == Type.Directory;
}

/** 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 SysTime accessTime, in SysTime modificationTime)
{
Expand Down
Loading