1
0
Fork 0
forked from hanez/xtc

Phase 28: add project diagnostics, dependency inspection and durability regressions

This commit is contained in:
Johannes Findeisen 2026-10-27 16:58:26 +01:00
commit ca6c869b0b
16 changed files with 495 additions and 42 deletions

View file

@ -417,3 +417,16 @@ test-parameters: $(BUILD_DIR)/test_parameters
./$(BUILD_DIR)/test_parameters
test: test-parameters
-include $(BUILD_DIR)/tests/unit/test_parameters.d
$(BUILD_DIR)/test_project_diagnostics: $(BUILD_DIR)/tests/unit/test_project_diagnostics.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl -Wl,--wrap=fflush,--wrap=fclose,--wrap=fsync,--wrap=rename
.PHONY: test-project-diagnostics
test-project-diagnostics: $(BUILD_DIR)/test_project_diagnostics
./$(BUILD_DIR)/test_project_diagnostics
test: test-project-diagnostics
-include $(BUILD_DIR)/tests/unit/test_project_diagnostics.d
.PHONY: test-project-inspection
test-project-inspection: $(BUILD_DIR)/xtc-check
$(PYTHON) tests/integration/test_project_inspection.py $(BUILD_DIR)/xtc-check
test: test-project-inspection

View file

@ -153,3 +153,11 @@ per-channel live MIDI CC bindings. Project v10 reads v1–v9. See
Phase 27 reserves undo storage before edits/publication, groups completed takes,
preserves surviving editor selections and improves context menus/focus feedback.
See [EDITING.md](docs/EDITING.md) for the workflow audit and failure boundaries.
Project troubleshooting: `xtc-check --validate PROJECT.xtc` reports metadata
errors with the expected record and syntax line. `xtc-check --inspect PROJECT.xtc`
lists resolved media/module paths and plugin identities without executing plugins
or starting JACK. Exit codes: 0 = valid metadata and available files, 2 = missing
or unreadable references, 1 = metadata/I/O error. File availability does not
certify WAV contents or plugin compatibility. See [PROJECT_FORMAT.md](docs/PROJECT_FORMAT.md)
for the compatibility and relocation policy.

View file

@ -65,3 +65,11 @@ PERFORMANCE.md, PLUGIN_HOSTING.md and RECOVERY.md for tested contracts/evidence.
or cancellable progress. Surviving selections are retained across undo but
historical selection/layout is not stored. Native context-menu/focus manual
qualification and broad accessibility testing remain pending. See EDITING.md.
Phase 28 project diagnostics use syntax-line locations; semantic cross-reference
failures identify the MODEL stage without a source line. `xtc-check --inspect`
checks dependency file availability only, without loading code or WAV contents.
The project format is Linux/POSIX oriented; absolute plugin paths, architecture,
ABI and JACK/device configuration must be supplied on another system. Unknown
fields are rejected, not preserved. See PROJECT_FORMAT.md for versions 1–10 and
RECOVERY.md for save failures after rename.

View file

