///|
/// Deterministic, in-memory filesystem for tests, tools, and non-native
/// hosts. All stored paths are normalized at construction time.
///
/// Fields stay private so callers cannot bypass path normalization or forge
/// adapter counters. Mutation remains available through `put`.
pub struct MemoryFileSystem {
priv files : Map[String, String]
priv home : String?
priv mut reads : Int
priv mut globs : Int
}
///|
/// Create an in-memory filesystem from display paths and text contents.
pub fn MemoryFileSystem::new(
files : Map[String, String],
home? : String,
) -> MemoryFileSystem {
let normalized : Map[String, String] = Map([])
for path, text in files {
normalized[normalize_path(path)] = text
}
{ files: normalized, home, reads: 0, globs: 0 }
}
///|
/// Update or add a virtual file using its normalized path.
pub fn MemoryFileSystem::put(
self : MemoryFileSystem,
path : String,
text : String,
) -> Unit {
self.files[normalize_path(path)] = text
}
///|
/// Number of `read_text` calls made through the `FileSystem` interface.
pub fn MemoryFileSystem::read_count(self : MemoryFileSystem) -> Int {
self.reads
}
///|
/// Number of `glob` calls made through the `FileSystem` interface.
pub fn MemoryFileSystem::glob_count(self : MemoryFileSystem) -> Int {
self.globs
}
///|
/// View this deterministic implementation through the portable backend trait.
pub fn MemoryFileSystem::as_file_system(self : MemoryFileSystem) -> &FileSystem {
self as &FileSystem
}
///|
fn chars(text : String) -> Array[Char] {
let result : Array[Char] = []
for char in text {
result.push(char)
}
result
}
///|
/// Match one normalized virtual path in O(pattern × path) time and O(path)
/// memory. `*` and `?` do not consume `/`, which keeps `conf.d/*.conf`
/// confined to its directory level.
fn path_glob_matches(pattern : String, path : String) -> Bool {
let pattern_chars = chars(pattern)
let path_chars = chars(path)
let mut previous = Array::make(path_chars.length() + 1, false)
previous[0] = true
for token in pattern_chars {
let current = Array::make(path_chars.length() + 1, false)
if token == '*' {
current[0] = previous[0]
for index in 1..<=path_chars.length() {
current[index] = previous[index] ||
(current[index - 1] && path_chars[index - 1] != '/')
}
} else {
for index in 1..<=path_chars.length() {
current[index] = previous[index - 1] &&
(if token == '?' {
path_chars[index - 1] != '/'
} else {
token == path_chars[index - 1]
})
}
}
previous = current
}
previous[path_chars.length()]
}
///|
impl FileSystem for MemoryFileSystem with fn read_text(
self : MemoryFileSystem,
path : String,
max_bytes? : Int,
) -> Result[String, FileSystemError] {
self.reads += 1
let normalized = normalize_path(path)
match self.files.get(normalized) {
Some(text) =>
match max_bytes {
Some(limit) if @utf8.encode(text).length() > limit =>
Err(ReadByteLimitExceeded(path~, limit~))
_ => Ok(text)
}
None => Err(NotFound(path~))
}
}
///|
impl FileSystem for MemoryFileSystem with fn canonical_identity(
self : MemoryFileSystem,
path : String,
) -> Result[String, FileSystemError] {
let normalized = normalize_path(path)
if self.files.contains(normalized) {
Ok(normalized)
} else {
Err(NotFound(path~))
}
}
///|
impl FileSystem for MemoryFileSystem with fn glob(
self : MemoryFileSystem,
pattern : String,
max_matches? : Int,
) -> Result[Array[String], FileSystemError] {
self.globs += 1
let normalized = normalize_path(pattern)
let matches : Array[String] = []
for path in self.files.keys() {
if path_glob_matches(normalized, path) {
match max_matches {
Some(limit) if matches.length() >= limit =>
return Err(GlobMatchLimitExceeded(path=pattern, limit~))
_ => ()
}
matches.push(path)
}
}
matches.sort()
Ok(matches)
}
///|
impl FileSystem for MemoryFileSystem with fn home_dir(self : MemoryFileSystem) -> String? {
self.home
}