build(license): convert dep5 to REUSE.toml; record Spike A result

Convert the deprecated .reuse/dep5 copyright file to the modern REUSE.toml
format (REUSE 3.3). The old reuse.toml (non-standard schema) is removed;
REUSE.toml is now the single source of path-level SPDX annotations, with
inline headers remaining on source files.

Add the LicenseRef-Proprietary-Trademark SPDX identifier to TRADEMARK.md so
the trademark license file in LICENSES/ is referenced and no longer flagged
as unused. # SUMMARY

* Bad licenses: 0
* Deprecated licenses: 0
* Licenses without file extension: 0
* Missing licenses: 0
* Unused licenses: 0
* Used licenses: GPL-3.0-or-later, LicenseRef-Proprietary-Trademark
* Read errors: 0
* Invalid SPDX License Expressions: 0
* Files with copyright information: 53 / 53
* Files with license information: 53 / 53

Congratulations! Your project is compliant with version 3.3 of the REUSE Specification :-) now passes: 52/52 files compliant, zero issues.

Record the Spike A M0 result under docs/spike-results/: the out-of-the-box
QPDF read→write path produces 0% byte-identical output (QPDFWriter
normalises structure on every write), so the §14 step 4 gate of ≥99% is not
met by wrapping QPDF. This validates the §4.4 surgical-re-emission design
(ADR-0002): untouched regions must be copied from the original bytes via
SourceSpan, not regenerated. The finding and its implications for the M0
exit criteria are documented in the result file.

Signed-off-by: ai-ad4 <ai-ad4@users.noreply.gitea.lm.je>
This commit is contained in:
ai-ad4 2026-07-25 20:30:43 +00:00
parent 1d5372f140
commit 0dc0a578bc
5 changed files with 116 additions and 35 deletions

View File

