@johnhenry/sitefile 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/CHANGELOG.md +152 -0
  2. package/LICENSE +21 -0
  3. package/README.md +513 -2
  4. package/lib/src/archive-writers.d.ts +31 -0
  5. package/lib/src/archive-writers.js +197 -0
  6. package/lib/src/attest.d.ts +126 -0
  7. package/lib/src/attest.js +220 -0
  8. package/lib/src/bake.d.ts +74 -0
  9. package/lib/src/bake.js +372 -0
  10. package/lib/src/base-image.d.ts +76 -0
  11. package/lib/src/base-image.js +138 -0
  12. package/lib/src/build-args.d.ts +26 -0
  13. package/lib/src/build-args.js +164 -0
  14. package/lib/src/cache-limit.d.ts +54 -0
  15. package/lib/src/cache-limit.js +239 -0
  16. package/lib/src/casefold-data.d.ts +2 -0
  17. package/lib/src/casefold-data.js +217 -0
  18. package/lib/src/cbor.d.ts +39 -0
  19. package/lib/src/cbor.js +174 -0
  20. package/lib/src/checklinks.d.ts +42 -0
  21. package/lib/src/checklinks.js +120 -0
  22. package/lib/src/cli/bake.d.ts +2 -0
  23. package/lib/src/cli/bake.js +179 -0
  24. package/lib/src/cli/build-image.d.ts +21 -0
  25. package/lib/src/cli/build-image.js +42 -0
  26. package/lib/src/cli/cache.d.ts +8 -0
  27. package/lib/src/cli/cache.js +119 -0
  28. package/lib/src/cli/common.d.ts +29 -0
  29. package/lib/src/cli/common.js +100 -0
  30. package/lib/src/cli/git-context.d.ts +18 -0
  31. package/lib/src/cli/git-context.js +48 -0
  32. package/lib/src/cli/images.d.ts +6 -0
  33. package/lib/src/cli/images.js +285 -0
  34. package/lib/src/cli/index.d.ts +10 -0
  35. package/lib/src/cli/index.js +95 -0
  36. package/lib/src/cli/network.d.ts +34 -0
  37. package/lib/src/cli/network.js +56 -0
  38. package/lib/src/cli/outdated.d.ts +5 -0
  39. package/lib/src/cli/outdated.js +99 -0
  40. package/lib/src/cli/registry.d.ts +2 -0
  41. package/lib/src/cli/registry.js +127 -0
  42. package/lib/src/cli/serve-worker.d.ts +2 -0
  43. package/lib/src/cli/serve-worker.js +42 -0
  44. package/lib/src/cli/servers.d.ts +15 -0
  45. package/lib/src/cli/servers.js +467 -0
  46. package/lib/src/cli/vendor.d.ts +2 -0
  47. package/lib/src/cli/vendor.js +97 -0
  48. package/lib/src/cli/watch.d.ts +26 -0
  49. package/lib/src/cli/watch.js +81 -0
  50. package/lib/src/cli.d.ts +12 -0
  51. package/lib/src/cli.js +522 -0
  52. package/lib/src/csv.d.ts +27 -0
  53. package/lib/src/csv.js +117 -0
  54. package/lib/src/deflate.d.ts +76 -0
  55. package/lib/src/deflate.js +604 -0
  56. package/lib/src/digest.d.ts +3 -0
  57. package/lib/src/digest.js +7 -0
  58. package/lib/src/errors.d.ts +56 -0
  59. package/lib/src/errors.js +29 -0
  60. package/lib/src/executor.d.ts +393 -0
  61. package/lib/src/executor.js +2201 -0
  62. package/lib/src/explain.d.ts +64 -0
  63. package/lib/src/explain.js +445 -0
  64. package/lib/src/export-formats.d.ts +23 -0
  65. package/lib/src/export-formats.js +63 -0
  66. package/lib/src/exporter.d.ts +103 -0
  67. package/lib/src/exporter.js +676 -0
  68. package/lib/src/fingerprint.d.ts +40 -0
  69. package/lib/src/fingerprint.js +168 -0
  70. package/lib/src/frontmatter.d.ts +22 -0
  71. package/lib/src/frontmatter.js +45 -0
  72. package/lib/src/index.d.ts +13 -0
  73. package/lib/src/index.js +13 -0
  74. package/lib/src/language/ast.d.ts +360 -0
  75. package/lib/src/language/ast.js +57 -0
  76. package/lib/src/language/parser.d.ts +3 -0
  77. package/lib/src/language/parser.js +612 -0
  78. package/lib/src/language/scanner.d.ts +110 -0
  79. package/lib/src/language/scanner.js +449 -0
  80. package/lib/src/language/validate.d.ts +13 -0
  81. package/lib/src/language/validate.js +367 -0
  82. package/lib/src/links.d.ts +79 -0
  83. package/lib/src/links.js +583 -0
  84. package/lib/src/lint.d.ts +10 -0
  85. package/lib/src/lint.js +223 -0
  86. package/lib/src/markdown.d.ts +29 -0
  87. package/lib/src/markdown.js +612 -0
  88. package/lib/src/media.d.ts +82 -0
  89. package/lib/src/media.js +650 -0
  90. package/lib/src/memo.d.ts +57 -0
  91. package/lib/src/memo.js +93 -0
  92. package/lib/src/oci.d.ts +178 -0
  93. package/lib/src/oci.js +380 -0
  94. package/lib/src/outdated.d.ts +46 -0
  95. package/lib/src/outdated.js +195 -0
  96. package/lib/src/pages.d.ts +10 -0
  97. package/lib/src/pages.js +54 -0
  98. package/lib/src/paths.d.ts +39 -0
  99. package/lib/src/paths.js +101 -0
  100. package/lib/src/planner.d.ts +297 -0
  101. package/lib/src/planner.js +410 -0
  102. package/lib/src/plugins/cache.d.ts +96 -0
  103. package/lib/src/plugins/cache.js +325 -0
  104. package/lib/src/plugins/exec.d.ts +51 -0
  105. package/lib/src/plugins/exec.js +144 -0
  106. package/lib/src/plugins/host.d.ts +75 -0
  107. package/lib/src/plugins/host.js +277 -0
  108. package/lib/src/plugins/wasm.d.ts +23 -0
  109. package/lib/src/plugins/wasm.js +249 -0
  110. package/lib/src/profile.d.ts +42 -0
  111. package/lib/src/profile.js +68 -0
  112. package/lib/src/ref.d.ts +40 -0
  113. package/lib/src/ref.js +79 -0
  114. package/lib/src/registry-auth.d.ts +32 -0
  115. package/lib/src/registry-auth.js +151 -0
  116. package/lib/src/registry-cache.d.ts +71 -0
  117. package/lib/src/registry-cache.js +333 -0
  118. package/lib/src/registry-ops.d.ts +51 -0
  119. package/lib/src/registry-ops.js +248 -0
  120. package/lib/src/registry.d.ts +115 -0
  121. package/lib/src/registry.js +524 -0
  122. package/lib/src/render.d.ts +134 -0
  123. package/lib/src/render.js +576 -0
  124. package/lib/src/resources/archive.d.ts +11 -0
  125. package/lib/src/resources/archive.js +306 -0
  126. package/lib/src/resources/bundle-data.d.ts +16 -0
  127. package/lib/src/resources/bundle-data.js +50 -0
  128. package/lib/src/resources/convert.d.ts +12 -0
  129. package/lib/src/resources/convert.js +63 -0
  130. package/lib/src/resources/documents.d.ts +35 -0
  131. package/lib/src/resources/documents.js +122 -0
  132. package/lib/src/resources/git.d.ts +103 -0
  133. package/lib/src/resources/git.js +720 -0
  134. package/lib/src/resources/https.d.ts +74 -0
  135. package/lib/src/resources/https.js +413 -0
  136. package/lib/src/resources/ignore.d.ts +33 -0
  137. package/lib/src/resources/ignore.js +119 -0
  138. package/lib/src/resources/local.d.ts +64 -0
  139. package/lib/src/resources/local.js +248 -0
  140. package/lib/src/resources/lockfile.d.ts +52 -0
  141. package/lib/src/resources/lockfile.js +172 -0
  142. package/lib/src/resources/netpolicy.d.ts +6 -0
  143. package/lib/src/resources/netpolicy.js +82 -0
  144. package/lib/src/resources/network.d.ts +70 -0
  145. package/lib/src/resources/network.js +304 -0
  146. package/lib/src/resources/types.d.ts +69 -0
  147. package/lib/src/resources/types.js +13 -0
  148. package/lib/src/resources/vendor.d.ts +94 -0
  149. package/lib/src/resources/vendor.js +296 -0
  150. package/lib/src/resources/wbn-reader.d.ts +44 -0
  151. package/lib/src/resources/wbn-reader.js +302 -0
  152. package/lib/src/schema.d.ts +25 -0
  153. package/lib/src/schema.js +169 -0
  154. package/lib/src/secrets.d.ts +21 -0
  155. package/lib/src/secrets.js +103 -0
  156. package/lib/src/serve-rules.d.ts +29 -0
  157. package/lib/src/serve-rules.js +123 -0
  158. package/lib/src/server.d.ts +45 -0
  159. package/lib/src/server.js +246 -0
  160. package/lib/src/servers.d.ts +85 -0
  161. package/lib/src/servers.js +225 -0
  162. package/lib/src/site.d.ts +60 -0
  163. package/lib/src/site.js +169 -0
  164. package/lib/src/store.d.ts +151 -0
  165. package/lib/src/store.js +596 -0
  166. package/lib/src/tree.d.ts +50 -0
  167. package/lib/src/tree.js +134 -0
  168. package/lib/src/values.d.ts +7 -0
  169. package/lib/src/values.js +37 -0
  170. package/lib/src/vendor.d.ts +19 -0
  171. package/lib/src/vendor.js +145 -0
  172. package/lib/src/watch.d.ts +44 -0
  173. package/lib/src/watch.js +137 -0
  174. package/lib/src/wbn.d.ts +26 -0
  175. package/lib/src/wbn.js +241 -0
  176. package/lib/src/yaml.d.ts +34 -0
  177. package/lib/src/yaml.js +562 -0
  178. package/package.json +59 -4
