Public Access
Notes: edit a text file of any size (#47)
The editor held the whole note in memory and stopped at 16 KB. It now keeps a window of the file around the cursor, and the rest on the card as a list of pieces (notes::NoteDocument). Memory with a note open is what it was. Up to 64 KB a save rewrites the file, as before. Above, the five-second save appends what changed to <note>.edit, and the file is rewritten on leaving the note, with a progress bar. After a power cut, opening the note picks the edit up where it was saved; a rewrite cut short is finished or dropped, never half applied. Also: Ctrl with Fn+Up/Down go to the start and end of the note; the consoles' `key` command takes ctrl-, alt- and shift-; the Storage App's `e` no longer refuses a big file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -0,0 +1,622 @@
|
||||
#include "note_document.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace roro::notes {
|
||||
|
||||
namespace {
|
||||
// The side file: this line, the note's size and two checksums of it (its first and last
|
||||
// kilobyte), then, in any order, text that left a window and snapshots of the list of pieces.
|
||||
// snapshot: "RSNP" cursor count { src at len }... crc32 length "PNSR" (numbers: 32 bits, low byte first)
|
||||
// The newest snapshot that checks out is the note as it was last saved. kDone at the very end:
|
||||
// the rewrite this file was for is complete in `<note>.tmp`, and only has to take the note's place.
|
||||
const char kMagic[] = "roro9stack note edits 1\n";
|
||||
constexpr size_t kMagicLen = sizeof(kMagic) - 1;
|
||||
constexpr size_t kHeaderLen = kMagicLen + 12;
|
||||
const char kSnap[] = "RSNP", kSnapEnd[] = "PNSR", kDone[] = "RDONE1\n\n";
|
||||
constexpr size_t kDoneLen = 8;
|
||||
constexpr size_t kCheck = 1024; // of each end of the note, in the header
|
||||
constexpr uint32_t kStepBytes = 64 * 1024; // a rewrite's step
|
||||
constexpr size_t kBlock = 4096;
|
||||
constexpr uint32_t kSeekNewline = 1024;
|
||||
constexpr size_t kMaxSnapshot = 12 + 9 * 4096 + 12;
|
||||
|
||||
bool continuation(int c) { return (c & 0xC0) == 0x80; }
|
||||
|
||||
uint32_t crc32(uint32_t crc, const uint8_t* data, size_t len) {
|
||||
crc = ~crc;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
crc ^= data[i];
|
||||
for (int k = 0; k < 8; k++) crc = (crc >> 1) ^ (0xEDB88320u & (0u - (crc & 1)));
|
||||
}
|
||||
return ~crc;
|
||||
}
|
||||
|
||||
void put32(std::string& s, uint32_t v) {
|
||||
for (int i = 0; i < 4; i++) s += static_cast<char>((v >> (8 * i)) & 0xFF);
|
||||
}
|
||||
|
||||
uint32_t get32(const uint8_t* p) { return p[0] | (p[1] << 8) | (p[2] << 16) | (static_cast<uint32_t>(p[3]) << 24); }
|
||||
|
||||
const uint8_t* bytes(const std::string& s) { return reinterpret_cast<const uint8_t*>(s.data()); }
|
||||
} // namespace
|
||||
|
||||
NoteDocument::NoteDocument(NoteCard& card, int cols, int rows) : card_(card), text_(cols, rows) { openNew(); }
|
||||
|
||||
void NoteDocument::reset() {
|
||||
path_.clear();
|
||||
sidePath_.clear();
|
||||
pieces_.clear();
|
||||
loaded_.clear();
|
||||
win_ = 0;
|
||||
windowLoaded_ = false;
|
||||
before_ = after_ = 0;
|
||||
droppedAtLoad_ = newlinesAtLoad_ = 0;
|
||||
flushedSinceSave_ = sidePending_ = false;
|
||||
sideSize_ = 0;
|
||||
windowSaved_.valid = false;
|
||||
rw_.active = false;
|
||||
std::vector<uint8_t>().swap(rw_.block);
|
||||
}
|
||||
|
||||
void NoteDocument::openNew() {
|
||||
reset();
|
||||
text_.buffer().clear();
|
||||
text_.refilled(0, 0);
|
||||
windowLoaded_ = true;
|
||||
loadedRevision_ = savedRevision_ = text_.revision();
|
||||
}
|
||||
|
||||
std::string NoteDocument::open(const std::string& path, std::string* told) {
|
||||
openNew();
|
||||
path_ = path;
|
||||
sidePath_ = side();
|
||||
uint32_t sideSize = 0, fileSize = 0, other = 0;
|
||||
bool hasSide = card_.size(sidePath_, sideSize);
|
||||
if (hasSide && sideIsDone(sideSize)) { // a rewrite was cut after its last write: finish it
|
||||
if (card_.size(tmp(), other)) {
|
||||
if (card_.size(path_, fileSize)) card_.remove(path_);
|
||||
card_.rename(tmp(), path_);
|
||||
}
|
||||
card_.remove(sidePath_);
|
||||
hasSide = false;
|
||||
}
|
||||
if (!card_.size(path_, fileSize)) {
|
||||
card_.done();
|
||||
openNew();
|
||||
return "The card refused to open it";
|
||||
}
|
||||
if (fileSize > NoteText::kMaxBytes && card_.freeBytes() < static_cast<uint64_t>(fileSize) + 16 * 1024) {
|
||||
card_.done();
|
||||
openNew();
|
||||
return "Not enough room on the card: saving it needs a second copy";
|
||||
}
|
||||
uint32_t cursor = 0;
|
||||
bool resumed = false;
|
||||
if (hasSide) {
|
||||
if (card_.size(tmp(), other)) card_.remove(tmp()); // a rewrite that didn't get that far
|
||||
sideSize_ = sideSize;
|
||||
Resume r = resume(fileSize, cursor);
|
||||
if (r == Resume::Ok) {
|
||||
resumed = sidePending_ = true;
|
||||
if (told) *told = "Your unsaved changes are back";
|
||||
} else {
|
||||
sideSize_ = 0;
|
||||
pieces_.clear();
|
||||
if (r == Resume::Mismatch) { // typed text is never thrown away without a word (Q227)
|
||||
std::string lost = sidePath_ + ".lost";
|
||||
card_.remove(lost);
|
||||
card_.rename(sidePath_, lost);
|
||||
if (told) *told = "The file changed: unsaved edits kept as .edit.lost";
|
||||
} else {
|
||||
card_.remove(sidePath_);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!resumed && fileSize) pieces_.push_back({0, 0, fileSize});
|
||||
windowLoaded_ = false;
|
||||
bool ok = load(cursor, cursor ? 1000 : 0, -1); // an edit picked up: its last lines above the cursor
|
||||
card_.done();
|
||||
if (!ok) {
|
||||
openNew();
|
||||
return "The card refused to read it";
|
||||
}
|
||||
savedRevision_ = text_.revision();
|
||||
return "";
|
||||
}
|
||||
|
||||
uint32_t NoteDocument::piecesBytes() const {
|
||||
uint32_t n = 0;
|
||||
for (const Piece& p : pieces_) n += p.len;
|
||||
return n;
|
||||
}
|
||||
|
||||
int NoteDocument::percent() const {
|
||||
uint32_t all = size();
|
||||
return all ? static_cast<int>(static_cast<uint64_t>(before_ + text_.top()) * 100 / all) : 0;
|
||||
}
|
||||
|
||||
size_t NoteDocument::readDoc(uint32_t at, uint8_t* into, size_t len) {
|
||||
size_t got = 0;
|
||||
uint32_t pos = 0;
|
||||
for (const Piece& p : pieces_) {
|
||||
if (got == len) break;
|
||||
if (at < pos + p.len) {
|
||||
uint32_t skip = at - pos;
|
||||
size_t n = std::min<size_t>(len - got, p.len - skip);
|
||||
size_t r = card_.read(fileOf(p), p.at + skip, into + got, n);
|
||||
got += r;
|
||||
at += static_cast<uint32_t>(r);
|
||||
if (r != n) break;
|
||||
}
|
||||
pos += p.len;
|
||||
}
|
||||
return got;
|
||||
}
|
||||
|
||||
int NoteDocument::byteAt(uint32_t at) {
|
||||
uint8_t b;
|
||||
return readDoc(at, &b, 1) == 1 ? b : -1;
|
||||
}
|
||||
|
||||
size_t NoteDocument::splitAt(uint32_t at) {
|
||||
uint32_t pos = 0;
|
||||
for (size_t i = 0; i < pieces_.size(); i++) {
|
||||
if (at == pos) return i;
|
||||
Piece& p = pieces_[i];
|
||||
if (at < pos + p.len) {
|
||||
uint32_t first = at - pos;
|
||||
Piece rest{p.src, p.at + first, p.len - first};
|
||||
p.len = first;
|
||||
pieces_.insert(pieces_.begin() + static_cast<long>(i) + 1, rest);
|
||||
return i + 1;
|
||||
}
|
||||
pos += p.len;
|
||||
}
|
||||
return pieces_.size();
|
||||
}
|
||||
|
||||
void NoteDocument::merge() {
|
||||
size_t kept = 0;
|
||||
for (size_t i = 0; i < pieces_.size(); i++) {
|
||||
const Piece p = pieces_[i];
|
||||
if (!p.len) continue;
|
||||
if (kept && pieces_[kept - 1].src == p.src && pieces_[kept - 1].at + pieces_[kept - 1].len == p.at) pieces_[kept - 1].len += p.len;
|
||||
else pieces_[kept++] = p;
|
||||
}
|
||||
pieces_.resize(kept);
|
||||
}
|
||||
|
||||
// The window dropped the CRs of the file's CRLFs when it was read. If it's put back untouched,
|
||||
// the cursor is further along in the file than in the window: by one for each line before it.
|
||||
uint32_t NoteDocument::noteCursor() const {
|
||||
size_t c = text_.cursor();
|
||||
if (windowLoaded_ && droppedAtLoad_ && text_.revision() == loadedRevision_) {
|
||||
const std::string& t = text_.text();
|
||||
if (droppedAtLoad_ == newlinesAtLoad_) c += static_cast<size_t>(std::count(t.begin(), t.begin() + static_cast<long>(c), '\n'));
|
||||
else if (!t.empty()) c += droppedAtLoad_ * c / t.size(); // a file of both kinds of line: near enough
|
||||
}
|
||||
return before_ + static_cast<uint32_t>(c);
|
||||
}
|
||||
|
||||
bool NoteDocument::headerFor(std::string& header) {
|
||||
uint32_t fileSize = 0;
|
||||
if (path_.empty() || !card_.size(path_, fileSize)) return false;
|
||||
std::vector<uint8_t> buf(kCheck);
|
||||
size_t n = std::min<size_t>(kCheck, fileSize);
|
||||
if (card_.read(path_, 0, buf.data(), n) != n) return false;
|
||||
uint32_t head = crc32(0, buf.data(), n);
|
||||
if (card_.read(path_, fileSize - static_cast<uint32_t>(n), buf.data(), n) != n) return false;
|
||||
uint32_t tail = crc32(0, buf.data(), n);
|
||||
header.assign(kMagic, kMagicLen);
|
||||
put32(header, fileSize);
|
||||
put32(header, head);
|
||||
put32(header, tail);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NoteDocument::ensureSide(std::string& why) {
|
||||
if (sideSize_) return true;
|
||||
std::string header;
|
||||
if (!headerFor(header)) {
|
||||
why = path_.empty() ? "the note has no file yet" : "the card refused to read the note";
|
||||
return false;
|
||||
}
|
||||
if (!card_.create(sidePath_) || !card_.append(sidePath_, bytes(header), header.size())) {
|
||||
card_.remove(sidePath_);
|
||||
why = "the card refused a write";
|
||||
return false;
|
||||
}
|
||||
sideSize_ = static_cast<uint32_t>(header.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NoteDocument::putBack(std::string& why) {
|
||||
if (!windowLoaded_) return true;
|
||||
if (text_.revision() == loadedRevision_) {
|
||||
pieces_.insert(pieces_.begin() + static_cast<long>(win_), loaded_.begin(), loaded_.end());
|
||||
} else {
|
||||
Piece p{1, 0, static_cast<uint32_t>(text_.size())};
|
||||
if (windowSaved_.valid && windowSaved_.revision == text_.revision()) {
|
||||
p.at = windowSaved_.at;
|
||||
} else if (p.len) {
|
||||
if (!ensureSide(why)) return false;
|
||||
if (!card_.append(sidePath_, bytes(text_.text()), p.len)) {
|
||||
card_.done();
|
||||
if (!card_.size(sidePath_, sideSize_)) sideSize_ = 0;
|
||||
why = "the card refused a write";
|
||||
return false;
|
||||
}
|
||||
p.at = sideSize_;
|
||||
sideSize_ += p.len;
|
||||
}
|
||||
if (p.len) pieces_.insert(pieces_.begin() + static_cast<long>(win_), p);
|
||||
flushedSinceSave_ = sidePending_ = true;
|
||||
}
|
||||
windowLoaded_ = false;
|
||||
loaded_.clear();
|
||||
windowSaved_.valid = false;
|
||||
before_ = after_ = 0;
|
||||
merge();
|
||||
return true;
|
||||
}
|
||||
|
||||
// The window's start is where a line starts on screen whenever that can be known: after a
|
||||
// newline, or where the window before had a line start. Otherwise the same text could wrap
|
||||
// differently from one window to the next.
|
||||
bool NoteDocument::load(uint32_t cursor, int row, int64_t startHint) {
|
||||
uint32_t total = piecesBytes();
|
||||
cursor = std::min(cursor, total);
|
||||
uint32_t s = 0, e = total;
|
||||
if (total > NoteText::kMaxBytes - kEdge) {
|
||||
uint32_t c = cursor > kHalf ? cursor - kHalf : 0;
|
||||
if (c == 0) {
|
||||
s = 0;
|
||||
} else if (startHint >= 0 && startHint <= static_cast<int64_t>(c)) {
|
||||
s = static_cast<uint32_t>(startHint);
|
||||
} else {
|
||||
uint8_t buf[128];
|
||||
uint32_t at = c, limit = std::min(c + kSeekNewline, cursor);
|
||||
bool found = false;
|
||||
while (at < limit && !found) {
|
||||
size_t n = readDoc(at, buf, std::min<size_t>(sizeof buf, limit - at));
|
||||
if (!n) break;
|
||||
for (size_t i = 0; i < n && !found; i++)
|
||||
if (buf[i] == '\n') {
|
||||
s = at + static_cast<uint32_t>(i) + 1;
|
||||
found = true;
|
||||
}
|
||||
at += static_cast<uint32_t>(n);
|
||||
}
|
||||
if (!found) {
|
||||
s = c;
|
||||
for (int k = 0; k < 3 && s < cursor && continuation(byteAt(s)); k++) s++;
|
||||
}
|
||||
}
|
||||
e = std::min(total, cursor + kHalf);
|
||||
for (int k = 0; k < 3 && e < total && continuation(byteAt(e)); k++) e++;
|
||||
if (e < total && e > 0 && byteAt(e) == '\n' && byteAt(e - 1) == '\r') e++;
|
||||
e = std::min<uint32_t>(e, s + NoteText::kMaxBytes);
|
||||
}
|
||||
size_t i0 = splitAt(s), i1 = splitAt(e);
|
||||
loaded_.assign(pieces_.begin() + static_cast<long>(i0), pieces_.begin() + static_cast<long>(i1));
|
||||
pieces_.erase(pieces_.begin() + static_cast<long>(i0), pieces_.begin() + static_cast<long>(i1));
|
||||
win_ = i0;
|
||||
before_ = s;
|
||||
after_ = total - e;
|
||||
std::string& b = text_.buffer();
|
||||
b.resize(e - s);
|
||||
size_t got = 0;
|
||||
bool ok = true;
|
||||
for (const Piece& p : loaded_) {
|
||||
size_t n = card_.read(fileOf(p), p.at, reinterpret_cast<uint8_t*>(&b[got]), p.len);
|
||||
got += n;
|
||||
if (n != p.len) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!ok) { // the note is whole in its pieces: stand on an empty window where the cursor was
|
||||
pieces_.insert(pieces_.begin() + static_cast<long>(i0), loaded_.begin(), loaded_.end());
|
||||
loaded_.clear();
|
||||
win_ = splitAt(cursor);
|
||||
before_ = cursor;
|
||||
after_ = total - cursor;
|
||||
s = cursor;
|
||||
b.clear();
|
||||
}
|
||||
droppedAtLoad_ = text_.refilled(cursor - s, row);
|
||||
newlinesAtLoad_ = static_cast<size_t>(std::count(b.begin(), b.end(), '\n'));
|
||||
loadedRevision_ = text_.revision();
|
||||
windowLoaded_ = true;
|
||||
windowSaved_.valid = false;
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool NoteDocument::wantsMove() const {
|
||||
if (!windowLoaded_) return true;
|
||||
size_t n = text_.size(), c = text_.cursor();
|
||||
if (n + kSpare >= NoteText::kMaxBytes) return true;
|
||||
if (before_ && c < kEdge) return true;
|
||||
return after_ && n - c < kEdge;
|
||||
}
|
||||
|
||||
bool NoteDocument::move(std::string& why) {
|
||||
uint32_t cursor = noteCursor();
|
||||
int row = text_.cursorRow();
|
||||
int64_t hint = -1;
|
||||
if (windowLoaded_ && !droppedAtLoad_ && cursor > kHalf) {
|
||||
uint32_t c = cursor - kHalf;
|
||||
if (c >= before_ && c < before_ + text_.size()) hint = static_cast<int64_t>(before_) + static_cast<int64_t>(text_.startOfLine(c - before_));
|
||||
}
|
||||
if (!putBack(why)) return false;
|
||||
bool ok = load(cursor, row, hint);
|
||||
card_.done();
|
||||
if (!ok) why = "the card refused to read";
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool NoteDocument::jump(uint32_t to, std::string& why) {
|
||||
to = std::min(to, size());
|
||||
if (windowLoaded_ && to == 0 && !before_) return text_.toStart(), true;
|
||||
if (windowLoaded_ && to == size() && !after_) return text_.toEnd(), true;
|
||||
if (!putBack(why)) return false;
|
||||
bool ok = load(to, to ? 1000 : 0, -1);
|
||||
card_.done();
|
||||
if (!ok) why = "the card refused to read";
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool NoteDocument::wantsRewrite() const { return size() <= kWholeLimit || sideSize_ > kSideLimit || pieces_.size() > kManyPieces; }
|
||||
|
||||
bool NoteDocument::journal(std::string& why) {
|
||||
if (path_.empty()) {
|
||||
why = "the note has no file yet";
|
||||
return false;
|
||||
}
|
||||
bool modified = text_.revision() != loadedRevision_;
|
||||
Piece w{1, 0, static_cast<uint32_t>(text_.size())};
|
||||
uint32_t cursor = noteCursor();
|
||||
if (!ensureSide(why)) return false;
|
||||
bool wrote = true;
|
||||
if (modified && w.len) {
|
||||
if (windowSaved_.valid && windowSaved_.revision == text_.revision()) {
|
||||
w.at = windowSaved_.at;
|
||||
} else if ((wrote = card_.append(sidePath_, bytes(text_.text()), w.len))) {
|
||||
w.at = sideSize_;
|
||||
sideSize_ += w.len;
|
||||
windowSaved_.valid = true;
|
||||
windowSaved_.at = w.at;
|
||||
windowSaved_.len = w.len;
|
||||
windowSaved_.revision = text_.revision();
|
||||
}
|
||||
}
|
||||
if (wrote) {
|
||||
std::vector<Piece> all(pieces_.begin(), pieces_.begin() + static_cast<long>(win_));
|
||||
if (!modified) all.insert(all.end(), loaded_.begin(), loaded_.end());
|
||||
else if (w.len) all.push_back(w);
|
||||
all.insert(all.end(), pieces_.begin() + static_cast<long>(win_), pieces_.end());
|
||||
std::string rec(kSnap, 4);
|
||||
put32(rec, cursor);
|
||||
put32(rec, static_cast<uint32_t>(all.size()));
|
||||
for (const Piece& p : all) {
|
||||
rec += static_cast<char>(p.src);
|
||||
put32(rec, p.at);
|
||||
put32(rec, p.len);
|
||||
}
|
||||
put32(rec, crc32(0, bytes(rec), rec.size()));
|
||||
put32(rec, static_cast<uint32_t>(rec.size()) + 8);
|
||||
rec.append(kSnapEnd, 4);
|
||||
wrote = card_.append(sidePath_, bytes(rec), rec.size());
|
||||
if (wrote) sideSize_ += static_cast<uint32_t>(rec.size());
|
||||
}
|
||||
card_.done();
|
||||
if (!wrote) {
|
||||
windowSaved_.valid = false;
|
||||
if (!card_.size(sidePath_, sideSize_)) sideSize_ = 0;
|
||||
why = "the card refused a write";
|
||||
return false;
|
||||
}
|
||||
savedRevision_ = text_.revision();
|
||||
flushedSinceSave_ = false;
|
||||
sidePending_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NoteDocument::rewriteStart(std::string& why) {
|
||||
if (path_.empty()) {
|
||||
why = "the note has no file yet";
|
||||
return false;
|
||||
}
|
||||
if (card_.freeBytes() < static_cast<uint64_t>(size()) + 16 * 1024) {
|
||||
why = "the card is full";
|
||||
return false;
|
||||
}
|
||||
if (!card_.create(tmp())) {
|
||||
why = "the card refused to open a file";
|
||||
return false;
|
||||
}
|
||||
rw_.active = true;
|
||||
rw_.stage = 0;
|
||||
rw_.index = 0;
|
||||
rw_.offset = rw_.done = rw_.wrote = rw_.wroteBefore = rw_.wroteAfter = 0;
|
||||
rw_.total = size();
|
||||
rw_.revision = text_.revision();
|
||||
rw_.carry = false;
|
||||
rw_.block.resize(kBlock);
|
||||
return true;
|
||||
}
|
||||
|
||||
int NoteDocument::rewriteStep(std::string& why) {
|
||||
if (!rw_.active) return -1;
|
||||
auto failed = [&](const char* what) {
|
||||
card_.done();
|
||||
card_.remove(tmp());
|
||||
rw_.active = false;
|
||||
std::vector<uint8_t>().swap(rw_.block);
|
||||
why = what;
|
||||
return -1;
|
||||
};
|
||||
auto out = [&](const uint8_t* data, size_t len) {
|
||||
if (!len) return true;
|
||||
if (!card_.append(tmp(), data, len)) return false;
|
||||
rw_.wrote += static_cast<uint32_t>(len);
|
||||
if (rw_.stage == 0) rw_.wroteBefore += static_cast<uint32_t>(len);
|
||||
if (rw_.stage == 2) rw_.wroteAfter += static_cast<uint32_t>(len);
|
||||
return true;
|
||||
};
|
||||
const uint8_t cr = '\r';
|
||||
uint32_t budget = kStepBytes;
|
||||
while (budget > 0 && rw_.stage < 3) {
|
||||
if (rw_.stage == 1) {
|
||||
uint32_t left = static_cast<uint32_t>(text_.size()) - rw_.offset;
|
||||
if (!left) {
|
||||
rw_.stage = 2;
|
||||
rw_.index = win_;
|
||||
rw_.offset = 0;
|
||||
continue;
|
||||
}
|
||||
uint32_t n = std::min(left, budget);
|
||||
if (!out(bytes(text_.text()) + rw_.offset, n)) return failed("the card refused a write");
|
||||
rw_.offset += n;
|
||||
rw_.done += n;
|
||||
budget -= n;
|
||||
continue;
|
||||
}
|
||||
size_t end = rw_.stage == 0 ? win_ : pieces_.size();
|
||||
bool pieceOver = rw_.index < end && rw_.offset >= pieces_[rw_.index].len;
|
||||
if (rw_.index >= end || pieceOver) {
|
||||
if (rw_.carry && !out(&cr, 1)) return failed("the card refused a write"); // a CR that ended its piece stays
|
||||
rw_.carry = false;
|
||||
rw_.offset = 0;
|
||||
if (pieceOver) rw_.index++;
|
||||
else rw_.stage++;
|
||||
continue;
|
||||
}
|
||||
const Piece& p = pieces_[rw_.index];
|
||||
uint32_t n = std::min<uint32_t>(std::min<uint32_t>(p.len - rw_.offset, budget), static_cast<uint32_t>(rw_.block.size()));
|
||||
uint8_t* b = rw_.block.data();
|
||||
if (card_.read(fileOf(p), p.at + rw_.offset, b, n) != n) return failed("the card refused to read the note");
|
||||
size_t m = n;
|
||||
if (p.src == 0) { // CRLF becomes LF (Q148, Q230), in the file's own text: what was typed has none
|
||||
if (rw_.carry && b[0] != '\n' && !out(&cr, 1)) return failed("the card refused a write");
|
||||
rw_.carry = false;
|
||||
m = 0;
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
if (b[i] == '\r') {
|
||||
if (i + 1 == n) {
|
||||
rw_.carry = true;
|
||||
continue;
|
||||
}
|
||||
if (b[i + 1] == '\n') continue;
|
||||
}
|
||||
b[m++] = b[i];
|
||||
}
|
||||
}
|
||||
if (!out(b, m)) return failed("the card refused a write");
|
||||
rw_.offset += n;
|
||||
rw_.done += n;
|
||||
budget -= n;
|
||||
}
|
||||
if (rw_.stage < 3) return std::min(99, static_cast<int>(static_cast<uint64_t>(rw_.done) * 100 / std::max<uint32_t>(1, rw_.total)));
|
||||
|
||||
// All of it is in the temporary file. From the mark in the side file on, the rewrite counts as
|
||||
// done: whatever is cut after that, opening the note finishes it.
|
||||
card_.done();
|
||||
uint32_t written = 0, old = 0;
|
||||
if (!card_.size(tmp(), written) || written != rw_.wrote) return failed("the card refused a write");
|
||||
if (sideSize_) {
|
||||
if (!card_.append(sidePath_, reinterpret_cast<const uint8_t*>(kDone), kDoneLen)) return failed("the card refused a write");
|
||||
sideSize_ += kDoneLen;
|
||||
card_.done();
|
||||
}
|
||||
rw_.active = false;
|
||||
std::vector<uint8_t>().swap(rw_.block);
|
||||
// FAT can't rename onto a file. Between these two lines only the temporary file exists: the
|
||||
// Notes list puts such a file back under its name.
|
||||
if ((card_.size(path_, old) && !card_.remove(path_)) || !card_.rename(tmp(), path_)) {
|
||||
card_.done();
|
||||
why = "the card refused to replace the note";
|
||||
return -1;
|
||||
}
|
||||
if (sideSize_) card_.remove(sidePath_);
|
||||
card_.done();
|
||||
sideSize_ = 0;
|
||||
uint32_t window = static_cast<uint32_t>(text_.size());
|
||||
pieces_.clear();
|
||||
loaded_.clear();
|
||||
if (rw_.wroteBefore) pieces_.push_back({0, 0, rw_.wroteBefore});
|
||||
win_ = pieces_.size();
|
||||
if (rw_.wroteAfter) pieces_.push_back({0, rw_.wroteBefore + window, rw_.wroteAfter});
|
||||
if (window) loaded_.push_back({0, rw_.wroteBefore, window});
|
||||
before_ = rw_.wroteBefore;
|
||||
after_ = rw_.wroteAfter;
|
||||
loadedRevision_ = savedRevision_ = rw_.revision;
|
||||
droppedAtLoad_ = 0;
|
||||
windowLoaded_ = true;
|
||||
flushedSinceSave_ = sidePending_ = false;
|
||||
windowSaved_.valid = false;
|
||||
return 100;
|
||||
}
|
||||
|
||||
bool NoteDocument::sideIsDone(uint32_t sideSize) {
|
||||
uint8_t tail[kDoneLen];
|
||||
return sideSize >= kHeaderLen + kDoneLen && card_.read(sidePath_, sideSize - kDoneLen, tail, kDoneLen) == kDoneLen &&
|
||||
std::memcmp(tail, kDone, kDoneLen) == 0;
|
||||
}
|
||||
|
||||
NoteDocument::Resume NoteDocument::resume(uint32_t fileSize, uint32_t& cursor) {
|
||||
uint8_t header[kHeaderLen];
|
||||
if (sideSize_ < kHeaderLen || card_.read(sidePath_, 0, header, kHeaderLen) != kHeaderLen || std::memcmp(header, kMagic, kMagicLen) != 0)
|
||||
return Resume::Nothing;
|
||||
|
||||
// The newest snapshot that checks out, looking back from the end: after it there may be text
|
||||
// that left a window, or a write the power cut short.
|
||||
auto snapshotEndingAt = [&](uint32_t end) {
|
||||
uint8_t lenBytes[4];
|
||||
if (end < kHeaderLen + 24 || card_.read(sidePath_, end - 8, lenBytes, 4) != 4) return false;
|
||||
uint32_t len = get32(lenBytes);
|
||||
if (len < 24 || len > kMaxSnapshot || len > end - kHeaderLen || (len - 24) % 9) return false;
|
||||
std::string rec(len, '\0');
|
||||
if (card_.read(sidePath_, end - len, reinterpret_cast<uint8_t*>(&rec[0]), len) != len) return false;
|
||||
const uint8_t* r = bytes(rec);
|
||||
if (std::memcmp(r, kSnap, 4) != 0 || get32(r + len - 12) != crc32(0, r, len - 12)) return false;
|
||||
uint32_t count = get32(r + 8);
|
||||
if (count != (len - 24) / 9) return false;
|
||||
std::vector<Piece> list;
|
||||
list.reserve(count);
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
const uint8_t* q = r + 12 + 9 * i;
|
||||
Piece p{q[0], get32(q + 1), get32(q + 5)};
|
||||
uint64_t stop = static_cast<uint64_t>(p.at) + p.len;
|
||||
if (p.src > 1 || !p.len) return false;
|
||||
if (p.src == 1 && (p.at < kHeaderLen || stop > end - len)) return false;
|
||||
list.push_back(p);
|
||||
}
|
||||
pieces_.swap(list);
|
||||
cursor = get32(r + 4);
|
||||
return true;
|
||||
};
|
||||
bool found = false;
|
||||
std::vector<uint8_t> buf(1024 + 3);
|
||||
for (uint32_t end = sideSize_; end > kHeaderLen && !found;) {
|
||||
uint32_t a = end > 1024 + kHeaderLen ? end - 1024 : static_cast<uint32_t>(kHeaderLen);
|
||||
size_t n = card_.read(sidePath_, a, buf.data(), std::min<size_t>(buf.size(), sideSize_ - a));
|
||||
for (size_t i = n >= 4 ? n - 4 + 1 : 0; i-- > 0 && !found;)
|
||||
if (std::memcmp(buf.data() + i, kSnapEnd, 4) == 0) found = snapshotEndingAt(a + static_cast<uint32_t>(i) + 4);
|
||||
end = a;
|
||||
}
|
||||
if (!found) return Resume::Nothing;
|
||||
std::string expect;
|
||||
if (!headerFor(expect) || std::memcmp(header, expect.data(), kHeaderLen) != 0) {
|
||||
pieces_.clear();
|
||||
return Resume::Mismatch;
|
||||
}
|
||||
for (const Piece& p : pieces_)
|
||||
if (p.src == 0 && static_cast<uint64_t>(p.at) + p.len > fileSize) return pieces_.clear(), Resume::Mismatch;
|
||||
merge();
|
||||
return Resume::Ok;
|
||||
}
|
||||
|
||||
} // namespace roro::notes
|
||||
Reference in New Issue
Block a user