Branch: harden-load → main
PR 1 of the review fixes plan (docs/review-fixes-plan.md). Part of 0.5.2; the version does not change in this PR.
Sample names stay inside the project (S1, load side). Loading a project now refuses it if any sample file name in the pool is not a plain file name. A plain name is not empty, not . or .., and has no /, \ or control characters. The same rule applies to each pad layer's sampleFile, which must be text or missing. Before, a pool path of ../../../secret.txt was joined to the data folder as it was. The error names the bad entry, e.g. samples[0].path: file name "../x.wav" contains a folder separator. The rule is project.PlainFileName, which PR 3 reuses at export.
Missing fields no longer crash (S3).
path. It also refuses a sequence without a whole-number key, an object value, a number bpm, a whole-number lengthBars or a text name. Each error names the entry, e.g. sequences[0]: bpm is missing or not a number. Before, the first two cases were nil-pointer panics.xpj.Node.Int and Float return an error on a nil node, and Get on a nil node returns an error. Lookup was already nil-safe. In project, the remaining direct .Text and .Float() uses on lookups now go through text(), num() and a new float().data.version now gives the warning "project data version is missing" instead of "version 0".main.go sets Wails's PanicHandler. A panic is logged with its stack and the window shows "Something went wrong inside MPCkit, and the last action may not have finished." Without the handler, Wails exits the app on a panic in an event handler such as file drop.defer, so a panic there cannot leave the lock held and freeze every later call. This was not in the plan, but it follows from the same finding.Main nodes found once (M2). wrap finds samples, tracks[0].samples, tracks[0].program.drum.instruments and sequences once and stores them on Project. pool(), trackPool(), pads() and sequences() return the stored nodes instead of looking them up again with the error dropped.
Tests.
TestLoadRejectsMalformed builds 16 broken copies of DemoFile in t.TempDir() and checks each error. The cases cover pool paths ../x.wav, a/b.wav, ..\b.wav, .., an empty name, a control character, a bad track pool entry, and a missing or non-text path. They also cover a bad layer sampleFile, and a sequence missing bpm, key, value or lengthBars, or with a non-text name.sampleFile (still loads), a missing data.version (warning), PlainFileName, and xpj getters on a nil node.FuzzLoad is the fuzz target the plan describes.TestBrokenFieldsDoNotPanic breaks DemoFile's fields one at a time, then runs every read and most edits on the result. None may panic. It covers 162 fields and 1,134 cases, and takes about 30 seconds on the server; go test -short skips it. See "Fuzzing" below for why it was added.path case panics.Choices the plan left open. Say if any should change:
tracks[0].samples holds the same entries as samples, so its paths get the same check.sampleFile is allowed. The device writes "" on every empty pad layer (1,018 of them in DemoFile), so an empty name means "no sample" and only non-empty names must be plain.key and lengthBars must be whole numbers, since both are used as integers. bpm may be any number.key outside 0–127, duplicate keys, and duplicate pool names.execs: 9 (0/sec) for most of the run. The fuzz function itself counted 42,097 calls in 15 seconds, so the 2-minute run made about 300,000.Fuzzing. FuzzLoad barely reaches the project code. The seeds are gzip files, and gzip checks a CRC at the end of the stream, so almost every mutated input fails at decompression. In a 15-second sample:
Fuzzing the decompressed JSON does not help either: each project is about 3 MB uncompressed, too big for Go's fuzzer to mutate usefully.
So I first ran a full sweep, not committed:
project reads in DemoFile in 7 ways: null, text, -1, 1000, an empty list, an empty object, or deleted.bajdev chose to keep FuzzLoad and also commit a cut-down sweep that runs in under a minute: TestBrokenFieldsDoNotPanic.
sliceInfo, rather than every field below the pad.On the server:
go test ./... passes, including the round-trip and golden tests, unchanged.go vet ./... and staticcheck ./... are clean.govulncheck ./... reports only GO-2026-6859 in the standard library's net/http. That needs Go 1.27.4 (PR 4).go test -fuzz=FuzzLoad -fuzztime=2m ./internal/project found nothing.TestBrokenFieldsDoNotPanic passes in about 30 seconds.npm install, npm run check (0 errors, 0 warnings) and npm run build pass. The lockfile npm install wrote was not committed.wails3 build builds.timeout 15 xvfb-run -a bin/mpckit starts and runs. The log holds only the usual headless GTK warnings.On the laptop (PowerShell, in the repo):
git fetch; git checkout harden-loadgo test ./internal/project ./internal/xpj: both ok (the project tests take about half a minute). On Windows, filepath also treats \ and drive letters as path parts, so this checks the name rule there.wails3 dev: load testdata\device\DemoFile.xpj. It opens as before: pads A1–A4, B1 and B2 filled; sequences Intro and Sequence 02.testdata\accepted\MpckitT1.xpj, then MpckitT3.xpj. Both open with no error or warning.