Fs / InMemoryFs.flix
InMemoryFs.flix
/*
* Copyright 2026 Flix Authors
*
* Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE.md file.
*/
/// Type of fully in-memory file systems, and its functions.
mod Fs.InMemoryFs {
import java.nio.file.FileSystems
import java.nio.file.PathMatcher
import java.nio.file.Paths
use IoError.IoError
use IoError.ErrorKind
use Time.Clock
/// Fully in-memory filesystem state, parameterized by region.
///
/// - `files`: map from normalized absolute paths to in-memory content (text or bytes).
/// - `dirs`: set of normalized absolute paths registered as directories.
/// - `times`: map from normalized absolute paths to `{atime, ctime, mtime}` records (ms since epoch).
/// - `counter`: monotonically increasing counter for generating unique temp directory names.
struct InMemoryFs[r: Region] {
mut files: Map[String, Entry],
mut dirs: Set[String],
mut times: Map[String, {atime = Int64, ctime = Int64, mtime = Int64}],
mut counter: Int32
}
/// Type of in-memory file contents, either text or bytes.
pub enum Entry with ToString {
case BinaryEntry(Vector[Int8])
case TextEntry(String)
}
/// Creates a fresh, empty in-memory filesystem in region `rc`.
pub def fresh(rc: Region[r]): Fs.InMemoryFs[r] \ r =
new Fs.InMemoryFs @ rc {files = Map.empty(), dirs = Set.empty(), times = Map.empty(), counter = 0}
// ─── Path helpers (private) ───────────────────────────────────────
def normalize(path: String): String =
unsafe IO { Paths.get(path).toAbsolutePath().normalize().toString() }
def parentDir(path: String): Option[String] =
unsafe IO {
let p = Paths.get(path).getParent();
if (Object.isNull(p)) None else Some(p.toString())
}
def fileName(path: String): String =
unsafe IO {
let p = Paths.get(path).getFileName();
if (Object.isNull(p)) path else p.toString()
}
def notFound(path: String): Result[IoError, a] =
Err(IoError(ErrorKind.NotFound, "in-memory fs: '${path}' does not exist"))
def alreadyExists(path: String): Result[IoError, a] =
Err(IoError(ErrorKind.AlreadyExists, "in-memory fs: '${path}' already exists"))
// ─── Timestamp helpers (private) ─────────────────────────────────
/// Sets all three timestamps to now (for newly created entries).
def touchNew(norm: String, o: Fs.InMemoryFs[r]): Unit \ r + Clock =
let t = Clock.now();
o->times = Map.insert(norm, {atime = t, ctime = t, mtime = t}, o->times)
/// Updates atime and mtime to now, preserving ctime (for modifications).
def touchModify(norm: String, o: Fs.InMemoryFs[r]): Unit \ r + Clock =
let t = Clock.now();
let ts = match Map.get(norm, o->times) {
case Some(prev) => {atime = t, ctime = prev#ctime, mtime = t}
case None => {atime = t, ctime = t, mtime = t}
};
o->times = Map.insert(norm, ts, o->times)
/// Updates atime to now, preserving ctime and mtime (for reads).
def touchAccess(norm: String, o: Fs.InMemoryFs[r]): Unit \ r + Clock =
let t = Clock.now();
let ts = match Map.get(norm, o->times) {
case Some(prev) => {atime = t, ctime = prev#ctime, mtime = prev#mtime}
case None => {atime = t, ctime = t, mtime = t}
};
o->times = Map.insert(norm, ts, o->times)
// ─── Low-level state queries (private) ────────────────────────────
def hasFile(path: String, o: Fs.InMemoryFs[r]): Bool \ r =
Map.memberOf(normalize(path), o->files)
def hasDir(path: String, o: Fs.InMemoryFs[r]): Bool \ r =
Set.memberOf(normalize(path), o->dirs)
def hasEntry(path: String, o: Fs.InMemoryFs[r]): Bool \ r =
hasFile(path, o) or hasDir(path, o)
// ─── Stat/test ops ───────────────────────────────────────────────
/// Returns `true` if `path` exists as a file or directory in `o`.
pub def exists(path: String, o: Fs.InMemoryFs[r]): Result[IoError, Bool] \ r =
Ok(hasEntry(path, o))
/// Returns `true` if `path` is a directory in `o`.
pub def isDirectory(path: String, o: Fs.InMemoryFs[r]): Result[IoError, Bool] \ r =
Ok(hasDir(path, o))
/// Returns `true` if `path` is a file in `o`.
pub def isRegularFile(path: String, o: Fs.InMemoryFs[r]): Result[IoError, Bool] \ r =
Ok(hasFile(path, o))
/// Returns `false`, since the in-memory file system has no symbolic links.
pub def isSymbolicLink(path: String, o: Fs.InMemoryFs[r]): Result[IoError, Bool] \ r =
discard hasEntry(path, o);
Ok(false)
/// Returns `true` if `path` exists in `o`, since every entry is readable.
pub def isReadable(path: String, o: Fs.InMemoryFs[r]): Result[IoError, Bool] \ r =
Ok(hasEntry(path, o))
/// Returns `true` if `path` exists in `o`, since every entry is writable.
pub def isWritable(path: String, o: Fs.InMemoryFs[r]): Result[IoError, Bool] \ r =
Ok(hasEntry(path, o))
/// Returns `false`, since no entry in the in-memory file system is executable.
pub def isExecutable(path: String, o: Fs.InMemoryFs[r]): Result[IoError, Bool] \ r =
discard hasEntry(path, o);
Ok(false)
/// Returns the last access time of `path` in `o` in milliseconds since the epoch.
pub def accessTime(path: String, o: Fs.InMemoryFs[r]): Result[IoError, Int64] \ r =
let norm = normalize(path);
match Map.get(norm, o->times) {
case Some(ts) => Ok(ts#atime)
case None => notFound(path)
}
/// Returns the creation time of `path` in `o` in milliseconds since the epoch.
pub def creationTime(path: String, o: Fs.InMemoryFs[r]): Result[IoError, Int64] \ r =
let norm = normalize(path);
match Map.get(norm, o->times) {
case Some(ts) => Ok(ts#ctime)
case None => notFound(path)
}
/// Returns the last modification time of `path` in `o` in milliseconds since the epoch.
pub def modificationTime(path: String, o: Fs.InMemoryFs[r]): Result[IoError, Int64] \ r =
let norm = normalize(path);
match Map.get(norm, o->times) {
case Some(ts) => Ok(ts#mtime)
case None => notFound(path)
}
/// Returns the size of the file `path` in `o`.
pub def size(path: String, o: Fs.InMemoryFs[r]): Result[IoError, Fs.Size] \ r =
use Fs.Size.bytes;
let norm = normalize(path);
match Map.get(norm, o->files) {
case Some(Entry.TextEntry(s)) => Ok(Fs.FsLayer.estimateStringBytes(s))
case Some(Entry.BinaryEntry(b)) => Ok(bytes(Int32.toInt64(Vector.length(b))))
case None => notFound(path)
}
// ─── Read ops ────────────────────────────────────────────────────
/// Returns the contents of the file `path` in `o` as a string.
pub def read(path: String, o: Fs.InMemoryFs[r]): Result[IoError, String] \ r + Clock =
let norm = normalize(path);
match Map.get(norm, o->files) {
case Some(Entry.TextEntry(s)) => touchAccess(norm, o); Ok(s)
case Some(Entry.BinaryEntry(b)) => touchAccess(norm, o); Ok(String.fromBytes(b))
case None => notFound(path)
}
/// Returns the lines of the file `path` in `o`.
pub def readLines(path: String, o: Fs.InMemoryFs[r]): Result[IoError, List[String]] \ r + Clock =
match read(path, o) {
case Ok(s) => Ok(String.lines(s))
case Err(e) => Err(e)
}
/// Returns the contents of the file `path` in `o` as a vector of bytes.
pub def readBytes(path: String, o: Fs.InMemoryFs[r]): Result[IoError, Vector[Int8]] \ r + Clock =
let norm = normalize(path);
match Map.get(norm, o->files) {
case Some(Entry.BinaryEntry(b)) => touchAccess(norm, o); Ok(b)
case Some(Entry.TextEntry(s)) => touchAccess(norm, o); Ok(String.toBytes(s))
case None => notFound(path)
}
// ─── Write ops ───────────────────────────────────────────────────
/// Writes the string `data` to the file `path` in `o`, replacing any existing contents.
pub def write(path: String, data: String, o: Fs.InMemoryFs[r]): Result[IoError, Unit] \ r + Clock =
let norm = normalize(path);
let existed = Map.memberOf(norm, o->times);
o->files = Map.insert(norm, Entry.TextEntry(data), o->files);
if (existed) touchModify(norm, o) else touchNew(norm, o);
Ok(())
/// Writes the given lines to the file `path` in `o`, replacing any existing contents.
pub def writeLines(path: String, lines: List[String], o: Fs.InMemoryFs[r]): Result[IoError, Unit] \ r + Clock =
write(path, String.intercalate(String.lineSeparator(), lines), o)
/// Writes the bytes `data` to the file `path` in `o`, replacing any existing contents.
pub def writeBytes(path: String, data: Vector[Int8], o: Fs.InMemoryFs[r]): Result[IoError, Unit] \ r + Clock =
let norm = normalize(path);
let existed = Map.memberOf(norm, o->times);
o->files = Map.insert(norm, Entry.BinaryEntry(data), o->files);
if (existed) touchModify(norm, o) else touchNew(norm, o);
Ok(())
// ─── Append ops ──────────────────────────────────────────────────
/// Appends the string `data` to the file `path` in `o`, creating it if it does not exist.
pub def append(path: String, data: String, o: Fs.InMemoryFs[r]): Result[IoError, Unit] \ r + Clock =
let norm = normalize(path);
let current = match Map.get(norm, o->files) {
case Some(Entry.TextEntry(s)) => s
case Some(Entry.BinaryEntry(b)) => String.fromBytes(b)
case None => ""
};
write(path, current + data, o)
/// Appends the given lines to the file `path` in `o`, creating it if it does not exist.
pub def appendLines(path: String, lines: List[String], o: Fs.InMemoryFs[r]): Result[IoError, Unit] \ r + Clock =
let suffix = String.intercalate(String.lineSeparator(), lines);
append(path, suffix, o)
/// Appends the bytes `data` to the file `path` in `o`, creating it if it does not exist.
pub def appendBytes(path: String, data: Vector[Int8], o: Fs.InMemoryFs[r]): Result[IoError, Unit] \ r + Clock =
let norm = normalize(path);
let current = match Map.get(norm, o->files) {
case Some(Entry.BinaryEntry(b)) => b
case Some(Entry.TextEntry(s)) => String.toBytes(s)
case None => Vector.empty()
};
writeBytes(path, Vector.append(current, data), o)
// ─── Other write ops ─────────────────────────────────────────────
/// Truncates the existing file `path` in `o` to zero length.
pub def truncate(path: String, o: Fs.InMemoryFs[r]): Result[IoError, Unit] \ r + Clock =
if (hasFile(path, o))
write(path, "", o)
else
notFound(path)
/// Copies the file `src` to `dst` in `o` using the given copy options `opts`.
pub def copy(src: String, dst: String, opts: Set[Fs.CopyOption], o: Fs.InMemoryFs[r]): Result[IoError, Unit] \ r + Clock =
let normSrc = normalize(src);
let normDst = normalize(dst);
match Map.get(normSrc, o->files) {
case Some(e) =>
if (Map.memberOf(normDst, o->files) and not Set.memberOf(Fs.CopyOption.ReplaceExisting, opts))
alreadyExists(dst)
else {
o->files = Map.insert(normDst, e, o->files);
if (Set.memberOf(Fs.CopyOption.CopyAttributes, opts))
match Map.get(normSrc, o->times) {
case Some(ts) => o->times = Map.insert(normDst, ts, o->times)
case None => touchNew(normDst, o)
}
else
touchNew(normDst, o);
Ok(())
}
case None => notFound(src)
}
/// Moves the file `src` to `dst` in `o` using the given move options `opts`.
pub def move(src: String, dst: String, opts: Set[Fs.MoveOption], o: Fs.InMemoryFs[r]): Result[IoError, Unit] \ r =
let normSrc = normalize(src);
let normDst = normalize(dst);
match Map.get(normSrc, o->files) {
case Some(e) =>
if (Map.memberOf(normDst, o->files) and not Set.memberOf(Fs.MoveOption.ReplaceExisting, opts))
alreadyExists(dst)
else {
o->files = Map.insert(normDst, e, o->files);
o->files = Map.remove(normSrc, o->files);
o->dirs = Set.remove(normSrc, o->dirs);
match Map.get(normSrc, o->times) {
case Some(ts) => o->times = Map.insert(normDst, ts, o->times)
case None => ()
};
o->times = Map.remove(normSrc, o->times);
Ok(())
}
case None => notFound(src)
}
/// Deletes the file or directory `path` from `o`.
pub def delete(path: String, o: Fs.InMemoryFs[r]): Result[IoError, Unit] \ r =
let norm = normalize(path);
if (hasEntry(path, o)) {
o->files = Map.remove(norm, o->files);
o->dirs = Set.remove(norm, o->dirs);
o->times = Map.remove(norm, o->times);
Ok(())
} else
notFound(path)
/// Creates the directory `path` in `o`, failing if it already exists.
pub def mkDir(path: String, o: Fs.InMemoryFs[r]): Result[IoError, Unit] \ r + Clock =
let norm = normalize(path);
if (hasEntry(path, o))
alreadyExists(path)
else {
o->dirs = Set.insert(norm, o->dirs);
touchNew(norm, o);
Ok(())
}
/// Creates the directory `path` and any missing parent directories in `o`.
pub def mkDirs(path: String, o: Fs.InMemoryFs[r]): Result[IoError, Unit] \ r + Clock =
let norm = normalize(path);
let alreadyExisted = Set.memberOf(norm, o->dirs);
o->dirs = Set.insert(norm, o->dirs);
if (not alreadyExisted) touchNew(norm, o) else ();
match parentDir(norm) {
case Some(parent) =>
if (not Set.memberOf(parent, o->dirs)) mkDirs(parent, o) else Ok(())
case None => Ok(())
}
/// Creates a new temporary directory in `o` whose name starts with `prefix` and returns its path.
pub def mkTempDir(prefix: String, o: Fs.InMemoryFs[r]): Result[IoError, String] \ r + Clock =
let n = o->counter;
o->counter = n + 1;
let name = "${prefix}inmemory-${n}";
o->dirs = Set.insert(name, o->dirs);
touchNew(name, o);
Ok(name)
// ─── Listing ops ─────────────────────────────────────────────────
/// Returns the names of the entries in the directory `dir` in `o`.
pub def list(dir: String, o: Fs.InMemoryFs[r]): Result[IoError, List[String]] \ r =
let norm = normalize(dir);
if (not hasDir(dir, o))
notFound(dir)
else {
let fileNames = Map.keysOf(o->files)
|> Set.filter(p -> parentDir(p) == Some(norm))
|> Set.map(p -> fileName(p));
let dirNames = o->dirs
|> Set.filter(p -> parentDir(p) == Some(norm))
|> Set.map(p -> fileName(p));
Ok(Set.toList(Set.union(fileNames, dirNames)))
}
/// Returns the paths in `o` under `base` that match the glob `pattern`.
pub def glob(base: String, pattern: String, o: Fs.InMemoryFs[r]): Result[IoError, List[String]] \ r =
if (not hasDir(base, o))
notFound(base)
else
unsafe IO {
let basePath = Paths.get(normalize(base));
let matcher: PathMatcher = FileSystems.getDefault().getPathMatcher("glob:${pattern}");
let matchesPattern = path -> {
let p = Paths.get(path);
let starts = p.startsWith(basePath);
if (starts) {
let rel = basePath.relativize(p);
matcher.matches(rel)
} else
false
};
let matched = Set.union(
Map.keysOf(o->files) |> Set.filter(matchesPattern),
o->dirs |> Set.filter(matchesPattern)
);
Ok(Set.toList(matched))
}
}