Skip to content

.mpcweb schema accepts unconstrained ids and numerics #24

Description

@BootBlock

The .mpcweb snapshot schema in src/core/project/mpcweb.ts declares every id as a bare z.string() and every numeric as a bare z.number(). Both assumptions are load-bearing further down, and neither is enforced. §9.6 requires the import to "Zod-validate manifest + snapshot", and §1.3 #11 makes Zod the guard for exactly this kind of external data.

1. Unconstrained ids corrupt the whole snapshot

remapSnapshot rewrites ids by global string replacement:

for (const [oldId, newId] of idMap) json = json.split(oldId).join(newId);

Its own doc comment states the precondition — "Ids are globally-unique 36-char strings" — but every id field is id: z.string().

From a hand-edited project.json:

  • midiEvents[0].id = "e" → every letter e in the serialised snapshot (project name, track names, the JSON keys "sequences", "velocity") is replaced with a UUID. The parse then throws, or worse succeeds with silently mangled names and payloads.
  • id = """".split("") explodes the document into individual characters, each rejoined with a UUID.
  • id = "1" → every digit 1 in every tick, velocity and timestamp is rewritten, producing a structurally valid project with corrupted musical data.

Fix: z.uuid() (or z.string().length(36)) on every id field, plus a non-empty guard before the split/join.

2. Unconstrained numerics reach tick arithmetic

songEntry.repeats is z.number(), and buildSongMap in src/core/sequencer/songMap.ts loops on it:

for (let repeat = 0; repeat < entry.repeats; repeat++) {

JSON.parse('{"repeats":1e999}') yields Infinity, which z.number() accepts — it rejects only NaN. The loop never terminates and pushes segments until the tab dies. repeats: 1e9 reaches the same end more slowly.

The same gap applies to tick_start, duration_ticks, note, velocity, length_bars and time_sig_denominator, all of which feed tick arithmetic that assumes finite in-range integers. sequence.tempo is z.number().nullable() and permits 0, which makes secondsPerTick in src/core/sequencer/ppqn.ts return Infinity and poisons every when in the scheduled batch — there is no bpm = 0 guard.

Fix: .int(), .min(), .max() and .finite() bounds matching the §9.3 DDL CHECK constraints and the §2.6 ranges, so the schema and the database agree.

Note the runtime store paths already clamp these (useTransportStore, ranges.ts); it is specifically the import path that bypasses those clamps.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingimport-exportProject interchange, bounce, and .mpcweb (spec 9.5/9.6)schemaData model, DDL, and migrations (spec 9.3)securitySecrets handling, privacy, and security concerns

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions