The first M1 (Viewer) increment: lift the Spike E RPC shape into src/ipc as a
Qt-free, unit-testable library implementing the §2.1 process split. The UI
process forks a sandboxed document process, hands it the document fd via
SCM_RIGHTS (§2.1: no FS access beyond handed fds), and talks Cap'n Proto
two-party RPC over a socketpair. The document process installs the seccomp
sandbox, receives the fd, and serves the DocumentProcess interface (open,
getPageCount, ping) backed by QPDF reading from the handed fd.
src/ipc/ (new, Qt-free so testable without QApplication):
- ipc.capnp: M1 protocol — DocumentProcess{open(fd via SCM_RIGHTS, pathHint)
-> OpenResult{pageCount, pdfVersion}; getPageCount; ping}. ADR-0004 trust
boundary: the UI validates every response; the doc bounds-checks requests.
- DocumentProcessServer: low-level wrapSocketFd + TwoPartyVatNetwork(SERVER)
+ makeRpcServer (the Spike E proven shape, NOT EzRpcServer which calls
accept() on a connected socketpair end). QPDF reads from the handed fd.
- DocumentProcessClient: the Qt<->KJ event-loop integration (§2.3) — runs the
KJ event loop on a dedicated worker thread (low-level makeRpcClient, not
EzRpcClient which uses a thread-local context that breaks cross-thread
destruction). Synchronous methods block the caller via std::promise.
- ProcessLauncher: fork + socketpair + send_fd + construct client.
- FdPassing: raw SCM_RIGHTS send/recv, orthogonal to the RPC transport.
- Sandbox: seccomp allow-list lifted from Spike E, +sendmsg/recvmsg (fd
passing), +fcntl/fstat/lseek/pread64 (QPDF/stdio on the handed fd).
src/app: wire File>Open and the command-line positional arg to launch the
document process off the GUI thread (QtConcurrent) and show the page count in
the status bar (QMetaObject::invokeMethod back to the GUI thread).
test/test_ipc: end-to-end test — launches the sandboxed document process,
opens a real PDF, verifies ping + pageCount + getPageCount. PASSES under the
seccomp sandbox; ASan-clean with sandbox disabled (ASan's pipe2 conflicts
with the allow-list, an ASan-only artefact). Registered as ctest ipc_end_to_end.
Both Spike E (raw protocol) and this (real RPC) now pass under the sandbox.
The QPDF 12.2 API differs from the initially-written calls:
- processInputFile → processFile
- getArrayAsArray → getArrayAsVector
- setOutputFile(string) → setOutputFilename(const char*)
- setQDF → setQDFMode
The spike CMakeLists now finds QPDF via either a CMake config target
(qpdf::libqpdf for system packages, QPDF::qpdf for vcpkg) or a pkg-config
fallback (PkgConfig::QPDF), so it builds against the Debian libqpdf-dev
package as well as a vcpkg manifest install.
The contract test target was missing SpikeRunner.cpp from its sources,
causing a link error; added it with the spike/ include directory.
ASan caught a heap-use-after-free in the initial write path: QPDFWriter
retains the const char* passed to setOutputFilename and dereferences it
during write(), after the temporary std::string from output.string() is
destroyed. Both the input and output filename strings are now kept alive
across the QPDF calls that retain them. This is exactly the bug class the
§7.2 sanitizers-in-CI posture exists to catch.
Builds and passes under GCC 14.2 + Qt 6.8.2 + QPDF 12.2, both Release and
ASan+UBSan configurations.
Signed-off-by: ai-ad4 <ai-ad4@users.noreply.gitea.lm.je>
Stand up the repository foundation described in docs/plan.md §14:
- CMake + vcpkg manifest mode (pinned baseline), presets for debug/release/asan/tsan/ci-release
- Release-build hardening module (§7.2): stack protector, libc++ hardening, CFI/CET
- Skeleton CPack packaging for all nine artifact formats (§12): .deb/.rpm/.tar.xz/.AppImage, .msi/.exe/.zip, .dmg/.pkg
- Linux integration: .desktop, hicolor icon, AppStream metainfo, man page, bash completion
- Empty-window Qt Widgets application shell with the §9 chrome layout
- Spike A harness: QPDF open → parse content streams → re-emit verbatim → save, measuring byte-identical round-trip rate over a corpus
- Pixel-diff harness scaffolding (§8.2 gate) with the spike-runner JSON contract
- Contract test pinning the spike-runner report format (dependency-free)
- Gitea Actions CI matrix (§13.3): build × {linux,macos,windows} × {debug,release,asan,tsan}, plus nightly packaging pipeline
- Tooling: clang-format, clang-tidy, gitleaks, REUSE config, pre-commit hooks, CODEOWNERS
- Governance: README, CONTRIBUTING, SECURITY, TRADEMARK, docs/plan.md, four M0 ADRs (§2.5, §4.4, §7.2, §2.1)
- REUSE-compliant SPDX headers and LICENSES/ directory (GPL-3.0-or-later)
No production parsing, rendering, or editing code yet — that lands in M1-M6.
The scaffolding is the hard-to-retrofit foundation: build system, packaging
pipeline, CI gates, governance, and the ADRs that fix the architectural
invariants before any code that depends on them is written.
Signed-off-by: ai-ad4 <ai-ad4@users.noreply.gitea.lm.je>