Updates: when an answer is the rest of the same file, and how long to try

For carrying on a download that broke (issue #54): the head's ETag and
Content-Range are read, a 206 is checked to be the rest of the same
file, and a policy says how many tries a download is worth. Host-tested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk
This commit is contained in:
2026-10-11 18:26:39 +02:00
co-authored by Claude Opus 5.5
parent 7e14e270df
commit 468dc0dc0d
4 changed files with 159 additions and 0 deletions
+11
View File
@@ -1,5 +1,7 @@
#include "http_head.h"
#include <cstdio>
#include <algorithm>
#include <cctype>
#include <cstdlib>
@@ -58,6 +60,15 @@ void HttpHeadParser::line() {
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;
else if (name == "etag") head_.etag = value;
else if (name == "content-range") {
long from = -1, to = -1, total = -1;
if (std::sscanf(lower(value).c_str(), "bytes %ld-%ld/%ld", &from, &to, &total) == 3 && from >= 0 && to >= from && total > to) {
head_.rangeFrom = from;
head_.rangeTo = to;
head_.rangeTotal = total;
}
}
}
size_t ChunkedDecoder::decode(const uint8_t* in, size_t len, uint8_t* out) {
+3
View File
@@ -12,6 +12,9 @@ struct HttpHead {
long contentLength = -1; // -1: not given
bool chunked = false;
std::string location, contentType;
std::string etag; // as sent, quotes included; "" if none
// "Content-Range: bytes 1000-2999/3000", the answer to a range request; -1: not given.
long rangeFrom = -1, rangeTo = -1, rangeTotal = -1;
};
class HttpHeadParser {
+53
View File
@@ -0,0 +1,53 @@
#pragma once
#include <string>
#include "http_head.h"
namespace roro::release {
// Carrying on a download that broke (issue #54): the connection is opened again with
// "Range: bytes=<from>-", and what was already received stays where it is. The Update File's own
// hash and signature are what decide whether the whole is good; these checks only keep the device
// from carrying on with something that plainly is not the rest of the same file.
// The validator to send as "If-Range", so a file that changed comes back whole (a 200) instead of
// in part: only a strong ETag may be used for that. "" for none.
inline std::string ifRangeFor(const std::string& etag) {
return etag.size() >= 2 && etag.front() == '"' && etag.back() == '"' ? etag : "";
}
// Is this answer the rest of the file, from byte `from` of `total`? "" if so, or why not, in
// words for the screen. `etag` is what the first answer gave ("" if nothing).
inline std::string resumeRefusal(const HttpHead& head, long from, long total, const std::string& etag) {
if (head.status == 200) return "The file changed on the server";
if (head.status != 206) return "The server answered " + std::to_string(head.status);
if (head.chunked || head.rangeFrom != from) return "The server sent another part of the file";
if (head.rangeTotal != total || head.rangeTo != total - 1 || head.contentLength != total - from) return "The file changed size on the server";
if (!etag.empty() && !head.etag.empty() && head.etag != etag) return "The file changed on the server";
return "";
}
// How long to keep trying. A break that came after new bytes is the link being poor, and is
// worth another try; breaks with nothing in between mean it is not coming back.
class ResumePolicy {
public:
static constexpr int kMaxResumes = 20; // in one download
static constexpr int kMaxStuck = 3; // in a row without a byte
// The connection broke with `received` bytes of the file here: try again?
bool again(long received) {
stuck_ = received > last_ ? 0 : stuck_ + 1;
last_ = received;
if (resumes_ >= kMaxResumes || stuck_ >= kMaxStuck) return false;
resumes_++;
return true;
}
int resumes() const { return resumes_; }
private:
long last_ = 0;
int resumes_ = 0, stuck_ = 0;
};
} // namespace roro::release