@ -1,6 +1,6 @@
# Project format v10 (reads v1–v9)
Phases 5–26 use a bounded text format implemented in C99 without a parser library.
Phases 5–28 use a bounded text format implemented in C99 without a parser library.
The extension is `.xtc`. File operations run exclusively on the control thread.
The project owns its graph, tracks and silent/audio arrangement regions. Audio samples remain in external WAV
files; MIDI notes are stored inline.
@ -104,8 +104,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 v7. Unknown versions are rejected without replacing the destination; older
applications cannot read v7. Migration and maximum-path roundtrips are tested.
Saving always writes v10. Unknown versions are rejected without replacing the destination; older
applications without v10 support cannot read current saves. Migration and maximum-path roundtrips are tested.
## Ownership and failures
@ -114,8 +114,9 @@ the destination. Any failure retains the complete previous project. Saving
validates the project, creates a mode-0600 temporary in the destination
directory, writes, flushes, fsyncs, closes and renames over the destination.
Failures before rename retain the previous destination and remove the temporary.
The parent directory is not fsynced: survival of the rename through sudden power
loss is not guaranteed. Save replaces symlinks themselves and does not preserve
After rename, the parent directory is fsynced. A directory-sync failure reports
I/O error even though the new complete destination may already be present.
Storage and filesystem behavior still determine power-loss durability. Save replaces symlinks themselves and does not preserve
existing file permissions. POSIX I/O is private to the Linux-first persistence
module; public headers contain no POSIX types.
@ -123,7 +124,8 @@ Saved data reproduces the arrangement, graph, track mix and automation, includin
ID allocation state. Undo history is not saved. See MIXER.md for pan/interpolation.
Graph node positions, timeline zoom/scroll/selection, transport position and DSP
oscillator phase are GUI/session state and are not saved. Asset references are
external absolute paths, not copied on Save As. Metadata loading preserves
absolute in memory; saves use relative paths for media inside the project folder,
otherwise absolute paths. Media are not copied on Save As. Metadata loading preserves
missing references; audio-plan preparation reports unreadable/invalid assets
and renders them silently. Relinking replaces a selected clip's path without
changing its IDs or arrangement. See AUDIO_FILES.md for recovery and limitations.
@ -139,7 +141,7 @@ writer and unfinished-recording behavior. Metadata validation is available via
## Phase 15: version 5 plugin metadata
The writer now emits `XTC_PROJECT 5`; versions 1–4 still load. All v4 records and
Phase 15 introduced `XTC_PROJECT 5`; its reader also supported versions 1–4. All v4 records and
order remain the same. Node type 8 is a LADSPA plugin; native pointer-bearing type
7 is still unsupported in files. Immediately after each type-8 `NODE id 8
00000000` record come:
@ -163,7 +165,7 @@ stored. See PLUGIN_HOSTING.md and decision 0009 for supported processors/limits.
## Phase 16: version 6
Writer emits v6 and readers accept v1–v6. After each plugin's CONTROL records,
Phase 16 introduced v6 and its reader accepted v1–v6. After each plugin's CONTROL records,
`PLUGIN_AUTOMATION count` is followed by exactly count (<=32) records:
`PARAM tick port mode float32_hex_bits`. Mode is 0 linear or 1 step. Duplicate
port/tick pairs, nonfinite values, unknown control ports and ticks beyond the
@ -205,7 +207,7 @@ the selected document. In-memory models and the existing destination are preserv
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
v1–v10 fixtures (including missing v5/v6 plugins), current 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
@ -240,7 +242,7 @@ format change (`make test test-portability test-audio-editing`).
## Phase 24: version 9 mix routing
The writer emits v9. Immediately after each TRACK's MIX record it writes
Phase 24 introduced v9. Immediately after each TRACK's MIX record it writes
`ROUTE is_bus bus_id`. `is_bus` is 0 or 1; `bus_id` is an existing bus track ID,
or decimal UINT64_MAX (18446744073709551615) for master. Buses must route to
master and cannot contain clips. Invalid destinations, flags and nested routing
@ -266,6 +268,72 @@ bindings and invalid interpolation modes reject the complete candidate. v1–v9
supply default 7/10/64 bindings on every channel. No event history/current live
controller values are serialized. Older readers reject v10.
Fixtures cover v1–v9 migration. Exact roundtrips, malformed metadata, step/linear
Fixtures cover v1–v10 loading and migration. Exact roundtrips, malformed metadata, step/linear
renders and remapped live events cover the new writer/reader. Public clients
must rebuild and initialize the appended `xtc_automation_point.step` field.
## Phase 28 compatibility and diagnostic contract
The current reader accepts versions 1–10 and the writer always emits version 10.
There is no format change in Phase 28. Saving an older document upgrades it;
retain an original backup if an older application must still open it. APP is
informational; compatibility depends on the format version, not the producer's
application version. A future incompatible schema requires a new format number.
Public C/C++ consumers must rebuild when public structs change.
| Version | Added representation | Defaults when loading earlier versions |
| --- | --- | --- |
| 1 | Graph, tracks, silent regions | Baseline |
| 2 | External WAV references | Empty asset references |
| 3 | MIDI clips and notes | Non-MIDI clips, no notes |
| 4 | Track mix and automation | Unity gain, center pan, mute/solo off, no points |
| 5 | LADSPA metadata | No plugin nodes |
| 6 | Plugin automation | Empty plugin lanes |
| 7 | Relative media references | Earlier media paths must be absolute |
| 8 | Clip gain and fades | Unity gain, zero explicit fades |
| 9 | Bus routing | Ordinary tracks routed directly to master |
| 10 | Track interpolation and controller bindings | Linear points; CC 7/10/64 on every channel |
The grammar has no optional extension namespace. Unknown records, extra fields,
node types or trailing data cannot be preserved by this model and are rejected
rather than silently removed. Supported older records migrate in a separate
candidate and pass complete model validation before adoption. Failed parsing,
reference validation or close retains the existing destination model. GUI Open
also prepares the candidate before replacing the live document; missing or
incompatible plugins retain the current document and identify the module.
Missing media references remain available for relinking and render silently.
`xtc_project_load_diagnostic` adds a copied result, parsed format version (zero
if unavailable), expected record/stage and message. Syntax locations are 1-based;
line zero denotes I/O, size, path resolution or whole-model validation. MODEL
errors do not identify the original line of a cross-reference. The original
`xtc_project_load` remains available with identical transactional semantics.
`xtc-check --validate PROJECT` checks bounded metadata without loading dependencies.
`xtc-check --inspect PROJECT` additionally lists resolved media and module paths,
plugin labels, unique IDs and descriptor indices. It checks readable regular-file
availability only. Exit status is 0 when references are available, 2 when valid
metadata has missing/unreadable references, and 1 for invalid metadata or I/O.
Neither command executes plugin code, decodes WAV data, connects JACK or verifies
hardware. A readable file can still be an invalid WAV or incompatible plugin;
actual preparation performs those checks. Paths and labels are escaped for
terminal output. There is no search-path substitution or automatic plugin relink.
Portability currently targets Linux/POSIX. The text representation is independent
of byte order and locale and requires IEEE binary32 floats. IDs used by the graph
must fit the receiving host's size_t; a 64-bit file is not guaranteed to fit a
32-bit build. Paths follow POSIX semantics; Windows drive letters and separators
are not translated. Relative media can travel with their project folder, while
absolute references and symlink targets must be restored separately. LADSPA
modules are absolute installed references: match architecture, ABI, descriptor
identity, ports and legal controls on the destination. Plugin binaries and state
are not bundled. JACK availability, sample rate, block limits and device routing
are runtime dependencies; loading metadata does not restore device connections.
`make test` includes v1–v10 migration, malformed/truncated/mutated/oversized input,
retained-state diagnostics, CLI dependency inspection, and deterministic save
faults at flush, file sync, close, rename and directory sync. Pre-rename faults
preserve the old bytes and remove the temporary. After rename, a sync failure
reports I/O while leaving a complete new document; inspect it before retrying.
Fault wrappers are linked into tests only. These tests complement killed-writer
coverage in `make test-recovery`; they do not qualify power-cut behavior.

