Public Access
A streaming JSON scanner (a 33 KB list of releases costs a few hundred bytes), the release reader built on it, HTTP response heads and chunked bodies, URLs, and the decisions: which release is an update, whether to announce it, and which download URLs the device takes. Tested against the real answers of git.twis.la. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
81 lines
3.1 KiB
C++
81 lines
3.1 KiB
C++
#include "release_info.h"
|
|
|
|
#include <cstdlib>
|
|
|
|
namespace roro::release {
|
|
|
|
namespace {
|
|
bool endsWith(const std::string& s, const char* tail) {
|
|
size_t n = std::char_traits<char>::length(tail);
|
|
return s.size() >= n && s.compare(s.size() - n, n, tail) == 0;
|
|
}
|
|
} // namespace
|
|
|
|
std::string firstParagraph(const std::string& text, bool truncated) {
|
|
size_t end = text.find("\n\n");
|
|
size_t crlf = text.find("\r\n\r\n");
|
|
if (crlf != std::string::npos && (end == std::string::npos || crlf < end)) end = crlf;
|
|
std::string p = text.substr(0, end);
|
|
size_t first = p.find_first_not_of(" \t\r\n");
|
|
if (first == std::string::npos) return "";
|
|
p.erase(0, first);
|
|
while (!p.empty() && (p.back() == ' ' || p.back() == '\t' || p.back() == '\r' || p.back() == '\n')) p.pop_back();
|
|
if (truncated && end == std::string::npos) p += "...";
|
|
return p;
|
|
}
|
|
|
|
ReleaseReader::ReleaseReader(size_t maxReleases)
|
|
: scanner_([this](const std::string& path, const std::string& text, bool isString, bool truncated) {
|
|
value(path, text, isString, truncated);
|
|
}),
|
|
max_(maxReleases) {}
|
|
|
|
void ReleaseReader::end() { flushAsset(); }
|
|
|
|
void ReleaseReader::flushAsset() {
|
|
if (assetRelease_ >= 0 && assetRelease_ < static_cast<int>(releases_.size()) && endsWith(assetName_, ".ota") && !assetUrl_.empty()) {
|
|
releases_[assetRelease_].otaUrl = assetUrl_;
|
|
releases_[assetRelease_].otaSize = assetSize_;
|
|
}
|
|
assetRelease_ = assetIndex_ = -1;
|
|
assetName_.clear();
|
|
assetUrl_.clear();
|
|
assetSize_ = 0;
|
|
}
|
|
|
|
void ReleaseReader::value(const std::string& path, const std::string& text, bool isString, bool truncated) {
|
|
size_t index = 0;
|
|
std::string rest = path;
|
|
if (!path.empty() && path[0] == '[') { // a list: "[2].tag_name"
|
|
char* end;
|
|
index = static_cast<size_t>(std::strtol(path.c_str() + 1, &end, 10));
|
|
rest = *end == ']' ? std::string(end + 1) : "";
|
|
if (!rest.empty() && rest[0] == '.') rest.erase(0, 1);
|
|
}
|
|
if (index >= max_) return;
|
|
if (releases_.size() <= index) releases_.resize(index + 1);
|
|
Release& r = releases_[index];
|
|
|
|
if (rest == "tag_name") r.tag = text;
|
|
else if (rest == "published_at") r.published = text;
|
|
else if (rest == "body") r.notes = firstParagraph(text, truncated);
|
|
else if (rest == "draft") r.draft = text == "true";
|
|
else if (rest == "prerelease") r.prerelease = text == "true";
|
|
else if (rest.compare(0, 7, "assets[") == 0) {
|
|
char* end;
|
|
int j = static_cast<int>(std::strtol(rest.c_str() + 7, &end, 10));
|
|
if (static_cast<int>(index) != assetRelease_ || j != assetIndex_) {
|
|
flushAsset();
|
|
assetRelease_ = static_cast<int>(index);
|
|
assetIndex_ = j;
|
|
}
|
|
std::string field = *end == ']' && end[1] == '.' ? std::string(end + 2) : "";
|
|
if (field == "name") assetName_ = text;
|
|
else if (field == "size") assetSize_ = static_cast<uint32_t>(std::strtoul(text.c_str(), nullptr, 10));
|
|
else if (field == "browser_download_url") assetUrl_ = text;
|
|
}
|
|
(void)isString;
|
|
}
|
|
|
|
} // namespace roro::release
|