( dir: string, ref: string, )
| 219 | } |
| 220 | |
| 221 | async function resolveRefInDir( |
| 222 | dir: string, |
| 223 | ref: string, |
| 224 | ): Promise<string | null> { |
| 225 | // Try loose ref file |
| 226 | try { |
| 227 | const content = (await readFile(join(dir, ref), 'utf-8')).trim() |
| 228 | if (content.startsWith('ref:')) { |
| 229 | const target = content.slice('ref:'.length).trim() |
| 230 | // Reject path traversal in a tampered symref chain. |
| 231 | if (!isSafeRefName(target)) { |
| 232 | return null |
| 233 | } |
| 234 | return resolveRef(dir, target) |
| 235 | } |
| 236 | // Loose ref content should be a raw SHA. Validate: an attacker-controlled |
| 237 | // ref file could contain shell metacharacters. |
| 238 | if (!isValidGitSha(content)) { |
| 239 | return null |
| 240 | } |
| 241 | return content |
| 242 | } catch { |
| 243 | // Loose ref doesn't exist, try packed-refs |
| 244 | } |
| 245 | |
| 246 | try { |
| 247 | const packed = await readFile(join(dir, 'packed-refs'), 'utf-8') |
| 248 | for (const line of packed.split('\n')) { |
| 249 | if (line.startsWith('#') || line.startsWith('^')) { |
| 250 | continue |
| 251 | } |
| 252 | const spaceIdx = line.indexOf(' ') |
| 253 | if (spaceIdx === -1) { |
| 254 | continue |
| 255 | } |
| 256 | if (line.slice(spaceIdx + 1) === ref) { |
| 257 | const sha = line.slice(0, spaceIdx) |
| 258 | return isValidGitSha(sha) ? sha : null |
| 259 | } |
| 260 | } |
| 261 | } catch { |
| 262 | // No packed-refs |
| 263 | } |
| 264 | |
| 265 | return null |
| 266 | } |
| 267 | |
| 268 | /** |
| 269 | * Read the `commondir` file to find the shared git directory. |
no test coverage detected