first commit
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
# `filename-codec` — caller key ↔ OPFS filename
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
OPFS filenames inherit filesystem-level constraints that caller keys must not (case-insensitivity on some platforms, no embedded `/` separator within a single entry name, byte-encoding ambiguity across browsers). This module wraps each key in RFC 4648 base64url over its UTF-8 byte representation, producing OPFS-safe filenames that round-trip every caller key — including unicode, mixed scripts, slashes, and any byte the caller threw at us. Storing the codec as a separate module keeps the algorithm independently testable (property-based round-trip suite) and replaceable (a future hash-addressed mode can plug in without touching `OpfsStorage`).
|
||||
|
||||
## Features
|
||||
|
||||
- RFC 4648 base64url with `+`→`-`, `/`→`_`, padding stripped.
|
||||
- UTF-8 fatal decode rejects malformed byte sequences with a typed `OpfsError`.
|
||||
- Round-trip invariant proven by a `fast-check` property test (200 random inputs).
|
||||
- Output regex: `^[A-Za-z0-9_-]*$` — accepted by every OPFS implementation.
|
||||
- Pure / synchronous / zero workspace dependencies.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
decodeFilename,
|
||||
encodeFilename,
|
||||
} from "@suaveplan/storage-browser-opfs/filename-codec";
|
||||
|
||||
const filename = encodeFilename("orders/2026/05.json"); // "b3JkZXJzLzIwMjYvMDUuanNvbg"
|
||||
const original = decodeFilename(filename); // "orders/2026/05.json"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
decodeFilename,
|
||||
encodeFilename,
|
||||
OPFS_CODES,
|
||||
OpfsError,
|
||||
} from "@suaveplan/storage-browser-opfs";
|
||||
|
||||
function listKeysSafely(entries: { name: string }[]): string[] {
|
||||
const out: string[] = [];
|
||||
for (const entry of entries) {
|
||||
try {
|
||||
out.push(decodeFilename(entry.name));
|
||||
} catch (err) {
|
||||
// Skip entries that aren't ours — a foreign tool may have written
|
||||
// something to the OPFS directory we share.
|
||||
if (
|
||||
err instanceof OpfsError &&
|
||||
err.code === OPFS_CODES.INVALID_KEY
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `encodeFilename(key: string): string` — converts to base64url-without-padding.
|
||||
- `decodeFilename(filename: string): string` — throws `OpfsError(OPFS.INVALID_KEY)` for malformed base64 or invalid UTF-8.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The codec is deliberately byte-identical to the WHATWG spec for base64url to keep filenames stable across runtimes — the same OPFS directory written from Chrome must round-trip from Safari and Firefox. `btoa`/`atob` are universally available in browsers and in Node.js 20+, so no polyfill is needed at runtime. The fatal `TextDecoder` makes the codec self-healing: a foreign tool that writes non-base64 entries to the OPFS directory will be skipped by `decodeFilename`-using callers rather than corrupting their key view.
|
||||
Reference in New Issue
Block a user