Files
roro9stack/lib/release/src/http_head.cpp
T
twislaandClaude Sonnet 5.5 b0e8226943 Updates from Gitea, step 1: the model (host-tested)
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
2026-10-06 15:11:58 +02:00

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