View file

@ -1,6 +1,6 @@
# Persistence and failure recovery
The project format remains v4. Readers migrate v1/v2/v3, validate bounded input
The project format is v10. Readers migrate v1–v9, validate bounded input
and atomically install a candidate only after complete success. Existing tests
cover exact migration/roundtrip, truncations, malformed references, counts,
IDs, nonfinite values and failed GUI/backend installation. Native processors
@ -10,22 +10,26 @@ process addresses or an unreadable project. See PROJECT_FORMAT.md.
Project Save writes a mode-0600 same-directory `NAME.tmp.XXXXXX`, flushes and
fsyncs it, closes and renames it over the destination. Before rename, handled
failures remove the temporary and retain the destination. POSIX rename gives
complete old-or-new destination contents during process failure. The directory
is not fsynced, so this is not a power-loss durability guarantee. Symlinks are
complete old-or-new destination contents during process failure. The containing directory is fsynced after rename. If that sync fails, Save
reports I/O failure but the complete new file may already be present. Preserve
and validate it before retrying; filesystem/hardware durability still varies. Symlinks are
replaced rather than followed at rename; original file permissions are not kept.
Recording similarly writes/finalizes a same-directory temporary, then renames
it. Stop must synchronize callbacks before draining/finalizing. Ring overflow
returns an explicit error and may finalize a partial take for manual recovery;
I/O failure retains the previous destination. A killed recorder does not finish
its WAV header. Source WAV files remain external absolute paths; Save As does
not collect them. Back up the project and assets together.
its WAV header. Source WAV files remain external; references inside the project folder are
saved relative to it. Save As does not collect them. Back up the project and assets together.
## After a crash
1. Preserve the destination and any `.tmp.*` files before attempting repair.
2. Run `xtc-check --validate project.xtc` for metadata validation without a GUI,
display or audio server. This does not verify assets or realtime preparation.
display or audio server. Run `xtc-check --inspect project.xtc` to list resolved
media/module paths and plugin identities: status 2 means missing/unreadable
references, 1 means invalid metadata/I/O, 0 means files are available. Neither
command verifies file contents, plugin initialization or realtime preparation.
3. A valid destination can be opened normally. If only a temporary contains useful
data, copy it to a separate candidate and validate that copy before replacing
anything. Partial project files are rejected; there is no salvage parser.
@ -75,3 +79,13 @@ retains the original graph, controls and history position. Cancel in the generic
parameter editor discards its temporary values. Reset defaults clears automation
only when applied. No host recovery can contain a crash or hang in trusted native
plugin code; LADSPA has no runtime error-return channel.
## Save diagnostics (Phase 28)
`make test-project-diagnostics` deterministically injects flush, file-sync, close,
rename and directory-sync errors. Pre-rename failures retain the previous bytes
and clean temporary files. Post-rename sync errors leave a complete new file and
return failure; there is no rollback after replacement. Load diagnostics identify
the expected record and syntax line, or line zero for whole-model/I/O stages.
Unknown versions report the supported range (1–10); unknown records/fields are
rejected without partially changing the current project.

View file

@ -774,6 +774,18 @@ Make project files robust, diagnosable, and maintainable across application vers
- Version incompatibilities and missing dependencies are clearly reported.
- Supported project versions have migration and regression tests.
### Implementation status
Completed: version-10 compatibility policy and v1–v10 fixture migration;
transactional diagnostic loading in the core and GUI; metadata-only dependency
inspection with explicit availability limits; relocation and unknown-field
policies; bounded-input and deterministic pre/post-rename save-failure coverage.
No format bump was needed. Linux/POSIX, plugin ABI/path and audio-backend limits
are documented in PROJECT_FORMAT.md and RECOVERY.md.
Validation: full core/GUI/plugin/JACK/workflow/recording/recovery tests, static
analysis, staged installation and binary packaging; minimal no-JACK/no-LADSPA
build and tests; ASan/UBSan core, reliability and recovery tests.
## Phase 29 — Performance, Scalability, and Stress Testing
### Goal

View file

