| 1 | /** |
| 2 | * Tree entry names the server refuses to write (and leaves out of archives): |
| 3 | * empty names, `.` and `..`, names with `/`, `\` or NUL, and anything a |
| 4 | * checkout on Windows or macOS would treat as `.git` (`.GIT`, `.git.`, |
| 5 | * `.git ` , `git~1`, `.git`). Git's own fsck rejects the same names. |
| 6 | */ |
| 7 | |
| 8 | // Characters HFS+ ignores when comparing names (zero-width joiners, direction marks, BOM). |
| 9 | const HFS_IGNORABLE = /[---]/g; |
| 10 | |
| 11 | export function isDotGitName(name: string): boolean { |
| 12 | const n = name.replace(HFS_IGNORABLE, "").toLowerCase().replace(/[. ]+$/, ""); |
| 13 | return n === ".git" || n === "git~1"; |
| 14 | } |
| 15 | |
| 16 | /** Why a single tree entry name is unsafe, or null when it is fine. */ |
| 17 | export function unsafeTreeName(name: string): string | null { |
| 18 | if (!name) return "empty name"; |
| 19 | if (name === "." || name === "..") return `'${name}' is not a valid name`; |
| 20 | if (name.includes("/") || name.includes("\\") || name.includes("\0")) return "names can't contain '/', '\\' or NUL"; |
| 21 | if (isDotGitName(name)) return "'.git' is reserved"; |
| 22 | if (name.length > 255) return "name is too long"; |
| 23 | return null; |
| 24 | } |
| 25 | |
| 26 | /** Why a slash-separated path is unsafe to write, or null. */ |
| 27 | export function unsafeTreePath(path: string): string | null { |
| 28 | if (path.includes("\\") || path.includes("\0")) return "paths can't contain '\\' or NUL"; |
| 29 | const parts = path.split("/").filter((p) => p !== ""); |
| 30 | if (!parts.length) return "empty path"; |
| 31 | if (path.length > 4096) return "path is too long"; |
| 32 | for (const p of parts) { |
| 33 | const why = unsafeTreeName(p); |
| 34 | if (why) return why; |
| 35 | } |
| 36 | return null; |
| 37 | } |