fix(cli): report byte counts from Apple openrsync (#1232)

macOS ships Apple's openrsync as `/usr/bin/rsync`, and its `--stats` output reads `Total transferred file size: 20 B`. The parser accepted only GNU rsync's `bytes`, so on macOS `bytesTransferred` stayed undefined: `rig file copy --json` printed `null` and the human summary left the count out. The parser now accepts `bytes` or `B` followed by a word boundary. GNU output is unchanged, units it doesn't understand (`KB`, `MB`, `KiB`) stay undefined rather than becoming a wrong number, and the rsync command and its arguments are unchanged. The count is shown only; nothing decides on it. A unit test covers openrsync's form and the unsupported units, and a native test copies a real file with whichever rsync the runner has and checks the byte count.
This commit is contained in:
Rudy Mizrahi Celekli authored and GitHub committed 2026-10-13 07:52:15 -07:00
1 parent 904fa99bc5
commit 6d5f6c54ab
2 files changed
+29 -1

No files matched your search

+1 -1
View File
@@ -309,7 +309,7 @@ const CONNECTION_FAILURE_PATTERNS = [
];
function parseStats(stdout: string): { bytesTransferred?: number; filesTransferred?: number } {
const bytes = stdout.match(/Total transferred file size:\s*([\d,.]+)\s*bytes/i);
const bytes = stdout.match(/Total transferred file size:\s*([\d,.]+)\s*(?:bytes|B)\b/i);
const files = stdout.match(/Number of (?:regular )?files transferred:\s*([\d,.]+)/i);
const num = (m: RegExpMatchArray | null) => (m ? Number(m[1]!.replace(/[,.](?=\d{3})/g, "").replace(/,/g, "")) : undefined);
return { bytesTransferred: num(bytes), filesTransferred: num(files) };
+28
View File
@@ -363,6 +363,15 @@ describe("classifyRsyncResult / runFileCopy", () => {
expect(res).toMatchObject({ ok: true, failedStep: "none", bytesTransferred: 2048, filesTransferred: 1 });
});
it("parses the byte unit emitted by Apple openrsync", () => {
const result = classifyRsyncResult(0, "Number of files transferred: 1\nTotal transferred file size: 21 B\n", "");
expect(result).toMatchObject({ ok: true, bytesTransferred: 21, filesTransferred: 1 });
});
it.each(["KB", "MB", "KiB"])("does not interpret the unsupported %s unit as bytes", (unit) => {
expect(classifyRsyncResult(0, `Total transferred file size: 21 ${unit}\n`, "").bytesTransferred).toBeUndefined();
});
it.each([
"user@host: Permission denied (publickey).",
"Could not query Keychain entry",
@@ -468,3 +477,22 @@ describe("local directory copy semantics", () => {
}
});
});
it.skipIf(!rsyncAvailable && process.platform !== "darwin")("native local file copies disclose the transferred byte count", async () => {
expect(rsyncAvailable, "macOS native copy proof requires the system rsync").toBe(true);
const root = mkdtempSync(path.join(process.cwd(), "openrig-copy-stats-"));
try {
const src = path.join(root, "source.txt");
const dst = path.join(root, "copied.txt");
const content = Buffer.from("owned stats fixture\n", "utf8");
writeFileSync(src, content);
const planned = planFileCopy(src, dst);
expect(planned.ok).toBe(true);
if (!planned.ok) return;
const result = await runFileCopy(planned.plan);
expect(result.ok, result.stderr).toBe(true);
expect(readFileSync(dst)).toEqual(content);
expect(result.bytesTransferred).toBe(content.length);
expect(result.filesTransferred).toBe(1);
} finally { rmSync(root, { recursive: true, force: true }); }
});