Public Access
G1 step 3: the Gemini fetcher (TOFU, redirects, floors, pages via the card)
GeminiService fetches on a short-lived task and hands the App a GeminiPage: header, the body as lines in 4 KB chunks (TextBuffer: no large block, no doubling copies), the final URL after up to 5 redirects. Certificates are pinned on first use per host and port; a change comes back as its own outcome with both fingerprints. No fetch starts below 55 KB free (Q86). With a card, the body streams to /gemini/cache/page.gmi in 1 KB pieces while the connection is open, then loads into RAM once its memory is back (Q87); StorageService::runAndWait (moved from the Debug Console) keeps every card access on the storage task. Without a card: RAM, with the steady and transient floors. Measured with IRC connected: Cosmos (31.6 KB) went from 4.6 KB to the whole page on the card and 20 KB on screen; lowest free heap 19.5 KB in transfer, 43 KB once loaded. `gemini get` and `gemini trust` on the console. 14 Gemini tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
+38
-35
@@ -11,6 +11,43 @@ std::string trim(const std::string& s) {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
GemLine parseGemLine(const std::string& line, bool& pre) {
|
||||
GemLine g;
|
||||
if (line.compare(0, 3, "```") == 0) {
|
||||
g.type = LineType::PreToggle;
|
||||
g.text = trim(line.substr(3));
|
||||
pre = !pre;
|
||||
} else if (pre) {
|
||||
g.type = LineType::Preformatted;
|
||||
g.text = line;
|
||||
} else if (line.compare(0, 2, "=>") == 0) {
|
||||
g.type = LineType::Link;
|
||||
std::string rest = trim(line.substr(2));
|
||||
size_t gap = rest.find_first_of(" \t");
|
||||
g.url = rest.substr(0, gap);
|
||||
g.text = gap == std::string::npos ? "" : trim(rest.substr(gap));
|
||||
if (g.text.empty()) g.text = g.url;
|
||||
} else if (line.compare(0, 3, "###") == 0) {
|
||||
g.type = LineType::Heading3;
|
||||
g.text = trim(line.substr(3));
|
||||
} else if (line.compare(0, 2, "##") == 0) {
|
||||
g.type = LineType::Heading2;
|
||||
g.text = trim(line.substr(2));
|
||||
} else if (line.compare(0, 1, "#") == 0) {
|
||||
g.type = LineType::Heading1;
|
||||
g.text = trim(line.substr(1));
|
||||
} else if (line.compare(0, 2, "* ") == 0) {
|
||||
g.type = LineType::ListItem;
|
||||
g.text = trim(line.substr(2));
|
||||
} else if (line.compare(0, 1, ">") == 0) {
|
||||
g.type = LineType::Quote;
|
||||
g.text = trim(line.substr(1));
|
||||
} else {
|
||||
g.text = line;
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
std::vector<GemLine> parseGemtext(const std::string& document) {
|
||||
std::vector<GemLine> lines;
|
||||
bool pre = false;
|
||||
@@ -21,41 +58,7 @@ std::vector<GemLine> parseGemtext(const std::string& document) {
|
||||
std::string line = document.substr(start, end - start);
|
||||
if (!line.empty() && line.back() == '\r') line.pop_back();
|
||||
start = end + 1;
|
||||
|
||||
GemLine g;
|
||||
if (line.compare(0, 3, "```") == 0) {
|
||||
g.type = LineType::PreToggle;
|
||||
g.text = trim(line.substr(3));
|
||||
pre = !pre;
|
||||
} else if (pre) {
|
||||
g.type = LineType::Preformatted;
|
||||
g.text = line;
|
||||
} else if (line.compare(0, 2, "=>") == 0) {
|
||||
g.type = LineType::Link;
|
||||
std::string rest = trim(line.substr(2));
|
||||
size_t gap = rest.find_first_of(" \t");
|
||||
g.url = rest.substr(0, gap);
|
||||
g.text = gap == std::string::npos ? "" : trim(rest.substr(gap));
|
||||
if (g.text.empty()) g.text = g.url;
|
||||
} else if (line.compare(0, 3, "###") == 0) {
|
||||
g.type = LineType::Heading3;
|
||||
g.text = trim(line.substr(3));
|
||||
} else if (line.compare(0, 2, "##") == 0) {
|
||||
g.type = LineType::Heading2;
|
||||
g.text = trim(line.substr(2));
|
||||
} else if (line.compare(0, 1, "#") == 0) {
|
||||
g.type = LineType::Heading1;
|
||||
g.text = trim(line.substr(1));
|
||||
} else if (line.compare(0, 2, "* ") == 0) {
|
||||
g.type = LineType::ListItem;
|
||||
g.text = trim(line.substr(2));
|
||||
} else if (line.compare(0, 1, ">") == 0) {
|
||||
g.type = LineType::Quote;
|
||||
g.text = trim(line.substr(1));
|
||||
} else {
|
||||
g.text = line;
|
||||
}
|
||||
lines.push_back(std::move(g));
|
||||
lines.push_back(parseGemLine(line, pre));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ struct GemLine {
|
||||
std::string text, url;
|
||||
};
|
||||
|
||||
// One line; `preformatted` carries the ``` state from line to line (start with false).
|
||||
GemLine parseGemLine(const std::string& line, bool& preformatted);
|
||||
std::vector<GemLine> parseGemtext(const std::string& document);
|
||||
|
||||
// For the Latin-1 fonts (Q79): valid UTF-8 up to U+00FF kept, anything else '?', and tabs
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
#include "text_buffer.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace roro::gemini {
|
||||
|
||||
void TextBuffer::append(const char* data, size_t len) {
|
||||
bytes_ += len;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (data[i] == '\n') endLine();
|
||||
else partial_ += data[i];
|
||||
}
|
||||
}
|
||||
|
||||
void TextBuffer::finish() {
|
||||
if (!partial_.empty()) endLine();
|
||||
}
|
||||
|
||||
void TextBuffer::endLine() {
|
||||
if (!partial_.empty() && partial_.back() == '\r') partial_.pop_back();
|
||||
size_t len = std::min<size_t>(partial_.size(), 0xFFFF);
|
||||
// A line never spans two chunks: a new chunk when it doesn't fit (bigger if the line is).
|
||||
if (chunks_.empty() || chunks_.back().size() + len > chunks_.back().capacity()) {
|
||||
chunks_.emplace_back();
|
||||
chunks_.back().reserve(std::max(kChunk, len));
|
||||
}
|
||||
std::string& chunk = chunks_.back();
|
||||
lines_.push_back({static_cast<uint16_t>(chunks_.size() - 1), static_cast<uint16_t>(chunk.size()),
|
||||
static_cast<uint16_t>(len)});
|
||||
chunk.append(partial_, 0, len);
|
||||
partial_.clear();
|
||||
}
|
||||
|
||||
std::string TextBuffer::line(size_t i) const {
|
||||
if (i >= lines_.size()) return "";
|
||||
const Ref& r = lines_[i];
|
||||
return chunks_[r.chunk].substr(r.offset, r.length);
|
||||
}
|
||||
|
||||
void TextBuffer::clear() {
|
||||
chunks_.clear();
|
||||
lines_.clear();
|
||||
partial_.clear();
|
||||
bytes_ = 0;
|
||||
}
|
||||
|
||||
} // namespace roro::gemini
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace roro::gemini {
|
||||
|
||||
// A page's text, kept as lines in 4 KB chunks: no large contiguous block (with IRC connected the
|
||||
// largest free block is about 31 KB) and no copy when it grows. About 4 bytes per line on top of
|
||||
// the text itself. Bytes are fed as they arrive; lines end at LF, a CR before it is dropped.
|
||||
class TextBuffer {
|
||||
public:
|
||||
static constexpr size_t kChunk = 4096;
|
||||
|
||||
void append(const char* data, size_t len);
|
||||
void finish(); // the last line, if it has no line end
|
||||
size_t lineCount() const { return lines_.size(); }
|
||||
std::string line(size_t i) const;
|
||||
size_t bytes() const { return bytes_; }
|
||||
void clear();
|
||||
|
||||
private:
|
||||
struct Ref {
|
||||
uint16_t chunk;
|
||||
uint16_t offset;
|
||||
uint16_t length;
|
||||
};
|
||||
void endLine();
|
||||
|
||||
std::vector<std::string> chunks_;
|
||||
std::vector<Ref> lines_;
|
||||
std::string partial_; // the line being received
|
||||
size_t bytes_ = 0;
|
||||
};
|
||||
|
||||
} // namespace roro::gemini
|
||||
Reference in New Issue
Block a user