feat(version): single-source semver, ext-state stamp, show-version action

Version "0.9.01" sourced once from CMake via configure_file'd header; pure
app_version module (parse/compare/classify) with CTest. Stamp rides the persist
save seam under "reasampler"/"version"; absent stamp reads as pre-versioning.
On-demand "show version" action, no startup print.
This commit is contained in:
2026-07-26 15:16:55 -04:00
parent 791a9c60eb
commit f6dddbf5a3
8 changed files with 409 additions and 2 deletions
+76
View File
@@ -0,0 +1,76 @@
// app_version.cpp — implementation of the pure version-identity core (Phase V, V1).
// See app_version.h for the contract. The version STRING itself comes from
// version_generated.h (produced by CMake configure_file from the one REASAMPLER_VERSION
// variable) — this TU just re-exports it and owns the pure parse/compare/classify logic.
#include "app_version.h"
#include <string>
#include "version_generated.h" // REASAMPLER_VERSION_STRING — configure_file'd from CMake
namespace reasampler {
const std::string& appVersion() {
// Function-local static: initialized once from the compile-time string, returned by
// const ref so callers share the one authoritative instance. The macro is the exact
// CMake value, leading zero and all.
static const std::string kVersion = REASAMPLER_VERSION_STRING;
return kVersion;
}
std::optional<Version> 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<int>(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<int>(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<Version> 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