Files
Danny Kim e0f4cf2c4b
CI / checks (push) Successful in 1m17s
WebDAV validates connections and preserves directory listings, with CI checking every change (#1)
Co-authored-by: Danny Kim <temeddix@gmail.com>
Co-committed-by: Danny Kim <temeddix@gmail.com>
2026-09-30 13:10:14 +00:00

202 lines
11 KiB
Plaintext

package memona:filesystem@0.1.0;
/// Host services scoped to the current connection. No ambient filesystem,
/// environment, sockets, credentials, or network permissions are inherited.
interface host {
/// Stable failures without backend messages or credential-bearing URLs.
enum host-error { denied, unavailable, invalid-input, cancelled, internal }
/// Severity of a diagnostic event. The host may discard message contents.
enum log-level { debug, info, warn, error }
/// Every method the host will send. A verb outside this list cannot be
/// spelled, so no plugin can smuggle one past the transport rules.
enum method {
get, head, post, put, patch, delete,
propfind, proppatch, mkcol, copy, move, lock, unlock, report,
}
/// An HTTP header. Host-controlled transport headers cannot be overridden.
record header { name: string, value: string }
/// Request head. At most 64 headers of 8 KiB each; the body is streamed.
record request { method: method, url: string, headers: list<header> }
/// Response head. The body is read from the exchange that produced it.
record response { status: u16, headers: list<header> }
/// One HTTP request and its response, streamed in bounded chunks so a
/// transfer is never held whole in memory. A body of any size crosses in
/// pieces, which is what multipart uploads of at least 5 MiB require.
/// Dropping the handle abandons the request.
resource exchange {
/// Appends at most 1,048,576 bytes to the request body. Every push
/// must land before `respond`.
push: func(data: list<u8>) -> result<_, host-error>;
/// Ends the request body and waits for the response head. Calling it
/// twice is invalid-input.
respond: func() -> result<response, host-error>;
/// Reads at most 1,048,576 bytes of the response body, after
/// `respond`. An empty result means the body is complete.
pull: func(limit: u32) -> result<list<u8>, host-error>;
}
/// Begins a request to any HTTP or HTTPS host, including LAN and loopback
/// addresses. This API has no per-connection network grant. Each redirect
/// and DNS result is checked, and DNS is pinned. Cross-origin redirects
/// remove request headers; HTTPS cannot downgrade. The host bounds total
/// bytes per exchange, not buffer size.
http: func(request: request) -> result<exchange, host-error>;
/// One open TCP connection. Dropping the handle closes the socket, and the
/// host closes every socket when the connection is disposed.
resource tcp {
/// Reads at most `limit` bytes, between 1 and 1,048,576. An empty
/// result means the peer closed its side, not that data is pending.
read: func(limit: u32) -> result<list<u8>, host-error>;
/// Writes at most 1,048,576 bytes and returns how many were accepted.
write: func(data: list<u8>) -> result<u32, host-error>;
}
/// Opens a socket to any host, including LAN and loopback addresses, on a
/// package-declared port. This API has no per-connection network grant.
/// The host resolves and pins DNS and starts TLS when asked. Ports
/// negotiated at runtime, such as an FTP data channel, must be declared.
connect: func(host: string, port: u16, tls: bool) -> result<tcp, host-error>;
/// One datagram socket connected to a single peer.
resource udp {
/// Sends one datagram of at most 65,507 bytes.
send: func(data: list<u8>) -> result<u32, host-error>;
/// Receives one datagram of at most `limit` bytes. An empty result
/// means none arrived inside the host's wait, not that the peer left.
receive: func(limit: u32) -> result<list<u8>, host-error>;
}
/// Connects a datagram socket under the same address and port rules as
/// `connect`, for the few filesystem protocols that still need one.
bind: func(host: string, port: u16) -> result<udp, host-error>;
/// A configuration value, typed as the package's manifest declared it.
/// A secret is never logged by the host and never leaves the connection.
variant setting { text(string), secret(string), toggle(bool) }
/// Reads configuration or a session-only credential for this connection.
config: func(key: string) -> option<setting>;
/// Emits a diagnostic event. Never include secrets or private document data.
log: func(level: log-level, message: string);
}
/// Filesystem operations for one connection. Paths are absolute within its
/// root, starting with `/`; they are never local OS paths or resource URIs.
/// Colons and percent signs are literal; paths must never be URL-decoded.
/// Dot segments, backslashes, and controls are invalid.
/// Every invocation is bounded by host fuel, memory, time, and cancellation.
interface provider {
use host.{setting};
/// Stable operation failures. A mutation interrupted after dispatch can
/// report outcome-unknown: inspect remote state before deciding to retry.
enum error-code {
not-found, denied, unsupported, conflict, unavailable, invalid-input,
cancelled, outcome-unknown, internal
}
/// Anything that is neither a file nor a directory, such as a link, is
/// reported as other rather than flattened into one of the two.
enum entry-kind { file, directory, other }
/// An opaque version token, at most 4096 UTF-8 bytes.
type revision = string;
/// How one resource is named: a path within the connection root.
variant locator { path(string) }
/// Optional operations. False capabilities must return unsupported.
/// Conditional-write promises atomic create-only and revision comparisons.
/// Virtual-directories says a directory is only ever a prefix of something
/// stored, as in a flat object store: the host then never offers to create
/// an empty one, warns that removing the last child removes the folder,
/// and does not call a vanished folder an error. Such a provider may still
/// accept mkdir by writing a marker, and must answer unsupported when
/// asked to rename a directory.
record capabilities {
write: bool, mkdir: bool, rename: bool, remove: bool,
conditional-write: bool, watch: bool,
virtual-directories: bool,
}
/// Byte length and an opaque revision, each if known.
record file-info {
kind: entry-kind,
size: u64,
revision: option<revision>,
}
/// One child name, without slashes, dot segments, or control characters.
record entry { name: string, info: file-info }
/// At most the requested number of entries. A cursor (maximum 4096 bytes)
/// identifies another page; absence means enumeration is complete.
record list-page { entries: list<entry>, cursor: option<string> }
/// Atomic preconditions: absent file, exact opaque revision, or unconditional
/// replacement. Providers must never simulate comparisons non-atomically.
variant write-condition { create-only, if-match(revision), overwrite }
/// Changes since a previous polling cursor.
enum change-kind { created, modified, removed }
/// A changed resource path relative to the connection root.
record change { path: string, kind: change-kind }
/// At most 256 changes and a cursor of at most 4096 UTF-8 bytes.
record change-page { changes: list<change>, cursor: option<string> }
/// One transfer being read. The handle survives between host calls, so a
/// file of any size is read as a series of separately bounded calls.
/// Dropping it releases whatever the provider held open.
resource reader {
/// Reads onward from the last position, at most `limit` bytes, with
/// `limit` between 1 and 1,048,576. An empty result means end of file.
pull: func(limit: u32) -> result<list<u8>, error-code>;
/// The revision this transfer is reading, once the provider knows it.
/// A provider that sees the revision change mid-transfer must fail the
/// next pull with conflict rather than mix two versions.
revision: func() -> option<revision>;
}
/// One transfer being written. Dropping the handle before it finishes
/// abandons the transfer, and the provider must leave no partial file.
resource writer {
/// Appends at most 1,048,576 bytes. Oversized pushes are rejected,
/// never truncated.
push: func(data: list<u8>) -> result<_, error-code>;
/// Commits the transfer and evaluates the write condition now, against
/// the state the caller last saw. A none result means the commit needs
/// another call: keep calling until it answers some, and treat an
/// interruption in between as outcome-unknown.
finish: func() -> result<option<file-info>, error-code>;
}
/// Opens exactly one host-owned connection per component instance. The host
/// passes only this connection's configuration and in-memory credentials.
open: func(provider-id: string, config: list<tuple<string, setting>>) -> result<capabilities, error-code>;
/// Best-effort cleanup before disposal. The host may dispose immediately
/// after a trap, cancellation, permission change, or process shutdown.
close: func();
/// Reads metadata without changing the resource.
stat: func(target: locator) -> result<file-info, error-code>;
/// Enumerates 1-256 direct children per request, without recursive traversal.
%list: func(target: locator, cursor: option<string>, limit: u32) -> result<list-page, error-code>;
/// Begins reading at a byte offset. Streaming, rather than one capped read,
/// is what lets a provider serve a file larger than a single call allows.
open-read: func(target: locator, offset: u64) -> result<reader, error-code>;
/// Begins replacing one complete file if the write capability is present.
/// The size hint, when the caller knows it, lets a provider choose between
/// a single request and a multipart upload before the first byte.
open-write: func(target: locator, condition: write-condition, size-hint: option<u64>) -> result<writer, error-code>;
/// Creates one directory if the mkdir capability is present.
mkdir: func(target: locator) -> result<file-info, error-code>;
/// Renames within this connection if the rename capability is present.
rename: func(target: locator, destination: string) -> result<file-info, error-code>;
/// Removes one file or empty directory if the remove capability is present.
remove: func(target: locator) -> result<_, error-code>;
/// Polls for a bounded change page if the watch capability is present. No
/// background callback or persistent subscription is installed by the host.
poll: func(cursor: option<string>) -> result<change-page, error-code>;
}
/// Memona filesystem extension ABI 0.1.0, distributed as a WebAssembly component.
/// Rust guests can target wasm32-wasip2. Selected WASI 0.2 CLI, clocks, random,
/// IO and filesystem interfaces are allowed for the standard library, with
/// empty arguments/environment, empty preopens, and discarded stdio. WASI
/// sockets and HTTP imports are not allowed; networking uses the host interface.
world filesystem {
import host;
export provider;
}