bitburner-src/src/Paths/FilePath.ts
Snarling e0272ad4af
FILES: Path rework & typesafety (#479)
* Added new types for various file paths, all in the Paths folder.
* TypeSafety and other helper functions related to these types
* Added basic globbing support with * and ?. Currently only implemented for Script/Text, on nano and download terminal commands
* Enforcing the new types throughout the codebase, plus whatever rewrites happened along the way
* Server.textFiles is now a map
* TextFile no longer uses a fn property, now it is filename
* Added a shared ContentFile interface for shared functionality between TextFile and Script.
* related to ContentFile change above, the player is now allowed to move a text file to a script file and vice versa.
* File paths no longer conditionally start with slashes, and all directory names other than root have ending slashes. The player is still able to provide paths starting with / but this now indicates that the player is specifying an absolute path instead of one relative to root.
* Singularized the MessageFilename and LiteratureName enums
* Because they now only accept correct types, server.writeToXFile functions now always succeed (the only reasons they could fail before were invalid filepath).
* Fix several issues with tab completion, which included pretty much a complete rewrite
* Changed the autocomplete display options so there's less chance it clips outside the display area.
* Turned CompletedProgramName into an enum.
* Got rid of programsMetadata, and programs and DarkWebItems are now initialized immediately instead of relying on initializers called from the engine.
* For any executable (program, cct, or script file) pathing can be used directly to execute without using the run command (previously the command had to start with ./ and it wasn't actually using pathing).
2023-04-24 10:26:57 -04:00

84 lines
3.8 KiB
TypeScript

import {
Directory,
isAbsolutePath,
BasicDirectory,
resolveValidatedDirectory,
oneValidCharacter,
directoryRegexString,
AbsolutePath,
} from "./Directory";
/** Filepath Rules:
* 1. File extension cannot contain a "/"
* 2. Last character before the extension cannot be a "/" as this would be a blank filename
* 3. Must not contain a leading "/"
* 4. Directory names cannot be 0-length (no "//")
* 5. The characters *, ?, [, and ] cannot exist in the filepath*/
type BasicFilePath = string & { __type: "FilePath" };
/** A file path that is also an absolute path. Additional absolute rules:
* 1. Specific directory names "." and ".." are disallowed
* Absoluteness is typechecked with isAbsolutePath in DirectoryPath.ts */
export type FilePath = BasicFilePath & AbsolutePath;
// Capturing group named file which captures the entire filename part of a file path.
const filenameRegexString = `(?<file>${oneValidCharacter}+\\.${oneValidCharacter}+)$`;
/** Regex made of the two above regex parts to test for a whole valid filepath. */
const basicFilePathRegex = new RegExp(directoryRegexString + filenameRegexString) as RegExp & {
exec: (path: string) => null | { groups: { directory: BasicDirectory; file: FilePath } };
};
/** Simple validation function with no modification. Can be combined with isAbsolutePath to get a real FilePath */
export function isFilePath(path: string): path is BasicFilePath {
return basicFilePathRegex.test(path);
}
export function asFilePath<T extends string>(input: T): T & FilePath {
if (isFilePath(input) && isAbsolutePath(input)) return input;
throw new Error(`${input} failed to validate as a FilePath.`);
}
export function getFilenameOnly<T extends BasicFilePath>(path: T): T & FilePath {
const start = path.lastIndexOf("/") + 1;
return path.substring(start) as T & FilePath;
}
/** Validate while also capturing and returning directory and file parts */
function getFileParts(path: string): { directory: BasicDirectory; file: FilePath } | null {
const result = basicFilePathRegex.exec(path) as null | { groups: { directory: BasicDirectory; file: FilePath } };
return result ? result.groups : null;
}
/** Sanitizes a player input and resolves a relative file path to an absolute one.
* @param path The player-provided path string. Can include relative directories.
* @param base The absolute base for resolving a relative path. */
export function resolveFilePath(path: string, base = "" as FilePath | Directory): FilePath | null {
if (isAbsolutePath(path)) {
if (path.startsWith("/")) path = path.substring(1);
// Because we modified the string since checking absoluteness, we have to assert that it's still absolute here.
return isFilePath(path) ? (path as FilePath) : null;
}
// Turn base into a DirectoryName in case it was not
base = getBaseDirectory(base);
const pathParts = getFileParts(path);
if (!pathParts) return null;
const directory = resolveValidatedDirectory(pathParts.directory, base);
// Have to specifically check null here instead of truthiness, because empty string is a valid DirectoryPath
return directory === null ? null : combinePath(directory, pathParts.file);
}
/** Remove the file part from an absolute path (FilePath or DirectoryPath - no modification is done for a DirectoryPath) */
function getBaseDirectory(path: FilePath | Directory): Directory {
return path.replace(/[^\/\*]+\.[^\/\*]+$/, "") as Directory;
}
/** Combine an absolute DirectoryPath and FilePath to create a new FilePath */
export function combinePath<T extends FilePath>(directory: Directory, file: T): T {
// Preserves the specific file type because the filepart is preserved.
return (directory + file) as T;
}
export function removeDirectoryFromPath(directory: Directory, path: FilePath): FilePath | null {
if (!path.startsWith(directory)) return null;
return path.substring(directory.length) as FilePath;
}