@cdxgen/cdxgen 13.3.0 → 13.4.0

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 (255) hide show
  1. package/README.md +9 -6
  2. package/bin/cdxgen.js +90 -26
  3. package/bin/evinse.js +18 -1
  4. package/bin/repl.js +86 -0
  5. package/bin/sign.js +45 -15
  6. package/bin/tracebom.js +5 -1
  7. package/bin/validate.js +84 -18
  8. package/bin/verify.js +155 -86
  9. package/data/helpers/init.gradle +46 -21
  10. package/data/remediations.json +61 -0
  11. package/data/rules/dependency-sources.yaml +67 -0
  12. package/data/rules/scala-evidence.yaml +115 -0
  13. package/lib/audit/index.js +38 -6
  14. package/lib/cli/index.js +113 -54
  15. package/lib/cli/jsBom.js +50 -29
  16. package/lib/cli/jvmBom.js +665 -138
  17. package/lib/cli/managedBom.js +320 -200
  18. package/lib/cli/nativeBom.js +139 -30
  19. package/lib/core/activity.js +12 -0
  20. package/lib/core/env.js +17 -9
  21. package/lib/core/fs.js +3 -2
  22. package/lib/core/httpClient.js +159 -24
  23. package/lib/core/pluginRun.js +184 -0
  24. package/lib/core/processTree.js +100 -0
  25. package/lib/core/runState.js +39 -0
  26. package/lib/core/toml.js +72 -0
  27. package/lib/ecosystems/asarutils.js +4 -2
  28. package/lib/ecosystems/bunutils.js +5 -7
  29. package/lib/ecosystems/cargoCache.js +231 -8
  30. package/lib/ecosystems/cmakeResolver.js +10 -21
  31. package/lib/ecosystems/cppBuildContext.js +631 -0
  32. package/lib/ecosystems/cppEvidence.js +337 -22
  33. package/lib/ecosystems/cppIncludeResolver.js +294 -0
  34. package/lib/ecosystems/denoutils.js +93 -66
  35. package/lib/ecosystems/ecosystems.js +1967 -788
  36. package/lib/ecosystems/golem.js +28 -12
  37. package/lib/ecosystems/gradleutils.js +314 -30
  38. package/lib/ecosystems/jsEvidence.js +85 -24
  39. package/lib/ecosystems/kosi.js +21 -4
  40. package/lib/ecosystems/npmutils.js +64 -0
  41. package/lib/ecosystems/parsers-dotnet.js +567 -151
  42. package/lib/ecosystems/parsers-elm.js +46 -28
  43. package/lib/ecosystems/parsers-gleam.js +3 -4
  44. package/lib/ecosystems/parsers-go.js +324 -61
  45. package/lib/ecosystems/parsers-js.js +92 -26
  46. package/lib/ecosystems/parsers-julia.js +2 -3
  47. package/lib/ecosystems/parsers-jvm.js +204 -12
  48. package/lib/ecosystems/parsers-misc.js +182 -55
  49. package/lib/ecosystems/parsers-python.js +141 -10
  50. package/lib/ecosystems/parsers-rust.js +4 -4
  51. package/lib/ecosystems/parsers-terraform.js +1573 -19
  52. package/lib/ecosystems/pythonutils.js +102 -6
  53. package/lib/ecosystems/remote/huggingface.js +3 -0
  54. package/lib/ecosystems/rubyutils.js +67 -17
  55. package/lib/ecosystems/rusi.js +22 -9
  56. package/lib/ecosystems/sbtutils.js +243 -187
  57. package/lib/ecosystems/scalacliutils.js +342 -0
  58. package/lib/ecosystems/scalasem.js +2618 -0
  59. package/lib/ecosystems/terraformRegistry.js +288 -0
  60. package/lib/ecosystems/utils.js +2 -0
  61. package/lib/ecosystems/vendoredCode.js +156 -0
  62. package/lib/evinser/evinser.js +578 -69
  63. package/lib/helpers/agenticToolutils.js +2 -2
  64. package/lib/helpers/bomSigner.js +576 -128
  65. package/lib/inventory/aiCollector.js +4 -1
  66. package/lib/inventory/analyzer.js +110 -127
  67. package/lib/inventory/atomUtils.js +171 -48
  68. package/lib/inventory/auditCategories.js +7 -0
  69. package/lib/inventory/buildToolRateLimit.js +144 -0
  70. package/lib/inventory/cacheCommands.js +2 -1
  71. package/lib/inventory/cmakeBuildDirs.js +254 -0
  72. package/lib/inventory/deps.js +501 -124
  73. package/lib/inventory/dosai.js +248 -44
  74. package/lib/inventory/dosaiParsers.js +310 -15
  75. package/lib/inventory/dynamic.js +52 -6
  76. package/lib/inventory/evidenceUtils.js +154 -109
  77. package/lib/inventory/fetchBatch.js +574 -38
  78. package/lib/inventory/fetchDiskCache.js +597 -0
  79. package/lib/inventory/forgeEnricher.js +14 -5
  80. package/lib/inventory/jvmLocalRepos.js +917 -0
  81. package/lib/inventory/mcpConfigParser.js +2 -3
  82. package/lib/inventory/parentSelection.js +146 -0
  83. package/lib/inventory/rustFormulationParser.js +3 -4
  84. package/lib/inventory/scalaCoords.js +153 -0
  85. package/lib/inventory/source.js +8 -8
  86. package/lib/inventory/spdx.js +101 -0
  87. package/lib/inventory/traceRunner.js +88 -7
  88. package/lib/inventory/tracedCrypto.js +294 -0
  89. package/lib/managers/docker.js +33 -32
  90. package/lib/managers/oci.js +24 -2
  91. package/lib/parsers/cmakeCache.js +69 -0
  92. package/lib/parsers/cmakeLists.js +16 -2
  93. package/lib/parsers/cmakePresets.js +400 -0
  94. package/lib/parsers/compileCommands.js +557 -0
  95. package/lib/parsers/strictJson.js +322 -0
  96. package/lib/parsers/terraformConfig.js +1073 -0
  97. package/lib/parsers/terraformSource.js +652 -0
  98. package/lib/server/server.js +23 -0
  99. package/lib/stages/postgen/introspection/markers.js +1 -1
  100. package/lib/stages/postgen/postgen.js +15 -1
  101. package/lib/stages/postgen/specVersionCompat.js +32 -0
  102. package/lib/validator/index.js +25 -10
  103. package/lib/validator/reporters/json.js +1 -0
  104. package/package.json +21 -21
  105. package/types/lib/audit/index.d.ts.map +1 -1
  106. package/types/lib/cli/index.d.ts.map +1 -1
  107. package/types/lib/cli/jsBom.d.ts.map +1 -1
  108. package/types/lib/cli/jvmBom.d.ts +24 -1
  109. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  110. package/types/lib/cli/managedBom.d.ts +7 -4
  111. package/types/lib/cli/managedBom.d.ts.map +1 -1
  112. package/types/lib/cli/nativeBom.d.ts +3 -1
  113. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  114. package/types/lib/core/activity.d.ts.map +1 -1
  115. package/types/lib/core/env.d.ts +9 -0
  116. package/types/lib/core/env.d.ts.map +1 -1
  117. package/types/lib/core/fs.d.ts.map +1 -1
  118. package/types/lib/core/httpClient.d.ts +13 -8
  119. package/types/lib/core/httpClient.d.ts.map +1 -1
  120. package/types/lib/core/pluginRun.d.ts +97 -0
  121. package/types/lib/core/pluginRun.d.ts.map +1 -0
  122. package/types/lib/core/processTree.d.ts +36 -0
  123. package/types/lib/core/processTree.d.ts.map +1 -0
  124. package/types/lib/core/runState.d.ts +29 -0
  125. package/types/lib/core/runState.d.ts.map +1 -0
  126. package/types/lib/core/toml.d.ts +20 -0
  127. package/types/lib/core/toml.d.ts.map +1 -0
  128. package/types/lib/ecosystems/asarutils.d.ts.map +1 -1
  129. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  130. package/types/lib/ecosystems/cargoCache.d.ts +12 -0
  131. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -1
  132. package/types/lib/ecosystems/cmakeResolver.d.ts +5 -3
  133. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  134. package/types/lib/ecosystems/cppBuildContext.d.ts +55 -0
  135. package/types/lib/ecosystems/cppBuildContext.d.ts.map +1 -0
  136. package/types/lib/ecosystems/cppEvidence.d.ts +18 -2
  137. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  138. package/types/lib/ecosystems/cppIncludeResolver.d.ts +92 -0
  139. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -0
  140. package/types/lib/ecosystems/denoutils.d.ts +9 -3
  141. package/types/lib/ecosystems/denoutils.d.ts.map +1 -1
  142. package/types/lib/ecosystems/ecosystems.d.ts +62 -13
  143. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  144. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  145. package/types/lib/ecosystems/gradleutils.d.ts +50 -0
  146. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  147. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  148. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  149. package/types/lib/ecosystems/npmutils.d.ts +15 -0
  150. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  151. package/types/lib/ecosystems/parsers-dotnet.d.ts +118 -4
  152. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  153. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -1
  154. package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -1
  155. package/types/lib/ecosystems/parsers-go.d.ts +44 -2
  156. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  157. package/types/lib/ecosystems/parsers-js.d.ts +6 -1
  158. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  159. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -1
  160. package/types/lib/ecosystems/parsers-jvm.d.ts +46 -1
  161. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  162. package/types/lib/ecosystems/parsers-misc.d.ts +3 -1
  163. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  164. package/types/lib/ecosystems/parsers-python.d.ts +53 -3
  165. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  166. package/types/lib/ecosystems/parsers-terraform.d.ts +38 -21
  167. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -1
  168. package/types/lib/ecosystems/pythonutils.d.ts +27 -2
  169. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  170. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
  171. package/types/lib/ecosystems/rubyutils.d.ts +21 -1
  172. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  173. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  174. package/types/lib/ecosystems/sbtutils.d.ts +88 -2
  175. package/types/lib/ecosystems/sbtutils.d.ts.map +1 -1
  176. package/types/lib/ecosystems/scalacliutils.d.ts +56 -0
  177. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -0
  178. package/types/lib/ecosystems/scalasem.d.ts +311 -0
  179. package/types/lib/ecosystems/scalasem.d.ts.map +1 -0
  180. package/types/lib/ecosystems/terraformRegistry.d.ts +9 -0
  181. package/types/lib/ecosystems/terraformRegistry.d.ts.map +1 -0
  182. package/types/lib/ecosystems/utils.d.ts +2 -2
  183. package/types/lib/ecosystems/utils.d.ts.map +1 -1
  184. package/types/lib/ecosystems/vendoredCode.d.ts +32 -0
  185. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -0
  186. package/types/lib/evinser/evinser.d.ts +75 -8
  187. package/types/lib/evinser/evinser.d.ts.map +1 -1
  188. package/types/lib/helpers/bomSigner.d.ts +80 -4
  189. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  190. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  191. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  192. package/types/lib/inventory/atomUtils.d.ts +82 -10
  193. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  194. package/types/lib/inventory/auditCategories.d.ts +3 -0
  195. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  196. package/types/lib/inventory/buildToolRateLimit.d.ts +38 -0
  197. package/types/lib/inventory/buildToolRateLimit.d.ts.map +1 -0
  198. package/types/lib/inventory/cacheCommands.d.ts +2 -1
  199. package/types/lib/inventory/cacheCommands.d.ts.map +1 -1
  200. package/types/lib/inventory/cmakeBuildDirs.d.ts +58 -0
  201. package/types/lib/inventory/cmakeBuildDirs.d.ts.map +1 -0
  202. package/types/lib/inventory/deps.d.ts +59 -1
  203. package/types/lib/inventory/deps.d.ts.map +1 -1
  204. package/types/lib/inventory/dosai.d.ts +57 -8
  205. package/types/lib/inventory/dosai.d.ts.map +1 -1
  206. package/types/lib/inventory/dosaiParsers.d.ts +53 -7
  207. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
  208. package/types/lib/inventory/dynamic.d.ts +10 -0
  209. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  210. package/types/lib/inventory/evidenceUtils.d.ts +19 -4
  211. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  212. package/types/lib/inventory/fetchBatch.d.ts +83 -14
  213. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  214. package/types/lib/inventory/fetchDiskCache.d.ts +169 -0
  215. package/types/lib/inventory/fetchDiskCache.d.ts.map +1 -0
  216. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  217. package/types/lib/inventory/jvmLocalRepos.d.ts +178 -0
  218. package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -0
  219. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  220. package/types/lib/inventory/parentSelection.d.ts +34 -0
  221. package/types/lib/inventory/parentSelection.d.ts.map +1 -0
  222. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
  223. package/types/lib/inventory/scalaCoords.d.ts +80 -0
  224. package/types/lib/inventory/scalaCoords.d.ts.map +1 -0
  225. package/types/lib/inventory/source.d.ts.map +1 -1
  226. package/types/lib/inventory/spdx.d.ts +9 -0
  227. package/types/lib/inventory/spdx.d.ts.map +1 -1
  228. package/types/lib/inventory/traceRunner.d.ts +12 -0
  229. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  230. package/types/lib/inventory/tracedCrypto.d.ts +42 -0
  231. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -0
  232. package/types/lib/managers/docker.d.ts.map +1 -1
  233. package/types/lib/managers/oci.d.ts +3 -1
  234. package/types/lib/managers/oci.d.ts.map +1 -1
  235. package/types/lib/parsers/cmakeCache.d.ts +16 -0
  236. package/types/lib/parsers/cmakeCache.d.ts.map +1 -1
  237. package/types/lib/parsers/cmakeLists.d.ts +2 -1
  238. package/types/lib/parsers/cmakeLists.d.ts.map +1 -1
  239. package/types/lib/parsers/cmakePresets.d.ts +78 -0
  240. package/types/lib/parsers/cmakePresets.d.ts.map +1 -0
  241. package/types/lib/parsers/compileCommands.d.ts +115 -0
  242. package/types/lib/parsers/compileCommands.d.ts.map +1 -0
  243. package/types/lib/parsers/strictJson.d.ts +46 -0
  244. package/types/lib/parsers/strictJson.d.ts.map +1 -0
  245. package/types/lib/parsers/terraformConfig.d.ts +70 -0
  246. package/types/lib/parsers/terraformConfig.d.ts.map +1 -0
  247. package/types/lib/parsers/terraformSource.d.ts +66 -0
  248. package/types/lib/parsers/terraformSource.d.ts.map +1 -0
  249. package/types/lib/server/server.d.ts.map +1 -1
  250. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  251. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  252. package/types/lib/validator/index.d.ts +8 -0
  253. package/types/lib/validator/index.d.ts.map +1 -1
  254. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  255. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +0 -462
