03 — The six phases

See: story index

Phase 1 — Transport infrastructure

Files: src/os/transport.h, src/os/unix/transport.cpp

The foundation. Define the PWSTransport ABI, write the plugin loader, and hook pws_os::FOpen / FClose in file.cpp. No actual network code yet — a URL at this point would load a plugin and call fetch, but there was no WebDAV plugin to load.

Key decisions made here: single-fd TOCTOU prevention, RFC 3986 scheme validation, cache dir at ~/.cache/pwsafe/, FILE*-keyed cache map.

Phase 2 — file: plugin and smoke test

Files: src/os/plugins/file/transport-file.cpp, src/os/unix/file.cpp

A file:///path/to/db.psafe3 URL now works end-to-end. The plugin just copies the file in and out. More importantly this tested the entire intercept machinery before adding real network code.

Also added: LockFile, UnlockFile, IsLockedFile in file.cpp — the lock plumbing that would later call the daemon.

First bug found: on a colon-free path, url.find(':') returns npos. On 64-bit, npos < 2 is false (npos is size_t(-1)), so the old check would have returned the entire path as the scheme. Fixed by adding pos == npos as the first guard.

Phase 3 — DbSelectionPanel URL-aware UI

Files: src/ui/wxWidgets/DbSelectionPanel.h/.cpp

The 'Open database' panel used a wxFilePickerCtrl — a widget that only accepts local file paths via the OS file dialog. Replaced with a wxComboBox (free text + dropdown history) plus a 'Browse…' button that opens wxFileDialog for local files.

DoValidation updated to check pws_is_transport_url(path) first. If true, calls pws_find_transport() to verify a plugin is available, then accepts the URL as-is. Local paths go through the original wxFileName validation path.

Phase 4 — WebDAV plugin (libcurl)

File: src/os/plugins/webdav/transport-webdav.cpp

libcurl drives all five operations: fetch (GET → cache file with O_CREAT 0600 permissions), store (PUT with If: (<token>) header when lock held per RFC 4918 §10.4.1), exists (HEAD), lock (OPTIONS probe for DAV:2 first; 5-minute timeout; stores token in s_lock_tokens), unlock (looks up token from map).

A shared make_curl() helper sets protocol restrictions, SSL verification, redirect policy, and timeouts uniformly — no per-operation curl configuration drift.

First live smoke test: opened https://webdav.critchley.biz/test/test2.psafe3 successfully.

Phase 5 — Lock daemon

File: src/os/unix/transport_lockd.cpp

The hardest phase. WebDAV LOCK/UNLOCK calls libcurl, which is not async-signal-safe. pwsafe's UnlockFile is called from signal handlers and destructors. Solution: fork a child process on the first lock acquisition; the child holds the lock token and handles all lock/unlock/store over a Unix socketpair. write(2) is async-signal-safe; the parent only ever writes to the socket.

Also fixed: after fork(), the child has its own copy of s_lock_tokens. If the parent called t->store() directly, it checked its own empty map, omitted If: (<token>), and got HTTP 423. Fix: FClose routes store through the daemon when pws_has_lock() is true.

Also fixed: IsLockedFile for transport URLs — now calls pws_has_lock(url) (in-process registry) instead of FileExists(url + ".plk") (WebDAV HEAD → 404 → always false).

Full details: 04-lockd

Phase 6 — Open URL… menu item

Files: OpenUrlDlg.h/.cpp, MenuFileHandlers.cpp, PasswordSafeFrame.h/.cpp

File → Open URL… opens a dialog with an editable wxComboBox pre-filled with URL history (stored in wxConfig under /URLHistory/url0…url9, newest-first, max 10 entries).

Clicking OK calls the existing Open(path) method — which calls SetCurFile → ReadCurFile → pws_os::FOpen — so the entire transport intercept fires exactly as if the user had typed the URL in DbSelectionPanel.

version1
created2026-02-27