Files
feedBack-desktop/src/audio/addon/NapiHelpers.h
T
OmikronApexandClaude Fable 5 d4e0bfc272 refactor(audio): extract AddonContext + NapiHelpers, guard raw N-API args (phase 6)
AddonContext (src/audio/addon/): engine/vstHost lifetime + snapshot rule,
the JUCE message thread with the macOS no-pump fork quarantined into ONE
file, the shutdown latch (exposed as isShuttingDown), doShutdown with a UI
teardown hook (NodeAddon points it at the editor-window nuke, #56), and the
pending-async-load registry. NodeAddon keeps using-declarations so the
binding bodies are unchanged. Also fixes SetBackingSpeed's bare `engine`
dereference — the one binding that dodged the file's own snapshot rule.

NapiHelpers (typed extractors argInt/argSlotId/argFiniteFloat/argBool/
argMidiChannel/argMidiByte) + rewrites of the unguarded bindings — the
deep-read §2 fix, done once: SetParameter/SetBypass/RemoveProcessor/
MoveProcessor/SetMultiBypass/SendMidiToSlot/SetGain plus the St-1 routing
quartet (SetPan/SetPostGain/SetBranch/SetBranchSrc). NaN slot ids no longer
coerce to slot 0; MIDI channel/program are range-checked before JUCE.

New gate: tests/napi-arg-fuzz.test.js — table-driven garbage (NaN/Inf/
negative/string/missing/object) against the real addon; chain state must be
byte-identical after the storm and a valid call must still apply.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 01:41:07 +02:00

67 lines
2.5 KiB
C++

#pragma once
// NapiHelpers — typed N-API argument extractors (TLC plan phase 6 / §3.2).
// Generalizes the getValidatedSource pattern so argument validation is
// structural, not per-binding: Int32Value() silently coerces NaN/Infinity
// into a valid index (NaN → 0), which let a malformed slot id hit a REAL
// slot (deep-read §2). Every extractor returns nullopt for a missing /
// non-Number / non-finite / out-of-range argument, and the binding no-ops —
// fail-soft, matching the addon's NAPI_DISABLE_CPP_EXCEPTIONS posture.
//
// New bindings should have no raw As<Napi::Number>() path to copy.
#include <napi.h>
#include <cmath>
#include <optional>
namespace slopsmith::addon {
// Finite integer in [minV, maxV]. The 4096 default ceiling keeps the cast
// well-defined for id-shaped args (slot ids, source ids, indices).
inline std::optional<int> argInt(const Napi::CallbackInfo& info, size_t i,
int minV = 0, int maxV = 4096)
{
if (i >= info.Length() || ! info[i].IsNumber()) return std::nullopt;
const double raw = info[i].As<Napi::Number>().DoubleValue();
if (! std::isfinite(raw) || raw != std::floor(raw)) return std::nullopt;
if (raw < (double) minV || raw > (double) maxV) return std::nullopt;
return (int) raw;
}
// Slot / source / param-index ids: finite non-negative integers.
inline std::optional<int> argSlotId(const Napi::CallbackInfo& info, size_t i)
{
return argInt(info, i);
}
// Finite float (parameter values, gains, pans). Range clamping stays with
// the engine-side sanitizers (GainSanitize.h) — this only rejects the
// NaN/Inf class that coercion would otherwise let through.
inline std::optional<float> argFiniteFloat(const Napi::CallbackInfo& info, size_t i)
{
if (i >= info.Length() || ! info[i].IsNumber()) return std::nullopt;
const double raw = info[i].As<Napi::Number>().DoubleValue();
if (! std::isfinite(raw)) return std::nullopt;
return (float) raw;
}
inline std::optional<bool> argBool(const Napi::CallbackInfo& info, size_t i)
{
if (i >= info.Length() || ! info[i].IsBoolean()) return std::nullopt;
return info[i].As<Napi::Boolean>().Value();
}
// MIDI channel: JUCE expects 1..16 and asserts otherwise.
inline std::optional<int> argMidiChannel(const Napi::CallbackInfo& info, size_t i)
{
return argInt(info, i, 1, 16);
}
// MIDI data byte (program / controller / value): 0..127.
inline std::optional<int> argMidiByte(const Napi::CallbackInfo& info, size_t i)
{
return argInt(info, i, 0, 127);
}
} // namespace slopsmith::addon