// capture_name — pure implementation. See the header. #include "core/capture/capture_name.h" #include namespace reasampler::capture { namespace { // A track name padded with spaces would render ragged in the label and as underscores in // the stem, so both ends are trimmed before anything else looks at it. std::string trimmed(const std::string& s) { std::size_t b = 0; std::size_t e = s.size(); auto isSpace = [](unsigned char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; }; while (b < e && isSpace(static_cast(s[b]))) ++b; while (e > b && isSpace(static_cast(s[e - 1]))) --e; return s.substr(b, e - b); } // Truncating mid-sequence would put invalid UTF-8 into the persisted label, so the cut // backs off over continuation bytes (10xxxxxx). The stem does not care — sanitizeStem // replaces every non-ASCII byte anyway — but one rule for both keeps them the same name. std::string truncateUtf8(const std::string& s, std::size_t maxBytes) { if (s.size() <= maxBytes) return s; std::size_t cut = maxBytes; while (cut > 0 && (static_cast(s[cut]) & 0xC0) == 0x80) --cut; return s.substr(0, cut); } int clampTo(int v, int lo, int hi) { return v < lo ? lo : (v > hi ? hi : v); } } // namespace std::string formatCaptureStamp(const CaptureStamp& stamp) { if (stamp.month < 1 || stamp.day < 1) return {}; char buf[24]; std::snprintf(buf, sizeof(buf), "%02d-%02d %02d%02d", clampTo(stamp.month, 1, 12), clampTo(stamp.day, 1, 31), clampTo(stamp.hour, 0, 23), clampTo(stamp.minute, 0, 59)); return buf; } CaptureName composeCaptureName(const CaptureNameInputs& in) { std::string base; int named = 0; for (const std::string& raw : in.sourceNames) { const std::string n = trimmed(raw); if (n.empty()) continue; if (base.empty()) base = n; ++named; } if (base.empty()) base = trimmed(in.fallback); if (base.empty()) base = "capture"; base = truncateUtf8(base, kMaxSourceNameBytes); // truncateUtf8 backs off over continuation bytes, so a name whose first kMaxSourceNameBytes // bytes are ALL continuation bytes (0x80-0xBF) backs off to nothing — re-apply the "never an // empty label" fallback after truncation, not just before it. if (base.empty()) base = "capture"; CaptureName out; out.label = base; out.stemBase = base; // Several sources collapse onto the first one's name plus a count of the rest — the // alternative (joining every name) produces a stem no one can read and a label that // no longer fits a card. if (named > 1) { const std::string extra = std::to_string(named - 1); out.label += " +" + extra; out.stemBase += "+" + extra; } if (in.ordinal > 0) { const std::string ord = std::to_string(in.ordinal); out.label += " #" + ord; out.stemBase += "-" + ord; } const std::string stamp = formatCaptureStamp(in.stamp); if (!stamp.empty()) out.label += " " + stamp; return out; } std::string captureTrackName(const std::string& sourceName) { const std::string prefix(kCaptureTrackPrefix); // A source with no readable name yields the bare word rather than a trailing // space; both spellings are fixed points, which is what makes the whole function // one (a track named exactly "Capture" must not become "Capture Capture"). Read // from kCaptureTrackPrefixBare rather than chopped off prefix, so the two names // can't drift out of sync with each other (both expand from the same header token). const std::string bare = kCaptureTrackPrefixBare; if (sourceName.empty()) return bare; if (sourceName == bare) return sourceName; if (sourceName.rfind(prefix, 0) == 0) return sourceName; return prefix + sourceName; } } // namespace reasampler::capture