9.0 KiB
Capability Spec Delta: storage-file (complete-storage-uri-adapters)
Package: @suaveplan/storage-file
Status: PROPOSED
Every requirement below carries a
**Verified by:**line at authoring time.
ADDED Requirements
Requirement: file:// URI plugin factory
The package SHALL export fileUriPlugin(options) from
./uri-plugin/uri-plugin returning a UriPlugin whose scheme is the
lowercase string "file" and whose adapter is the caller-supplied
NodeFsStorage instance. The factory MUST validate options.rootDir is
a non-empty absolute path; failure raises
StorageFileUriError(STORAGE_FILE_URI.INVALID_URI) synchronously.
Scenario: factory returns a UriPlugin with scheme "file"
- WHEN
fileUriPlugin({ adapter, rootDir: "/var/data" })is called - THEN the returned plugin's
scheme === "file",adapteris the same reference, andrunUriPluginConformance(plugin, fixtures)passes
Scenario: empty rootDir is rejected
- WHEN
fileUriPlugin({ adapter, rootDir: "" })is called - THEN the call throws
StorageFileUriErrorwithcode === "STORAGE_FILE_URI.INVALID_URI"
Verified by: src/uri-plugin/uri-plugin.test.ts::"exports a UriPlugin with scheme file" AND src/uri-plugin/uri-plugin.test.ts::"rejects empty rootDir"
Requirement: RFC 8089 authority handling
The plugin SHALL accept file:///... and file://localhost/... as
equivalent absolute-path forms. Any other non-empty authority SHALL
raise STORAGE_FILE_URI.UNSUPPORTED_AUTHORITY so network-file-system
URIs (file://server/share/...) do not silently degrade to local lookups.
Scenario: empty authority resolves to local path
- WHEN
plugin.parse("file:///etc/hosts")is called - THEN the result has
keyequal to"etc/hosts"(or platform equivalent relative torootDir)
Scenario: localhost authority resolves identically to empty
- WHEN
plugin.parse("file://localhost/etc/hosts")is called - THEN the result matches the empty-authority form exactly
Scenario: non-empty non-localhost authority is rejected
- WHEN
plugin.parse("file://nas.example.com/share/x")is called - THEN the call throws
StorageFileUriErrorwithcode === "STORAGE_FILE_URI.UNSUPPORTED_AUTHORITY"
Verified by: src/uri-plugin/uri-plugin.test.ts::"resolves empty authority to local path" AND src/uri-plugin/uri-plugin.test.ts::"treats localhost authority as local" AND src/uri-plugin/uri-plugin.test.ts::"rejects non-empty non-localhost authority"
Requirement: Windows drive-letter preservation
The plugin SHALL detect Windows drive-letter form
file:///<drive>:/path (the leading / followed by <ALPHA>:) and
preserve the drive letter through parse and build. Malformed
drive-letter forms (e.g. file:///C/path or file:///CC:/path) SHALL
raise STORAGE_FILE_URI.WINDOWS_DRIVE_INVALID.
Scenario: well-formed drive-letter parses
- WHEN
plugin.parse("file:///C:/Users/x.txt")is called - THEN the parsed key preserves
C:/Users/x.txt(verbatim, no percent-encoding of the colon)
Scenario: malformed drive-letter is rejected
- WHEN
plugin.parse("file:///CC:/x")is called - THEN the call throws
StorageFileUriErrorwithcode === "STORAGE_FILE_URI.WINDOWS_DRIVE_INVALID"
Verified by: src/uri-plugin/uri-plugin.test.ts::"preserves Windows drive letters" AND src/uri-plugin/uri-plugin.test.ts::"rejects malformed drive-letter forms"
Requirement: .. traversal rejection
The plugin SHALL reject any URI whose decoded path resolves outside
rootDir after .. normalisation. Detection runs BEFORE the call
reaches the underlying adapter.
Scenario: parent-directory escape is rejected
- WHEN
plugin.parse("file:///../../etc/passwd")is called against a plugin withrootDir: "/var/data" - THEN the call throws
StorageFileUriErrorwithcode === "STORAGE_FILE_URI.PATH_ESCAPE"
Scenario: same-directory descent succeeds
- WHEN
plugin.parse("file:///a/b/../c.txt")is called with the same rootDir - THEN the parse succeeds with the normalised key
a/c.txt
Verified by: src/uri-plugin/uri-plugin.test.ts::"rejects path traversal escapes" AND src/uri-plugin/uri-plugin.test.ts::"normalises in-tree traversal"
Requirement: Permission-denied passthrough
The plugin SHALL re-wrap permission-denied failures raised by the bound
NodeFsStorage (STORAGE_FILE.PERMISSION_DENIED from EACCES/EPERM) as
StorageFileUriError(STORAGE_FILE_URI.PERMISSION_DENIED) and MUST
preserve the original on cause.
Scenario: adapter EACCES becomes STORAGE_FILE_URI.PERMISSION_DENIED
- WHEN the resolver routes
read("file:///protected/x")and the underlyingadapter.getthrowsSTORAGE_FILE.PERMISSION_DENIED - THEN the rejection is
StorageFileUriErrorwithcode === "STORAGE_FILE_URI.PERMISSION_DENIED"andcauseset to the originalStorageFileError
Verified by: src/uri-plugin/uri-plugin.test.ts::"wraps EACCES as STORAGE_FILE_URI.PERMISSION_DENIED"
Requirement: build(key) inverse with percent-encoding
The plugin SHALL implement build(key) returning a file://-scheme URI
that round-trips through parse for every key produced by the bound
adapter's list operation. Path segments SHALL be percent-encoded per
RFC 3986 §3.3; Windows drive letters SHALL be preserved verbatim.
Scenario: POSIX key round-trips
- WHEN
plugin.build("a/b%file.txt")is called - THEN the result
parses back to aParsedUriwhosekey === "a/b%file.txt"
Scenario: Windows key round-trips with drive letter
- WHEN
plugin.build("C:/Users/x.txt")is called - THEN the result is
"file:///C:/Users/x.txt"andparses back
Verified by: src/uri-plugin/uri-plugin.test.ts::"build round-trips POSIX keys" AND src/uri-plugin/uri-plugin.test.ts::"build round-trips Windows keys"
Requirement: STORAGE_FILE_URI_CODES registry
The package SHALL export STORAGE_FILE_URI_CODES produced by
defineCodes("STORAGE_FILE_URI", [...] as const) covering exactly 6
codes: INVALID_URI, UNSUPPORTED_AUTHORITY, PATH_ESCAPE,
PERMISSION_DENIED, WINDOWS_DRIVE_INVALID, KEY_TOO_LONG. Every
code SHALL be reachable from a real call site exercised by the test
suite, and the structured error class StorageFileUriError SHALL extend
SuaveplanError<StorageFileUriErrorCode> carrying optional
uri/scheme discriminants.
Scenario: every code is reachable
- WHEN the test suite exercises every documented failure path
- THEN every member of
STORAGE_FILE_URI_CODESappears on at least one raisedStorageFileUriErrorinstance
Scenario: error preserves cause
- WHEN the plugin re-wraps an adapter
StorageFileError - THEN the resulting
StorageFileUriError.causeis the original adapter error instance
Verified by: src/uri-plugin/error-codes.test.ts::"exposes 6 codes all reachable" AND src/uri-plugin/error-codes.test.ts::"preserves cause when re-wrapping"
Requirement: AbortSignal threading
The plugin SHALL forward an AbortSignal (received via the
resolver's options bag) unchanged to every adapter call. A pre-aborted
signal SHALL short-circuit with STORAGE_FILE.ABORTED (the adapter's
existing code) without invoking the adapter's I/O methods.
Scenario: pre-aborted signal short-circuits
- WHEN the resolver routes
read("file:///x", AbortSignal.abort()) - THEN the rejection has
code === "STORAGE_FILE.ABORTED"and the adapter'sgetis NEVER invoked
Scenario: live signal is forwarded
- WHEN
read("file:///x", signal)is called with a live signal - THEN the adapter receives the same signal reference
Verified by: src/uri-plugin/uri-plugin.test.ts::"short-circuits on pre-aborted signal" AND src/uri-plugin/uri-plugin.test.ts::"forwards live signal to adapter"
Requirement: Subpath exports
The package SHALL declare subpath exports for
"./uri-plugin/uri-plugin" and "./uri-plugin/error-codes" in
package.json so consumers can tree-shake the URI plugin independently.
Scenario: subpath imports resolve
- WHEN
import("@suaveplan/storage-file/uri-plugin/uri-plugin")is executed - THEN the module resolves and re-exports
fileUriPlugin
Verified by: src/uri-plugin/uri-plugin.test.ts::"is importable via the documented subpath"
Requirement: Conformance harness pass
fileUriPlugin SHALL pass runUriPluginConformance from
@suaveplan/storage-uri/conformance/conformance with fixtures covering
POSIX paths, Windows drive-letter paths, localhost authority,
malformed inputs (empty path, non-localhost authority, traversal
escape), and a non-empty buildRoundTrip set.
Scenario: conformance fixtures pass
- WHEN
runUriPluginConformance(fileUriPlugin(opts), fixtures)runs with the documented fixture set - THEN the call returns without throwing
Verified by: src/uri-plugin/uri-plugin.test.ts::"passes runUriPluginConformance"