1import { describe, expect, it } from "vite-plus/test";
2
3import { quoteGitPatchPath, unquoteGitPatchPath } from "./gitPatchPath.ts";
4
5const BELL = "\u0007";
6const UNIT_SEPARATOR = "\u001f";
7const DELETE = "\u007f";
8
9describe("quoteGitPatchPath", () => {
10 it("leaves a name a header can carry as itself", () => {
11 expect(quoteGitPatchPath("src/app.ts")).toBe("src/app.ts");
12 expect(quoteGitPatchPath("with space.txt")).toBe("with space.txt");
13 // Outside ASCII is only quoted for what a terminal can show, and a diff viewer is not one.
14 expect(quoteGitPatchPath("café/résumé.ts")).toBe("café/résumé.ts");
15 expect(quoteGitPatchPath("🚀.ts")).toBe("🚀.ts");
16 });
17
18 it("writes git's escape for a character the header would read as its own", () => {
19 expect(quoteGitPatchPath("tab\tfile.txt")).toBe('"tab\\tfile.txt"');
20 expect(quoteGitPatchPath("line\nfile.txt")).toBe('"line\\nfile.txt"');
21 expect(quoteGitPatchPath('quo"te.txt')).toBe('"quo\\"te.txt"');
22 expect(quoteGitPatchPath("back\\slash.txt")).toBe('"back\\\\slash.txt"');
23 expect(quoteGitPatchPath(`ring${BELL}.txt`)).toBe('"ring\\a.txt"');
24 });
25
26 it("writes the octal of a control character git has no name for", () => {
27 expect(quoteGitPatchPath(`unit${UNIT_SEPARATOR}.txt`)).toBe('"unit\\037.txt"');
28 expect(quoteGitPatchPath(`del${DELETE}.txt`)).toBe('"del\\177.txt"');
29 });
30});
31
32describe("a name written into a header and read back out", () => {
33 const names = [
34 "src/app.ts",
35 "with space.txt",
36 "café/résumé.ts",
37 "🚀.ts",
38 "tab\tfile.txt",
39 "line\nfile.txt",
40 "carriage\rfile.txt",
41 'quo"te.txt',
42 "back\\slash.txt",
43 `ring${BELL}.txt`,
44 `unit${UNIT_SEPARATOR}.txt`,
45 `del${DELETE}.txt`,
46 `every\t\n\r"\\${BELL}.txt`,
47 ];
48
49 it.each(names)("is the name that went in: %j", (name) => {
50 const written = quoteGitPatchPath(name);
51 expect(unquoteGitPatchPath(written)).toBe(name);
52 // A parser that takes the quotes off itself, as the clients' one does, gets there too.
53 const unwrapped = written.startsWith('"') ? written.slice(1, -1) : written;
54 expect(unquoteGitPatchPath(unwrapped)).toBe(name);
55 });
56
57 it("carries the whole name past the first thing a header stops at", () => {
58 const written = quoteGitPatchPath("tab\tfile.txt");
59
60 // The bug this guards: a header line ends at a tab and splits on a space, so the name written
61 // as itself would be read as `tab` and marked viewed under a path no host has.
62 expect(written).not.toContain("\t");
63 expect(written).not.toContain("\n");
64 expect(unquoteGitPatchPath(written)).not.toBe("tab");
65 });
66});
67
68describe("unquoteGitPatchPath", () => {
69 it("leaves a token git saw no need to quote alone", () => {
70 expect(unquoteGitPatchPath("src/app.ts")).toBe("src/app.ts");
71 expect(unquoteGitPatchPath("with space.txt")).toBe("with space.txt");
72 expect(unquoteGitPatchPath("")).toBe("");
73 expect(unquoteGitPatchPath('"')).toBe('"');
74 });
75
76 it("reads the octal escapes a host with core.quotePath on writes", () => {
77 expect(unquoteGitPatchPath('"caf\\303\\251/r\\303\\251sum\\303\\251.ts"')).toBe(
78 "café/résumé.ts",
79 );
80 expect(unquoteGitPatchPath('"\\360\\237\\232\\200.ts"')).toBe("🚀.ts");
81 });
82});
83
84describe("a name a parser handed back with its quotes already off", () => {
85 it("is read for the escapes it still carries", () => {
86 expect(unquoteGitPatchPath("tab\\tfile.txt")).toBe("tab\tfile.txt");
87 expect(unquoteGitPatchPath("caf\\303\\251.ts")).toBe("café.ts");
88 });
89
90 it("reads an escape git would never write the way C reads it", () => {
91 expect(unquoteGitPatchPath("odd\\zname.txt")).toBe("oddzname.txt");
92 expect(unquoteGitPatchPath("trailing\\")).toBe("trailing\\");
93 expect(unquoteGitPatchPath("short\\12.txt")).toBe("short12.txt");
94 });
95});