Public Access
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
623 lines
23 KiB
C++
623 lines
23 KiB
C++
#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
|