Port fs.find to CraftOS

Also add support for "?" style wildcards.

Closes #1455.
This commit is contained in:
Jonathan Coates 2023-06-15 18:54:34 +01:00
parent 77ac04cb7a
commit c7f3d4f45d
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
4 changed files with 140 additions and 70 deletions

View File

@ -443,28 +443,6 @@ public final Object getFreeSpace(String path) throws LuaException {
}
}
/**
* Searches for files matching a string with wildcards.
* <p>
* This string is formatted like a normal path string, but can include any
* number of wildcards ({@code *}) to look for files matching anything.
* For example, <code>rom/&#42;/command*</code> will look for any path starting with
* {@code command} inside any subdirectory of {@code /rom}.
*
* @param path The wildcard-qualified path to search for.
* @return A list of paths that match the search string.
* @throws LuaException If the path doesn't exist.
* @cc.since 1.6
*/
@LuaFunction
public final String[] find(String path) throws LuaException {
try (var ignored = environment.time(Metrics.FS_OPS)) {
return getFileSystem().find(path);
} catch (FileSystemException e) {
throw new LuaException(e.getMessage());
}
}
/**
* Returns the capacity of the drive the path is located on.
*

View File

@ -166,47 +166,6 @@ public synchronized String[] list(String path) throws FileSystemException {
return array;
}
private void findIn(String dir, List<String> matches, Pattern wildPattern) throws FileSystemException {
var list = list(dir);
for (var entry : list) {
var entryPath = dir.isEmpty() ? entry : dir + "/" + entry;
if (wildPattern.matcher(entryPath).matches()) {
matches.add(entryPath);
}
if (isDir(entryPath)) {
findIn(entryPath, matches, wildPattern);
}
}
}
public synchronized String[] find(String wildPath) throws FileSystemException {
// Match all the files on the system
wildPath = sanitizePath(wildPath, true);
// If we don't have a wildcard at all just check the file exists
var starIndex = wildPath.indexOf('*');
if (starIndex == -1) {
return exists(wildPath) ? new String[]{ wildPath } : new String[0];
}
// Find the all non-wildcarded directories. For instance foo/bar/baz* -> foo/bar
var prevDir = wildPath.substring(0, starIndex).lastIndexOf('/');
var startDir = prevDir == -1 ? "" : wildPath.substring(0, prevDir);
// If this isn't a directory then just abort
if (!isDir(startDir)) return new String[0];
// Scan as normal, starting from this directory
var wildPattern = Pattern.compile("^\\Q" + wildPath.replaceAll("\\*", "\\\\E[^\\\\/]*\\\\Q") + "\\E$");
List<String> matches = new ArrayList<>();
findIn(startDir, matches, wildPattern);
// Return matches
var array = new String[matches.size()];
matches.toArray(array);
return array;
}
public synchronized boolean exists(String path) throws FileSystemException {
path = sanitizePath(path);
var mount = getMount(path);
@ -400,21 +359,20 @@ private static String sanitizePath(String path) {
private static final Pattern threeDotsPattern = Pattern.compile("^\\.{3,}$");
// IMPORTANT: Both arrays are sorted by ASCII value.
private static final char[] specialChars = new char[]{ '"', '*', ':', '<', '>', '?', '|' };
private static final char[] specialCharsAllowWildcards = new char[]{ '"', ':', '<', '>', '|' };
public static String sanitizePath(String path, boolean allowWildcards) {
// Allow windowsy slashes
path = path.replace('\\', '/');
// Clean the path or illegal characters.
final var specialChars = new char[]{
'"', ':', '<', '>', '?', '|', // Sorted by ascii value (important)
};
var cleanName = new StringBuilder();
var allowedChars = allowWildcards ? specialCharsAllowWildcards : specialChars;
for (var i = 0; i < path.length(); i++) {
var c = path.charAt(i);
if (c >= 32 && Arrays.binarySearch(specialChars, c) < 0 && (allowWildcards || c != '*')) {
cleanName.append(c);
}
if (c >= 32 && Arrays.binarySearch(allowedChars, c) < 0) cleanName.append(c);
}
path = cleanName.toString();

View File

@ -133,6 +133,93 @@ function fs.complete(sPath, sLocation, bIncludeFiles, bIncludeDirs)
return {}
end
local function find_aux(path, parts, i, out)
local part = parts[i]
if not part then
-- If we're at the end of the pattern, ensure our path exists and append it.
if fs.exists(path) then out[#out + 1] = path end
elseif part.exact then
-- If we're an exact match, just recurse into this directory.
return find_aux(fs.combine(path, part.contents), parts, i + 1, out)
else
-- Otherwise we're a pattern. Check we're a directory, then recurse into each
-- matching file.
if not fs.isDir(path) then return end
local files = fs.list(path)
for j = 1, #files do
local file = files[j]
if file:find(part.contents) then find_aux(fs.combine(path, file), parts, i + 1, out) end
end
end
end
local find_escape = {
-- Escape standard Lua pattern characters
["^"] = "%^", ["$"] = "%$", ["("] = "%(", [")"] = "%)", ["%"] = "%%",
["."] = "%.", ["["] = "%[", ["]"] = "%]", ["+"] = "%+", ["-"] = "%-",
-- Aside from our wildcards.
["*"] = ".*",
["?"] = ".",
}
--[[- Searches for files matching a string with wildcards.
This string looks like a normal path string, but can include wildcards, which
can match multiple paths:
- "?" matches any single character in a file name.
- "*" matches any number of characters.
For example, `rom/*/command*` will look for any path starting with `command`
inside any subdirectory of `/rom`.
Note that these wildcards match a single segment of the path. For instance
`rom/*.lua` will include `rom/startup.lua` but _not_ include `rom/programs/list.lua`.
@tparam string path The wildcard-qualified path to search for.
@treturn { string... } A list of paths that match the search string.
@throws If the supplied path was invalid.
@since 1.6
@changed 1.106.0 Added support for the `?` wildcard.
@usage List all Markdown files in the help folder
fs.find("rom/help/*.md")
]]
function fs.find(pattern)
expect(1, pattern, "string")
pattern = fs.combine(pattern) -- Normalise the path, removing ".."s.
-- If the pattern is trying to search outside the computer root, just abort.
-- This will fail later on anyway.
if pattern == ".." or pattern:sub(1, 3) == "../" then
error("/" .. pattern .. ": Invalid Path", 2)
end
-- If we've no wildcards, just check the file exists.
if not pattern:find("[*?]") then
if fs.exists(pattern) then return { pattern } else return {} end
end
local parts = {}
for part in pattern:gmatch("[^/]+") do
if part:find("[*?]") then
parts[#parts + 1] = {
exact = false,
contents = "^" .. part:gsub(".", find_escape) .. "$",
}
else
parts[#parts + 1] = { exact = true, contents = part }
end
end
local out = {}
find_aux("", parts, 1, out)
return out
end
--- Returns true if a path is mounted to the parent filesystem.
--
-- The root filesystem "/" is considered a mount, along with disk folders and

View File

@ -87,6 +87,53 @@ describe("The fs library", function()
end)
end)
describe("fs.find", function()
it("fails on invalid paths", function()
expect.error(fs.find, ".."):eq("/..: Invalid Path")
expect.error(fs.find, "../foo/bar"):eq("/../foo/bar: Invalid Path")
end)
it("returns nothing on non-existent files", function()
expect(fs.find("no/such/file")):same {}
expect(fs.find("no/such/*")):same {}
expect(fs.find("no/*/file")):same {}
end)
it("returns a single file", function()
expect(fs.find("rom")):same { "rom" }
expect(fs.find("rom/motd.txt")):same { "rom/motd.txt" }
end)
it("supports the '*' wildcard", function()
expect(fs.find("rom/*")):same {
"rom/apis",
"rom/autorun",
"rom/help",
"rom/modules",
"rom/motd.txt",
"rom/programs",
"rom/startup.lua",
}
expect(fs.find("rom/*/command")):same {
"rom/apis/command",
"rom/modules/command",
"rom/programs/command",
}
expect(fs.find("rom/*/lua*")):same {
"rom/help/lua.txt",
"rom/programs/lua.lua",
}
end)
it("supports the '?' wildcard", function()
expect(fs.find("rom/programs/mo??.lua")):same {
"rom/programs/motd.lua",
"rom/programs/move.lua",
}
end)
end)
describe("fs.combine", function()
it("removes . and ..", function()
expect(fs.combine("./a/b")):eq("a/b")