refactor(setup): share publication byte verification

Use one bounded positional byte comparator for client configuration and skill
publication readback. Keep pathname identity, rollback, mode restoration,
cleanup and retry authority in the existing publication owners.

Preserve short-read progress, EOF, empty-file equality and original I/O errors.
Extend existing byte-exact skill recovery coverage to empty originals.

Validation: client configuration transaction, skill publication recovery and
process-global rollback suites passed; static, docs and discovery checks passed.
This commit is contained in:
morluto
2026-10-24 02:57:11 +08:00
parent e21b131f25
commit 21ff4911bf
4 changed files with 37 additions and 57 deletions
@@ -1,16 +1,9 @@
import { constants, type Stats } from "node:fs";
import {
link,
lstat,
mkdtemp,
open,
realpath,
rename,
type FileHandle,
} from "node:fs/promises";
import { link, lstat, mkdtemp, open, realpath, rename } from "node:fs/promises";
import { basename, dirname, join, resolve, sep } from "node:path";
import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js";
import { fileHandleMatchesBytes } from "../filesystem/fileHandleMatchesBytes.js";
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
import { OwnedStagedOutput } from "../filesystem/OwnedStagedOutput.js";
import {
@@ -160,7 +153,10 @@ export class ClientConfigurationPublication {
!before.isFile() ||
named.isSymbolicLink() ||
!sameRegularFileState(before, named) ||
!(await this.#hasPublishedBytes(handle)) ||
!(await fileHandleMatchesBytes(
handle,
Buffer.from(this.contents, "utf8"),
)) ||
!sameRegularFileState(before, await handle.stat()) ||
!sameRegularFileState(before, await lstat(this.path))
)
@@ -185,29 +181,6 @@ export class ClientConfigurationPublication {
if (!sameRegularFileState(await handle.stat(), await lstat(this.path)))
throw new RegularFileChangedError(this.path);
}
async #hasPublishedBytes(handle: FileHandle): Promise<boolean> {
const expected = Buffer.from(this.contents, "utf8");
if ((await handle.stat()).size !== expected.length) return false;
const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, expected.length));
for (let position = 0; position < expected.length;) {
const { bytesRead } = await handle.read(
chunk,
0,
Math.min(chunk.length, expected.length - position),
position,
);
if (
bytesRead === 0 ||
!chunk
.subarray(0, bytesRead)
.equals(expected.subarray(position, position + bytesRead))
)
return false;
position += bytesRead;
}
return true;
}
}
const assertRequestedConfigurationPath = async (
+3 -24
View File
@@ -3,6 +3,7 @@ import { link, lstat, mkdtemp, open, realpath, rename } from "node:fs/promises";
import { basename, dirname, join } from "node:path";
import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js";
import { fileHandleMatchesBytes } from "../filesystem/fileHandleMatchesBytes.js";
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
import { OwnedStagedOutput } from "../filesystem/OwnedStagedOutput.js";
import {
@@ -163,7 +164,7 @@ export class SkillFilePublication {
)
throw new RegularFileChangedError(this.path);
await this.#assertNamed(before);
if (!(await this.#matches(this.bytes)))
if (!(await fileHandleMatchesBytes(this.owner.handle, this.bytes)))
throw new RegularFileChangedError(this.path);
if (!sameRegularFileState(before, await this.owner.handle.stat()))
throw new RegularFileChangedError(this.path);
@@ -203,7 +204,7 @@ export class SkillFilePublication {
await this.owner.handle.chmod(original.mode);
await this.owner.handle.sync();
await this.#assertNamed(await this.owner.handle.stat());
if (!(await this.#matches(original.bytes)))
if (!(await fileHandleMatchesBytes(this.owner.handle, original.bytes)))
throw new RegularFileChangedError(this.path);
this.#restored = true;
}
@@ -222,28 +223,6 @@ export class SkillFilePublication {
throw new RegularFileChangedError(this.path);
}
async #matches(expected: Buffer): Promise<boolean> {
if ((await this.owner.handle.stat()).size !== expected.length) return false;
const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, expected.length));
for (let position = 0; position < expected.length;) {
const { bytesRead } = await this.owner.handle.read(
chunk,
0,
Math.min(chunk.length, expected.length - position),
position,
);
if (
bytesRead === 0 ||
!chunk
.subarray(0, bytesRead)
.equals(expected.subarray(position, position + bytesRead))
)
return false;
position += bytesRead;
}
return true;
}
async #assertSelected(): Promise<void> {
const current = await optionalState(this.path);
if (
+27
View File
@@ -0,0 +1,27 @@
import type { FileHandle } from "node:fs/promises";
/** Compare descriptor bytes without taking ownership or validating pathname identity. */
export const fileHandleMatchesBytes = async (
handle: Pick<FileHandle, "stat" | "read">,
expected: Buffer,
): Promise<boolean> => {
if ((await handle.stat()).size !== expected.length) return false;
const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, expected.length));
for (let position = 0; position < expected.length;) {
const { bytesRead } = await handle.read(
chunk,
0,
Math.min(chunk.length, expected.length - position),
position,
);
if (
bytesRead === 0 ||
!chunk
.subarray(0, bytesRead)
.equals(expected.subarray(position, position + bytesRead))
)
return false;
position += bytesRead;
}
return true;
};
@@ -123,6 +123,7 @@ describe("skill publication ownership", () => {
describe("skill publication recovery", () => {
it.each([
Buffer.alloc(0),
Buffer.from([0xff, 0xfe, 0x61, 0x00]),
Buffer.from("\ufefforiginal BOM instructions\r\n"),
])(