-
Notifications
You must be signed in to change notification settings - Fork 352
Fix C-quoted filename handling in push_signed_commits.cjs #26277
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,79 @@ const fs = require("fs"); | |
| const path = require("path"); | ||
| const { ERR_API } = require("./error_codes.cjs"); | ||
|
|
||
| /** | ||
| * Unescape a C-quoted path returned by `git diff-tree --raw`. | ||
| * | ||
| * git wraps paths that contain special characters (spaces, non-ASCII bytes, | ||
| * control characters, etc.) in double-quotes and encodes each "unusual" byte | ||
| * as a C-style escape sequence. This function strips the surrounding quotes | ||
| * and decodes the escape sequences back to the original byte sequence, then | ||
| * interprets the result as UTF-8. | ||
| * | ||
| * Supported escape sequences: `\\`, `\"`, `\a`, `\b`, `\f`, `\n`, `\r`, | ||
| * `\t`, `\v`, and octal `\NNN` (1–3 octal digits). | ||
| * | ||
| * @param {string} s - Raw path token from git output (may or may not be quoted) | ||
| * @returns {string} Unescaped path | ||
| */ | ||
| function unquoteCPath(s) { | ||
| if (!s.startsWith('"')) return s; | ||
| // Strip surrounding double-quotes | ||
| const inner = s.slice(1, s.endsWith('"') ? s.length - 1 : s.length); | ||
| const bytes = []; | ||
| let i = 0; | ||
| while (i < inner.length) { | ||
| if (inner[i] === "\\") { | ||
| i++; | ||
| if (i < inner.length && inner[i] >= "0" && inner[i] <= "7") { | ||
| // Octal sequence – collect up to 3 octal digits | ||
| let oct = ""; | ||
| while (i < inner.length && inner[i] >= "0" && inner[i] <= "7" && oct.length < 3) { | ||
| oct += inner[i++]; | ||
| } | ||
| bytes.push(parseInt(oct, 8)); | ||
| } else { | ||
| const esc = inner[i++]; | ||
| switch (esc) { | ||
|
Comment on lines
+41
to
+43
|
||
| case "\\": | ||
| bytes.push(0x5c); | ||
| break; | ||
| case '"': | ||
| bytes.push(0x22); | ||
| break; | ||
| case "a": | ||
| bytes.push(0x07); | ||
| break; | ||
| case "b": | ||
| bytes.push(0x08); | ||
| break; | ||
| case "f": | ||
| bytes.push(0x0c); | ||
| break; | ||
| case "n": | ||
| bytes.push(0x0a); | ||
| break; | ||
| case "r": | ||
| bytes.push(0x0d); | ||
| break; | ||
| case "t": | ||
| bytes.push(0x09); | ||
| break; | ||
| case "v": | ||
| bytes.push(0x0b); | ||
| break; | ||
| default: | ||
| // Unknown escape: preserve backslash and the character as-is | ||
| bytes.push(0x5c, esc.charCodeAt(0)); | ||
| } | ||
| } | ||
| } else { | ||
| bytes.push(inner.charCodeAt(i++)); | ||
| } | ||
| } | ||
| return Buffer.from(bytes).toString("utf8"); | ||
| } | ||
|
|
||
| /** | ||
| * @fileoverview Signed Commit Push Helper | ||
| * | ||
|
|
@@ -88,13 +161,13 @@ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, c | |
| const status = modeFields[4]; // A=Added, M=Modified, D=Deleted, R=Renamed, C=Copied | ||
|
|
||
| const paths = line.slice(tabIdx + 1).split("\t"); | ||
| const filePath = paths[0]; | ||
| const filePath = unquoteCPath(paths[0]); | ||
|
|
||
| if (status === "D") { | ||
| deletions.push({ path: filePath }); | ||
| } else if (status && status.startsWith("R")) { | ||
| // Rename: source path is deleted, destination path is added | ||
| const renamedPath = paths[1]; | ||
| const renamedPath = unquoteCPath(paths[1]); | ||
| if (!renamedPath) { | ||
| core.warning(`pushSignedCommits: rename entry missing destination path, skipping: ${line}`); | ||
|
Comment on lines
168
to
172
|
||
| continue; | ||
|
|
@@ -111,7 +184,7 @@ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, c | |
| additions.push({ path: renamedPath, contents: content.toString("base64") }); | ||
| } else if (status && status.startsWith("C")) { | ||
| // Copy: source path is kept (no deletion), only the destination path is added | ||
| const copiedPath = paths[1]; | ||
| const copiedPath = unquoteCPath(paths[1]); | ||
| if (!copiedPath) { | ||
| core.warning(`pushSignedCommits: copy entry missing destination path, skipping: ${line}`); | ||
| continue; | ||
|
|
@@ -242,4 +315,4 @@ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, c | |
| } | ||
| } | ||
|
|
||
| module.exports = { pushSignedCommits }; | ||
| module.exports = { pushSignedCommits, unquoteCPath }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot add test suite for this function
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a dedicated
unquoteCPathtest suite in commit f01052f. The 9 tests cover: plain strings returned unchanged, quote stripping, all standard C escape sequences (\\,\",\a/\b/\f/\n/\r/\t/\v), octal sequences decoding to UTF-8 bytes (e.g.\303\251→é), filenames with spaces, unknown escapes (backslash preserved), edge cases (single char, empty string, 1/2/3-digit octals). The function is also now exported from the module to make it directly importable in tests.