@@ -0,0 +1,597 @@
1
+ /**
2
+ * The on-disk response cache of the JS batch pool.
3
+ *
4
+ * cdxrs keeps registry JSON on disk between runs. The requests it cannot carry
5
+ * run on the JS pool instead: crates.io and package.elm-lang.org, which have
6
+ * one rate gate in JS; the GitHub API, whose quota headers cdxrs does not
7
+ * report; HTML pages such as pkg.go.dev and podspecs; and POMs. Without a disk
8
+ * cache of their own, the most tightly limited hosts were asked again on every
9
+ * run. All of them are kept here, in the cdxrs layout and under the same rules:
10
+ *
11
+ * - `<cacheDir>/cdxrs-fetch/<host>/<hash>.json`, written atomically, mode 0600,
12
+ * with the URL redacted;
13
+ * - keyed by URL, method, `Accept` and the auth realm, hashed as cdxrs hashes
14
+ * them, so a JSON entry either side writes serves the other. A text body adds
15
+ * its type to the key, so cdxrs never reads it as JSON;
16
+ * - fresh for `CDXGEN_CACHE_TTL` seconds (24 hours by default, 0 for never);
17
+ * a stale entry with an ETag or Last-Modified is revalidated, and is served
18
+ * when the server fails;
19
+ * - loopback hosts are not cached unless `CDXGEN_CACHE_LOOPBACK=1`;
20
+ * - least recently used entries are removed past 256 MB.
21
+ *
22
+ * Two rules are the JS pool's own. A release POM never changes once published,
23
+ * so it does not expire. A 404 or 410 is kept for the same TTL, or for 24
24
+ * hours when the TTL is 0, under a key of its own that cdxrs never reads.
25
+ *
26
+ * Requests that carry their own headers, such as a registry token, are never
27
+ * cached here, and nothing is read or written during a dry run, for a host
28
+ * outside `CDXGEN_ALLOWED_HOSTS`, or over plain HTTP in secure mode, so the
29
+ * cache never answers a request the policy would have refused. The directory
30
+ * is cdxgen's own, and its files are written as cdxrs writes them, without an
31
+ * activity record per entry.
32
+ */
33
+
34
+ import { Buffer } from "node:buffer";
35
+ import { createHash } from "node:crypto";
36
+ import {
37
+ closeSync,
38
+ mkdirSync,
39
+ openSync,
40
+ readdirSync,
41
+ readFileSync,
42
+ renameSync,
43
+ rmSync,
44
+ statSync,
45
+ utimesSync,
46
+ writeSync,
47
+ } from "node:fs";
48
+ import path from "node:path";
49
+ import process from "node:process";
50
+
51
+ import {
52
+ isAllowedHttpHost,
53
+ isDryRun,
54
+ isSecureMode,
55
+ readEnvironmentVariable,
56
+ } from "../core/activity.js";
57
+ import { fetchCacheDir, resolveCacheDir } from "./cacheDir.js";
58
+
59
+ /** Matches `CACHE_SCHEMA_VERSION` in cdxrs. */
60
+ const CACHE_SCHEMA_VERSION = 2;
61
+
62
+ /** Matches cdxrs's `DEFAULT_CACHE_TTL_SECS`. */
63
+ const DEFAULT_CACHE_TTL_SECS = 24 * 60 * 60;
64
+
65
+ /** Matches cdxrs's `DEFAULT_MAX_CACHE_BYTES`. */
66
+ const MAX_CACHE_BYTES = 256 * 1024 * 1024;
67
+
68
+ /** Bytes written between byte-ceiling passes, as cdxrs computes it. */
69
+ const ENFORCEMENT_SLACK_BYTES = Math.max(MAX_CACHE_BYTES / 8, 8 * 1024 * 1024);
70
+
71
+ /** A temp file older than this is a crashed writer's. */
72
+ const ORPHAN_TEMP_MIN_AGE_SECS = 60 * 60;
73
+
74
+ let bytesSinceEnforcement = 0;
75
+ let writeSequence = 0;
76
+ /** Host directories swept for expired entries by this process. */
77
+ const sweptHostDirs = new Set();
78
+
79
+ /**
80
+ * @typedef {Object} DiskCacheKey
81
+ * @property {string} url Request URL.
82
+ * @property {string} [accept] `Accept` header.
83
+ * @property {string} [authRealm] Identity of the credential, never itself.
84
+ * @property {string} [responseType] `json`, `text` or `buffer`.
85
+ */
86
+
87
+ /**
88
+ * @typedef {Object} DiskCacheEntry
89
+ * @property {number} v Schema version.
90
+ * @property {string} url Redacted URL.
91
+ * @property {string} method Always `GET`.
92
+ * @property {number} status HTTP status.
93
+ * @property {string|null} etag ETag validator.
94
+ * @property {string|null} last_modified Last-Modified validator.
95
+ * @property {number} fetched_at Unix seconds when fetched or revalidated.
96
+ * @property {*} body Response body.
97
+ * @property {boolean} [immutable] Never expires.
98
+ */
99
+
100
+ /**
101
+ * Seconds since the epoch.
102
+ *
103
+ * @returns {number}
104
+ */
105
+ function unixNow() {
106
+ return Math.floor(Date.now() / 1000);
107
+ }
108
+
109
+ /**
110
+ * The TTL for successful responses, in seconds; 0 means they never expire.
111
+ *
112
+ * @returns {number}
113
+ */
114
+ function cacheTtlSecs() {
115
+ const raw = readEnvironmentVariable("CDXGEN_CACHE_TTL");
116
+ if (raw == null || raw === "") {
117
+ return DEFAULT_CACHE_TTL_SECS;
118
+ }
119
+ const ttl = Number.parseInt(raw, 10);
120
+ return Number.isFinite(ttl) && ttl >= 0 ? ttl : DEFAULT_CACHE_TTL_SECS;
121
+ }
122
+
123
+ /**
124
+ * The TTL for a recorded 404 or 410, in seconds. A miss always expires, so a
125
+ * document published later is found.
126
+ *
127
+ * @returns {number}
128
+ */
129
+ function missTtlSecs() {
130
+ return cacheTtlSecs() || DEFAULT_CACHE_TTL_SECS;
131
+ }
132
+
133
+ /**
134
+ * The cdxrs cache key hash: SHA-256 over the schema version and each field
135
+ * length-prefixed, with a distinct marker for an absent field, truncated to
136
+ * 16 bytes. Fields past the four cdxrs uses only ever make a key cdxrs does
137
+ * not read.
138
+ *
139
+ * @param {Array<string|undefined>} fields Key fields, in order.
140
+ * @returns {string} 32 hex characters.
141
+ */
142
+ function hashKeyFields(fields) {
143
+ const hash = createHash("sha256");
144
+ const version = Buffer.alloc(4);
145
+ version.writeUInt32LE(CACHE_SCHEMA_VERSION);
146
+ hash.update(version);
147
+ for (const field of fields) {
148
+ const length = Buffer.alloc(8);
149
+ if (field === undefined || field === null) {
150
+ length.writeBigUInt64LE(0xffffffffffffffffn);
151
+ hash.update(length);
152
+ continue;
153
+ }
154
+ const bytes = Buffer.from(field, "utf-8");
155
+ length.writeBigUInt64LE(BigInt(bytes.length));
156
+ hash.update(length);
157
+ hash.update(bytes);
158
+ }
159
+ return hash.digest("hex").slice(0, 32);
160
+ }
161
+
162
+ /**
163
+ * Hash of a cache key, as cdxrs computes it for a JSON response.
164
+ *
165
+ * @param {DiskCacheKey} key Cache key.
166
+ * @param {{miss?: boolean}} [opts] Whether this is the key of a recorded miss.
167
+ * @returns {string}
168
+ */
169
+ export function diskCacheKeyHash(key, { miss = false } = {}) {
170
+ const fields = [key.url, "GET", key.accept, key.authRealm];
171
+ const responseType = key.responseType || "json";
172
+ if (responseType !== "json") {
173
+ fields.push(`response-type:${responseType}`);
174
+ }
175
+ if (miss) {
176
+ fields.push("miss");
177
+ }
178
+ return hashKeyFields(fields);
179
+ }
180
+
181
+ /**
182
+ * A URL without credentials, query or fragment, as cdxrs stores it.
183
+ *
184
+ * @param {string} url URL.
185
+ * @returns {string}
186
+ */
187
+ function redactUrl(url) {
188
+ try {
189
+ const parsed = new URL(url);
190
+ parsed.username = "";
191
+ parsed.password = "";
192
+ parsed.search = "";
193
+ parsed.hash = "";
194
+ return parsed.toString();
195
+ } catch (_err) {
196
+ return "<unparseable url>";
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Whether a host is a loopback address, which is not cached by default: test
202
+ * doubles there are keyed by ports that are never reused.
203
+ *
204
+ * @param {string} hostname Hostname.
205
+ * @returns {boolean}
206
+ */
207
+ function isLoopbackHost(hostname) {
208
+ if (readEnvironmentVariable("CDXGEN_CACHE_LOOPBACK") === "1") {
209
+ return false;
210
+ }
211
+ const host = hostname.replace(/^\[|\]$/g, "").toLowerCase();
212
+ return (
213
+ host === "localhost" ||
214
+ host === "::1" ||
215
+ (host.startsWith("127.") && host.split(".").length === 4)
216
+ );
217
+ }
218
+
219
+ /**
220
+ * Whether a release POM, which a repository never changes once published.
221
+ *
222
+ * @param {string} url Request URL.
223
+ * @returns {boolean}
224
+ */
225
+ function isReleasePom(url) {
226
+ const pathname = new URL(url).pathname;
227
+ return pathname.endsWith(".pom") && !pathname.includes("-SNAPSHOT");
228
+ }
229
+
230
+ /**
231
+ * Whether the disk cache may serve or store a request.
232
+ *
233
+ * @param {{url: string, headers?: Object, responseType?: string}} request
234
+ * @returns {boolean}
235
+ */
236
+ export function diskCacheable(request) {
237
+ const value = readEnvironmentVariable("CDXGEN_NO_CACHE");
238
+ if (value === "true" || value === "1" || isDryRun) {
239
+ return false;
240
+ }
241
+ if (request.responseType === "buffer") {
242
+ return false;
243
+ }
244
+ if (request.headers && Object.keys(request.headers).length) {
245
+ return false;
246
+ }
247
+ let url;
248
+ try {
249
+ url = new URL(request.url);
250
+ } catch (_err) {
251
+ return false;
252
+ }
253
+ if (!["http:", "https:"].includes(url.protocol)) {
254
+ return false;
255
+ }
256
+ if (isSecureMode && url.protocol !== "https:") {
257
+ return false;
258
+ }
259
+ if (!isAllowedHttpHost(url.hostname) || isLoopbackHost(url.hostname)) {
260
+ return false;
261
+ }
262
+ return Boolean(resolveCacheDir());
263
+ }
264
+
265
+ /**
266
+ * The file a key's entry lives in.
267
+ *
268
+ * @param {DiskCacheKey} key Cache key.
269
+ * @param {{miss?: boolean}} [opts] Whether this is the key of a recorded miss.
270
+ * @returns {string|undefined}
271
+ */
272
+ function entryPath(key, opts) {
273
+ const root = resolveCacheDir();
274
+ if (!root) {
275
+ return undefined;
276
+ }
277
+ const host = new URL(key.url).hostname;
278
+ // A host name is a single path segment; anything else is not written.
279
+ if (!host || host.includes("/") || host.includes("\\") || host === "..") {
280
+ return undefined;
281
+ }
282
+ return path.join(
283
+ fetchCacheDir(root),
284
+ host,
285
+ `${diskCacheKeyHash(key, opts)}.json`,
286
+ );
287
+ }
288
+
289
+ /**
290
+ * Read and parse one entry, dropping it when it cannot be read.
291
+ *
292
+ * @param {string} file Entry file.
293
+ * @returns {DiskCacheEntry|undefined}
294
+ */
295
+ function readEntry(file) {
296
+ let entry;
297
+ try {
298
+ entry = JSON.parse(readFileSync(file, "utf-8"));
299
+ } catch (err) {
300
+ if (err?.code !== "ENOENT") {
301
+ rmSync(file, { force: true });
302
+ }
303
+ return undefined;
304
+ }
305
+ return entry?.v === CACHE_SCHEMA_VERSION ? entry : undefined;
306
+ }
307
+
308
+ /**
309
+ * Whether an entry is still fresh.
310
+ *
311
+ * @param {DiskCacheEntry} entry Entry.
312
+ * @param {number} ttl TTL in seconds; 0 for never expiring.
313
+ * @returns {boolean}
314
+ */
315
+ function isFresh(entry, ttl) {
316
+ if (entry.immutable || ttl === 0) {
317
+ return true;
318
+ }
319
+ return unixNow() - (entry.fetched_at || 0) <= ttl;
320
+ }
321
+
322
+ /**
323
+ * Move an entry to the front of the eviction order without changing it.
324
+ *
325
+ * @param {string} file Entry file.
326
+ * @returns {void}
327
+ */
328
+ function touch(file) {
329
+ try {
330
+ const now = new Date();
331
+ utimesSync(file, now, now);
332
+ } catch (_err) {
333
+ // Only the eviction order suffers.
334
+ }
335
+ }
336
+
337
+ /**
338
+ * @typedef {Object} DiskCacheLookup
339
+ * @property {"fresh"|"stale"|"miss"|"none"} state `fresh` and `miss` are
340
+ * answers; `stale` needs revalidation; `none` means nothing is stored.
341
+ * @property {DiskCacheEntry} [entry] The stored entry.
342
+ */
343
+
344
+ /**
345
+ * Look a request up.
346
+ *
347
+ * @param {DiskCacheKey} key Cache key.
348
+ * @returns {DiskCacheLookup}
349
+ */
350
+ export function diskCacheGet(key) {
351
+ const file = entryPath(key);
352
+ if (!file) {
353
+ return { state: "none" };
354
+ }
355
+ sweepHostDirOnce(path.dirname(file));
356
+ const entry = readEntry(file);
357
+ if (entry) {
358
+ if (isFresh(entry, cacheTtlSecs())) {
359
+ touch(file);
360
+ return { state: "fresh", entry };
361
+ }
362
+ if (entry.etag || entry.last_modified) {
363
+ return { state: "stale", entry };
364
+ }
365
+ }
366
+ const missFile = entryPath(key, { miss: true });
367
+ const miss = missFile ? readEntry(missFile) : undefined;
368
+ if (miss && isFresh(miss, missTtlSecs())) {
369
+ return { state: "miss", entry: miss };
370
+ }
371
+ return { state: "none" };
372
+ }
373
+
374
+ /**
375
+ * Write an entry atomically, readable by its owner only.
376
+ *
377
+ * @param {string} file Entry file.
378
+ * @param {DiskCacheEntry} entry Entry.
379
+ * @returns {void}
380
+ */
381
+ function writeEntry(file, entry) {
382
+ const dir = path.dirname(file);
383
+ try {
384
+ mkdirSync(dir, { recursive: true });
385
+ const data = JSON.stringify(entry);
386
+ writeSequence += 1;
387
+ const temp = `${file.slice(0, -".json".length)}.tmp.${process.pid}.${writeSequence}`;
388
+ const fd = openSync(temp, "wx", 0o600);
389
+ try {
390
+ writeSync(fd, data);
391
+ } finally {
392
+ closeSync(fd);
393
+ }
394
+ renameSync(temp, file);
395
+ bytesSinceEnforcement += Buffer.byteLength(data);
396
+ } catch (_err) {
397
+ // The cache is an optimisation; a failed write costs one request later.
398
+ return;
399
+ }
400
+ if (bytesSinceEnforcement >= ENFORCEMENT_SLACK_BYTES) {
401
+ bytesSinceEnforcement = 0;
402
+ enforceByteCeiling();
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Store a successful response.
408
+ *
409
+ * @param {DiskCacheKey} key Cache key.
410
+ * @param {{statusCode: number, headers?: Object, body: *}} response Response.
411
+ * @returns {void}
412
+ */
413
+ export function diskCachePut(key, response) {
414
+ const file = entryPath(key);
415
+ if (!file) {
416
+ return;
417
+ }
418
+ writeEntry(file, {
419
+ v: CACHE_SCHEMA_VERSION,
420
+ url: redactUrl(key.url),
421
+ method: "GET",
422
+ status: response.statusCode,
423
+ etag: response.headers?.etag || null,
424
+ last_modified: response.headers?.["last-modified"] || null,
425
+ fetched_at: unixNow(),
426
+ body: response.body,
427
+ ...(response.statusCode === 200 && isReleasePom(key.url)
428
+ ? { immutable: true }
429
+ : {}),
430
+ });
431
+ const missFile = entryPath(key, { miss: true });
432
+ if (missFile) {
433
+ rmSync(missFile, { force: true });
434
+ }
435
+ }
436
+
437
+ /**
438
+ * Record that a request answered 404 or 410, and forget any body stored for it.
439
+ *
440
+ * @param {DiskCacheKey} key Cache key.
441
+ * @param {number} status HTTP status.
442
+ * @returns {void}
443
+ */
444
+ export function diskCachePutMiss(key, status) {
445
+ const file = entryPath(key, { miss: true });
446
+ if (!file) {
447
+ return;
448
+ }
449
+ writeEntry(file, {
450
+ v: CACHE_SCHEMA_VERSION,
451
+ url: redactUrl(key.url),
452
+ method: "GET",
453
+ status,
454
+ etag: null,
455
+ last_modified: null,
456
+ fetched_at: unixNow(),
457
+ body: null,
458
+ });
459
+ const bodyFile = entryPath(key);
460
+ if (bodyFile) {
461
+ rmSync(bodyFile, { force: true });
462
+ }
463
+ }
464
+
465
+ /**
466
+ * Mark a stale entry fresh again after a 304.
467
+ *
468
+ * @param {DiskCacheKey} key Cache key.
469
+ * @param {DiskCacheEntry} entry The stale entry.
470
+ * @param {Object} [headers] Headers of the 304, which may carry new validators.
471
+ * @returns {void}
472
+ */
473
+ export function diskCacheRefresh(key, entry, headers = {}) {
474
+ const file = entryPath(key);
475
+ if (!file) {
476
+ return;
477
+ }
478
+ writeEntry(file, {
479
+ ...entry,
480
+ etag: headers.etag || entry.etag,
481
+ last_modified: headers["last-modified"] || entry.last_modified,
482
+ fetched_at: unixNow(),
483
+ });
484
+ }
485
+
486
+ /**
487
+ * Remove a host directory's expired entries, once per process, as cdxrs does
488
+ * before its first read there.
489
+ *
490
+ * @param {string} hostDir Host directory.
491
+ * @returns {void}
492
+ */
493
+ function sweepHostDirOnce(hostDir) {
494
+ if (sweptHostDirs.has(hostDir)) {
495
+ return;
496
+ }
497
+ sweptHostDirs.add(hostDir);
498
+ const ttl = cacheTtlSecs();
499
+ const now = unixNow();
500
+ let names;
501
+ try {
502
+ names = readdirSync(hostDir);
503
+ } catch (_err) {
504
+ return;
505
+ }
506
+ for (const name of names) {
507
+ const file = path.join(hostDir, name);
508
+ let stat;
509
+ try {
510
+ stat = statSync(file);
511
+ } catch (_err) {
512
+ continue;
513
+ }
514
+ const ageSecs = now - Math.floor(stat.mtimeMs / 1000);
515
+ if (name.includes(".tmp.")) {
516
+ if (ageSecs > ORPHAN_TEMP_MIN_AGE_SECS) {
517
+ rmSync(file, { force: true });
518
+ }
519
+ continue;
520
+ }
521
+ // Recent use moves the mtime forward, so only an older file can be
522
+ // worth reading. The entry's own fetch time decides.
523
+ if (!name.endsWith(".json") || ageSecs <= missTtlSecs()) {
524
+ continue;
525
+ }
526
+ const entry = readEntry(file);
527
+ if (!entry || entry.immutable || entry.etag || entry.last_modified) {
528
+ continue;
529
+ }
530
+ const entryTtl = entry.status >= 400 ? missTtlSecs() : ttl;
531
+ if (!isFresh(entry, entryTtl)) {
532
+ rmSync(file, { force: true });
533
+ }
534
+ }
535
+ }
536
+
537
+ /**
538
+ * Evict the least recently used entries until the cache is within its ceiling.
539
+ *
540
+ * @returns {void}
541
+ */
542
+ function enforceByteCeiling() {
543
+ const root = resolveCacheDir();
544
+ if (!root) {
545
+ return;
546
+ }
547
+ const files = [];
548
+ let total = 0;
549
+ const fetchDir = fetchCacheDir(root);
550
+ let hosts;
551
+ try {
552
+ hosts = readdirSync(fetchDir);
553
+ } catch (_err) {
554
+ return;
555
+ }
556
+ for (const host of hosts) {
557
+ let names;
558
+ try {
559
+ names = readdirSync(path.join(fetchDir, host));
560
+ } catch (_err) {
561
+ continue;
562
+ }
563
+ for (const name of names) {
564
+ const file = path.join(fetchDir, host, name);
565
+ try {
566
+ const stat = statSync(file);
567
+ if (stat.isFile()) {
568
+ files.push({ file, size: stat.size, mtimeMs: stat.mtimeMs });
569
+ total += stat.size;
570
+ }
571
+ } catch (_err) {
572
+ // A concurrent eviction removed it.
573
+ }
574
+ }
575
+ }
576
+ if (total <= MAX_CACHE_BYTES) {
577
+ return;
578
+ }
579
+ files.sort((a, b) => a.mtimeMs - b.mtimeMs || a.file.localeCompare(b.file));
580
+ for (const { file, size } of files) {
581
+ if (total <= MAX_CACHE_BYTES) {
582
+ break;
583
+ }
584
+ rmSync(file, { force: true });
585
+ total -= size;
586
+ }
587
+ }
588
+
589
+ /**
590
+ * Forget which host directories were swept. Tests only.
591
+ *
592
+ * @returns {void}
593
+ */
594
+ export function resetDiskCacheState() {
595
+ sweptHostDirs.clear();
596
+ bytesSinceEnforcement = 0;
597
+ }
@@ -27,17 +27,26 @@ async function prefetchCommitLookups(urls, headers) {
27
27
  /**
28
28
  * Read a prefetched commit lookup as the response shape the loop expects.
29
29
  *
30
- * The loop tests `statusCode === 200`, and a batch entry records only whether
31
- * the request succeeded, so a hit becomes a 200 and anything else is left for
32
- * the caller to fetch itself.
30
+ * The loop tests `statusCode === 200`, and a batch entry records whether the
31
+ * request succeeded. A hit becomes a 200, and a definite answer with an
32
+ * error status becomes that status, which the loop's own check treats as "no
33
+ * pull request" without asking again: a 404 is the forge saying the commit
34
+ * has none, and a 429 or 5xx is the answer after the batch's own retries.
35
+ * Only a transport failure, with no status, is left to the caller to retry.
33
36
  *
34
- * @param {Map<string, {ok?: boolean, body?: *}>} prefetched
37
+ * @param {Map<string, {ok?: boolean, status?: number, body?: *}>} prefetched
35
38
  * @param {string} url
36
39
  * @returns {{statusCode: number, body: *}|undefined}
37
40
  */
38
41
  function prefetchedCommitLookup(prefetched, url) {
39
42
  const entry = prefetched.get(url);
40
- return entry?.ok ? { body: entry.body, statusCode: 200 } : undefined;
43
+ if (entry?.ok) {
44
+ return { body: entry.body, statusCode: 200 };
45
+ }
46
+ if (entry && entry.status !== undefined) {
47
+ return { body: entry.body, statusCode: entry.status };
48
+ }
49
+ return undefined;
41
50
  }
42
51
 
43
52
  // Only accept single path segments made of git-safe characters, and never the