@ -152,3 +152,11 @@ right-click no longer immediately deletes notes. Graph right-click targets the
node/wire under the pointer. Undo preserves surviving selections. Completed
takes add one undo step; undo removes references and keeps WAV files. See
EDITING.md for grouped commands, failure handling and keyboard/focus behavior.
Project troubleshooting: `xtc-check --validate PROJECT.xtc` reports metadata
errors with the expected record and syntax line. `xtc-check --inspect PROJECT.xtc`
lists resolved media/module paths and plugin identities without executing plugins
or starting JACK. Exit codes: 0 = valid metadata and available files, 2 = missing
or unreadable references, 1 = metadata/I/O error. File availability does not
certify WAV contents or plugin compatibility. See [PROJECT_FORMAT.md](PROJECT_FORMAT.md)
for the compatibility and relocation policy.

View file

@ -224,6 +224,17 @@ xtc_result xtc_project_sample_to_tick(const xtc_project *project, uint64_t sampl
* @return XTC_OK on success, or an error code describing the failure.
*/
xtc_result xtc_project_save(const xtc_project *project, const char *path);
/* Optional copied control-thread diagnostics. line is 1-based for syntax errors,
* 0 for I/O/whole-model validation; record identifies the expected record or
* validation stage, never untrusted file text. format_version is 0 if unknown.
* No media/plugin loading, engine creation or mutation on load failure. */
typedef struct {
xtc_result result;
uint64_t format_version;
size_t line;
char record[32], message[192];
} xtc_project_diagnostic;
xtc_result xtc_project_load_diagnostic(xtc_project *project, const char *path, xtc_project_diagnostic *diagnostic);
/**
* @brief Loads and validates a project before transactionally replacing the current state.
*

View file

@ -961,8 +961,14 @@ xtc_result MainWindow::open_project(const char *path)
std::string loaded_path(path);
xtc_project *candidate = nullptr;
xtc_result r = xtc_project_create(&candidate);
if (r == XTC_OK)
r = xtc_project_load(candidate, path);
xtc_project_diagnostic diagnostic{};
if (r == XTC_OK) {
r = xtc_project_load_diagnostic(candidate,path,&diagnostic);
if (r != XTC_OK) {
project_issue_ = "Project load line " + std::to_string(diagnostic.line) + " (" + diagnostic.record + "): " + diagnostic.message;
report_error(project_issue_.c_str(),r);
}
}
if (r == XTC_OK)
r = install_project(candidate);
if (r != XTC_OK) {

View file

@ -1,3 +1,4 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc.h"
#include "xtc/xtc_version.h"
@ -8,11 +9,67 @@
#include "xtc/xtc_audio.h"
#include "xtc/xtc_midi.h"
#include <limits.h>
#include <inttypes.h>
#include <sys/stat.h>
#include <unistd.h>
#include "xtc/xtc_plugin.h"
/* Dependency inspection checks readable regular files, never plugin code or
* audio contents. Escape untrusted path/label bytes in terminal output. */
static void quoted(const char *text)
{
const unsigned char *p = (const unsigned char *)text;
putchar('"');
for (; *p; ++p) {
if (*p == '"' || *p == '\\') printf("\\%c",*p);
else if (*p < 32 || *p == 127) printf("\\x%02x",(unsigned int)*p);
else putchar(*p);
}
putchar('"');
}
static int readable_file(const char *path)
{
struct stat info;
return stat(path,&info) == 0 && S_ISREG(info.st_mode) && access(path,R_OK) == 0;
}
static int inspect(const xtc_project *project, uint64_t version)
{
size_t tracks = 0, clips = 0, nodes = 0, edges = 0, i, missing = 0;
const xtc_graph *graph = xtc_project_graph(project);
xtc_project_get_counts(project,&tracks,&clips);
xtc_graph_get_counts(graph,&nodes,&edges);
printf("Project format %" PRIu64 " (writer %u): %zu tracks/buses, %zu clips, %zu graph nodes, %zu edges.\n",
version,XTC_PROJECT_FORMAT_VERSION,tracks,clips,nodes,edges);
for (i = 0; i < clips; ++i) {
xtc_clip_info clip;
xtc_project_get_clip(project,i,&clip);
if (clip.asset_path[0]) {
int available = readable_file(clip.asset_path);
printf("Media clip %" PRIu64 ": %s ",clip.id,available ? "available" : "missing/unreadable");
quoted(clip.asset_path); putchar('\n');
if (!available) ++missing;
}
}
for (i = 0; i < nodes; ++i) {
xtc_node_info node; xtc_plugin_spec spec;
xtc_graph_get_node(graph,i,&node);
if (xtc_graph_get_plugin(graph,node.id,&spec) == XTC_OK) {
int available = readable_file(spec.path);
printf("LADSPA node %zu ID %u descriptor %u ",node.id,spec.unique_id,spec.index);
quoted(spec.label); printf(": %s ",available ? "available" : "missing/unreadable");
quoted(spec.path); putchar('\n');
if (!available) ++missing;
}
}
printf("Dependency references missing/unreadable: %zu.\n",missing);
puts("File availability only: WAV contents, plugin identity/initialization, JACK and hardware not checked; no plugin code executed.");
return missing ? 2 : EXIT_SUCCESS;
}
/**
* @brief Runs the xtc-check command-line entry point.
*
* Supports --version, --help, and --validate PROJECT. With no arguments it
* Supports version/help, metadata validation and dependency inspection. With no arguments it
* creates and destroys the application shell to check core startup/shutdown.
* Any other argument count prints usage to stderr.
*
@ -38,7 +95,7 @@ int main(int argc, char **argv)
return EXIT_SUCCESS;
}
if (argc == 2 && !strcmp(argv[1], "--help")) {
puts("Usage: xtc-check [--version | --help | --diagnose | --validate PROJECT | --recover-recording TEMP NEW.wav]\nNo arguments checks core startup/shutdown. "
puts("Usage: xtc-check [--version | --help | --diagnose | --validate PROJECT | --inspect PROJECT | --recover-recording TEMP NEW.wav]\nNo arguments checks core startup/shutdown. "
"Validation checks metadata only.");
return EXIT_SUCCESS;
}
@ -52,21 +109,28 @@ int main(int argc, char **argv)
puts("Complete recorded frames recovered; source retained.");
return EXIT_SUCCESS;
}
if (argc == 3 && !strcmp(argv[1], "--validate")) {
if (argc == 3 && (!strcmp(argv[1], "--validate") || !strcmp(argv[1], "--inspect"))) {
xtc_project *project = NULL;
xtc_project_diagnostic diagnostic = {0};
xtc_result result = xtc_project_create(&project);
if (result == XTC_OK)
result = xtc_project_load(project, argv[2]);
xtc_project_destroy(project);
if (result == XTC_OK) result = xtc_project_load_diagnostic(project,argv[2],&diagnostic);
if (result != XTC_OK) {
fprintf(stderr, "Project validation failed: %s\n", xtc_result_string(result));
fprintf(stderr,"Project validation failed: %s; line %zu, %s: %s\n",xtc_result_string(result),
diagnostic.line,diagnostic.record,diagnostic.message);
xtc_project_destroy(project);
return EXIT_FAILURE;
}
if (!strcmp(argv[1],"--inspect")) {
int code = inspect(project,diagnostic.format_version);
xtc_project_destroy(project);
return code;
}
xtc_project_destroy(project);
puts("Project metadata valid (external assets not loaded).");
return EXIT_SUCCESS;
}
if (argc != 1) {
fputs("Usage: xtc-check [--version | --help | --diagnose | --validate PROJECT | --recover-recording TEMP NEW.wav]\n", stderr);
fputs("Usage: xtc-check [--version | --help | --diagnose | --validate PROJECT | --inspect PROJECT | --recover-recording TEMP NEW.wav]\n", stderr);
return EXIT_FAILURE;
}

View file

@ -12,10 +12,13 @@
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
typedef struct {
FILE *file;
size_t bytes;
size_t bytes, line_number, record_line;
uint64_t version;
char expected[32];
char line[1536];
char *tokens[12];
size_t count;
@ -33,6 +36,7 @@ static xtc_result line(reader *input)
int byte;
char *cursor;
input->count = 0U;
input->record_line = ++input->line_number;
while ((byte = fgetc(input->file)) != EOF) {
if (++input->bytes > 4194304U)
return XTC_ERR_FORMAT;
@ -166,7 +170,9 @@ static int decode_float(const char *text, float *value)
#define READ(tag, total) \
do { \
xtc_result read_result = line(input); \
xtc_result read_result; \
snprintf(input->expected, sizeof(input->expected), "%s", (tag)); \
read_result = line(input); \
if (read_result != XTC_OK) \
return read_result; \
if (input->count != (total) || strcmp(input->tokens[0], (tag)) != 0) \
@ -193,6 +199,7 @@ static xtc_result read_project(reader *input, xtc_project *project, const char *
xtc_graph *graph = project->graph;
READ("XTC_PROJECT", 2U);
NUM(1U, version);
input->version = version;
if (version < 1U || version > XTC_PROJECT_FORMAT_VERSION)
return XTC_ERR_UNSUPPORTED;
format_version = version;
@ -435,6 +442,8 @@ static xtc_result read_project(reader *input, xtc_project *project, const char *
return XTC_ERR_FORMAT;
if (ferror(input->file))
return XTC_ERR_IO;
input->record_line = 0;
snprintf(input->expected,sizeof(input->expected),"%s","ASSET_PATH");
if (format_version >= 7) for (i = 0; i < project->clip_count; ++i) {
char *asset = project->clips[i].asset_path;
if (asset[0] && asset[0] != '/') {
@ -444,6 +453,7 @@ static xtc_result read_project(reader *input, xtc_project *project, const char *
memcpy(asset,directory,root); asset[root] = '/';
}
}
snprintf(input->expected,sizeof(input->expected),"%s","MODEL");
return xtc_project_validate(project);
}
@ -612,36 +622,64 @@ static char *project_folder(const char *path)
* @param path NUL-terminated file or asset path.
* @return XTC_OK on success, or an error code describing the failure.
*/
xtc_result xtc_project_load(xtc_project *project, const char *path)
static xtc_result load_report(const reader *input, xtc_result result, xtc_project_diagnostic *out)
{
xtc_project_diagnostic d = {0};
d.result = result;
d.format_version = input->version;
d.line = result == XTC_OK ? 0 : input->record_line;
if (result != XTC_OK) snprintf(d.record,sizeof(d.record),"%s",input->expected);
if (result == XTC_OK) snprintf(d.message,sizeof(d.message),"Metadata valid; external media/plugins not loaded.");
else if (result == XTC_ERR_UNSUPPORTED && !strcmp(input->expected,"XTC_PROJECT"))
snprintf(d.message,sizeof(d.message),"Project format %" PRIu64 " is unsupported; this build reads versions 1..%u.",input->version,XTC_PROJECT_FORMAT_VERSION);
else if (result == XTC_ERR_UNSUPPORTED) snprintf(d.message,sizeof(d.message),"Unsupported project feature or node type; no data was discarded.");
else if (result == XTC_ERR_FORMAT && !strcmp(input->expected,"SIZE")) snprintf(d.message,sizeof(d.message),"Project exceeds the 4 MiB input limit.");
else if (result == XTC_ERR_FORMAT && !strcmp(input->expected,"MODEL")) snprintf(d.message,sizeof(d.message),"Invalid project references, IDs, graph, routing or values.");
else if (result == XTC_ERR_FORMAT) snprintf(d.message,sizeof(d.message),"Invalid, unknown or truncated record; expected %s. Unknown fields are rejected.",input->expected);
else if (result == XTC_ERR_IO) snprintf(d.message,sizeof(d.message),"Cannot read project or resolve its containing directory; check path and permissions.");
else if (result == XTC_ERR_CAPACITY) snprintf(d.message,sizeof(d.message),"Project capacity or resolved media path exceeds supported limits.");
else snprintf(d.message,sizeof(d.message),"Project load failed: %s.",xtc_result_string(result));
if (out) *out = d;
return result;
}
xtc_result xtc_project_load_diagnostic(xtc_project *project, const char *path, xtc_project_diagnostic *diagnostic)
{
xtc_project *candidate = NULL;
reader input = {0};
char *directory;
xtc_result result;
struct stat info;
snprintf(input.expected,sizeof(input.expected),"%s","OPEN");
if (project == NULL || path_length(path) == 0U)
return XTC_ERR_INVALID_ARGUMENT;
return load_report(&input,XTC_ERR_INVALID_ARGUMENT,diagnostic);
input.file = fopen(path, "rb");
if (input.file == NULL)
return XTC_ERR_IO;
if (input.file == NULL) return load_report(&input,XTC_ERR_IO,diagnostic);
if (fstat(fileno(input.file),&info) == 0 && S_ISREG(info.st_mode) && info.st_size > 4194304) {
snprintf(input.expected,sizeof(input.expected),"%s","SIZE");
fclose(input.file);
return load_report(&input,XTC_ERR_FORMAT,diagnostic);
}
{
char *canonical = realpath(path,NULL);
directory = canonical ? project_folder(canonical) : NULL;
free(canonical);
}
if (!directory) { fclose(input.file); return XTC_ERR_IO; }
if (!directory) { fclose(input.file); return load_report(&input,XTC_ERR_IO,diagnostic); }
result = xtc_project_create(&candidate);
if (result == XTC_OK)
result = read_project(&input, candidate, directory);
if (result == XTC_OK) result = read_project(&input, candidate, directory);
free(directory);
if (fclose(input.file) != 0 && result == XTC_OK)
result = XTC_ERR_IO;
if (fclose(input.file) != 0 && result == XTC_OK) result = XTC_ERR_IO;
if (result == XTC_OK) {
xtc_graph *old = project->graph;
*project = *candidate;
candidate->graph = old;
}
xtc_project_destroy(candidate);
return result;
return load_report(&input,result,diagnostic);
}
xtc_result xtc_project_load(xtc_project *project, const char *path)
{
return xtc_project_load_diagnostic(project,path,NULL);
}
/**

View file

@ -3,7 +3,7 @@
Store small, redistributable fixtures here. Document their origin and license.
Do not commit large recordings or assets with unclear licensing.
`project-v1.xtc` through `project-v9.xtc` are hand-authored xtc metadata fixtures
`project-v1.xtc` through `project-v10.xtc` are hand-authored xtc metadata fixtures
created for this repository, under the project license. They contain no audio
or third-party resources. The v5/v6 plugin fixtures intentionally reference a
missing module to exercise metadata migration without loading code.

39
tests/fixtures/project-v10.xtc vendored Normal file
View file

@ -0,0 +1,39 @@
XTC_PROJECT 10
APP 0 1 0
TEMPO 120000
IDS 1 1
GRAPH 1 2 2 1
NODE 0 1 3f000000
NODE 1 4 00000000
EDGE 0 1
TRACKS 1
TRACK 0 4b657973
MIX 3f800000 00000000 0 0
ROUTE 0 18446744073709551615
CLIPS 1
CLIP 0 0 0 960 0 960 5665727365
ASSET -
MIDI 1
AUDIO 3f800000 0 0
NOTES 1 1
NOTE 0 0 0 480 69 100 0
AUTOMATION 1
POINT 0 0 0 3f800000 0
CONTROL_MAPS 16
MAP 0 7 10 64
MAP 1 7 10 64
MAP 2 7 10 64
MAP 3 7 10 64
MAP 4 7 10 64
MAP 5 7 10 64
MAP 6 7 10 64
MAP 7 7 10 64
MAP 8 7 10 64
MAP 9 7 10 64
MAP 10 7 10 64
MAP 11 7 10 64
MAP 12 7 10 64
MAP 13 7 10 64
MAP 14 7 10 64
MAP 15 7 10 64
END

View file

@ -19,7 +19,7 @@ int main(void)
assert(mkdtemp(root));
assert(xtc_project_create(&p) == XTC_OK);
snprintf(file,sizeof(file),"%s/migration.xtc",root);
for (version = 1; version <= 9; ++version) {
for (version = 1; version <= 10; ++version) {
xtc_track_info t; xtc_clip_info c; size_t notes, points;
snprintf(old,sizeof(old),"tests/fixtures/project-v%u.xtc",version);
assert(xtc_project_load(p,old) == XTC_OK);
@ -89,6 +89,6 @@ int main(void)
assert(xtc_project_load(p,file) == XTC_ERR_FORMAT);
{ xtc_clip_info c; assert(xtc_project_find_clip(p,clip,&c) == XTC_OK && c.asset_path[0]); } }
xtc_project_destroy(p); unlink(file); rmdir(moved);
puts("v1-v9 migration, current folder relocation, cwd independence and missing-media preservation passed.");
puts("v1-v10 migration, current folder relocation, cwd independence and missing-media preservation passed.");
return 0;
}

View file

@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""Metadata diagnostics and dependency inspection without a server or modules."""
from pathlib import Path
import subprocess
import sys
import tempfile
binary = str(Path(sys.argv[1]).resolve())
def check(option, path, code, *messages):
result = subprocess.run([binary, option, str(path)], capture_output=True, text=True)
assert result.returncode == code, (result.returncode, result.stdout, result.stderr)
for message in messages:
assert message in result.stdout + result.stderr, (message, result.stdout, result.stderr)
base = Path("tests/fixtures/project-v10.xtc").read_text()
check("--inspect", "tests/fixtures/project-v10.xtc", 0, "Project format 10", "missing/unreadable: 0")
with tempfile.TemporaryDirectory(prefix="xtc-inspect-") as directory:
root = Path(directory)
project = root / "project.xtc"
media = root / "media.wav"
# Relative paths resolve against the project, independently of the subprocess cwd.
audio = base.replace("ASSET -", "ASSET " + b"media.wav".hex()).replace("MIDI 1", "MIDI 0")
audio = audio.replace("NOTES 1 1\nNOTE 0 0 0 480 69 100 0", "NOTES 0 0")
project.write_text(audio)
check("--validate", project, 0, "metadata valid")
check("--inspect", project, 2, "Media clip 0", str(media), "missing/unreadable: 1")
media.write_text("Deliberately not WAV data")
check("--inspect", project, 0, "available", "WAV contents", "not checked")
module = root / "module.so"
plugin = Path("tests/fixtures/project-plugin-v6.xtc").read_text()
plugin = plugin.replace(b"/missing/test.so".hex(), str(module).encode().hex())
project.write_text(plugin)
check("--validate", project, 0)
check("--inspect", project, 2, "LADSPA node 0 ID 900000 descriptor 0", "xtc_test_gain", str(module))
# Inspection succeeds for readable files without trying dlopen or decoding.
module.write_text("Deliberately not an executable module")
check("--inspect", project, 0, "no plugin code executed")
for text, messages in [
(base.replace("XTC_PROJECT 10", "XTC_PROJECT 999"), ("line 1", "999", "1..10")),
("XTC_PROJECT 10\n", ("line 2", "APP")),
(base.replace("TEMPO 120000", "TEMPO 0"), ("line 3", "TEMPO")),
(base + "OPTIONAL_FUTURE 1\n", ("Unknown fields",)),
(base.replace("APP 0 1 0", "APP 0 1 0 extra"), ("APP",)),
]:
project.write_text(text)
check("--inspect", project, 1, *messages)
with project.open("wb") as stream:
stream.truncate(4194305)
check("--inspect", project, 1, "SIZE", "4 MiB")
check("--inspect", root / "absent.xtc", 1, "OPEN")
print("Project inspection availability, diagnostics and strict compatibility passed.")

View file

@ -0,0 +1,112 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_project.h"
#include <assert.h>
#include <dirent.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
/* Linker interposition injects errors only into this test executable. */
static int fault;
int __real_fflush(FILE *file);
int __real_fclose(FILE *file);
int __real_fsync(int descriptor);
int __real_rename(const char *old, const char *next);
int __wrap_fflush(FILE *file)
{
if (fault == 1) { errno = EIO; return EOF; }
return __real_fflush(file);
}
int __wrap_fclose(FILE *file)
{
int result = __real_fclose(file);
if (fault == 4) { errno = EIO; return EOF; }
return result;
}
int __wrap_fsync(int descriptor)
{
struct stat info;
assert(fstat(descriptor,&info) == 0);
if ((fault == 2 && S_ISREG(info.st_mode)) || (fault == 5 && S_ISDIR(info.st_mode))) { errno = EIO; return -1; }
return __real_fsync(descriptor);
}
int __wrap_rename(const char *old, const char *next)
{
if (fault == 3) { errno = EIO; return -1; }
return __real_rename(old,next);
}
static void write_text(const char *path, const char *text)
{
FILE *file = fopen(path,"wb"); assert(file);
assert(fwrite(text,1,strlen(text),file) == strlen(text)); assert(fclose(file) == 0);
}
static size_t bytes(const char *path, char out[4096])
{
FILE *file = fopen(path,"rb"); size_t n;
assert(file); n = fread(out,1,4096,file); assert(!ferror(file) && feof(file)); assert(fclose(file) == 0); return n;
}
static void no_temporaries(const char *path)
{
DIR *directory = opendir(path); struct dirent *entry; assert(directory);
while ((entry = readdir(directory))) assert(!strstr(entry->d_name,".tmp."));
assert(closedir(directory) == 0);
}
int main(void)
{
char root[] = "/tmp/xtc-project-report-XXXXXX", song[256], bad[256];
char before[4096], after[4096]; size_t size, n;
xtc_project *old = NULL, *next = NULL, *loaded = NULL;
xtc_project_diagnostic diagnostic;
xtc_track_info track;
uint64_t id;
int mode;
assert(mkdtemp(root));
snprintf(song,sizeof(song),"%s/song.xtc",root); snprintf(bad,sizeof(bad),"%s/bad.xtc",root);
assert(xtc_project_create(&old) == XTC_OK && xtc_project_create(&next) == XTC_OK && xtc_project_create(&loaded) == XTC_OK);
assert(xtc_project_add_track(old,"Old",&id) == XTC_OK && xtc_project_add_track(next,"New",&id) == XTC_OK);
assert(xtc_project_save(old,song) == XTC_OK); size = bytes(song,before);
assert(xtc_project_load_diagnostic(loaded,song,&diagnostic) == XTC_OK && diagnostic.result == XTC_OK &&
diagnostic.format_version == 10 && !diagnostic.line);
for (mode = 1; mode <= 5; ++mode) {
fault = mode;
assert(xtc_project_save(next,song) == XTC_ERR_IO);
fault = 0;
no_temporaries(root);
n = bytes(song,after);
if (mode < 5) assert(n == size && !memcmp(before,after,size));
assert(xtc_project_load(loaded,song) == XTC_OK);
assert(xtc_project_get_track(loaded,0,&track) == XTC_OK && !strcmp(track.name,mode < 5 ? "Old" : "New"));
}
write_text(bad,"XTC_PROJECT 999\n");
assert(xtc_project_load_diagnostic(loaded,bad,&diagnostic) == XTC_ERR_UNSUPPORTED && diagnostic.line == 1 &&
diagnostic.format_version == 999 && !strcmp(diagnostic.record,"XTC_PROJECT") && strstr(diagnostic.message,"1..10"));
write_text(bad,"XTC_PROJECT 10\nAPP 0 1 0\nTEMPO nope\n");
assert(xtc_project_load_diagnostic(loaded,bad,&diagnostic) == XTC_ERR_FORMAT && diagnostic.line == 3 &&
!strcmp(diagnostic.record,"TEMPO"));
write_text(bad,"XTC_PROJECT 10\n");
assert(xtc_project_load_diagnostic(loaded,bad,&diagnostic) == XTC_ERR_FORMAT && diagnostic.line == 2 &&
!strcmp(diagnostic.record,"APP"));
write_text(bad,"XTC_PROJECT 10\nAPP 0 1 0\nTEMPO 120000\nIDS 0 0\nGRAPH 1 1 1 0\nNODE 0 7 00000000\n");
assert(xtc_project_load_diagnostic(loaded,bad,&diagnostic) == XTC_ERR_UNSUPPORTED && diagnostic.line == 6 &&
!strcmp(diagnostic.record,"NODE") && strstr(diagnostic.message,"node type"));
{
FILE *file = fopen(bad,"wb"); assert(file);
assert(ftruncate(fileno(file),4194305) == 0 && fclose(file) == 0);
assert(xtc_project_load_diagnostic(loaded,bad,&diagnostic) == XTC_ERR_FORMAT && !diagnostic.line &&
!strcmp(diagnostic.record,"SIZE") && strstr(diagnostic.message,"4 MiB"));
}
{
FILE *file = fopen(bad,"wb"); size_t i; assert(file);
for (i = 0; i < 1536; ++i) assert(fputc('A',file) != EOF);
assert(fputc('\n',file) != EOF && fclose(file) == 0);
assert(xtc_project_load_diagnostic(loaded,bad,&diagnostic) == XTC_ERR_FORMAT && diagnostic.line == 1);
}
assert(xtc_project_get_track(loaded,0,&track) == XTC_OK && !strcmp(track.name,"New"));
assert(xtc_project_load_diagnostic(loaded,"/absent/xtc-report.xtc",&diagnostic) == XTC_ERR_IO && !diagnostic.line);
xtc_project_destroy(old); xtc_project_destroy(next); xtc_project_destroy(loaded);
unlink(song); unlink(bad); assert(rmdir(root) == 0);
puts("Project load diagnostics, retained state, bounded input and pre/post-rename save faults passed.");
return 0;
}