Skip to content

Commit 3c618f8

Browse files
committed
Substantially improved binary support across commands and test coverage
1 parent 7cdbf34 commit 3c618f8

File tree

17 files changed

+1347
-53
lines changed

17 files changed

+1347
-53
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { describe, expect, it } from "vitest";
2+
import { Bash } from "../../Bash.js";
3+
4+
describe("base64 with binary data", () => {
5+
describe("binary file encoding", () => {
6+
it("should encode binary file with high bytes", async () => {
7+
const env = new Bash({
8+
files: {
9+
"/binary.bin": new Uint8Array([0x80, 0x90, 0xa0, 0xb0, 0xff]),
10+
},
11+
});
12+
13+
const result = await env.exec("base64 /binary.bin");
14+
expect(result.exitCode).toBe(0);
15+
// Verify it produces valid base64 output
16+
expect(result.stdout.trim()).toMatch(/^[A-Za-z0-9+/]+=*$/);
17+
});
18+
19+
it("should encode and decode file with null bytes", async () => {
20+
const env = new Bash({
21+
files: {
22+
"/nulls.bin": new Uint8Array([0x41, 0x00, 0x42, 0x00, 0x43]),
23+
},
24+
});
25+
26+
await env.exec("base64 /nulls.bin > /encoded.txt");
27+
const decodeResult = await env.exec("base64 -d /encoded.txt");
28+
29+
expect(decodeResult.stdout).toBe("A\0B\0C");
30+
});
31+
32+
it("should encode and decode file with all byte values", async () => {
33+
const env = new Bash({
34+
files: {
35+
"/allbytes.bin": new Uint8Array(
36+
Array.from({ length: 256 }, (_, i) => i),
37+
),
38+
},
39+
});
40+
41+
await env.exec("base64 /allbytes.bin > /encoded.txt");
42+
const decodeResult = await env.exec("base64 -d /encoded.txt");
43+
44+
expect(decodeResult.stdout.length).toBe(256);
45+
for (let i = 0; i < 256; i++) {
46+
expect(decodeResult.stdout.charCodeAt(i)).toBe(i);
47+
}
48+
});
49+
});
50+
51+
describe("binary stdin piping", () => {
52+
it("should encode binary data from stdin", async () => {
53+
const env = new Bash({
54+
files: {
55+
"/binary.bin": new Uint8Array([0x80, 0xff, 0x90, 0xab]),
56+
},
57+
});
58+
59+
const result = await env.exec("cat /binary.bin | base64");
60+
// Verify it encodes without error
61+
expect(result.exitCode).toBe(0);
62+
expect(result.stdout.trim().length).toBeGreaterThan(0);
63+
});
64+
65+
it("should round-trip binary data through stdin", async () => {
66+
const env = new Bash({
67+
files: {
68+
"/binary.bin": new Uint8Array([0x80, 0xff, 0x90, 0xab, 0xcd]),
69+
},
70+
});
71+
72+
await env.exec("cat /binary.bin | base64 > /encoded.txt");
73+
const result = await env.exec("base64 -d /encoded.txt");
74+
75+
expect(result.stdout.charCodeAt(0)).toBe(0x80);
76+
expect(result.stdout.charCodeAt(1)).toBe(0xff);
77+
expect(result.stdout.charCodeAt(2)).toBe(0x90);
78+
expect(result.stdout.charCodeAt(3)).toBe(0xab);
79+
expect(result.stdout.charCodeAt(4)).toBe(0xcd);
80+
});
81+
82+
it("should decode base64 from stdin", async () => {
83+
const env = new Bash({
84+
files: {
85+
// "Hello" in base64
86+
"/encoded.txt": "SGVsbG8=\n",
87+
},
88+
});
89+
90+
const result = await env.exec("cat /encoded.txt | base64 -d");
91+
expect(result.stdout).toBe("Hello");
92+
});
93+
94+
it("should decode valid base64 from stdin", async () => {
95+
const env = new Bash({
96+
files: {
97+
// "ABC" in base64
98+
"/encoded.txt": "QUJD\n",
99+
},
100+
});
101+
102+
const result = await env.exec("cat /encoded.txt | base64 -d");
103+
expect(result.stdout).toBe("ABC");
104+
});
105+
});
106+
107+
describe("round-trip integrity", () => {
108+
it("should round-trip text content", async () => {
109+
const env = new Bash({
110+
files: {
111+
"/data.txt": "Hello World 123",
112+
},
113+
});
114+
115+
await env.exec("base64 /data.txt > /encoded.txt");
116+
const result = await env.exec("base64 -d /encoded.txt");
117+
118+
expect(result.stdout).toBe("Hello World 123");
119+
});
120+
121+
it("should round-trip via stdin", async () => {
122+
const env = new Bash({
123+
files: {
124+
"/data.txt": "test content",
125+
},
126+
});
127+
128+
await env.exec("cat /data.txt | base64 > /encoded.txt");
129+
const result = await env.exec("base64 -d /encoded.txt");
130+
131+
expect(result.stdout).toBe("test content");
132+
});
133+
});
134+
});

