pwsafe — Loadable Transport Architecture

See parent: pwsafe | See also: webdav design

Design for making remote storage (WebDAV, S3, etc.) a dynamically loadable plugin. The main pwsafe binary gains a small transport dispatch layer; each remote store is a separate .so that registers itself at load time. Zero changes to crypto, record-parsing, or UI layers.

Key Foundation: pws_os::LoadLibrary Already Exists

pwsafe already has a cross-platform dynamic library abstraction in src/os/lib.h: pws_os::LoadLibrary, pws_os::GetFunction, pws_os::FreeLibrary. These wrap dlopen/dlsym on Unix/Mac and LoadLibrary/GetProcAddress on Windows. The cross-platform boilerplate is already solved.

Part 1 — Transport Dispatch (changes to pwsafe proper)

A new src/os/transport.h defines the plugin interface as a plain C struct (for ABI stability across compilers):

typedef struct {
  int  abi_version;              // set to PWSTransport_ABI_VERSION
  const char *scheme;            // e.g. "http", "https", "s3"
  int  (*fetch )(const char *url, const char *local_path);
  int  (*store )(const char *local_path, const char *url);
  int  (*exists)(const char *url);
  int  (*lock  )(const char *url, char *token_out, size_t len);
  int  (*unlock)(const char *url, const char *token);
  void (*cleanup)(void);
} PWSTransport;

A new src/os/transport.cpp provides:

pws_register_transport(PWSTransport*) — called by plugins to register a scheme • pws_find_transport(const char *url) — returns loaded transport for scheme, or null • Plugin loader — lazy: triggered on first URL open, not at startup. Looks up pwsafe-<scheme>.so by name in app binary dir (+ cwd in DEVELOPMENT builds). See ident for full load sequence and lifetime rules. • Cache path resolver — maps URL to deterministic local path, e.g. ~/.pwsafe/cache/<sha256_of_url>.psafe3 • Offline fallback logic — if fetch fails and cache exists, warn and continue

Modified src/os/unix/file.cpp (and Windows equivalent) — FOpen, FClose, FileExists, LockFile, UnlockFile each call pws_find_transport() first; if a transport is found, delegate to it and work against the local cache file; otherwise fall through to normal local file handling unchanged.

The FILE* map (std::map<FILE*, CacheInfo>) that bridges FOpen and FClose lives in transport.cpp. Plugins never see FILE*; they only handle URLs and local paths.

Part 2 — Plugin .so (one per transport)

Each plugin is a standalone shared library that:

• Links its own dependencies (e.g. libcurl for WebDAV; AWS SDK or libcurl for S3) • Exports a single entry point: void pws_plugin_init(pws_register_fn_t reg) • Calls reg(&my_transport) for each scheme it handles • Has zero link dependency on the pwsafe main binary

Example plugins:

pwsafe-transport-webdav.so — registers http and https; implements WebDAV LOCK/UNLOCK (see webdav design) • pwsafe-transport-s3.so — registers s3; no locking needed (S3 has no atomic replace either, but single-user use is safe) • pwsafe-transport-sftp.so — registers sftp; could use libssh2

Plugin Init Linkage

The plugin calls pws_register_transport() which lives in the main binary. On Linux this works naturally if the main binary is linked with -Wl,--export-dynamic (or the symbol is in a shared lib). On Windows, pass the registration function pointer as a parameter to pws_plugin_init — this is why the init signature takes reg as an argument rather than calling a global directly. This is the portable pattern.

ABI Stability

The abi_version field in PWSTransport must be set by the plugin to the value of PWSTransport_ABI_VERSION at compile time. The loader checks this and refuses to load (with a clear error) if versions mismatch. New fields may be appended to the struct; the loader uses abi_version to know which fields are safe to read. This prevents silent crashes when the interface evolves.

Cache and Offline Behaviour

Cache management lives entirely in Part 1 (transport.cpp), not in the plugins:

• On open: call fetch(url, cache_path); if it fails and cache exists, offer offline mode • On save: write to cache_path (normal pwsafe flow); then call store(cache_path, url); if it fails, warn — cache is always consistent • Backups (incremental .ibak files) are taken against the cache path — no changes to BackupCurFile() • Locking: if lock() returns a "not supported" code, warn once and proceed

Build System

In CMakeLists.txt:

transport.cpp is compiled into the main binary (or a static lib linked into it) • Each plugin is a separate add_library(pwsafe-transport-webdav MODULE ...) target • Plugin targets link their own dependencies; the main binary does not link libcurl • Plugins are optional: cmake -DWITH_WEBDAV=ON etc.

Detail Notes

ident — embedded identity string format, pre-scan, scheme dispatch rules • file-plugin — file: plugin as reference implementation and test harness • plan — phased implementation plan with open questions

Estimated Scope

PWSTransport interface + registry + loader: ~150 lines • Cache management + offline fallback in transport.cpp: ~150 lines • Dispatch hooks in file.cpp (Unix + Windows): ~100 lines • WebDAV plugin (libcurl GET/PUT/HEAD/OPTIONS/LOCK/UNLOCK): ~300 lines • CMake wiring: ~30 lines Total: ~730 lines. All remote-storage code is isolated in plugins; adding S3 or SFTP later requires no changes to pwsafe itself.

version4
created2026-02-24
updated2026-02-24