The smallmind-file module is a container for alternative java.nio.file
FileSystem implementations — file systems that plug into the standard JDK
NIO SPI (java.nio.file.spi.FileSystemProvider) but store, route, or gate
their data in ways the built-in file: provider cannot.
It is organized as two independently publishable artifacts:
-
file-ephemeral— an in-memory, heap-backedFileSystemwith a transparent fall-through to a native provider for paths outside its configured roots. Can be installed as the JVM default through-Djava.nio.file.spi.DefaultFileSystemProvider=…. -
file-jailed— a chroot-styleFileSystemthat confines all path resolution to a subtree of an underlying native file system. The jail boundary can be fixed at construction time or resolved per-call from a thread-bound context.
Neither artifact has a runtime dependency on the other. Both depend only on
smallmind-nutsnbolts; the jailed artifact additionally has an optional
compile-time dependency on jakarta.xml.bind-api for the JAXB annotations
on RootedFileSystemContext.
The JDK ships exactly one production-grade FileSystemProvider, the
platform file: provider. Everything else — in-memory sandboxes, chroot
confinements, overlay trees for tests, watched virtual directories —
either has to be implemented by hand or pulled in from a heavyweight
dependency such as Jimfs, Memoryfs, or a full VFS library.
The two modules in smallmind-file are the minimum useful answer to two
recurring needs that do not justify either of those choices:
-
Ephemeral, default-installable in-memory storage with native pass-through. Tests want a
Files.createDirectories/Files.write/Files.readStringloop that leaves no disk artefacts, runs at memory speed, and still reads real classpath resources through the ordinaryfile:provider. A production service that wants an overlay — for example, a scratch area under/opt/epicenter/twimblethat must behave like a real directory to legacy code but never touch disk — wants the same thing, but installed JVM-wide. -
A hard boundary around a subtree of the real file system. Code that accepts user-controlled path inputs — uploaded file managers, sandboxed plugin hosts, multi-tenant storage services — needs a
FileSystemwhose absolute/is someone else’s/var/lib/service/tenant-42, with every path translation gated so../../etc/passwdcannot escape. The JDK provides no such type; this module provides two concrete strategies for it.
Both modules exist as ordinary FileSystemProvider implementations so
that they integrate through java.nio.file.Files, java.nio.file.Path,
and java.nio.file.Paths without any code on the caller’s side knowing
that a non-standard provider is in play.
-
Not a full VFS (no zip, no SFTP, no S3). The two provided file systems are the two the repository needs; adding more is possible but is not the purpose of the module.
-
Not a security boundary on its own. The jailed provider enforces path containment at the path translation layer; it does not sign, encrypt, or audit I/O, and it does not defend against kernel-level escapes such as symlink traversal on the underlying native file system. A caller that wants those properties must compose them over the jailed provider.
-
Not a persistence layer. The ephemeral provider is, by construction, lost on JVM exit.
| If you are … | Read, in this order |
|---|---|
… a test author who wants a throwaway in-memory file system for the duration of a single test |
|
… a service author installing ephemeral storage as the JVM default provider |
Installing as the JVM default provider → Configuration → Native delegation in detail. |
… a library author building a user-facing upload surface that must not escape a tenant directory |
Jailed quick start → Translator strategies → Deriving the jail boundary per request. |
… someone trying to understand why a call is throwing
|
| Artifact | Ships | Reach for it when |
|---|---|---|
|
|
You need a |
|
|
You need a |
A few terms recur throughout this module.
- Provider-managed singleton
-
Both
EphemeralFileSystemProviderandJailedFileSystemProvidermaintain exactly oneFileSysteminstance per provider. Calls tonewFileSystem(URI, Map)always throwFileSystemAlreadyExistsException;getFileSystem(URI)returns the single pre-constructed instance after validating the URI. This is a deliberate choice: it keeps the mapping between scheme and provider unambiguous when the provider is registered through the SPI. - Scheme mirroring
-
The ephemeral provider has a no-arg constructor using the scheme
"ephemeral"and aEphemeralFileSystemProvider(FileSystemProvider)constructor that mirrors the scheme of another provider (typically the platformfile:provider) and captures a reference to that provider’s root file system. When the provider is installed as the JVM default and mirrors thefile:scheme, ephemeral paths and native paths can coexist under the samePaths.get(…)call, with theEphemeralFileSystemConfigurationdeciding which ones are "ours". - Native delegation
-
In the ephemeral provider, any path that does not start with one of the configured roots is wrapped in a
NativePathadapter and all operations on it are forwarded, unchanged, to the native provider. This is what makes the ephemeral provider safe to install as the JVM default: real files underC:\Users\…or/usr/…are not intercepted; only paths that match the configured overlay roots are served from the heap. - Path translation
-
In the jailed provider, every operation goes through a
JailedPathTranslator.unwrapPath(Path)call that converts the jailed absolute path into a native path rooted at the jail’s configured root directory. Listing operations wrap the returned native paths back intoJailedPathinstances viawrapPath(…), which throwsSecurityExceptionif the native path does not start with the configured root. - Heap tree
-
Internal name for the in-memory data structure that backs the ephemeral file store: a tree of
DirectoryNodeandFileNodeobjects, each descending from a single anonymous root. File content lives in aByteArrayIOBuffer(fromsmallmind-nutsnbolts) and is exposed to clients throughEphemeralSeekableByteChannel. - Heap event / watch translation
-
Mutations to the heap tree raise
HeapEventobjects whose path is the changed entry. Each event bubbles up the heap-node parent chain. AnEphemeralHeapEventListeneris installed per watched directory and carries that directory’s path. When bubbling reaches the listener, it forwards the event to theEphemeralWatchService, which computes theWatchEventcontext()aswatchedPath.relativize(changedPath)so that the standard NIOWatchServicecontract is honoured end-to-end, entirely in heap.
Both artifacts are published under org.smallmind:
<dependency>
<groupId>org.smallmind</groupId>
<artifactId>file-ephemeral</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.smallmind</groupId>
<artifactId>file-jailed</artifactId>
<version>${project.version}</version>
</dependency>Use whichever one you need; depending on both is allowed but unusual.
file-ephemeral-
Requires
smallmind-nutsnbolts(forByteArrayIOBuffer,ByteArrayIOStream,SingleItemIterable,SnowflakeId, andUnknownSwitchCaseException). Everything else is JDK-only. file-jailed-
Requires
smallmind-nutsnbolts(forContextFactory/Context). Thejakarta.xml.bind-apidependency is marked<optional>true</optional>— it is needed only if you intend to unmarshalRootedFileSystemContextfrom XML. If you construct contexts programmatically, JAXB need not be on the classpath.
The jailed artifact ships a META-INF service entry:
META-INF/services/java.nio.file.spi.FileSystemProvider
org.smallmind.file.jailed.JailedFileSystemProviderso simply placing file-jailed on the classpath registers the jailed:
scheme with FileSystems.newFileSystem(URI, Map).
The ephemeral artifact does not ship a service entry. Installation is
controlled explicitly: the provider is instantiated either directly by
the application or by the JVM via
-Djava.nio.file.spi.DefaultFileSystemProvider=… (see
Installing as the JVM default provider). This is deliberate — the ephemeral
provider’s whole point, when installed as a default, is to replace the
platform file: provider, which is a decision an application must make
on purpose.
The ephemeral provider is a complete NIO file system backed entirely by heap memory, with built-in pass-through to a native provider for paths the configuration does not claim.
-
Serves a single
EphemeralFileSysteminstance per provider, with oneEphemeralFileStoreof configurable total capacity and block size. -
Recognises a configurable set of path prefixes (roots) and routes any path that matches one of them through the in-memory heap tree.
-
Wraps every other path in a
NativePathadapter that delegates to the native provider captured at construction. From the caller’s perspective,Paths.get(…)returns a seamlessPathregardless of whether it resolves to heap or disk. -
Implements the full
SeekableByteChannel,DirectoryStream, andSecureDirectoryStreamcontracts against the heap, soFiles.newByteChannel,Files.newDirectoryStream, andFiles.walkFileTreeall work. -
Implements
WatchServiceagainst heap-bubbled events (HeapEvent→WatchEvent.Kindtranslation), soFiles.newWatchService()produces a working in-memory watcher. -
Exposes a
clear()method on bothEphemeralFileSystemandEphemeralFileStorethat drops every node, useful for test teardown.
-
No symbolic link support.
EphemeralBasicFileAttributes.isSymbolicLinkalways returnsfalse;toRealPathis equivalent tonormalize().toAbsolutePath(). -
No POSIX permission model. The only
FileAttributeaccepted bycreateDirectoryandnewByteChannelis"posix:permissions", and even that is validated only by name — the value is not stored or enforced. -
No
FileChannel/AsynchronousFileChannelsupport for ephemeral paths. Those methods exist only to delegate to the native provider when given aNativePath; ephemeral paths throwUnsupportedOperationException. -
No
UserPrincipalLookupServicebacking store. Lookups return freshEphemeralUserPrincipal/EphemeralGroupPrincipalinstances on every call. -
Not a security boundary. The ephemeral provider offers no access control of its own; use the jailed provider on top if path-space containment is required.
| Class / interface | Role |
|---|---|
|
Entry point. Manages a single |
|
The |
|
Immutable configuration carrier: capacity, block size, and roots.
The no-arg constructor reads system properties; an explicit
constructor accepts the three values directly. |
|
The heap file store. Implements every mutating and attribute-reading
operation exposed by the provider ( |
|
The heap-resident |
|
Adapter |
|
|
|
Typed attribute view for the file store. Attribute values are read by
reflection against declared fields (see
|
|
|
|
Channel backed by the |
|
Trivial |
|
Glob-to-regex translator and the compiled-regex-backed |
|
URI validation ( |
The heap subpackage, org.smallmind.file.ephemeral.heap, owns the
internal data structures:
| Class | Role |
|---|---|
|
Abstract base for tree nodes. Holds the parent reference, the node
name, the |
|
Map of child names to child nodes. |
|
Leaf node holding a |
|
Enum with two constants, |
|
|
|
Callback interface invoked during |
|
Enum whose three constants ( |
The watch subpackage, org.smallmind.file.ephemeral.watch, ties the
heap events back to NIO’s WatchService:
| Class | Role |
|---|---|
|
|
|
|
|
|
|
Bridge from |
EphemeralFileSystemConfiguration has two constructors:
// reads system properties; falls back to defaults
public EphemeralFileSystemConfiguration ();
// explicit
public EphemeralFileSystemConfiguration (long capacity,
int blockSize,
String... roots);The three knobs are:
| Knob | Default | Notes |
|---|---|---|
|
|
Reported through |
|
|
Passed to the |
|
|
The set of prefixes that route a path through the heap. A root must
start with |
The no-arg constructor reads:
org.smallmind.file.ephemeral.configuration.capacity = <long>
org.smallmind.file.ephemeral.configuration.blockSize = <int>
org.smallmind.file.ephemeral.configuration.roots = <comma-separated
list, optionally
bracketed>For roots, both "/opt/tmp,/var/scratch" and "[/opt/tmp, /var/scratch]"
are accepted; entries that do not start with "/" are prefixed with one
automatically.
To replace the platform file: provider:
-Djava.nio.file.spi.DefaultFileSystemProvider=org.smallmind.file.ephemeral.EphemeralFileSystemProvider
# plus any of:
-Dorg.smallmind.file.ephemeral.configuration.capacity=1073741824
-Dorg.smallmind.file.ephemeral.configuration.blockSize=4096
-Dorg.smallmind.file.ephemeral.configuration.roots=/opt/overlay,/var/scratchWhen the JVM bootstraps the default file system through this property,
it calls the EphemeralFileSystemProvider(FileSystemProvider)
constructor, passing the platform file: provider. The resulting
provider:
-
reports
getScheme() == "file", -
answers
isDefault() == true, -
captures the platform provider’s root file system in its
nativeFileSystemfield, and -
routes every non-matching path through that captured file system.
From the application’s perspective, Paths.get("/opt/overlay/foo") is
an EphemeralPath while Paths.get("/etc/hosts") is a NativePath
wrapping the platform provider’s path — both returned by the same
FileSystems.getDefault().getPath(…) call.
|
Note
|
|
The test in the source tree shows the smallest realistic loop:
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.smallmind.file.ephemeral.EphemeralFileSystem;
import org.smallmind.file.ephemeral.EphemeralFileSystemProvider;
public class Demo {
public static void main (String[] args)
throws Exception {
EphemeralFileSystemProvider.waitForInitialization(30, TimeUnit.SECONDS); (1)
Path real = Paths.get("C:\\Users\\david\\Documents\\response.txt"); (2)
System.out.println(Files.isRegularFile(real));
System.out.println(Files.readString(real, StandardCharsets.UTF_8));
Path eph = Paths.get("/opt/epicenter/twimble/farkle"); (3)
Files.createDirectories(eph);
try (SeekableByteChannel bc = Files.newByteChannel(
eph.resolve("sparkle.txt"),
Set.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE))) {
ByteBuffer bb = ByteBuffer.allocate(1024);
bb.put("Hello out there!".getBytes(StandardCharsets.UTF_8));
bb.flip();
bc.write(bb);
}
System.out.println(new String(Files.readAllBytes(eph.resolve("sparkle.txt"))));
((EphemeralFileSystem)FileSystems.getDefault()).clear(); (4)
}
}-
Blocks until the default-provider constructor has built the heap file system. Safe to call even when you are not using the default-provider install; it simply returns immediately.
-
A path that does not match any configured root. Wrapped in a
NativePathand forwarded to the real platform provider. -
A path under a configured root (or under
"/"by default). Served entirely from heap. Notice that from `Files’ point of view the two paths are indistinguishable. -
Drops every node. Useful at the end of a test, or between test methods, to reset the store to an empty state.
The ephemeral provider is perfectly usable as a secondary file system.
Construct one directly and obtain paths through its getPath(…):
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import org.smallmind.file.ephemeral.EphemeralFileSystemProvider;
EphemeralFileSystemProvider provider = new EphemeralFileSystemProvider("overlay");
FileSystem fs = provider.getFileSystem(URI.create("overlay:///"));
Path p = fs.getPath("/scratch/demo.txt");
Files.createDirectories(p.getParent());
Files.writeString(p, "hello");The scheme ("overlay" here) is decoupled from "file", so nothing
touches the default provider and the two can coexist in the same JVM.
Note that when constructed via the no-arg or String-scheme constructor
the provider has no native file system captured, so any path that does
not match a configured root will cause a NullPointerException the
moment the provider tries to delegate. Mix-mode usage requires the
EphemeralFileSystemProvider(FileSystemProvider) form.
The watch service is a full NIO WatchService:
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
public void watch (Path root)
throws Exception {
Files.createDirectories(root);
try (WatchService watcher = root.getFileSystem().newWatchService()) {
root.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY); (1)
// ... in another thread ...
Files.writeString(root.resolve("new.txt"), "hello"); (2)
WatchKey key = watcher.take(); (3)
for (WatchEvent<?> event : key.pollEvents()) {
System.out.println(event.kind() + " " + event.count());
}
key.reset(); (4)
}
}-
Registration creates a fresh
EphemeralHeapEventListenerbound to the watched path and attaches it to the directory’sDirectoryNode. The first registration per path attaches the listener; the last cancel detaches it. -
Any mutation whose
HeapEventbubbles through the watched directory fires the listener, which forwards the event to the service together with the changed path. The service translates the event type to aWatchEvent.Kind(ENTRY_CREATE,ENTRY_DELETE,ENTRY_MODIFY) and dispatches to the registered keys withwatchedPath.relativize(changedPath)as the context. -
take()polls the service’s ready queue in 500 ms increments so that a mid-waitclose()raisesClosedWatchServiceExceptionpromptly rather than hanging. -
Forget to call
reset()and the key will never be signalled again. If events arrived while the key was being processed,reset()requeues it immediately so it is returned by the nexttake().
Whenever a Path reaches any provider method, the provider performs
three checks in this order:
-
The path’s file system must be an
EphemeralFileSystem. If not, aProviderMismatchExceptionis thrown with an explanatory message. This is the common failure mode when a caller mixes paths obtained from different providers. -
If the path is a
NativePath, the operation is forwarded verbatim to the native provider’s implementation of the same operation, using the wrapped native path. Return values (including directory stream iterators and attribute views) come back from the native provider unchanged; they are not re-wrapped as ephemeral types. -
Otherwise the path is treated as an
EphemeralPath, normalised, and passed to theEphemeralFileStorefor the actual operation.
For two-argument operations (copy, move, isSameFile) both paths
must be associated with an EphemeralFileSystem; otherwise
ProviderMismatchException is raised. Cross-provider copies (ephemeral
→ native and vice versa) must be performed through
Files.copy(InputStream, Path) / Files.copy(Path, OutputStream)
rather than Files.copy(Path, Path).
The NativePath adapter is the seam that makes default-provider
installation feel transparent. NativePath.getFileSystem() reports the
ephemeral file system so that NIO type checks pass, but every structural
operation (getRoot, resolve, normalize, toUri, compareTo,
startsWith, endsWith, etc.) delegates to the wrapped native path.
The jailed provider takes an existing native file system and serves it
to callers as if its root were some other configured directory on that
file system. Absolute paths under the jail look like "/foo/bar"; the
provider translates them to
<configured-root>/foo/bar on the native side before delegating.
-
Serves a single
JailedFileSysteminstance per provider under thejailed:URI scheme by default (or any scheme supplied explicitly). -
Translates every path supplied to any provider method into a native path via a
JailedPathTranslator, and forwards the operation to the native provider. -
For listing operations, wraps returned native paths back into
JailedPathinstances viatranslator.wrapPath(…), which throwsSecurityExceptionif the native path escapes the jail. -
Exposes two translator strategies out of the box:
RootedPathTranslator(fixed native root) andContextSensitiveRootedPathTranslator(native root read from a thread-boundRootedFileSystemContext).
-
No native-to-jail path re-entry guard beyond
startsWith(rootPath). Symbolic links on the underlying native file system that point outside the root are not detected; the caller is responsible for choosing a root whose contents do not include escape-capable links. -
No watch service translation.
JailedFileSystem.newWatchService()delegates straight to the native file system, which returns nativeWatchKeyinstances whoseWatchableis a native path. Callers that need jail-scoped watch events must translate on the way out. -
No in-jail file store abstraction.
FileStorequeries delegate to the native provider. -
No jail-level quota, audit, or interception hooks. The translator answers a single question — "is this path inside the jail?" — and everything else is native behaviour.
| Class / interface | Role |
|---|---|
|
Entry point. Manages a single |
|
|
|
Custom |
|
Strategy interface: |
|
Base class providing reusable |
|
Fixed-root translator. The native root is passed at construction and never changes. |
|
Per-call-root translator. Reads a |
|
JAXB-annotated |
|
Static URI validation and conversion helpers, mirroring
|
The translator interface is the policy boundary for the jail. The module ships two implementations:
A single native root decided at construction time. Appropriate when the jail boundary is known when the translator is created and never needs to change:
import java.nio.file.FileSystems;
import java.nio.file.Path;
import org.smallmind.file.jailed.JailedFileSystemProvider;
import org.smallmind.file.jailed.RootedPathTranslator;
Path nativeRoot = FileSystems.getDefault().getPath("/var/lib/tenant-42");
JailedFileSystemProvider provider =
new JailedFileSystemProvider("jailed", new RootedPathTranslator(nativeRoot));A JailedPath.resolve("/etc/passwd") through this provider will
translate to /var/lib/tenant-42/etc/passwd on the native side — which
either resolves to a file that tenant has deliberately placed there or
to NoSuchFileException. What it never resolves to is the real
/etc/passwd, because RootedPathTranslator.unwrapPath(jailedPath)
appends segments to nativeRoot rather than treating them as absolute.
The native root is read on every call from a thread-bound
RootedFileSystemContext. Appropriate when the same provider must serve
different tenants, sessions, or request scopes in the same JVM:
import java.nio.file.FileSystems;
import org.smallmind.file.jailed.ContextSensitiveRootedPathTranslator;
import org.smallmind.file.jailed.JailedFileSystemProvider;
import org.smallmind.file.jailed.RootedFileSystemContext;
import org.smallmind.nutsnbolts.context.ContextFactory;
JailedFileSystemProvider provider =
new JailedFileSystemProvider(
"jailed",
new ContextSensitiveRootedPathTranslator(FileSystems.getDefault()));
// later, at request time:
RootedFileSystemContext ctx = new RootedFileSystemContext();
ctx.setRoot("/var/lib/tenant-42");
ContextFactory.pushContext(ctx); (1)
try {
// all jailed-path operations on this thread now translate against
// /var/lib/tenant-42
} finally {
ContextFactory.popContext(RootedFileSystemContext.class); (2)
}-
The push/pop API here is the
smallmind-nutsnboltsContextFactorycontract; consult that module for the exact method names and thread-binding semantics. -
If the context is missing or its
getRoot()isnullwhen a jailed-file-system operation runs,ContextSensitiveRootedPathTranslatorthrowsSecurityExceptionwith the message"No authorization for path". The jail fails closed.
The context-sensitive translator is the recommended pattern for services
that handle multiple tenants, users, or request scopes through one
JailedFileSystemProvider. The typical shape is:
-
A request-scoped interceptor (servlet filter, Jersey filter, RPC request handler, …) resolves the authorised native root for the current caller.
-
The interceptor pushes a
RootedFileSystemContextwith that root onto theContextFactorystack. -
Business logic performs
Files.*operations onJailedPathinstances. Each call hitsunwrapPath(…), which consults the context, and produces an absolute native path anchored at the request’s root. -
The interceptor pops the context on request completion. Any subsequent operation without a context fails closed with
SecurityException.
Because the context is consulted per call, the caller never has to manage a file-system instance per tenant; the single provider serves all of them.
The no-arg JailedFileSystemProvider() is what the SPI instantiates
when the artifact is on the classpath. It calls:
this("jailed", new ContextSensitiveRootedPathTranslator(FileSystems.getDefault()));That means just dropping file-jailed on the classpath registers a
jailed: provider whose root must be supplied at call time via a
RootedFileSystemContext. Without a context in scope, every call fails
with SecurityException.
A service that wants different defaults — a fixed root, a different
scheme, a non-default native file system — must construct and register
its own JailedFileSystemProvider explicitly. The SPI-installed
default is intentionally useless without a context: fail-closed is the
safer initial state.
Using RootedPathTranslator with a fixed root:
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import org.smallmind.file.jailed.JailedFileSystemProvider;
import org.smallmind.file.jailed.RootedPathTranslator;
public class JailedDemo {
public static void main (String[] args)
throws Exception {
Path root = FileSystems.getDefault().getPath("/var/lib/tenant-42"); (1)
Files.createDirectories(root);
JailedFileSystemProvider provider =
new JailedFileSystemProvider("jailed", new RootedPathTranslator(root)); (2)
FileSystem fs = provider.getFileSystem(URI.create("jailed:///")); (3)
Path inside = fs.getPath("/reports/2026/q2.csv"); (4)
Files.createDirectories(inside.getParent());
Files.writeString(inside, "col1,col2\n1,2\n");
Path escapeAttempt = fs.getPath("/../../etc/passwd"); (5)
try {
Files.readAllBytes(escapeAttempt);
} catch (SecurityException ignored) {
// expected: translator rejects the attempt on wrap/unwrap
}
}
}-
The native root. Whatever this directory contains is what the jail exposes under
/. -
Construct the provider with a fixed-root translator.
-
The
URIhere must be"jailed:///"— scheme matches, authority is absent, path is exactly"/", no query, no fragment (seeJailedURIUtility.checkUri). -
Creating a directory and writing a file inside the jail — the
Files.*calls see a straightforward absolute path; the translator converts it to/var/lib/tenant-42/reports/2026/q2.csvon the way into the native provider. -
Attempting to escape.
..segments are not expanded by the translator itself; normalisation happens on the native side, and if the native path falls outside the root,wrapPath’s `startsWithcheck rejects it when the result of a listing iterator needs to be re-wrapped. In practice the escape surface is the set of places wherewrapPathruns — primarily directory listings and any caller that passes a native path back through.
|
Note
|
The translator’s |
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import org.smallmind.file.ephemeral.EphemeralFileSystem;
import org.smallmind.file.ephemeral.EphemeralFileSystemProvider;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
public class FooTest {
private EphemeralFileSystemProvider provider;
private FileSystem fs;
@BeforeClass
public void setup () {
provider = new EphemeralFileSystemProvider("test");
fs = provider.getFileSystem(URI.create("test:///"));
}
@AfterMethod
public void resetHeap () {
((EphemeralFileSystem)fs).clear();
}
// tests use fs.getPath(...) freely; each test starts with an empty heap.
}Mount an in-memory scratch area at /opt/scratch while leaving
everything else on real disk. Start the JVM with:
-Djava.nio.file.spi.DefaultFileSystemProvider=org.smallmind.file.ephemeral.EphemeralFileSystemProvider
-Dorg.smallmind.file.ephemeral.configuration.roots=/opt/scratch
-Dorg.smallmind.file.ephemeral.configuration.capacity=2147483648Paths.get("/opt/scratch/session-123/state.bin") is now an ephemeral
path, served from heap, with all NIO operations including watch
available. Paths.get("/etc/hosts") still reads the real file.
Request handler pseudocode, using the context-sensitive translator installed by the SPI default:
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import org.smallmind.file.jailed.RootedFileSystemContext;
import org.smallmind.nutsnbolts.context.ContextFactory;
public void handleUpload (String tenantId, String relativePath, byte[] bytes)
throws IOException {
RootedFileSystemContext ctx = new RootedFileSystemContext();
ctx.setRoot("/var/lib/tenants/" + tenantId);
ContextFactory.pushContext(ctx);
try {
FileSystem fs = FileSystems.getFileSystem(URI.create("jailed:///"));
Path target = fs.getPath("/").resolve(relativePath).normalize(); (1)
Files.createDirectories(target.getParent());
Files.write(target, bytes);
} finally {
ContextFactory.popContext(RootedFileSystemContext.class);
}
}-
Normalise inside the jail so that
..sequences collapse within theJailedPathbefore the translator sees them. Combined with a symlink-free tenant root, this is the minimum precaution against path-traversal escapes.
EphemeralFileSystem.getPath(first, more) decides between
EphemeralPath and NativePath by calling
EphemeralFileSystemConfiguration.isOurs(first, more), which performs
a character-level prefix match. The match walks the concatenated path
character-by-character without first building a full path string; a
root matches when all of its characters are consumed in order by the
leading portion of the assembled path.
Implication: configuration entries like "/opt/scratch" and
"/opt" behave differently. "/opt/scratchy/foo" matches "/opt" but
not "/opt/scratch". Plan the root set to avoid accidental capture of
sibling trees.
EphemeralFileSystem.close() is a no-op when the file system is
installed as the default provider (isDefault() == true). Otherwise it
sets the closed flag. The class deliberately does not close open
channels, directory streams, or the watch service; a TODO in the source
acknowledges the gap. Callers who need a hard shutdown for a
non-default instance should close outstanding resources themselves
before calling close().
-
Registrations install a per-watched-path heap listener on first register per path and remove it on last cancel.
-
Heap mutations performed through the file store and the seekable byte channel (
createDirectory,delete,copy,move,newByteChannelwithCREATE/CREATE_NEW/TRUNCATE_EXISTING, channelwrite/truncate) bubble events up to ancestor watchers, including watchers registered on directories above the changed entry. TheWatchEvent.context()is the path relative to the watched directory. -
Polling:
poll(timeout, unit)with a timeout shorter than 500 ms delegates to the underlyingLinkedBlockingQueue.poll. With a longer timeout, the service polls in 500 ms increments, checkingclosedbetween polls so thatclose()mid-wait raisesClosedWatchServiceExceptionpromptly. -
take()behaves identically but has no upper bound. -
A
WatchKeythat is fired while already signalled does not re-queue; subsequent fires are still added to the key’s event queue. Consumers who neglect to callreset()afterpollEvents()silently stop receiving signals for that key.
EphemeralFileStore.copy(source, target, options) and
move(source, target, options) both:
-
accept only
StandardCopyOptionvalues; anything else throwsUnsupportedOperationException; -
honour only
StandardCopyOption.REPLACE_EXISTING(others are accepted but have no effect); -
treat
source.equals(target)as a no-op; -
reject attempts to overwrite the root directory (throw
IOExceptionwith message"Not Allowed").
The difference: copy allocates a fresh ByteArrayIOBuffer per file
copy (content duplication), while move transfers the source’s
ByteArrayIOBuffer by reference and then unlinks the source node
(zero-copy content transfer).
Every cross-path operation on a JailedPath (startsWith, endsWith,
resolve, relativize, compareTo) requires the other argument to
be a JailedPath. A foreign Path raises ProviderMismatchException
with no message. This is a stricter contract than the JDK’s default
Path types; be careful when mixing JailedPath with paths returned
from other file systems.
JailedFileSystemProvider.getFileStore(Path),
readAttributes(…), getFileAttributeView(…), and
setAttribute(…) all translate the path and forward to the native
provider. The returned FileStore / FileAttributeView /
BasicFileAttributes instances are the native types, not
jail-specific wrappers. Callers who need jail-scoped attributes (for
example, a file size that is relative to an accounting quota, not the
native store’s total capacity) must wrap the returned values
themselves.
ProviderMismatchException-
Thrown whenever an operation receives a
Pathwhose file system is not anEphemeralFileSystem. The message identifies the offending path and the expected class. Two-argument operations (copy,move,isSameFile) also raise this exception when the two paths belong to differentEphemeralFileSysteminstances, with the message"The source and target are associated with different file systems". ClosedFileSystemException-
Thrown by
EphemeralFileStore,EphemeralFileSystem, andEphemeralWatchServicewhenever a post-close method is called. FileSystemAlreadyExistsException-
Always thrown by
EphemeralFileSystemProvider.newFileSystem(URI, Map). The provider pre-constructs the singleton in its constructor; callers must usegetFileSystem(URI)instead.
NoSuchFileException / FileAlreadyExistsException /
DirectoryNotEmptyException::
Standard NIO semantics, produced by EphemeralFileStore for the
matching conditions (missing parent, existing non-REPLACE_EXISTING
target, non-empty directory on delete or directory-target copy).
UnsupportedOperationException-
Raised when a caller passes a non-
StandardOpenOptiontonewByteChannel, a non-StandardCopyOptiontocopy/move, or aFileAttributewhose name is not"posix:permissions"tocreateDirectory/newByteChannel. Also raised bynewFileChannel/newAsynchronousFileChannelwhen the path is not aNativePath.
SecurityException-
Raised by
AbstractJailedPathTranslator.wrapPathwhen a native path does not start with the configured root, and byContextSensitiveRootedPathTranslator.wrapPath/unwrapPathwhen noRootedFileSystemContextis present or its root isnull. The message is always"No authorization for path". ProviderMismatchException-
Raised by any
JailedPathcross-path operation that receives a non-JailedPathargument. FileSystemAlreadyExistsException-
Always thrown by
JailedFileSystemProvider.newFileSystem(URI, Map). IllegalArgumentException-
Raised by
JailedURIUtilityfor URIs that are not well-formed for the provider (wrong scheme, non-empty authority, path other than"/", any query or fragment). Also raised byJailedPath.relativizewhen the two paths differ in absoluteness.
| Setting | Default | Where set |
|---|---|---|
Ephemeral capacity |
|
|
Ephemeral block size |
|
|
Ephemeral roots |
|
|
Ephemeral provider scheme (no-arg) |
|
|
Ephemeral watch poll interval |
|
|
Jailed provider scheme (no-arg) |
|
|
Jailed default translator (no-arg) |
|
|
Jailed failure mode with no context |
|
|
Jailed path separator |
|
|
ProviderMismatchException: The path(X) is not associated with the EphemeralFileSystem-
A
Paththat did not come from anEphemeralFileSystemwas handed to the ephemeral provider. Common cause: mixingjava.nio.file.Paths.get(…)output when the ephemeral provider is not installed as the default with a non-default ephemeral provider instance. Resolve by obtaining paths from the specific file system you intend to use (ephemeralFs.getPath(…)). NullPointerExceptiondeep insideEphemeralFileSystemProvideron a non-matching path-
A provider constructed with the no-arg or
String-scheme form has no native file system captured. Paths outside the configured roots hitprovider.getNativeFileSystem()which isnull. Use theEphemeralFileSystemProvider(FileSystemProvider)constructor, or ensure every path matches a configured root. EphemeralFileSystemoperations hang at startup under the default-provider install-
Some thread is calling
FileSystems.getDefault()(orPaths.get(…)) before the provider constructor has completed. CallEphemeralFileSystemProvider.waitForInitialization(timeout, unit)from your startup path, or sequence your initialization so the provider constructs before concurrent callers run. SecurityException("No authorization for path")in an apparently simple jailed call-
The current thread has no
RootedFileSystemContextin the context stack, or the context’s root isnull. Push a context before the operation, and guarantee the push happens even when an exception is thrown by the path-setup code — the whole point ofSecurityExceptionhere is that the jail fails closed. JailedPathoperations return a path that "escapes" the jail-
Check whether the root directory contains symlinks. The translator only validates
startsWith(rootPath); the underlying native provider is free to follow symlinks out of the root. Either remove the symlinks or put a filesystem-level protection around the root. IllegalArgumentException("Path component should be '/'")when obtaining aJailedFileSystem-
getFileSystem(URI)was called with a URI other than"jailed:///". The path component of the URI must be exactly"/". Paths within the jail are built withfs.getPath(…), not URIs.
- No symbolic links in the ephemeral heap
-
EphemeralBasicFileAttributes.isSymbolicLinkalways returnsfalse;toRealPathisnormalize().toAbsolutePath(). Code paths that depend on symlink semantics will behave differently on ephemeral paths than on native ones. - Ephemeral
close()does not close open resources -
A
close()call on anEphemeralFileSystemthat is not the default provider flips theclosedflag but leaves open channels and directory streams alone. The file store’s open-resource tracking is a known TODO in the source. - No
FileChannel/AsynchronousFileChannelfor ephemeral paths -
EphemeralFileSystemProvider.newFileChannelandnewAsynchronousFileChannelthrowUnsupportedOperationExceptionunless the path is aNativePath. Code that needs memory-mapped access to ephemeral content must work throughEphemeralSeekableByteChannelor read the full buffer into heap. - Ephemeral attribute model is deliberately shallow
-
Only the
basicview is supported.posix,dos,owner, andaclviews are not implemented.setAttributerecognises onlycreationTime,lastModifiedTime, andlastAccessTime; other names are silently ignored. - Jailed
register(WatchService, …)is unimplemented -
JailedPath.register(…)throwsUnsupportedOperationException. Watching inside a jail requires using the native file system’s watch service and translating paths manually. - Jailed translators do not defend against native symlink traversal
-
The containment guarantee is
startsWith(rootPath)at the path level. Whether the native provider respects that containment when it dereferences symlinks is out of scope; callers must curate the root directory accordingly. - Single-instance per provider
-
Both providers hold exactly one
FileSysteminstance. Multiple jails or multiple ephemeral overlays in the same JVM require multiple provider instances, each constructed by the application (not through the default SPI registration).
smallmind-file supplies two small, complete java.nio.file file
system implementations that fill two concrete gaps in the JDK:
-
file-ephemeralgives you an in-memoryFileSystemwith native pass-through and watch support, installable as the JVM default for transparent overlay behaviour or usable side-by-side with the platform provider for tests and scratch areas. -
file-jailedgives you a chroot-styleFileSystemon top of an existing native one, with a pluggable translator strategy that supports both fixed-root and per-request root selection.
Both modules integrate through the standard Paths, Files, and
FileSystems APIs and depend on nothing outside smallmind-nutsnbolts.
Pick whichever one matches the need: heap-speed storage that behaves
like a real file system, or a hard boundary around a subtree of a real
file system.