Phase 18: preserve portable project media paths and verify format migrations

This commit is contained in:
Johannes Findeisen 2026-10-27 06:31:59 +01:00
commit 1369e4d463
15 changed files with 329 additions and 13 deletions

View file

@ -1,4 +1,4 @@
# Project format v5 (reads v1/v2/v3/v4)
# Project format v7 (reads v1–v6)
Phases 5–15 use a bounded text format implemented in C99 without a parser library.
The extension is `.xtc`. File operations run exclusively on the control thread.
@ -12,7 +12,7 @@ Fields use ASCII spaces. Integers are unsigned decimal, without signs.
Counts determine the number of subsequent records:
```text
XTC_PROJECT 5
XTC_PROJECT 7
APP 0 1 0
TEMPO 120000
IDS 1 1
@ -82,8 +82,8 @@ node types 1..4, migrating their clips to empty asset paths. Version 2 accepts
node types 1..5 and ASSET records. Both migrate to MIDI=0 and no notes.
Version 3 accepts instrument nodes and MIDI/NOTES records. Versions 1–3 migrate
all tracks to unity gain, centered pan, mute/solo off and no automation.
Saving always writes v5. Unknown versions are rejected without replacing the destination; older
applications cannot read v5. Migration and maximum-path roundtrips are tested.
Saving always writes v7. Unknown versions are rejected without replacing the destination; older
applications cannot read v7. Migration and maximum-path roundtrips are tested.
## Ownership and failures
@ -148,3 +148,43 @@ port/tick pairs, nonfinite values, unknown control ports and ticks beyond the
project limit are rejected. Loaded plugin tempo comes from TEMPO. Range and
discrete-mode compatibility are checked on control-thread compilation. Versions
1–5 migrate with empty plugin lanes. See CONTROL.md for timing/ordering.
## Phase 18: version 7 and project folders
Current writer emits v7; readers migrate v1–v6. Record ordering and field bounds
are otherwise unchanged from v6. v7 ASSET may contain a relative path, interpreted
against the actual containing directory of the named project file (resolved on
the control thread), never the launch working directory. After loading, all core
asset references are absolute, including missing media. Media inside the save
folder are written relative; references outside remain absolute. Save As resolves
against its new folder and preserves media identity rather than rebasing it by
accident. Copy/move a project folder together with its referenced media to retain
portability. Saving does not copy assets: import/record into that folder first.
Plugin binaries remain trusted absolute module references, not bundled resources.
Empty assets still use `-`. Paths exclude embedded NUL/control bytes and must fit
511 bytes after resolution; overly long resolved references fail transactionally.
Relative references can name outside the folder using ordinary POSIX semantics;
this is not a sandbox. Symlinks follow filesystem behavior, so portability requires
moving their targets too. Old formats continue to require absolute media paths.
No unknown version/node/record is silently discarded; unsupported versions/types
or malformed/trailing data reject the candidate and retain the current project.
Parsing never opens media/plugins or constructs an audio engine.
Save validates the complete model, writes/fsyncs a same-folder temporary, closes
it, atomically renames it, then fsyncs the containing directory. Failures before
rename retain the old destination. A directory sync failure *after* rename reports
I/O error but the new complete file may already be present: inspect/reopen it;
there is no rollback across that durability boundary. Filesystem/hardware power
loss guarantees still depend on the underlying storage. Recording destinations
have their separate no-clobber policy; ordinary project Save intentionally replaces
the selected document. In-memory models and the existing destination are preserved
on load failure. No automatic unknown-field stripping or media deletion occurs.
`make test-portability test test-plugins test-recovery test-recording` covers checked-in
v1–v6 fixtures (including missing v5/v6 plugins), v7 exact roundtrips, moved-folder
playback from a different cwd, missing media retention, unknown records, every
truncation and deterministic byte mutations, malformed plugin metadata, interrupted
writers and write-limit failures. ASan/UBSan/static analysis additionally audit the
bounded parser. This is regression/fault-injection coverage, not exhaustive fuzzing
or a claim of power-cut qualification.