import { Buffer } from 'node:buffer'; export const HEADER_V6 = Buffer.from('reMarkable .lines file, version=6 ', 'ascii'); export class EOFError extends Error { constructor (msg = 'Unexpected end of data') { super(msg); this.name = 'EOFError'; } } export class UnexpectedBlockError extends Error { constructor (msg) { super(msg); this.name = 'UnexpectedBlockError'; } } export const TagType = Object.freeze({ ID: 0xF, Length4: 0xC, Byte8: 0x8, Byte4: 0x4, Byte1: 0x1 }); const VALID_TAG_TYPES = new Set(Object.values(TagType)); export class CrdtId { constructor (part1, part2) { this.part1 = part1; this.part2 = part2; } toKey () { return `${this.part1}:${this.part2}`; } toString () { return `CrdtId(${this.part1}, ${this.part2})`; } } export class LwwValue { constructor (timestamp, value) { this.timestamp = timestamp; this.value = value; } } export class DataReader { constructor (buf) { this._buf = Buffer.isBuffer(buf) ? buf : Buffer.from(buf); this._pos = 0; } tell () { return this._pos; } seek (pos) { this._pos = pos; } get length () { return this._buf.length; } readHeader () { const header = this.readBytes(HEADER_V6.length); if (!header.equals(HEADER_V6)) throw new Error(`Wrong header: ${header.toString('hex')}`); } readBytes (n) { if (this._pos + n > this._buf.length) throw new EOFError(); const result = this._buf.slice(this._pos, this._pos + n); this._pos += n; return result; } checkTag (expectedIndex, expectedType) { const pos = this._pos; try { const [index, tagType] = this._readTagValues(); return index === expectedIndex && tagType === expectedType; } catch { return false; } finally { this._pos = pos; } } readTag (expectedIndex, expectedType) { const pos = this._pos; let index, tagType; try { [index, tagType] = this._readTagValues(); } catch (e) { this._pos = pos; throw e; } if (index !== expectedIndex) { this._pos = pos; throw new UnexpectedBlockError(`Expected index ${expectedIndex}, got ${index}, at position ${this._pos}`); } if (tagType !== expectedType) { this._pos = pos; throw new UnexpectedBlockError( `Expected tag type ${expectedType} (0x${expectedType.toString(16).toUpperCase()}), got 0x${tagType.toString(16).toUpperCase()} at position ${this._pos}` ); } return [index, tagType]; } _readTagValues () { const x = this.readVarint(); const index = Math.floor(x / 16); const tagTypeVal = x % 16; if (!VALID_TAG_TYPES.has(tagTypeVal)) throw new Error(`Bad tag type 0x${tagTypeVal.toString(16).toUpperCase()} at position ${this._pos}`); return [index, tagTypeVal]; } readBool () { return this.readBytes(1)[0] !== 0; } readUint8 () { return this.readBytes(1)[0]; } readUint16 () { return this.readBytes(2).readUInt16LE(0); } readUint32 () { return this.readBytes(4).readUInt32LE(0); } readFloat32 () { return this.readBytes(4).readFloatLE(0); } readFloat64 () { return this.readBytes(8).readDoubleLE(0); } readVarint () { let result = 0, multiplier = 1; while (true) { const byte = this.readBytes(1)[0]; result += (byte & 0x7F) * multiplier; multiplier *= 128; if (!(byte & 0x80)) break; } return result; } readCrdtId () { return new CrdtId(this.readUint8(), this.readVarint()); } } export class DataWriter { constructor () { this._chunks = []; this._pos = 0; } tell () { return this._pos; } writeHeader () { this.writeBytes(HEADER_V6); } getBuffer () { return Buffer.concat(this._chunks); } writeBytes (buf) { if (!Buffer.isBuffer(buf)) buf = Buffer.from(buf); this._chunks.push(buf); this._pos += buf.length; } writeTag (index, tagType) { this.writeVarint(index * 16 + tagType); } writeBool (v) { const b = Buffer.allocUnsafe(1); b[0] = v ? 1 : 0; this.writeBytes(b); } writeUint8 (v) { const b = Buffer.allocUnsafe(1); b[0] = v & 0xFF; this.writeBytes(b); } writeUint16 (v) { const b = Buffer.allocUnsafe(2); b.writeUInt16LE(v, 0); this.writeBytes(b); } writeUint32 (v) { const b = Buffer.allocUnsafe(4); b.writeUInt32LE(v >>> 0, 0); this.writeBytes(b); } writeFloat32 (v) { const b = Buffer.allocUnsafe(4); b.writeFloatLE(v, 0); this.writeBytes(b); } writeFloat64 (v) { const b = Buffer.allocUnsafe(8); b.writeDoubleLE(v, 0); this.writeBytes(b); } writeVarint (value) { if (value < 0) throw new Error('value is negative'); const bytes = []; while (true) { const byte = value % 128; value = Math.floor(value / 128); bytes.push(value > 0 ? (byte | 0x80) : byte); if (!value) break; } this.writeBytes(Buffer.from(bytes)); } writeCrdtId (value) { this.writeUint8(value.part1); this.writeVarint(value.part2); } } export function uuidFromBytesLE (buf) { const b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf); return [ b[3].toString(16).padStart(2, '0'), b[2].toString(16).padStart(2, '0'), b[1].toString(16).padStart(2, '0'), b[0].toString(16).padStart(2, '0'), '-', b[5].toString(16).padStart(2, '0'), b[4].toString(16).padStart(2, '0'), '-', b[7].toString(16).padStart(2, '0'), b[6].toString(16).padStart(2, '0'), '-', b[8].toString(16).padStart(2, '0'), b[9].toString(16).padStart(2, '0'), '-', Array.from(b.slice(10, 16)).map(x => x.toString(16).padStart(2, '0')).join(''), ].join(''); } export function uuidToBytesLE (uuid) { const hex = uuid.replace(/-/g, ''); const bytes = Buffer.from(hex, 'hex'); const r = Buffer.allocUnsafe(16); r[0] = bytes[3]; r[1] = bytes[2]; r[2] = bytes[1]; r[3] = bytes[0]; r[4] = bytes[5]; r[5] = bytes[4]; r[6] = bytes[7]; r[7] = bytes[6]; bytes.copy(r, 8, 8, 16); return r; } export function compareVersions (a, b) { const ap = String(a).split('.').map(Number); const bp = String(b).split('.').map(Number); const len = Math.max(ap.length, bp.length); for (let i = 0; i < len; i++) { const av = ap[i] || 0, bv = bp[i] || 0; if (av < bv) return -1; if (av > bv) return 1; } return 0; } export const versionGTE = (a, b) => compareVersions(a, b) >= 0; export const versionGT = (a, b) => compareVersions(a, b) > 0;