#include "note_document.h" #include #include 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 `.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((v >> (8 * i)) & 0xFF); } uint32_t get32(const uint8_t* p) { return p[0] | (p[1] << 8) | (p[2] << 16) | (static_cast(p[3]) << 24); } const uint8_t* bytes(const std::string& s) { return reinterpret_cast(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().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(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(static_cast(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(len - got, p.len - skip); size_t r = card_.read(fileOf(p), p.at + skip, into + got, n); got += r; at += static_cast(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(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(std::count(t.begin(), t.begin() + static_cast(c), '\n')); else if (!t.empty()) c += droppedAtLoad_ * c / t.size(); // a file of both kinds of line: near enough } return before_ + static_cast(c); } bool NoteDocument::headerFor(std::string& header) { uint32_t fileSize = 0; if (path_.empty() || !card_.size(path_, fileSize)) return false; std::vector buf(kCheck); size_t n = std::min(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(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(header.size()); return true; } bool NoteDocument::putBack(std::string& why) { if (!windowLoaded_) return true; if (text_.revision() == loadedRevision_) { pieces_.insert(pieces_.begin() + static_cast(win_), loaded_.begin(), loaded_.end()); } else { Piece p{1, 0, static_cast(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(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(c)) { s = static_cast(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(sizeof buf, limit - at)); if (!n) break; for (size_t i = 0; i < n && !found; i++) if (buf[i] == '\n') { s = at + static_cast(i) + 1; found = true; } at += static_cast(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(e, s + NoteText::kMaxBytes); } size_t i0 = splitAt(s), i1 = splitAt(e); loaded_.assign(pieces_.begin() + static_cast(i0), pieces_.begin() + static_cast(i1)); pieces_.erase(pieces_.begin() + static_cast(i0), pieces_.begin() + static_cast(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(&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(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(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(before_) + static_cast(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(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 all(pieces_.begin(), pieces_.begin() + static_cast(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(win_), pieces_.end()); std::string rec(kSnap, 4); put32(rec, cursor); put32(rec, static_cast(all.size())); for (const Piece& p : all) { rec += static_cast(p.src); put32(rec, p.at); put32(rec, p.len); } put32(rec, crc32(0, bytes(rec), rec.size())); put32(rec, static_cast(rec.size()) + 8); rec.append(kSnapEnd, 4); wrote = card_.append(sidePath_, bytes(rec), rec.size()); if (wrote) sideSize_ += static_cast(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(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().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(len); if (rw_.stage == 0) rw_.wroteBefore += static_cast(len); if (rw_.stage == 2) rw_.wroteAfter += static_cast(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(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(std::min(p.len - rw_.offset, budget), static_cast(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(static_cast(rw_.done) * 100 / std::max(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(kDone), kDoneLen)) return failed("the card refused a write"); sideSize_ += kDoneLen; card_.done(); } rw_.active = false; std::vector().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(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(&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 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(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 buf(1024 + 3); for (uint32_t end = sideSize_; end > kHeaderLen && !found;) { uint32_t a = end > 1024 + kHeaderLen ? end - 1024 : static_cast(kHeaderLen); size_t n = card_.read(sidePath_, a, buf.data(), std::min(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(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(p.at) + p.len > fileSize) return pieces_.clear(), Resume::Mismatch; merge(); return Resume::Ok; } } // namespace roro::notes