Skip to content

Commit 5be282e

Browse files
authored
fix(backup): accept root-relative hardlink targets (#89328)
1 parent 4df8324 commit 5be282e

2 files changed

Lines changed: 43 additions & 4 deletions

File tree

src/commands/backup-verify.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,39 @@ describe("backupVerifyCommand", () => {
358358
}
359359
});
360360

361+
it("accepts root-relative internal hardlink targets from older backups", async () => {
362+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-backup-rootless-linkpath-"));
363+
const archivePath = path.join(tempDir, "backup.tar.gz");
364+
const rootRelativeTargetPath = "payload/posix/tmp/.openclaw/target.txt";
365+
const payloadArchivePath = `${TEST_ARCHIVE_ROOT}/${rootRelativeTargetPath}`;
366+
const hardlinkArchivePath = `${TEST_ARCHIVE_ROOT}/payload/posix/tmp/.openclaw/hardlink.txt`;
367+
try {
368+
const archive = gzipSync(
369+
Buffer.concat([
370+
encodeTarEntry({
371+
path: `${TEST_ARCHIVE_ROOT}/manifest.json`,
372+
contents: `${JSON.stringify(createBackupManifest(payloadArchivePath), null, 2)}\n`,
373+
}),
374+
encodeTarEntry({ path: payloadArchivePath, contents: "payload\n" }),
375+
encodeTarEntry({
376+
path: hardlinkArchivePath,
377+
type: "Link",
378+
linkpath: rootRelativeTargetPath,
379+
}),
380+
Buffer.alloc(1024),
381+
]),
382+
);
383+
await fs.writeFile(archivePath, archive);
384+
385+
const runtime = createBackupVerifyRuntime();
386+
await expect(backupVerifyCommand(runtime, { archive: archivePath })).resolves.toMatchObject({
387+
ok: true,
388+
});
389+
} finally {
390+
await fs.rm(tempDir, { recursive: true, force: true });
391+
}
392+
});
393+
361394
it("rejects hardlink targets missing from archive entries", async () => {
362395
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-backup-missing-linkpath-"));
363396
const archivePath = path.join(tempDir, "broken.tar.gz");

src/commands/backup-verify.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -301,14 +301,20 @@ function verifyHardlinkTargetsAgainstArchiveRoot(
301301
): void {
302302
const normalizedRoot = normalizeArchiveRoot(archiveRoot);
303303
for (const target of hardlinkTargets) {
304-
if (!isArchivePathWithin(target.normalized, normalizedRoot)) {
304+
// Older backup archives may store hardlink linkpath values relative to the
305+
// archive root instead of including the root segment. Accept that form only
306+
// when it resolves to a real entry inside this archive.
307+
const normalizedTarget = isArchivePathWithin(target.normalized, normalizedRoot)
308+
? target.normalized
309+
: path.posix.join(normalizedRoot, target.normalized);
310+
if (!isArchivePathWithin(normalizedTarget, normalizedRoot)) {
305311
throw new Error(
306-
`Archive hardlink target is outside the declared archive root: ${target.entryPath} -> ${target.normalized}`,
312+
`Archive hardlink target is outside the declared archive root: ${target.entryPath} -> ${normalizedTarget}`,
307313
);
308314
}
309-
if (!entries.has(target.normalized)) {
315+
if (!entries.has(normalizedTarget)) {
310316
throw new Error(
311-
`Archive hardlink target is missing from archive entries: ${target.entryPath} -> ${target.normalized}`,
317+
`Archive hardlink target is missing from archive entries: ${target.entryPath} -> ${normalizedTarget}`,
312318
);
313319
}
314320
}

0 commit comments

Comments
 (0)