@@ -0,0 +1,720 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { inflateSync } from 'node:zlib';
3
+ import { SitefileError } from '../errors.js';
4
+ import { nfc } from '../paths.js';
5
+ import { fetchRemote, redactUrl } from './https.js';
6
+ /**
7
+ * Git sources over smart HTTP (issue #91): `IMPORT name FROM git+https://host/repo.git#ref:path` and
8
+ * `sitefile build https://host/repo.git#ref:dir`.
9
+ *
10
+ * This is a small client for Git's protocol v2 (gitprotocol-v2, gitprotocol-http), written against
11
+ * the build's HTTPS client so it inherits its network policy unchanged (https only unless
12
+ * --allow-http, no credentials in URLs, the SSRF address check at connect time, redirects re-checked
13
+ * hop by hop, the download cap and the timeout). No `git` binary runs, so there are no hooks, no
14
+ * config, no credential helpers and no filters:
15
+ *
16
+ * 1. GET <repo>/info/refs?service=git-upload-pack (Git-Protocol: version=2): capabilities
17
+ * 2. POST <repo>/git-upload-pack command=ls-refs (only when the ref is not a commit)
18
+ * 3. POST <repo>/git-upload-pack command=fetch want <commit>, deepen 1, done
19
+ *
20
+ * The packfile is parsed here (zlib objects, OFS_DELTA and REF_DELTA), every object's SHA-1 is
21
+ * recomputed, and the pack checksum is verified, so the tree that is imported is the one the commit
22
+ * names. Only SHA-1 repositories are supported. Submodules (gitlinks) are refused; symbolic links are
23
+ * resolved inside the imported tree with the same rules as local symlinks (see `walkGitTree`).
24
+ */
25
+ export const GIT_COMMIT_RE = /^[0-9a-f]{40}$/;
26
+ const USER_AGENT = 'sitefile/0.1';
27
+ /** True for URIs with a `git+` scheme (`git+https:`, `git+http:`, `git+ssh:`, …). */
28
+ export function isGitUri(uri) {
29
+ return /^git\+[A-Za-z][A-Za-z0-9+.-]*:/i.test(uri);
30
+ }
31
+ function badRef(ref, span) {
32
+ return new SitefileError('E_VALIDATION', `invalid Git ref '${ref}' (see git check-ref-format)`, span);
33
+ }
34
+ /** Git's ref name rules (git check-ref-format), applied to the ref in a Git URL. */
35
+ export function checkRefName(ref, span) {
36
+ if (ref === '')
37
+ return;
38
+ // eslint-disable-next-line no-control-regex
39
+ if (/[\x00-\x20\x7f~^:?*[\\]/.test(ref) || ref.includes('..') || ref.includes('@{') || ref === '@')
40
+ throw badRef(ref, span);
41
+ if (ref.startsWith('/') || ref.endsWith('/') || ref.endsWith('.') || ref.startsWith('-') || ref.includes('//'))
42
+ throw badRef(ref, span);
43
+ for (const part of ref.split('/'))
44
+ if (part.startsWith('.') || part.endsWith('.lock'))
45
+ throw badRef(ref, span);
46
+ }
47
+ /**
48
+ * Parse `git+https://host/repo[.git][#ref[:path]]` (`git+http:` too; plain http still needs
49
+ * --allow-http when fetched). `scheme` errors are E_PROTOCOL, a bad ref E_VALIDATION, a path with
50
+ * `..` E_PATH_ESCAPE.
51
+ */
52
+ export function parseGitUri(uri, span) {
53
+ // Schemes are case-insensitive: GIT+HTTPS:// is git+https://.
54
+ const m = /^git\+([A-Za-z][A-Za-z0-9+.-]*):/i.exec(uri);
55
+ const scheme = m ? m[1].toLowerCase() : '';
56
+ if (scheme !== 'https' && scheme !== 'http') {
57
+ throw new SitefileError('E_PROTOCOL', `unsupported Git transport 'git+${scheme}:' (supported: git+https)`, span);
58
+ }
59
+ const hash = uri.indexOf('#');
60
+ const repo = (hash === -1 ? uri : uri.slice(0, hash)).slice('git+'.length);
61
+ const fragment = hash === -1 ? '' : uri.slice(hash + 1);
62
+ let u;
63
+ try {
64
+ u = new URL(repo);
65
+ }
66
+ catch (e) {
67
+ throw new SitefileError('E_PROTOCOL', `invalid Git URL '${redactUrl(repo)}'`, span, { cause: e });
68
+ }
69
+ if (u.username !== '' || u.password !== '') {
70
+ throw new SitefileError('E_PROTOCOL', `URL ${redactUrl(repo)} contains credentials; credentials in URLs are not allowed`, span);
71
+ }
72
+ if (u.search !== '')
73
+ throw new SitefileError('E_PROTOCOL', `Git URL ${redactUrl(repo)} must not have a query string`, span);
74
+ const colon = fragment.indexOf(':');
75
+ const ref = colon === -1 ? fragment : fragment.slice(0, colon);
76
+ const rawPath = colon === -1 ? '' : fragment.slice(colon + 1);
77
+ checkRefName(ref, span);
78
+ const path = gitPath(rawPath, span);
79
+ const base = { url: uri, repo, ref, path };
80
+ return GIT_COMMIT_RE.test(ref) ? { ...base, commit: ref } : base;
81
+ }
82
+ /** The path inside a repository: segments without `.`, empty segments or backslashes; `..` is E_PATH_ESCAPE. */
83
+ function gitPath(raw, span) {
84
+ const segs = [];
85
+ for (const seg of raw.split('/')) {
86
+ if (seg === '' || seg === '.')
87
+ continue;
88
+ if (seg === '..')
89
+ throw new SitefileError('E_PATH_ESCAPE', `path '${raw}' in a Git URL must not contain '..'`, span);
90
+ if (seg.includes('\\') || seg.includes('\0'))
91
+ throw new SitefileError('E_PATH', `invalid path '${raw}' in a Git URL`, span);
92
+ segs.push(nfc(seg));
93
+ }
94
+ return segs.join('/');
95
+ }
96
+ /** The URL of a location with the ref replaced by the commit it resolved to (provenance). */
97
+ export function pinnedGitUrl(loc, commit) {
98
+ return `git+${loc.repo}#${commit}${loc.path === '' ? '' : ':' + loc.path}`;
99
+ }
100
+ // ---------------------------------------------------------------- pkt-line
101
+ const enc = new TextEncoder();
102
+ function pkt(line) {
103
+ const body = enc.encode(line);
104
+ const head = enc.encode((body.byteLength + 4).toString(16).padStart(4, '0'));
105
+ const out = new Uint8Array(head.byteLength + body.byteLength);
106
+ out.set(head);
107
+ out.set(body, 4);
108
+ return out;
109
+ }
110
+ const FLUSH = enc.encode('0000');
111
+ const DELIM = enc.encode('0001');
112
+ function concat(parts) {
113
+ let n = 0;
114
+ for (const p of parts)
115
+ n += p.byteLength;
116
+ const out = new Uint8Array(n);
117
+ let o = 0;
118
+ for (const p of parts) {
119
+ out.set(p, o);
120
+ o += p.byteLength;
121
+ }
122
+ return out;
123
+ }
124
+ function gitError(repo, why, span) {
125
+ return new SitefileError('E_GIT', `${redactUrl(repo)}: ${why}`, span);
126
+ }
127
+ function* packets(bytes, repo, span) {
128
+ let i = 0;
129
+ const dec = new TextDecoder();
130
+ while (i < bytes.byteLength) {
131
+ if (i + 4 > bytes.byteLength)
132
+ throw gitError(repo, 'truncated pkt-line', span);
133
+ const head = dec.decode(bytes.subarray(i, i + 4));
134
+ if (!/^[0-9a-f]{4}$/.test(head))
135
+ throw gitError(repo, 'malformed pkt-line (not a Git smart HTTP response?)', span);
136
+ const n = parseInt(head, 16);
137
+ if (n === 0)
138
+ yield { kind: 'flush' };
139
+ else if (n === 1)
140
+ yield { kind: 'delim' };
141
+ else if (n === 2)
142
+ yield { kind: 'end' };
143
+ else if (n < 4)
144
+ throw gitError(repo, 'malformed pkt-line', span);
145
+ else {
146
+ if (i + n > bytes.byteLength)
147
+ throw gitError(repo, 'truncated pkt-line', span);
148
+ yield { kind: 'data', data: bytes.subarray(i + 4, i + n) };
149
+ }
150
+ i += n < 4 ? 4 : n;
151
+ }
152
+ }
153
+ /** Printable text of a server message (control characters dropped, at most 200 characters). */
154
+ function serverText(data) {
155
+ // eslint-disable-next-line no-control-regex
156
+ return new TextDecoder().decode(data).replace(/[\x00-\x1f\x7f]+/g, ' ').trim().slice(0, 200);
157
+ }
158
+ function lineOf(data) {
159
+ const s = new TextDecoder().decode(data);
160
+ return s.endsWith('\n') ? s.slice(0, -1) : s;
161
+ }
162
+ const ADVERTISEMENT = 'application/x-git-upload-pack-advertisement';
163
+ const RESULT = 'application/x-git-upload-pack-result';
164
+ /** Step 1: the v2 capability advertisement. Redirects are followed here only (as Git does). */
165
+ async function connect(repo, t) {
166
+ const trimmed = repo.replace(/\/+$/, '');
167
+ const url = `${trimmed}/info/refs?service=git-upload-pack`;
168
+ const r = await fetchRemote(url, t.policy, t.span, {
169
+ headers: { 'git-protocol': 'version=2', accept: ADVERTISEMENT, 'user-agent': USER_AGENT },
170
+ ...(t.originHeaders ? { originHeaders: t.originHeaders } : {}),
171
+ });
172
+ if (r.mediaType !== ADVERTISEMENT) {
173
+ throw gitError(repo, `not a Git smart HTTP server (content-type ${r.mediaType ?? 'missing'}; Git protocol v2 is required)`, t.span);
174
+ }
175
+ const suffix = '/info/refs?service=git-upload-pack';
176
+ if (!r.finalUrl.endsWith(suffix))
177
+ throw gitError(repo, `redirected to ${redactUrl(r.finalUrl)}, which is not a Git repository URL`, t.span);
178
+ const base = r.finalUrl.slice(0, -suffix.length);
179
+ let v2 = false;
180
+ let fetch;
181
+ let lsRefs = false;
182
+ let format = 'sha1';
183
+ for (const p of packets(r.bytes, repo, t.span)) {
184
+ if (p.kind !== 'data')
185
+ continue;
186
+ const line = lineOf(p.data);
187
+ if (line === 'version 2')
188
+ v2 = true;
189
+ else if (line === 'ls-refs' || line.startsWith('ls-refs='))
190
+ lsRefs = true;
191
+ else if (line === 'fetch' || line.startsWith('fetch='))
192
+ fetch = line.slice('fetch='.length);
193
+ else if (line.startsWith('object-format='))
194
+ format = line.slice('object-format='.length);
195
+ }
196
+ if (!v2 || fetch === undefined || !lsRefs)
197
+ throw gitError(repo, 'the server does not speak Git protocol v2 (ls-refs and fetch are required)', t.span);
198
+ if (format !== 'sha1')
199
+ throw gitError(repo, `object format '${format}' is not supported (only sha1 repositories)`, t.span);
200
+ return { base, shallow: fetch.split(' ').includes('shallow') };
201
+ }
202
+ async function command(remote, repo, body, t) {
203
+ const r = await fetchRemote(`${remote.base}/git-upload-pack`, t.policy, t.span, {
204
+ method: 'POST',
205
+ body,
206
+ headers: { 'git-protocol': 'version=2', 'content-type': 'application/x-git-upload-pack-request', accept: RESULT, 'user-agent': USER_AGENT },
207
+ ...(t.originHeaders ? { originHeaders: t.originHeaders } : {}),
208
+ });
209
+ if (r.mediaType !== RESULT)
210
+ throw gitError(repo, `unexpected content-type ${r.mediaType ?? 'missing'} from git-upload-pack`, t.span);
211
+ return r.bytes;
212
+ }
213
+ /**
214
+ * The commit `ref` names on the server: '' is the remote HEAD; `refs/…` is exact; otherwise
215
+ * `refs/tags/<ref>` then `refs/heads/<ref>` (Git's own order). Annotated tags are peeled.
216
+ */
217
+ export async function resolveRef(repo, ref, t) {
218
+ const remote = await connect(repo, t);
219
+ return lsRef(remote, repo, ref, t);
220
+ }
221
+ async function lsRef(remote, repo, ref, t) {
222
+ const candidates = ref === '' ? ['HEAD'] : ref.startsWith('refs/') ? [ref] : [`refs/tags/${ref}`, `refs/heads/${ref}`];
223
+ const parts = [pkt('command=ls-refs\n'), pkt(`agent=${USER_AGENT}\n`), DELIM, pkt('peel\n'), pkt('symrefs\n')];
224
+ for (const c of candidates)
225
+ parts.push(pkt(`ref-prefix ${c}\n`));
226
+ parts.push(FLUSH);
227
+ const bytes = await command(remote, repo, concat(parts), t);
228
+ const found = new Map();
229
+ for (const p of packets(bytes, repo, t.span)) {
230
+ if (p.kind === 'flush')
231
+ break;
232
+ if (p.kind !== 'data')
233
+ continue;
234
+ const line = lineOf(p.data);
235
+ if (line.startsWith('ERR '))
236
+ throw gitError(repo, `remote error: ${serverText(p.data.subarray(4))}`, t.span);
237
+ const [oid, name, ...attrs] = line.split(' ');
238
+ if (oid === undefined || name === undefined || !GIT_COMMIT_RE.test(oid))
239
+ throw gitError(repo, 'malformed ls-refs response', t.span);
240
+ const peeled = attrs.find((a) => a.startsWith('peeled:'))?.slice('peeled:'.length);
241
+ found.set(name, peeled !== undefined && GIT_COMMIT_RE.test(peeled) ? peeled : oid);
242
+ }
243
+ for (const c of candidates) {
244
+ const oid = found.get(c);
245
+ if (oid !== undefined)
246
+ return oid;
247
+ }
248
+ throw new SitefileError('E_NOT_FOUND', ref === '' ? `${redactUrl(repo)} has no HEAD` : `Git ref '${ref}' not found in ${redactUrl(repo)}`, t.span);
249
+ }
250
+ /**
251
+ * Fetch `commit` (when given) or the commit `ref` names now: one shallow fetch (`deepen 1`) of that
252
+ * commit alone. The pack is checked as described in the module comment.
253
+ */
254
+ export async function fetchGitCommit(repo, ref, commit, t, limits) {
255
+ const remote = await connect(repo, t);
256
+ const want = commit ?? (await lsRef(remote, repo, ref, t));
257
+ const parts = [pkt('command=fetch\n'), pkt(`agent=${USER_AGENT}\n`), DELIM, pkt(`want ${want}\n`)];
258
+ if (remote.shallow)
259
+ parts.push(pkt('deepen 1\n'));
260
+ parts.push(pkt('no-progress\n'), pkt('ofs-delta\n'), pkt('done\n'), FLUSH);
261
+ const bytes = await command(remote, repo, concat(parts), t);
262
+ const pack = packfileOf(bytes, repo, t.span);
263
+ const objects = parsePack(pack, repo, limits, t.span);
264
+ const c = objects.get(want);
265
+ if (!c)
266
+ throw gitError(repo, `the server did not send commit ${want}`, t.span);
267
+ if (c.type !== 'commit')
268
+ throw gitError(repo, `${want} is a ${c.type}, not a commit`, t.span);
269
+ return { commit: want, objects, packBytes: pack.byteLength };
270
+ }
271
+ /** The packfile section of a fetch response, with side-band-64k framing removed. */
272
+ function packfileOf(bytes, repo, span) {
273
+ const chunks = [];
274
+ let section = '';
275
+ let header = true;
276
+ for (const p of packets(bytes, repo, span)) {
277
+ if (p.kind === 'flush' || p.kind === 'end') {
278
+ if (section === 'packfile')
279
+ break;
280
+ header = true;
281
+ continue;
282
+ }
283
+ if (p.kind !== 'data') {
284
+ header = true;
285
+ continue;
286
+ }
287
+ if (header) {
288
+ const line = lineOf(p.data);
289
+ if (line.startsWith('ERR '))
290
+ throw gitError(repo, `remote error: ${serverText(p.data.subarray(4))}`, span);
291
+ section = line;
292
+ header = false;
293
+ if (section !== 'packfile' && section !== 'shallow-info' && section !== 'acknowledgments' && section !== 'wanted-refs') {
294
+ throw gitError(repo, `unexpected fetch response section '${serverText(p.data)}'`, span);
295
+ }
296
+ continue;
297
+ }
298
+ if (section !== 'packfile')
299
+ continue;
300
+ const band = p.data[0];
301
+ if (band === 1)
302
+ chunks.push(p.data.subarray(1));
303
+ else if (band === 3)
304
+ throw gitError(repo, `remote error: ${serverText(p.data.subarray(1))}`, span);
305
+ else if (band !== 2)
306
+ throw gitError(repo, 'malformed side-band packet', span);
307
+ }
308
+ if (section !== 'packfile')
309
+ throw gitError(repo, 'the fetch response has no packfile', span);
310
+ return concat(chunks);
311
+ }
312
+ // ---------------------------------------------------------------- packfile
313
+ const TYPES = ['', 'commit', 'tree', 'blob', 'tag'];
314
+ function sha1Hex(...parts) {
315
+ const h = createHash('sha1');
316
+ for (const p of parts)
317
+ h.update(p);
318
+ return h.digest('hex');
319
+ }
320
+ const hexOf = (b) => Buffer.from(b.buffer, b.byteOffset, b.byteLength).toString('hex');
321
+ /** The SHA-1 object id of `data` as a loose object of `type`. */
322
+ export function gitObjectId(type, data) {
323
+ return sha1Hex(enc.encode(`${type} ${data.byteLength}\0`), data);
324
+ }
325
+ /**
326
+ * Parse a version 2 or 3 packfile into objects by id. Every object is inflated at most to its
327
+ * declared size, deltas are applied (bases must be in the pack: thin packs are not requested), and
328
+ * the total inflated size is bounded by `limits.maxTotalBytes`.
329
+ */
330
+ export function parsePack(pack, repo, limits, span) {
331
+ const bad = (why) => gitError(repo, `malformed packfile: ${why}`, span);
332
+ if (pack.byteLength < 32 || new TextDecoder().decode(pack.subarray(0, 4)) !== 'PACK')
333
+ throw bad('missing PACK header');
334
+ const view = new DataView(pack.buffer, pack.byteOffset, pack.byteLength);
335
+ const version = view.getUint32(4);
336
+ if (version !== 2 && version !== 3)
337
+ throw bad(`unsupported version ${version}`);
338
+ const count = view.getUint32(8);
339
+ const end = pack.byteLength - 20;
340
+ if (sha1Hex(pack.subarray(0, end)) !== hexOf(pack.subarray(end)))
341
+ throw bad('checksum mismatch');
342
+ // Each object takes at least a few bytes, so the count is bounded by the pack size too.
343
+ if (count > end)
344
+ throw bad('object count exceeds the pack size');
345
+ let total = 0;
346
+ const charge = (n) => {
347
+ total += n;
348
+ if (total > limits.maxTotalBytes)
349
+ throw new SitefileError('E_ARCHIVE_LIMIT', `${redactUrl(repo)}: Git objects expand beyond ${limits.maxTotalBytes} bytes`, span);
350
+ };
351
+ const entries = [];
352
+ let pos = 12;
353
+ for (let i = 0; i < count; i++) {
354
+ const offset = pos;
355
+ if (pos >= end)
356
+ throw bad('truncated');
357
+ let c = pack[pos++];
358
+ const type = (c >> 4) & 7;
359
+ let size = c & 15;
360
+ let shift = 4;
361
+ while (c & 0x80) {
362
+ if (pos >= end || shift > 49)
363
+ throw bad('bad object header');
364
+ c = pack[pos++];
365
+ size += (c & 0x7f) * 2 ** shift;
366
+ shift += 7;
367
+ }
368
+ let baseOffset;
369
+ let baseId;
370
+ if (type === 6) {
371
+ if (pos >= end)
372
+ throw bad('truncated');
373
+ c = pack[pos++];
374
+ let n = c & 0x7f;
375
+ while (c & 0x80) {
376
+ if (pos >= end || n > 2 ** 45)
377
+ throw bad('bad delta offset');
378
+ c = pack[pos++];
379
+ n = (n + 1) * 128 + (c & 0x7f);
380
+ }
381
+ baseOffset = offset - n;
382
+ if (baseOffset < 12 || n === 0)
383
+ throw bad('delta base offset out of range');
384
+ }
385
+ else if (type === 7) {
386
+ if (pos + 20 > end)
387
+ throw bad('truncated');
388
+ baseId = hexOf(pack.subarray(pos, pos + 20));
389
+ pos += 20;
390
+ }
391
+ else if (type < 1 || type > 4) {
392
+ throw bad(`unknown object type ${type}`);
393
+ }
394
+ charge(size);
395
+ let res;
396
+ try {
397
+ res = inflateSync(pack.subarray(pos, end), { info: true, maxOutputLength: Math.max(1, size) });
398
+ }
399
+ catch (e) {
400
+ throw bad(`object at offset ${offset} does not inflate (${e.message})`);
401
+ }
402
+ if (res.buffer.byteLength !== size)
403
+ throw bad(`object at offset ${offset} has the wrong size`);
404
+ pos += res.engine.bytesWritten;
405
+ const data = new Uint8Array(res.buffer.buffer, res.buffer.byteOffset, res.buffer.byteLength);
406
+ entries.push({ offset, type, data, ...(baseOffset !== undefined ? { baseOffset } : {}), ...(baseId !== undefined ? { baseId } : {}) });
407
+ }
408
+ if (pos !== end)
409
+ throw bad('trailing data after the last object');
410
+ const byOffset = new Map(entries.map((e) => [e.offset, e]));
411
+ const resolved = new Map();
412
+ const objects = new Map();
413
+ const offsetOf = new Map();
414
+ for (const e of entries) {
415
+ if (e.type <= 4) {
416
+ const o = { type: TYPES[e.type], data: e.data };
417
+ resolved.set(e.offset, o);
418
+ const id = gitObjectId(o.type, o.data);
419
+ objects.set(id, o);
420
+ offsetOf.set(id, e.offset);
421
+ }
422
+ }
423
+ // Deltas: resolve each chain from its first resolved base (iteratively; REF_DELTA bases may come later).
424
+ let progress = true;
425
+ while (progress) {
426
+ progress = false;
427
+ for (const e of entries) {
428
+ if (resolved.has(e.offset))
429
+ continue;
430
+ const chain = [e];
431
+ let base;
432
+ for (;;) {
433
+ const top = chain[chain.length - 1];
434
+ const bOff = top.baseOffset ?? (top.baseId !== undefined ? offsetOf.get(top.baseId) : undefined);
435
+ if (bOff === undefined)
436
+ break; // a REF_DELTA base not seen yet
437
+ base = resolved.get(bOff);
438
+ if (base)
439
+ break;
440
+ const next = byOffset.get(bOff);
441
+ if (!next)
442
+ throw bad('delta base is not an object in the pack');
443
+ if (chain.length > count)
444
+ throw bad('delta cycle');
445
+ chain.push(next);
446
+ }
447
+ if (!base)
448
+ continue;
449
+ for (let i = chain.length - 1; i >= 0; i--) {
450
+ const d = chain[i];
451
+ const data = applyDelta(base.data, d.data, bad);
452
+ charge(data.byteLength);
453
+ base = { type: base.type, data };
454
+ resolved.set(d.offset, base);
455
+ const id = gitObjectId(base.type, base.data);
456
+ objects.set(id, base);
457
+ offsetOf.set(id, d.offset);
458
+ }
459
+ progress = true;
460
+ }
461
+ }
462
+ if (resolved.size !== entries.length)
463
+ throw bad('delta base missing from the pack');
464
+ return objects;
465
+ }
466
+ function applyDelta(base, delta, bad) {
467
+ let pos = 0;
468
+ const varint = () => {
469
+ let n = 0;
470
+ let shift = 0;
471
+ let c;
472
+ do {
473
+ if (pos >= delta.byteLength || shift > 49)
474
+ throw bad('truncated delta');
475
+ c = delta[pos++];
476
+ n += (c & 0x7f) * 2 ** shift;
477
+ shift += 7;
478
+ } while (c & 0x80);
479
+ return n;
480
+ };
481
+ if (varint() !== base.byteLength)
482
+ throw bad('delta base size mismatch');
483
+ const size = varint();
484
+ const out = new Uint8Array(size);
485
+ let o = 0;
486
+ while (pos < delta.byteLength) {
487
+ const op = delta[pos++];
488
+ if (op & 0x80) {
489
+ let off = 0;
490
+ let len = 0;
491
+ for (let i = 0; i < 4; i++)
492
+ if (op & (1 << i))
493
+ off += (delta[pos++] ?? 0) * 2 ** (8 * i);
494
+ for (let i = 0; i < 3; i++)
495
+ if (op & (0x10 << i))
496
+ len += (delta[pos++] ?? 0) * 2 ** (8 * i);
497
+ if (len === 0)
498
+ len = 0x10000;
499
+ if (pos > delta.byteLength || off + len > base.byteLength || o + len > size)
500
+ throw bad('delta copy out of range');
501
+ out.set(base.subarray(off, off + len), o);
502
+ o += len;
503
+ }
504
+ else if (op !== 0) {
505
+ if (pos + op > delta.byteLength || o + op > size)
506
+ throw bad('delta insert out of range');
507
+ out.set(delta.subarray(pos, pos + op), o);
508
+ pos += op;
509
+ o += op;
510
+ }
511
+ else {
512
+ throw bad('reserved delta opcode 0');
513
+ }
514
+ }
515
+ if (o !== size)
516
+ throw bad('delta result size mismatch');
517
+ return out;
518
+ }
519
+ class Repo {
520
+ objs;
521
+ repo;
522
+ span;
523
+ cache = new Map();
524
+ constructor(objs, repo, span) {
525
+ this.objs = objs;
526
+ this.repo = repo;
527
+ this.span = span;
528
+ }
529
+ object(id, type) {
530
+ const o = this.objs.objects.get(id);
531
+ if (!o)
532
+ throw gitError(this.repo, `object ${id} is missing from the pack`, this.span);
533
+ if (o.type !== type)
534
+ throw gitError(this.repo, `object ${id} is a ${o.type}, expected a ${type}`, this.span);
535
+ return o.data;
536
+ }
537
+ rootTree() {
538
+ const text = new TextDecoder().decode(this.object(this.objs.commit, 'commit'));
539
+ const m = /^tree ([0-9a-f]{40})\n/.exec(text);
540
+ if (!m)
541
+ throw gitError(this.repo, `commit ${this.objs.commit} has no tree`, this.span);
542
+ return m[1];
543
+ }
544
+ entries(id, where) {
545
+ const hit = this.cache.get(id);
546
+ if (hit)
547
+ return hit;
548
+ const data = this.object(id, 'tree');
549
+ const out = [];
550
+ const dec = new TextDecoder('utf-8', { fatal: true });
551
+ let i = 0;
552
+ while (i < data.byteLength) {
553
+ const sp = data.indexOf(0x20, i);
554
+ const nul = sp === -1 ? -1 : data.indexOf(0, sp);
555
+ if (sp === -1 || nul === -1 || nul + 21 > data.byteLength)
556
+ throw gitError(this.repo, `malformed tree ${id}`, this.span);
557
+ const mode = new TextDecoder().decode(data.subarray(i, sp));
558
+ let name;
559
+ try {
560
+ name = dec.decode(data.subarray(sp + 1, nul));
561
+ }
562
+ catch {
563
+ throw new SitefileError('E_ENCODING', `${redactUrl(this.repo)}: a file name in '${where || '/'}' is not valid UTF-8`, this.span);
564
+ }
565
+ const shown = where === '' ? name : `${where}/${name}`;
566
+ if (name === '' || name === '.' || name === '..' || name.includes('/') || name.includes('\\') || name.toLowerCase() === '.git') {
567
+ throw new SitefileError('E_ARCHIVE_UNSAFE', `${redactUrl(this.repo)}: unsafe tree entry '${shown}'`, this.span);
568
+ }
569
+ const kind = mode === '40000' ? 'tree' : mode === '100644' || mode === '100755' || mode === '100664' ? 'blob' : mode === '120000' ? 'link' : mode === '160000' ? 'gitlink' : undefined;
570
+ if (!kind)
571
+ throw gitError(this.repo, `tree entry '${shown}' has unknown mode ${mode}`, this.span);
572
+ out.push({ name, kind, exec: mode === '100755', id: hexOf(data.subarray(nul + 1, nul + 21)) });
573
+ i = nul + 21;
574
+ }
575
+ this.cache.set(id, out);
576
+ return out;
577
+ }
578
+ /** The entry named `seg` (compared in NFC) in tree `id`. Two names with one NFC form are E_PATH_COLLISION. */
579
+ child(id, where, seg) {
580
+ const want = nfc(seg);
581
+ const matches = this.entries(id, where).filter((e) => nfc(e.name) === want);
582
+ if (matches.length > 1)
583
+ throw collision(where === '' ? want : `${where}/${want}`, this.repo, this.span);
584
+ return matches[0];
585
+ }
586
+ linkTarget(id) {
587
+ return new TextDecoder().decode(this.object(id, 'blob'));
588
+ }
589
+ }
590
+ function collision(path, repo, span) {
591
+ return new SitefileError('E_PATH_COLLISION', `${redactUrl(repo)}: two names in the repository have the same Unicode NFC form '${path}'; rename one`, span);
592
+ }
593
+ const MAX_LINKS = 40;
594
+ /**
595
+ * Resolve `segs` from the repository root, following symbolic links (relative targets only; at
596
+ * most 40). The result must lie under `boundary` (E_PATH_ESCAPE). `link` names the symlink being
597
+ * resolved for messages (dangling links are E_NOT_FOUND, loops E_UNSUPPORTED_FILE).
598
+ */
599
+ function resolvePath(r, root, segs, boundary, shown, link, repo, span) {
600
+ const escape = () => new SitefileError('E_PATH_ESCAPE', link ? `symlink '${shown}' points outside the imported tree` : `'${shown}' resolves outside the repository`, span);
601
+ const missing = () => new SitefileError('E_NOT_FOUND', link ? `symlink '${shown}' is dangling` : `'${shown}' does not exist in ${redactUrl(repo)}`, span);
602
+ const pending = [...segs];
603
+ const stack = [];
604
+ let cur = { kind: 'tree', id: root, exec: false };
605
+ let hops = 0;
606
+ while (pending.length > 0) {
607
+ const seg = pending.shift();
608
+ if (seg === '' || seg === '.')
609
+ continue;
610
+ if (seg === '..') {
611
+ if (cur.kind !== 'tree')
612
+ throw missing();
613
+ if (stack.length === 0)
614
+ throw escape();
615
+ stack.pop();
616
+ cur = { kind: 'tree', id: stack.length ? stack[stack.length - 1].id : root, exec: false };
617
+ continue;
618
+ }
619
+ if (cur.kind !== 'tree')
620
+ throw missing();
621
+ const where = stack.map((s) => s.name).join('/');
622
+ const e = r.child(cur.id, where, seg);
623
+ if (!e)
624
+ throw missing();
625
+ if (e.kind === 'gitlink')
626
+ throw submodule(where === '' ? e.name : `${where}/${e.name}`, span);
627
+ if (e.kind === 'link') {
628
+ if (++hops > MAX_LINKS)
629
+ throw new SitefileError('E_UNSUPPORTED_FILE', `'${shown}' is a symlink loop`, span);
630
+ const target = r.linkTarget(e.id);
631
+ if (target.startsWith('/') || target === '')
632
+ throw escape();
633
+ pending.unshift(...target.split('/'));
634
+ continue;
635
+ }
636
+ stack.push({ name: nfc(e.name), id: e.id });
637
+ cur = { kind: e.kind, id: e.id, exec: e.exec };
638
+ }
639
+ const real = stack.map((s) => s.name);
640
+ if (boundary.some((b, i) => real[i] !== b) || real.length < boundary.length)
641
+ throw escape();
642
+ return { entry: cur, real };
643
+ }
644
+ function submodule(path, span) {
645
+ return new SitefileError('E_UNSUPPORTED_FILE', `'${path}' is a Git submodule; submodules are not supported (import that repository separately)`, span);
646
+ }
647
+ /**
648
+ * The content of `loc.path` in a fetched commit: a tree's files (symlinks resolved, like a local
649
+ * directory walk: a link must stay inside the imported tree, directory links back to an ancestor
650
+ * are skipped) or one file. Entry names are checked like archive entries; the file count and total
651
+ * size are bounded by `limits`.
652
+ */
653
+ export function selectGitPath(objs, loc, limits, span) {
654
+ const r = new Repo(objs, loc.repo, span);
655
+ const root = r.rootTree();
656
+ const segs = loc.path === '' ? [] : loc.path.split('/');
657
+ const top = resolvePath(r, root, segs, [], loc.path, false, loc.repo, span);
658
+ if (top.entry.kind === 'blob') {
659
+ const bytes = r.object(top.entry.id, 'blob');
660
+ return { kind: 'file', bytes, mode: top.entry.exec ? 0o755 : 0o644, basename: segs[segs.length - 1] ?? '' };
661
+ }
662
+ const files = [];
663
+ let total = 0;
664
+ const walk = (id, real, logical, ancestors) => {
665
+ const where = real.join('/');
666
+ const seen = new Set();
667
+ const entries = [...r.entries(id, where)].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
668
+ for (const e of entries) {
669
+ const name = nfc(e.name);
670
+ const shownLogical = logical === '' ? name : `${logical}/${name}`;
671
+ if (seen.has(name))
672
+ throw collision(shownLogical, loc.repo, span);
673
+ seen.add(name);
674
+ let kind;
675
+ let entryId;
676
+ let exec;
677
+ let childReal;
678
+ if (e.kind === 'gitlink')
679
+ throw submodule(where === '' ? name : `${where}/${name}`, span);
680
+ if (e.kind === 'link') {
681
+ const shown = where === '' ? name : `${where}/${name}`;
682
+ const res = resolvePath(r, root, [...real, name], top.real, shown, true, loc.repo, span);
683
+ kind = res.entry.kind;
684
+ entryId = res.entry.id;
685
+ exec = res.entry.exec;
686
+ childReal = res.real;
687
+ }
688
+ else {
689
+ kind = e.kind;
690
+ entryId = e.id;
691
+ exec = e.exec;
692
+ childReal = [...real, name];
693
+ }
694
+ if (kind === 'blob') {
695
+ const bytes = r.object(entryId, 'blob');
696
+ if (files.length + 1 > limits.maxEntries)
697
+ throw new SitefileError('E_ARCHIVE_LIMIT', `${redactUrl(loc.repo)}: more than ${limits.maxEntries} files`, span);
698
+ total += bytes.byteLength;
699
+ if (total > limits.maxTotalBytes)
700
+ throw new SitefileError('E_ARCHIVE_LIMIT', `${redactUrl(loc.repo)}: files exceed ${limits.maxTotalBytes} bytes`, span);
701
+ files.push(['/' + shownLogical, { bytes, mode: exec ? 0o755 : 0o644 }]);
702
+ }
703
+ else {
704
+ const key = childReal.join('/');
705
+ if (ancestors.has(key))
706
+ continue; // directory symlink cycle
707
+ ancestors.add(key);
708
+ try {
709
+ walk(entryId, childReal, shownLogical, ancestors);
710
+ }
711
+ finally {
712
+ ancestors.delete(key);
713
+ }
714
+ }
715
+ }
716
+ };
717
+ walk(top.entry.id, top.real, '', new Set([top.real.join('/')]));
718
+ return { kind: 'tree', files };
719
+ }
720
+ //# sourceMappingURL=git.js.map