Skip to content

Commit 77d1157

Browse files
jason-allen-onealsteipete
authored andcommitted
fix(backup): reject missing hardlink targets
1 parent d8a2cd5 commit 77d1157

2 files changed

Lines changed: 44 additions & 1 deletion

File tree

src/commands/backup-verify.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,39 @@ describe("backupVerifyCommand", () => {
343343
}
344344
});
345345

346+
it("rejects hardlink targets missing from archive entries", async () => {
347+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-backup-missing-linkpath-"));
348+
const archivePath = path.join(tempDir, "broken.tar.gz");
349+
const payloadArchivePath = `${TEST_ARCHIVE_ROOT}/payload/posix/tmp/.openclaw/target.txt`;
350+
const hardlinkArchivePath = `${TEST_ARCHIVE_ROOT}/payload/posix/tmp/.openclaw/hardlink.txt`;
351+
const missingTargetPath = `${TEST_ARCHIVE_ROOT}/payload/posix/tmp/.openclaw/missing-target.txt`;
352+
try {
353+
const archive = gzipSync(
354+
Buffer.concat([
355+
encodeTarEntry({
356+
path: `${TEST_ARCHIVE_ROOT}/manifest.json`,
357+
contents: `${JSON.stringify(createBackupManifest(payloadArchivePath), null, 2)}\n`,
358+
}),
359+
encodeTarEntry({ path: payloadArchivePath, contents: "payload\n" }),
360+
encodeTarEntry({
361+
path: hardlinkArchivePath,
362+
type: "Link",
363+
linkpath: missingTargetPath,
364+
}),
365+
Buffer.alloc(1024),
366+
]),
367+
);
368+
await fs.writeFile(archivePath, archive);
369+
370+
const runtime = createBackupVerifyRuntime();
371+
await expect(backupVerifyCommand(runtime, { archive: archivePath })).rejects.toThrow(
372+
/hardlink target is missing from archive entries/i,
373+
);
374+
} finally {
375+
await fs.rm(tempDir, { recursive: true, force: true });
376+
}
377+
});
378+
346379
it("ignores payload manifest.json files when locating the backup manifest", async () => {
347380
const archiveDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-backup-verify-out-"));
348381
try {

src/commands/backup-verify.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ function verifyManifestAgainstEntries(manifest: BackupManifest, entries: Set<str
262262
function verifyHardlinkTargetsAgainstArchiveRoot(
263263
hardlinkTargets: Array<{ entryPath: string; normalized: string }>,
264264
archiveRoot: string,
265+
entries: Set<string>,
265266
): void {
266267
const normalizedRoot = normalizeArchiveRoot(archiveRoot);
267268
for (const target of hardlinkTargets) {
@@ -270,6 +271,11 @@ function verifyHardlinkTargetsAgainstArchiveRoot(
270271
`Archive hardlink target is outside the declared archive root: ${target.entryPath} -> ${target.normalized}`,
271272
);
272273
}
274+
if (!entries.has(target.normalized)) {
275+
throw new Error(
276+
`Archive hardlink target is missing from archive entries: ${target.entryPath} -> ${target.normalized}`,
277+
);
278+
}
273279
}
274280
}
275281

@@ -338,7 +344,11 @@ export async function backupVerifyCommand(
338344
const manifestRaw = await extractManifest({ archivePath, manifestEntryPath });
339345
const manifest = parseManifest(manifestRaw);
340346
verifyManifestAgainstEntries(manifest, normalizedEntrySet);
341-
verifyHardlinkTargetsAgainstArchiveRoot(hardlinkTargets, manifest.archiveRoot);
347+
verifyHardlinkTargetsAgainstArchiveRoot(
348+
hardlinkTargets,
349+
manifest.archiveRoot,
350+
normalizedEntrySet,
351+
);
342352

343353
const result: BackupVerifyResult = {
344354
ok: true,

0 commit comments

Comments
 (0)