| 1 | import { runGitBuffer } from '../exec/run-git.js'; |
| 2 | |
| 3 | export interface SignedCommit |
| 4 | /** The commit object with its signature header removed, as git signs it. */ |
| 5 | payload: string; |
| 6 | /** Armored signature taken from the `gpgsig` header. */ |
| 7 | signature: string; |
| 8 | } |
| 9 | |
| 10 | const PGP_SIGNATURE_HEADER = ' |
| 11 | |
| 12 | /** |
| 13 | * The signature of every named commit, keyed by sha. Commits that carry none - and signatures in a format other than OpenPGP, such as the SSH ones git can also write - are simply absent from the map. |
| 14 | * |
| 15 | * One `cat-file --batch` answers a whole page: `git log` cannot hand back the signature header, and a spawn per commit would be a spawn per row. |
| 16 | */ |
| 17 | export async function readSignedCommits |
| 18 | gitDir, |
| 19 | env, |
| 20 | shas, |
| 21 | }: { |
| 22 | gitDir: string |
| 23 | env?: Record |
| 24 | shas: string |
| 25 | }): Promise<Map |
| 26 | const found = |
| 27 | if (shas.length |
| 28 | |
| 29 | const raw = |
| 30 | args: ['cat-file', |
| 31 | gitDir, |
| 32 | env, |
| 33 | input: Buffer.from |
| 34 | }); |
| 35 | |
| 36 | // `<sha> <type> <size>\n<size bytes>\n`, repeated. The size is the only safe boundary: a commit message can hold anything, newlines included. |
| 37 | let offset = |
| 38 | while (offset < |
| 39 | const newline = |
| 40 | if (newline === |
| 41 | |
| 42 | const [sha, |
| 43 | .subarray(offset, |
| 44 | .toString('utf8') |
| 45 | .split(' ') |
| 46 | // A sha git could not read is reported as `<sha> missing`, with no body. |
| 47 | if (!size) |
| 48 | offset = newline |
| 49 | continue; |
| 50 | } |
| 51 | |
| 52 | const start = |
| 53 | const end = |
| 54 | if (type === |
| 55 | const signed = |
| 56 | if (signed) found |
| 57 | } |
| 58 | offset = end |
| 59 | } |
| 60 | |
| 61 | return found; |
| 62 | } |
| 63 | |
| 64 | /** |
| 65 | * Splits a raw commit into what was signed and the signature over it. |
| 66 | * |
| 67 | * git signs the commit object as it would read without the `gpgsig` header, so exactly that header and its continuation lines come out - every other byte is payload. |
| 68 | */ |
| 69 | export function splitSignature(commit |
| 70 | const lines = |
| 71 | const payload: |
| 72 | const signature: |
| 73 | let inSignature = |
| 74 | |
| 75 | for (const [index |
| 76 | if (inSignature) { |
| 77 | // Headers end at the blank line before the message; a continuation line of the signature is the only other line that starts with a space. |
| 78 | if (line.startsWith |
| 79 | signature.push(line |
| 80 | continue; |
| 81 | } |
| 82 | inSignature = false; |
| 83 | } |
| 84 | |
| 85 | // Only a header can carry the signature, and headers stop at the first blank line - a message body quoting `gpgsig ` must not be mistaken for it. |
| 86 | if ( |
| 87 | signature.length === 0 |
| 88 | line.startsWith('gpgsig |
| 89 | lines.slice(0, |
| 90 | ) { |
| 91 | signature.push(line |
| 92 | inSignature = true; |
| 93 | continue; |
| 94 | } |
| 95 | |
| 96 | payload.push(line |
| 97 | } |
| 98 | |
| 99 | if (signature.length |
| 100 | |
| 101 | const armored = |
| 102 | if (!armored.startsWith |
| 103 | |
| 104 | return { payload: |
| 105 | } |