Files
roro9stack/lib/ota/src/file_receiver.cpp
T
twislaandClaude Opus 5.5 a0e3868934 Debug Console put: verify the card's copy, never zero-fill after a failed write
Found by M3's shared-bus test: when the card refused a write, the retry
closed the file (losing up to 3 KB of earlier chunks still in the write
buffer), then truncate() extended it back with zeros. The checksum only
covered the received bytes, so `put` reported success with 3 KB of zeros
on the card. Now a retry gives up if the card lost data, and the finished
file is read back and must hash the same before it's renamed.

The card refuses a write about once in five 1.7 MB uploads, with the
radio asleep as often as listening.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
2026-10-05 19:13:25 +02:00

120 lines
3.7 KiB
C++

#include "file_receiver.h"
#include <algorithm>
#include <cstdlib>
#include <cstring>
namespace roro {
namespace {
int hexDigit(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
// Splits on spaces (no iostreams: they cost about 200 KB of flash on the device).
std::vector<std::string> words(const std::string& s) {
std::vector<std::string> out;
size_t i = 0;
while (i < s.size()) {
if (s[i] == ' ') {
i++;
continue;
}
size_t end = s.find(' ', i);
if (end == std::string::npos) end = s.size();
out.push_back(s.substr(i, end - i));
i = end;
}
return out;
}
} // namespace
std::string FileReceiver::begin(const std::string& args, uint32_t nowMs) {
reset();
std::vector<std::string> w = words(args);
if (w.size() != 3) return "usage: sd put <path> <size> <sha256>";
const std::string &path = w[0], &size = w[1], &sha = w[2];
if (path.empty() || path[0] != '/' || path.size() > 128 || path.find("..") != std::string::npos)
return "the path must be absolute, without ..";
if (size.empty() || size.size() > 9 || size.find_first_not_of("0123456789") != std::string::npos)
return "bad size";
uint32_t bytes = static_cast<uint32_t>(std::strtoul(size.c_str(), nullptr, 10));
if (bytes == 0 || bytes > kMaxBytes) return "bad size";
if (sha.size() != 64) return "bad sha256";
for (size_t i = 0; i < 32; i++) {
int hi = hexDigit(sha[2 * i]), lo = hexDigit(sha[2 * i + 1]);
if (hi < 0 || lo < 0) return "bad sha256";
expected_[i] = static_cast<uint8_t>(hi << 4 | lo);
}
path_ = path;
size_ = bytes;
lastActivityMs_ = nowMs;
chunk_.reserve(kChunk);
state_ = State::Receiving;
return "";
}
size_t FileReceiver::wanted() const {
if (state_ != State::Receiving) return 0;
return std::min<size_t>(kChunk, size_ - received_) - chunk_.size();
}
size_t FileReceiver::feed(const uint8_t* data, size_t len, uint32_t nowMs) {
if (state_ != State::Receiving || len == 0) return 0;
size_t take = std::min(len, wanted());
chunk_.insert(chunk_.end(), data, data + take);
sha_.update(data, take);
lastActivityMs_ = nowMs;
if (wanted() > 0) return take;
if (received_ + chunk_.size() == size_) {
// The last chunk: refuse it before writing if the file arrived damaged.
uint8_t got[32];
sha_.finish(got);
if (std::memcmp(got, expected_, sizeof got) != 0) {
fail("checksum mismatch");
return take;
}
}
state_ = State::Writing;
return take;
}
void FileReceiver::chunkWritten(bool ok, uint32_t nowMs) {
if (state_ != State::Writing) return;
if (!ok) return fail("write failed");
received_ += static_cast<uint32_t>(chunk_.size());
chunk_.clear();
lastActivityMs_ = nowMs;
state_ = received_ == size_ ? State::Finishing : State::Receiving;
}
void FileReceiver::cardChecked(const uint8_t digest[32]) {
if (state_ != State::Finishing) return;
if (std::memcmp(digest, expected_, sizeof expected_) != 0) fail("the copy on the card differs");
}
void FileReceiver::finished(bool ok) {
if (state_ != State::Finishing) return;
if (!ok) return fail("rename failed");
state_ = State::Done;
}
void FileReceiver::tick(uint32_t nowMs) {
if (state_ != State::Receiving && state_ != State::Writing && state_ != State::Finishing) return;
if (nowMs - lastActivityMs_ >= kTimeoutMs) fail(state_ == State::Receiving ? "timed out" : "card not writable");
}
void FileReceiver::fail(const char* why) {
error_ = why;
chunk_.clear();
state_ = State::Failed;
}
} // namespace roro