See: story index | App docs: transport | webdav
A minimal C struct — eight function pointers, a version field, and a scheme string. Version 1 has never changed:
struct PWSTransport {
int abi_version; /* = 1 */
const char *scheme; /* "https,http" or "file" or … */
int (*fetch )(url, local_path); /* GET → local file */
int (*store )(local_path, url); /* PUT ← local file */
int (*exists)(url); /* HEAD */
int (*lock )(url, token_out, token_len); /* LOCK */
int (*unlock)(url, token); /* UNLOCK */
void (*cleanup)(void);
};
All error returns are POSIX errno values. ENOENT = resource not found, EBUSY = locked by another client, ENOTSUP = operation not supported.
Each plugin embeds an identity string in the ELF .comment section: PWS_TRANSPORT_INFO:1:https,http:WebDAV transport. The loader reads this with mmap + memmem before dlopen, preventing accidental loading of an unrelated .so.
Key decisions:
• Scheme extraction — extract_scheme(url) finds the colon, validates every character against RFC 3986 §3.1, and returns "" for anything that isn't a well-formed scheme. This prevents path-traversal: a URL like https/../evil:foo can never produce a scheme with '/' in it, so the plugin filename pwsafe-<scheme>.so can never escape the plugin directory.
• Single fd, no TOCTOU — the loader opens the plugin file once with O_RDONLY | O_CLOEXEC | O_NOFOLLOW, scans the identity string via mmap on that fd, then passes the same fd to dlopen as /proc/self/fd/<n>. No race between 'check this .so' and 'load this .so'.
• Search path — app binary directory (from /proc/self/exe), then cwd in DEVELOPMENT builds only. The production binary never looks in cwd.
• Lazy loading — plugins are loaded on first use and cached. s_transports maps scheme → PWSTransport*; s_handles maps scheme → dlhandle.
These two functions are the only I/O entry points in the codebase:
pws_os::FOpen(path, mode)
if pws_is_transport_url(path):
t = pws_find_transport(path) ← loads plugin if needed
cache = pws_get_cache_path(path) ← ~/.cache/pwsafe/<sanitised>
t->fetch(path, cache) ← download
fd = fopen(cache, mode) ← open local copy
pws_cache_register(fd, path, cache, is_write)
return fd
else:
return fopen(path, mode) ← normal local file
pws_os::FClose(fd)
if pws_cache_lookup(fd, url, cache, is_write):
pws_cache_remove(fd) ← BEFORE fclose
fclose(fd)
if is_write:
if pws_has_lock(url):
pws_lockd_store(cache, url) ← daemon has the lock token
else:
t->store(cache, url) ← direct call
else:
fclose(fd)
The cache directory (~/.cache/pwsafe/) is created with 0700 permissions so no other local user can read the cached (encrypted) database file.
• pwsafe-https.so — WebDAV plugin, handles https and http
• pwsafe-http.so → pwsafe-https.so — symlink (loader's way of handling multiple schemes from one .so)
• pwsafe-file.so — file:// plugin (reference implementation and offline test harness)