|
| 1 | +// Validate the APK/source locks without selecting replacement versions. |
| 2 | +import assert from "node:assert/strict"; |
| 3 | + |
| 4 | +export function runtimePins(text, arch) { |
| 5 | + assert.ok(["aarch64", "x86_64"].includes(arch), `Unsupported APK architecture: ${arch}`); |
| 6 | + const base = new Map(), overrides = new Map(); |
| 7 | + for (const line of text.trim().split(/\r?\n/)) { |
| 8 | + const match = /^(?:(aarch64|x86_64):)?([a-z0-9][a-z0-9+_.-]*)=([0-9][a-zA-Z0-9._+]*-r\d+)$/.exec(line); |
| 9 | + assert.ok(match, `APK pin must be exact: ${line}`); |
| 10 | + const [, target, name, version] = match; |
| 11 | + const pins = target ? overrides : base; |
| 12 | + const key = target ? `${target}:${name}` : name; |
| 13 | + assert.ok(!pins.has(key), `Duplicate APK pin: ${key}`); |
| 14 | + pins.set(key, version); |
| 15 | + } |
| 16 | + for (const [key, version] of overrides) { |
| 17 | + const [target, name] = key.split(":"); |
| 18 | + assert.ok(base.has(name), `Override names no pinned package: ${key}`); |
| 19 | + if (target === arch) base.set(name, version); |
| 20 | + } |
| 21 | + return base; |
| 22 | +} |
| 23 | + |
| 24 | +const linkedPackages = { |
| 25 | + gcc: ["gcc", "libgcc", "libstdc++", "libstdc++-dev"], |
| 26 | + musl: ["musl-dev"], |
| 27 | + mimalloc2: ["mimalloc2", "mimalloc2-dev", "mimalloc2-insecure"], |
| 28 | + zlib: ["zlib-dev", "zlib-static"], |
| 29 | + zstd: ["zstd", "zstd-dev", "zstd-libs", "zstd-static"], |
| 30 | +}; |
| 31 | + |
| 32 | +export function validateRuntimeSourcePins(text, sources) { |
| 33 | + assert.equal(sources.schema, 1); |
| 34 | + assert.deepEqual(sources.packages.map(p => p.name).sort(), Object.keys(linkedPackages).sort(), "Every linked Alpine dependency needs retained sources"); |
| 35 | + for (const arch of ["aarch64", "x86_64"]) { |
| 36 | + const pins = runtimePins(text, arch); |
| 37 | + for (const source of sources.packages) { |
| 38 | + for (const name of linkedPackages[source.name]) |
| 39 | + assert.equal(pins.get(name), source.version, `${arch}: ${name} differs from retained ${source.name} sources`); |
| 40 | + assert.match(source.aportsCommit, /^[a-f0-9]{40}$/); |
| 41 | + const names = source.files.map(file => file.file); |
| 42 | + assert.equal(new Set(names).size, names.length, `${source.name}: duplicate source file`); |
| 43 | + assert.ok(names.includes("APKBUILD") && names.includes(source.archive), `${source.name}: recipe and archive required`); |
| 44 | + for (const file of source.files) { |
| 45 | + assert.match(file.sha512, /^[a-f0-9]{128}$/); |
| 46 | + assert.ok(!/[\\/]/.test(file.file) && file.file !== "..", "Safe retained source filename"); |
| 47 | + const url = new URL(file.url); |
| 48 | + assert.equal(url.protocol, "https:"); |
| 49 | + if (file.file === "APKBUILD" || url.hostname === "raw.githubusercontent.com") { |
| 50 | + assert.equal(url.hostname, "raw.githubusercontent.com"); |
| 51 | + assert.ok(url.pathname.startsWith(`/alpinelinux/aports/${source.aportsCommit}/`), `${source.name}: recipe/patch must match its immutable aports commit`); |
| 52 | + } |
| 53 | + } |
| 54 | + } |
| 55 | + } |
| 56 | +} |
| 57 | + |
| 58 | +// Expected provenance comes from our lock mapping, never from untrusted index |
| 59 | +// metadata: an absent/wrong origin must not turn a linked-library check off. |
| 60 | +export function runtimeIndexError(name, record, sources) { |
| 61 | + if (!record) return "exact pin missing from official indexes"; |
| 62 | + const expected = Object.entries(linkedPackages).find(([, packages]) => packages.includes(name))?.[0]; |
| 63 | + if (!expected) return null; // Build tools only require the exact available pin. |
| 64 | + if (record.o !== expected) return `APK origin must be ${expected}`; |
| 65 | + const source = sources.packages.find(source => source.name === expected); |
| 66 | + if (!source || source.aportsCommit !== record.c) return "retained recipe differs from APK build commit"; |
| 67 | + return null; |
| 68 | +} |
0 commit comments