// app_version.cpp — implementation of the pure version-identity core (Phase V, V1 + V4). // See app_version.h for the contract. The version STRING and the channel bit both come // from version_generated.h (produced by CMake configure_file from the one // REASAMPLER_VERSION variable + the REASAMPLER_CHANNEL flag) — this TU re-exports them and // owns every pure derivation: the channel-qualified identity strings (V4) and the // parse/compare/classify logic (V1). No #ifdef forks leak beyond this file; the shells // consume the accessors below, so channel identity is one auditable definition. #include "app_version.h" #include #include "version_generated.h" // REASAMPLER_VERSION_STRING + REASAMPLER_CHANNEL_IS_BETA namespace reasampler { namespace { // The one channel predicate every derivation below branches on — the single point the // configure_file'd bit enters the pure module. constexpr so the branches fold at compile // time; the accessors still return by const ref for a stable shared instance. constexpr bool kIsBeta = (REASAMPLER_CHANNEL_IS_BETA != 0); } // namespace Channel channel() { return kIsBeta ? Channel::Beta : Channel::Stable; } bool isBeta() { return kIsBeta; } const std::string& appVersion() { // The user-visible render. Stable: EXACTLY the CMake string (leading zero and all). // Beta: the same numeric string plus a plain "-beta" suffix (V2). Function-local // static so callers share one authoritative instance. static const std::string kVersion = kIsBeta ? std::string(REASAMPLER_VERSION_STRING) + "-beta" : std::string(REASAMPLER_VERSION_STRING); return kVersion; } const std::string& stampVersion() { // The ext-state stamp value — the NUMERIC TRIPLE ONLY, IDENTICAL on both channels. // No "-beta" suffix: it must parse as Stamped on read-back (a suffixed stamp classifies // as Unknown), and stable's stamp stays byte-identical regardless of the channel build. // The channel is carried by extStateNamespace(), never baked into the stamp. static const std::string kStamp = REASAMPLER_VERSION_STRING; return kStamp; } const std::string& extStateNamespace() { // Stable "reasampler" is byte-identical to the pre-V4 build; beta is isolated. // FOREVER-STABLE per channel. static const std::string kNs = kIsBeta ? "reasampler_beta" : "reasampler"; return kNs; } const std::string& commandIdPrefix() { // Stable prefix is byte-identical to every shipped command id; beta is a distinct // forever-family. FOREVER-STABLE per channel. static const std::string kPrefix = kIsBeta ? "CEREBELLUM_REASAMPLER_BETA_" : "CEREBELLUM_REASAMPLER_"; return kPrefix; } const std::string& actionDisplayPrefix() { // Actions-list legibility: two channels must be distinguishable by name. Trailing // space so callers append the action phrase directly. static const std::string kDisp = kIsBeta ? "ReaSampler beta: " : "ReaSampler: "; return kDisp; } const std::string& binaryName() { static const std::string kName = kIsBeta ? "reaper_reasampler_beta" : "reaper_reasampler"; return kName; } const std::string& dockTitle() { static const std::string kTitle = kIsBeta ? "ReaSampler Bank beta" : "ReaSampler Bank"; return kTitle; } const std::string& dockIdent() { // Persisted dock-position ident — FOREVER-STABLE per channel (changing it strands the // saved dock slot). Beta qualified so the two panels do not fight over one slot. static const std::string kIdent = kIsBeta ? "reasampler_bank_panel_beta" : "reasampler_bank_panel"; return kIdent; } const std::string& vstOutputName() { // The .vst3 module OUTPUT_NAME base — FOREVER-STABLE per channel. Stable is // byte-identical to pre-S18 ("reasampler_9000"); beta is isolated so both install // side-by-side without a filename collision. static const std::string kName = kIsBeta ? "reasampler_9000_beta" : "reasampler_9000"; return kName; } const std::string& vstPluginName() { // The factory display name / editor title / embed label. Stable is byte-identical to // pre-S18 ("ReaSampler 9000"); beta appends " beta" so the two channels are distinct // plugins in the FX browser. static const std::string kName = kIsBeta ? "ReaSampler 9000 beta" : "ReaSampler 9000"; return kName; } std::string channelCommandId(const std::string& suffix) { return commandIdPrefix() + suffix; } std::string channelActionName(const std::string& phrase) { return actionDisplayPrefix() + phrase; } std::optional parseVersion(const std::string& s) { // Split on '.' into exactly three non-empty all-digit components. No sign, no // whitespace, no trailing garbage. Leading zeros are allowed (0.9.01 parses to // {0,9,1}) — the display fidelity of the zero is the string's job, not this parse's. Version v; int component = 0; // 0=major, 1=minor, 2=patch long long acc = 0; // current component accumulator (long long guards overflow) bool digitsInComponent = false; int* const out[3] = {&v.major, &v.minor, &v.patch}; for (char c : s) { if (c == '.') { if (!digitsInComponent) return std::nullopt; // empty component (".", "1..2") if (component >= 2) return std::nullopt; // too many dots *out[component] = static_cast(acc); ++component; acc = 0; digitsInComponent = false; continue; } if (c < '0' || c > '9') return std::nullopt; // non-digit (sign, letter, ws) acc = acc * 10 + (c - '0'); if (acc > 1'000'000'000LL) return std::nullopt; // absurdly large -> reject digitsInComponent = true; } if (component != 2 || !digitsInComponent) return std::nullopt; // too few components *out[2] = static_cast(acc); return v; } bool versionLess(const Version& a, const Version& b) { if (a.major != b.major) return a.major < b.major; if (a.minor != b.minor) return a.minor < b.minor; return a.patch < b.patch; } WritingVersion classifyWritingVersion(const std::string& rawStamp) { WritingVersion wv; if (rawStamp.empty()) { wv.kind = WritingVersion::Kind::PreVersioning; // no stamp -> pre-versioning return wv; } std::optional parsed = parseVersion(rawStamp); if (!parsed) { wv.kind = WritingVersion::Kind::Unknown; // present but malformed -> ignored wv.raw = rawStamp; return wv; } wv.kind = WritingVersion::Kind::Stamped; wv.raw = rawStamp; wv.parsed = *parsed; return wv; } } // namespace reasampler