Writing a pwsafe Transport Plugin

See parent: pwsafe | ABI spec: transport | Reference impl: file-plugin

Minimal skeleton

A plugin is a shared library (.so) with a single exported entry point: pws_plugin_init

#include "transport.h"
#include <string.h>    // memset

// Identity string — survives link-time optimization
// Format: PWS_TRANSPORT_INFO:<abi>:<schemes>:<description>
static const char ident[]
    __attribute__((section(".comment"), used)) =
    "PWS_TRANSPORT_INFO:1:myscheme:My transport";

static int my_fetch(const char *url, const char *local_path) {
    // download url → local_path
    return 0;  // or errno on failure
}
static int my_store(const char *local_path, const char *url) {
    // upload local_path → url
    return 0;
}
static int my_exists(const char *url) {
    // return 0 if exists, ENOENT if not
    return ENOENT;
}
static int my_lock(const char *url, char *token_out, size_t len) {
    return ENOTSUP;  // no locking support
}
static int my_unlock(const char *url, const char *token) {
    return ENOTSUP;
}
static void my_cleanup(void) { }

static PWSTransport t = {
    PWSTransport_ABI_VERSION,
    "myscheme",
    my_fetch, my_store, my_exists, my_lock, my_unlock, my_cleanup
};

extern "C" void pws_plugin_init(pws_register_fn_t reg) {
    reg(&t);
}

Locking

Locking is optional. If your transport has no server-side locking, return ENOTSUP from lock() and unlock(). The transport layer treats ENOTSUP as 'proceed without a lock, warn once'.

If you implement locking, store the token internally — the caller passes an empty string for token to unlock(). Your plugin must maintain its own URL → token map.

Calling context after first lock

IMPORTANT: once the first lock is acquired, all transport operations (fetch, store, lock, unlock) run in the lock daemon child process, not in the parent. The child inherits your plugin's loaded shared library and function pointers, but NOT any state set by the parent after fork.

In practice: initialise all state before the first lock() call, or initialise lazily inside each function. Do not rely on parent-set state for post-fork calls.

Return codes

• 0 — success

• ENOENT — resource not found (404 equivalent)

• EBUSY — locked by another client (423 equivalent)

• ENOTSUP — operation not supported (lock/unlock not available)

• EIO — I/O or protocol error

• EPERM — authentication failure

Any POSIX errno value is acceptable; the above are the ones the transport layer handles specially.

Multiple schemes from one .so

List all schemes in the identity string, comma-separated: PWS_TRANSPORT_INFO:1:https,http:WebDAV transport

Create a symlink for each additional scheme: pwsafe-http.so → pwsafe-https.so. The loader discovers it via the symlink but loads the same .so.

CMakeLists.txt snippet

add_library(pwsafe-myscheme MODULE transport-myscheme.cpp)
set_target_properties(pwsafe-myscheme PROPERTIES
    PREFIX ""
    SUFFIX ".so"
    POSITION_INDEPENDENT_CODE ON)
target_link_libraries(pwsafe-myscheme PRIVATE <your-deps>)
# Copy to build dir so binary can find it
add_custom_command(TARGET pwsafe-myscheme POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy
        $<TARGET_FILE:pwsafe-myscheme>
        ${CMAKE_BINARY_DIR}/pwsafe-myscheme.so)

Testing pattern

Compile standalone, gate live-server sections behind an env var, skip with exit 77 if unset:

g++ -std=c++17 -o /tmp/test_myplugin test_myplugin.cpp -ldl
# Live tests:
MYSCHEME_TEST_URL=myscheme://server/path /tmp/test_myplugin

Reference implementations: file:// plugin (simplest)https:/http: plugin (full locking)

version1
created2026-02-27