From 6e0dc24ecae47ba3175ce1574a86c6724fbc98aa Mon Sep 17 00:00:00 2001 From: Girish Ramakrishnan Date: Sun, 15 Mar 2026 09:20:38 +0530 Subject: [PATCH] rsync: escape U+2028/U+2029 JSON strings can contain unescaped U+2028 LINE SEPARATOR and U+2029 PARAGRAPH SEPARATOR characters while ECMAScript strings cannot. ES2019 now allows those unescaped. The integrity code assumes that each JSON is a single line but that assumption does not hold true when these characters are there in the string. Fix is to escape them. --- src/syncer.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/syncer.js b/src/syncer.js index 51932e25b..46bdd8883 100644 --- a/src/syncer.js +++ b/src/syncer.js @@ -10,6 +10,11 @@ import util from 'node:util'; const { log } = logger('syncer'); +// JSON.stringify does not escape U+2028/U+2029 and readline treats them as line terminators +// https://github.com/tc39/proposal-json-superset +function jsonLineStringify(obj) { + return JSON.stringify(obj).replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029'); +} function readCache(cacheFile) { assert.strictEqual(typeof cacheFile, 'string'); @@ -111,7 +116,7 @@ async function sync(dataLayout, cacheFile) { if (!entryStat.isDirectory() && !entryStat.isFile()) continue; // ignore non-files and dirs if (entryStat.isSymbolicLink()) continue; - safe.fs.appendFileSync(newCacheFd, JSON.stringify({ path: entryPath, stat: { mtime: entryStat.mtime.getTime(), size: entryStat.size, inode: entryStat.inode, mode: entryStat.mode } }) + '\n'); + safe.fs.appendFileSync(newCacheFd, jsonLineStringify({ path: entryPath, stat: { mtime: entryStat.mtime.getTime(), size: entryStat.size, inode: entryStat.inode, mode: entryStat.mode } }) + '\n'); if (curCacheIndex !== cache.length && cache[curCacheIndex].path < entryPath) { // files disappeared. first advance cache as needed advanceCache(entryPath); @@ -185,7 +190,7 @@ async function finalize(integrityMap, cacheFile) { cacheEntry.integrity = integrityMap.get(cacheEntry.path); // { size, sha256 } if (typeof cacheEntry.integrity === 'undefined') throw new BoxError(BoxError.INTERNAL_ERROR, `No integrity information for ${cacheEntry.path}`); } - safe.fs.appendFileSync(tempCacheFd, JSON.stringify(cacheEntry) + '\n'); + safe.fs.appendFileSync(tempCacheFd, jsonLineStringify(cacheEntry) + '\n'); } safe.fs.closeSync(tempCacheFd);