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
159 lines
5.5 KiB
C++
159 lines
5.5 KiB
C++
#include "http_head.h"
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <cstdlib>
|
|
|
|
namespace roro::release {
|
|
|
|
namespace {
|
|
std::string lower(std::string s) {
|
|
for (char& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
return s;
|
|
}
|
|
} // namespace
|
|
|
|
size_t HttpHeadParser::feed(const char* data, size_t len) {
|
|
size_t used = 0;
|
|
while (used < len && !complete_ && !failed_) {
|
|
char c = data[used++];
|
|
total_++;
|
|
if (total_ > kMaxBytes) {
|
|
failed_ = true;
|
|
break;
|
|
}
|
|
if (c == '\n') {
|
|
if (!line_.empty() && line_.back() == '\r') line_.pop_back();
|
|
line();
|
|
line_.clear();
|
|
} else {
|
|
line_ += c;
|
|
}
|
|
}
|
|
return used;
|
|
}
|
|
|
|
void HttpHeadParser::line() {
|
|
if (first_) { // "HTTP/1.1 200 OK"
|
|
first_ = false;
|
|
if (line_.compare(0, 5, "HTTP/") != 0) {
|
|
failed_ = true;
|
|
return;
|
|
}
|
|
size_t space = line_.find(' ');
|
|
head_.status = space == std::string::npos ? 0 : std::atoi(line_.c_str() + space + 1);
|
|
if (head_.status < 100 || head_.status > 599) failed_ = true;
|
|
return;
|
|
}
|
|
if (line_.empty()) {
|
|
complete_ = true;
|
|
return;
|
|
}
|
|
size_t colon = line_.find(':');
|
|
if (colon == std::string::npos) return; // not a header: ignored
|
|
std::string name = lower(line_.substr(0, colon));
|
|
size_t from = line_.find_first_not_of(" \t", colon + 1);
|
|
std::string value = from == std::string::npos ? "" : line_.substr(from);
|
|
if (name == "content-length") head_.contentLength = std::atol(value.c_str());
|
|
else if (name == "transfer-encoding") head_.chunked = lower(value).find("chunked") != std::string::npos;
|
|
else if (name == "location") head_.location = value;
|
|
else if (name == "content-type") head_.contentType = value;
|
|
}
|
|
|
|
size_t ChunkedDecoder::decode(const uint8_t* in, size_t len, uint8_t* out) {
|
|
size_t written = 0;
|
|
for (size_t i = 0; i < len && !failed_ && state_ != State::Done; i++) {
|
|
uint8_t c = in[i];
|
|
switch (state_) {
|
|
case State::Size: {
|
|
int digit = c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : c >= 'A' && c <= 'F' ? c - 'A' + 10 : -1;
|
|
if (digit >= 0) {
|
|
if (remaining_ > (SIZE_MAX >> 5)) failed_ = true;
|
|
remaining_ = remaining_ * 16 + digit;
|
|
anyDigit_ = true;
|
|
} else if (c == ';' && anyDigit_) {
|
|
state_ = State::Extension;
|
|
} else if (c == '\r' && anyDigit_) {
|
|
state_ = State::SizeLf;
|
|
} else {
|
|
failed_ = true;
|
|
}
|
|
break;
|
|
}
|
|
case State::Extension:
|
|
if (c == '\r') state_ = State::SizeLf;
|
|
break;
|
|
case State::SizeLf:
|
|
if (c != '\n') {
|
|
failed_ = true;
|
|
} else if (remaining_ == 0) {
|
|
state_ = State::Trailer;
|
|
trailerLine_ = 0;
|
|
} else {
|
|
state_ = State::Data;
|
|
}
|
|
break;
|
|
case State::Data: {
|
|
size_t take = std::min(remaining_, len - i);
|
|
for (size_t k = 0; k < take; k++) out[written + k] = in[i + k];
|
|
written += take;
|
|
remaining_ -= take;
|
|
i += take - 1;
|
|
if (remaining_ == 0) state_ = State::DataCr;
|
|
break;
|
|
}
|
|
case State::DataCr:
|
|
if (c != '\r') failed_ = true;
|
|
else state_ = State::DataLf;
|
|
break;
|
|
case State::DataLf:
|
|
if (c != '\n') {
|
|
failed_ = true;
|
|
} else {
|
|
state_ = State::Size;
|
|
anyDigit_ = false;
|
|
}
|
|
break;
|
|
case State::Trailer: // header lines after the last chunk, until an empty one
|
|
if (c == '\n') {
|
|
if (trailerLine_ == 0) state_ = State::Done;
|
|
trailerLine_ = 0;
|
|
} else if (c != '\r') {
|
|
trailerLine_++;
|
|
}
|
|
break;
|
|
case State::Done: break;
|
|
}
|
|
}
|
|
return written;
|
|
}
|
|
|
|
Url parseUrl(const std::string& url) {
|
|
Url u;
|
|
size_t scheme = url.find("://");
|
|
if (scheme == std::string::npos) return u;
|
|
std::string s = lower(url.substr(0, scheme));
|
|
if (s != "http" && s != "https") return u;
|
|
u.https = s == "https";
|
|
size_t hostStart = scheme + 3, pathStart = url.find('/', hostStart);
|
|
std::string authority = url.substr(hostStart, pathStart == std::string::npos ? std::string::npos : pathStart - hostStart);
|
|
u.path = pathStart == std::string::npos ? "/" : url.substr(pathStart);
|
|
if (authority.empty() || authority.find('@') != std::string::npos) return u; // no credentials in a URL
|
|
size_t colon = authority.rfind(':');
|
|
u.port = u.https ? 443 : 80;
|
|
if (colon != std::string::npos) {
|
|
u.port = std::atoi(authority.c_str() + colon + 1);
|
|
authority.resize(colon);
|
|
if (u.port <= 0 || u.port > 65535) return u;
|
|
}
|
|
for (unsigned char c : authority)
|
|
if (!(std::isalnum(c) || c == '.' || c == '-')) return u;
|
|
for (unsigned char c : u.path)
|
|
if (c <= ' ' || c == 0x7F) return u;
|
|
u.host = lower(authority);
|
|
u.ok = !u.host.empty();
|
|
return u;
|
|
}
|
|
|
|
} // namespace roro::release
|