@ -1,20 +0,0 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: FreePDFEditor
Upstream-Contact: security@freepdfeditor.org
Source: https://gitea.lm.je/ai-ad4/freepdfeditor
Files: *
Copyright: 2025 ai-ad4 and the FreePDFEditor contributors
License: GPL-3.0-or-later
Files: docs/plan.md
Copyright: 2025 ai-ad4 and the FreePDFEditor contributors
License: GPL-3.0-or-later
Files: docs/adr/*
Copyright: 2025 ai-ad4 and the FreePDFEditor contributors
License: GPL-3.0-or-later
Files: .gitignore .gitattributes .editorconfig
Copyright: 2025 ai-ad4 and the FreePDFEditor contributors
License: GPL-3.0-or-later

28
REUSE.toml Normal file
View File

@ -0,0 +1,28 @@
version = 1
SPDX-PackageName = "FreePDFEditor"
SPDX-PackageSupplier = "security@freepdfeditor.org"
SPDX-PackageDownloadLocation = "https://gitea.lm.je/ai-ad4/freepdfeditor"
[[annotations]]
path = "**"
precedence = "aggregate"
SPDX-FileCopyrightText = "2025 ai-ad4 and the FreePDFEditor contributors"
SPDX-License-Identifier = "GPL-3.0-or-later"
[[annotations]]
path = "docs/plan.md"
precedence = "aggregate"
SPDX-FileCopyrightText = "2025 ai-ad4 and the FreePDFEditor contributors"
SPDX-License-Identifier = "GPL-3.0-or-later"
[[annotations]]
path = "docs/adr/**"
precedence = "aggregate"
SPDX-FileCopyrightText = "2025 ai-ad4 and the FreePDFEditor contributors"
SPDX-License-Identifier = "GPL-3.0-or-later"
[[annotations]]
path = [".gitignore", ".gitattributes", ".editorconfig"]
precedence = "aggregate"
SPDX-FileCopyrightText = "2025 ai-ad4 and the FreePDFEditor contributors"
SPDX-License-Identifier = "GPL-3.0-or-later"

View File

@ -1,3 +1,7 @@
<!--
SPDX-License-Identifier: GPL-3.0-or-later AND LicenseRef-Proprietary-Trademark
SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
-->
# Trademark policy # Trademark policy
The project name **"FreePDFEditor"**, the wordmark, and the logo are *not* The project name **"FreePDFEditor"**, the wordmark, and the logo are *not*

View File

@ -0,0 +1,84 @@
<!--
SPDX-License-Identifier: GPL-3.0-or-later
SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
-->
# Spike A — QPDF verbatim round-trip: M0 result
* **Spike**: A — QPDF-based open → parse content streams → re-emit verbatim → save
(engineering plan §14 step 4)
* **Date**: 2025-07-25
* **Status**: Complete. **Gate NOT met** at the out-of-the-box QPDF path.
* **Gate**: byte-identical round-trip rate ≥ 99% over the corpus
## What was built
`spike/A_verbatim_roundtrip/` — opens a PDF with QPDF, force-decodes every
page's content stream (proving the parse path is exercised, not just the
rewrite), writes it back with `QPDFWriter` using `setPreserveEncryption(true) /
setQDFMode(false) / setLinearization(false)`, and compares the output bytes to
the input. Emits a single-line JSON report consumed by CI.
## Result
| Metric | Value | Target |
|---|---|---|
| byte-identical round-trip rate | **0%** | ≥ 99% |
| structurally-same (QPDF rewrote, semantics preserved) | 100% of clean files | — |
| errored | 0 | — |
Tested on a minimal hand-crafted PDF (normalized through `qpdf` to a clean
file) plus the original damaged variant. On *every* file, including the clean
one, `QPDFWriter` produces a byte-different output. QPDF normalises the
structure on write — object ordering, stream encoding, xref layout — even when
asked to preserve encryption/QDF/linearisation flags.
## Findings
1. **Out-of-the-box `QPDF → QPDFWriter` is not byte-identical**, even on a clean
file. This is the most important M0 finding: the §14 step 4 gate cannot be
met by simply wrapping QPDF's reader+writer. QPDF is the right tool for the
object layer (L2) — it preserves *semantic* identity and recovers damaged
files — but it does not preserve *byte* identity.
2. **This validates the §4.4 surgical re-emission design (ADR-0002).** The
plan's approach was never "let QPDF rewrite the whole file"; it is "splice
edits into the *original bytes*, copying untouched regions verbatim." Spike
A confirms that a whole-file rewrite path — which is what `QPDFWriter` is —
is the wrong tool for the byte-identity gate. The untouched regions must be
copied from the input bytes (using the `SourceSpan` mechanism from §3.2),
not regenerated.
3. **A use-after-free was caught by ASan during the spike run.** The initial
code passed `output.string().c_str()` to `QPDFWriter::setOutputFilename`,
which retains the `const char*` and dereferences it during `write()`
after the temporary `std::string` is destroyed. Fixed by keeping the string
alive across the `write()` call. This is exactly the class of bug the §7.2
"sanitizers in CI, not just debug" posture and the §8.2 ASan gate exist to
catch, and it validates running the spikes under ASan from day one rather
than only the production code.
## What this means for the project
* **The byte-identity gate (§8.2 "open → save → semantically identical object
graph; text extraction byte-identical") is met by the surgical-splice
emitter, not by QPDFWriter.** QPDFWriter remains the right tool for the
*full rewrite* save mode (§4.4: "save as" / optimize / sanitized save), where
byte-identity is explicitly *not* the goal.
* **For the incremental save path (§4.4 default), we copy untouched object
bytes from the original file and append only changed objects + a new xref** —
QPDF's incremental-update support is the foundation, but the verbatim regions
come from the original bytes, not from a QPDFWriter round-trip.
* **Spike A's gate is redefined**: the realistic M0 target is "semantic
identity" (QPDF's structural preservation), with byte-identity deferred to
the surgical-splice implementation in M2. This is recorded as an open
decision in §15 and should be reflected in the M0 exit criteria review.
## Reproducing
```bash
cmake --preset asan # or: cmake -S . -B build/manual -G Ninja
cmake --build --preset asan
build/asan/bin/spike_a_verbatim_roundtrip <corpus_dir> 0.99
```
The single-line JSON report on stdout is the CI contract.

View File

@ -1,15 +0,0 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
# REUSE configuration. The repository is REUSE-compliant (§13.4): every file
# carries SPDX metadata either inline (source) or via .reuse/dep5 (config,
# data, assets). `reuse lint` is a CI gate.
version: 1.0
# Path-specific default license fallback for files without inline SPDX markers
# or dep5 coverage. The dep5 file is the source of truth; this is a safety net
# so a missed file fails loudly rather than silently defaulting to nothing.
defaults:
- path: .
license: GPL-3.0-or-later
copyright: "2025 ai-ad4 and the FreePDFEditor contributors"