src/commands/base64/base64.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,19 @@ async function readBinary(
3030
): Promise<{ ok: true; data: Uint8Array } | { ok: false; error: ExecResult }> {
3131
// No files - read from stdin
3232
if (files.length === 0 || (files.length === 1 && files[0] === "-")) {
33-
return { ok: true, data: new TextEncoder().encode(ctx.stdin) };
33+
// Convert binary string directly to bytes without UTF-8 re-encoding
34+
return {
35+
ok: true,
36+
data: Uint8Array.from(ctx.stdin, (c) => c.charCodeAt(0)),
37+
};
3438
}
3539

3640
// Read and concatenate all files as binary
3741
const chunks: Uint8Array[] = [];
3842
for (const file of files) {
3943
if (file === "-") {
40-
chunks.push(new TextEncoder().encode(ctx.stdin));
44+
// Convert binary string directly to bytes without UTF-8 re-encoding
45+
chunks.push(Uint8Array.from(ctx.stdin, (c) => c.charCodeAt(0)));
4146
continue;
4247
}
4348
try {
@@ -88,11 +93,11 @@ export const base64Command: Command = {
8893
// For decoding, read as text and strip whitespace
8994
const readResult = await readBinary(ctx, files, "base64");
9095
if (!readResult.ok) return readResult.error;
91-
const input = new TextDecoder().decode(readResult.data);
96+
// Use binary string (latin1) to preserve bytes for input
97+
const input = String.fromCharCode(...readResult.data);
9298
const cleaned = input.replace(/\s/g, "");
93-
// Decode base64 to bytes, then to UTF-8 string for output
94-
const bytes = Uint8Array.from(atob(cleaned), (c) => c.charCodeAt(0));
95-
const decoded = new TextDecoder().decode(bytes);
99+
// Decode base64 to binary string (each char code = byte value)
100+
const decoded = atob(cleaned);
96101
return { stdout: decoded, stderr: "", exitCode: 0 };
97102
}
98103

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { describe, expect, it } from "vitest";
2+
import { Bash } from "../../Bash.js";
3+
4+
describe("diff with binary data", () => {
5+
describe("binary file comparison", () => {
6+
it("should detect identical binary files", async () => {
7+
const env = new Bash({
8+
files: {
9+
"/a.bin": new Uint8Array([0x80, 0x90, 0xa0, 0xb0, 0xff]),
10+
"/b.bin": new Uint8Array([0x80, 0x90, 0xa0, 0xb0, 0xff]),
11+
},
12+
});
13+
14+
const result = await env.exec("diff /a.bin /b.bin");
15+
16+
expect(result.exitCode).toBe(0);
17+
expect(result.stdout).toBe("");
18+
});
19+
20+
it("should handle files with null bytes - identical", async () => {
21+
const env = new Bash({
22+
files: {
23+
"/a.bin": new Uint8Array([0x41, 0x00, 0x42]),
24+
"/b.bin": new Uint8Array([0x41, 0x00, 0x42]),
25+
},
26+
});
27+
28+
const result = await env.exec("diff /a.bin /b.bin");
29+
30+
expect(result.exitCode).toBe(0);
31+
});
32+
33+
it("should detect difference in files with null bytes", async () => {
34+
const env = new Bash({
35+
files: {
36+
"/a.bin": new Uint8Array([0x41, 0x00, 0x42]),
37+
"/b.bin": new Uint8Array([0x41, 0x00, 0x43]),
38+
},
39+
});
40+
41+
const result = await env.exec("diff /a.bin /b.bin");
42+
43+
expect(result.exitCode).toBe(1);
44+
});
45+
46+
it("should detect difference in text files with high bytes", async () => {
47+
const env = new Bash({
48+
files: {
49+
// Single line of text with high bytes
50+
"/a.txt": "hello\x80world\n",
51+
"/b.txt": "hello\x81world\n",
52+
},
53+
});
54+
55+
const result = await env.exec("diff /a.txt /b.txt");
56+
57+
expect(result.exitCode).toBe(1);
58+
});
59+
});
60+
61+
describe("binary stdin comparison", () => {
62+
it("should detect difference with binary stdin", async () => {
63+
const env = new Bash({
64+
files: {
65+
"/a.bin": new Uint8Array([0x80, 0x91, 0xa0]),
66+
"/b.bin": new Uint8Array([0x80, 0x90, 0xa0]),
67+
},
68+
});
69+
70+
const result = await env.exec("cat /a.bin | diff - /b.bin");
71+
72+
expect(result.exitCode).toBe(1);
73+
});
74+
});
75+
});
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { describe, expect, it } from "vitest";
2+
import { Bash } from "../../Bash.js";
3+
4+
describe("echo with binary data", () => {
5+
describe("hex escape sequences with -e", () => {
6+
it("should output binary bytes via hex escapes", async () => {
7+
const env = new Bash();
8+
9+
const result = await env.exec("echo -ne '\\x80\\x90\\xa0\\xb0\\xff'");
10+
11+
expect(result.exitCode).toBe(0);
12+
expect(result.stdout.length).toBe(5);
13+
expect(result.stdout.charCodeAt(0)).toBe(0x80);
14+
expect(result.stdout.charCodeAt(1)).toBe(0x90);
15+
expect(result.stdout.charCodeAt(2)).toBe(0xa0);
16+
expect(result.stdout.charCodeAt(3)).toBe(0xb0);
17+
expect(result.stdout.charCodeAt(4)).toBe(0xff);
18+
});
19+
20+
it("should output null bytes via hex escapes", async () => {
21+
const env = new Bash();
22+
23+
const result = await env.exec("echo -ne 'A\\x00B\\x00C'");
24+
25+
expect(result.exitCode).toBe(0);
26+
expect(result.stdout).toBe("A\0B\0C");
27+
});
28+
29+
it("should redirect binary hex output to file", async () => {
30+
const env = new Bash();
31+
32+
await env.exec("echo -ne '\\x80\\xff\\x90' > /binary.bin");
33+
const result = await env.exec("cat /binary.bin");
34+
35+
expect(result.stdout.charCodeAt(0)).toBe(0x80);
36+
expect(result.stdout.charCodeAt(1)).toBe(0xff);
37+
expect(result.stdout.charCodeAt(2)).toBe(0x90);
38+
});
39+
});
40+
41+
describe("octal escape sequences with -e", () => {
42+
it("should output binary bytes via octal escapes", async () => {
43+
const env = new Bash();
44+
45+
const result = await env.exec("echo -ne '\\0200\\0220\\0240'");
46+
47+
expect(result.exitCode).toBe(0);
48+
expect(result.stdout.length).toBe(3);
49+
expect(result.stdout.charCodeAt(0)).toBe(0o200); // 128
50+
expect(result.stdout.charCodeAt(1)).toBe(0o220); // 144
51+
expect(result.stdout.charCodeAt(2)).toBe(0o240); // 160
52+
});
53+
54+
it("should redirect binary octal output to file", async () => {
55+
const env = new Bash();
56+
57+
await env.exec("echo -ne '\\0200\\0377' > /binary.bin");
58+
const result = await env.exec("cat /binary.bin");
59+
60+
expect(result.stdout.charCodeAt(0)).toBe(0o200); // 128
61+
expect(result.stdout.charCodeAt(1)).toBe(0o377); // 255
62+
});
63+
});
64+
65+
describe("round-trip through pipe", () => {
66+
it("should preserve binary data through cat pipe", async () => {
67+
const env = new Bash();
68+
69+
await env.exec("echo -ne '\\x80\\xff\\x00\\x90' > /input.bin");
70+
await env.exec("cat /input.bin > /output.bin");
71+
const result = await env.exec("cat /output.bin");
72+
73+
expect(result.stdout.length).toBe(4);
74+
expect(result.stdout.charCodeAt(0)).toBe(0x80);
75+
expect(result.stdout.charCodeAt(1)).toBe(0xff);
76+
expect(result.stdout.charCodeAt(2)).toBe(0x00);
77+
expect(result.stdout.charCodeAt(3)).toBe(0x90);
78+
});
79+
});
80+
});

0 commit comments

Comments
 (0)