mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
fix(release): align public Snap verifier with the shipped snap layout
The publish-snap verifier had never run against a real release and encoded three stale expectations that the tag build's own validators do not share: - it required the app under usr/lib/iptvnator inside the snap, while Electron Builder's snap target ships the app at the snap root (/iptvnator.bin, /resources/**) — the layout the packaged smoke tests exercise; - it validated the source archive's runtime manifest with the raw source-build validator, but the archive carries the STAGED manifest (origin "vendored-lgpl" + sourceBuildOrigin) written by stage-runtime.mjs; the staged envelope is now checked explicitly and the remaining fields still go through the shared validator via an origin projection; - it deep-equaled the snap's bundled sourceRuntime against the archive manifest, but the snap bundles the builder view (no staging envelope); the binding now projects the envelope away first. Verified end-to-end in a Linux container against the real v0.23.0 release assets: release-snap-assets.cjs verify now passes and emits the sealed snapshot receipt. Regression tests cover the legacy usr/lib layout and staged-envelope mismatches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f52337d05e
commit
0adf562177
5 files changed
+138
-35
No files matched your search
@@ -1058,8 +1058,9 @@ hashes, the exact VCS-free libplacebo tree inventory/digest, and byte-identical
|
||||
tooling from the released tag. Checkout and both artifact-transfer actions use
|
||||
full pinned commits, and checkout sets `persist-credentials: false`.
|
||||
The bounded SquashFS preflight and extraction then require the canonical
|
||||
`/usr/lib/iptvnator` layout and reuse the static package validator for every
|
||||
selected Snap. The public-release verifier separately reapplies the exact
|
||||
snap-root layout (Electron app at `/`, so `/iptvnator.bin` and
|
||||
`/resources/**` — the layout Electron Builder's Snap target produces) and
|
||||
reuse the static package validator for every selected Snap. The public-release verifier separately reapplies the exact
|
||||
strict `meta/snap.yaml` graphics/shared-memory/layout contract and enumerates
|
||||
the extracted `resources/app.asar`; any archived
|
||||
`electron-backend/native/**` entry fails before Store publication. The bounded
|
||||
|
||||
@@ -395,8 +395,9 @@ inventory/digest, released tooling, and runtime manifest. Checkout and both
|
||||
artifact-transfer actions use full pinned commits, and checkout does not
|
||||
persist its repository credential. The verifier bounds
|
||||
source members, the archive, SquashFS listing, extracted size, entry count,
|
||||
command time, and job time; every Snap must use the canonical
|
||||
`/usr/lib/iptvnator` layout and pass the existing static package validator.
|
||||
command time, and job time; every Snap must use the canonical snap-root
|
||||
layout (Electron app at `/`, so `/iptvnator.bin` and `/resources/**`) and
|
||||
pass the existing static package validator.
|
||||
The public-release boundary also reapplies the exact strict
|
||||
`meta/snap.yaml` graphics/shared-memory/layout contract and enumerates the
|
||||
extracted `resources/app.asar`, rejecting any archived
|
||||
|
||||
@@ -187,6 +187,9 @@ function createSourceBindingFixture() {
|
||||
const repositoryRevision = 'a'.repeat(40);
|
||||
const archiveSha256 = 'b'.repeat(64);
|
||||
const sourceRuntime = {
|
||||
origin: 'vendored-lgpl',
|
||||
sourceBuildOrigin: 'vendored-lgpl-source-build',
|
||||
stagedAt: '2026-07-18T01:00:00.000Z',
|
||||
generatedAt: '2026-07-18T00:00:00.000Z',
|
||||
packages: {
|
||||
ffmpeg: {
|
||||
@@ -238,6 +241,14 @@ function createSourceBindingFixture() {
|
||||
archiveFiles: sourceIndex.archives,
|
||||
compliance: sourceCompliance,
|
||||
};
|
||||
// The snap bundles the from-source builder view of the staged manifest:
|
||||
// origin restored, staging envelope (sourceBuildOrigin, stagedAt) absent.
|
||||
const snapSourceRuntime = {
|
||||
...sourceRuntime,
|
||||
origin: 'vendored-lgpl-source-build',
|
||||
};
|
||||
delete snapSourceRuntime.sourceBuildOrigin;
|
||||
delete snapSourceRuntime.stagedAt;
|
||||
const snapPayloads = {
|
||||
'IPTVnator-amd64.snap': {
|
||||
assetName: 'IPTVnator-amd64.snap',
|
||||
@@ -255,7 +266,7 @@ function createSourceBindingFixture() {
|
||||
sha256: 'd'.repeat(64),
|
||||
repositoryRevision,
|
||||
},
|
||||
sourceRuntime,
|
||||
sourceRuntime: snapSourceRuntime,
|
||||
},
|
||||
},
|
||||
'IPTVnator-arm64.snap': {
|
||||
@@ -316,6 +327,37 @@ test('binds source metadata and checksums to every selected Snap before publicat
|
||||
inspectedSnaps,
|
||||
selection.snapAssets.map(({ name }) => name)
|
||||
);
|
||||
|
||||
for (const [field, value] of [
|
||||
['origin', 'vendored-lgpl-source-build'],
|
||||
['sourceBuildOrigin', undefined],
|
||||
]) {
|
||||
const stagedMismatch = structuredClone(fixture.sourceInspection);
|
||||
if (value === undefined) {
|
||||
delete stagedMismatch.sourceRuntime[field];
|
||||
} else {
|
||||
stagedMismatch.sourceRuntime[field] = value;
|
||||
}
|
||||
assert.throws(
|
||||
() =>
|
||||
helper.verifySnapReleaseCorrespondence(
|
||||
selection,
|
||||
temporaryRoot,
|
||||
{
|
||||
expectedRepositoryRevision: fixture.repositoryRevision,
|
||||
expectedSourceSnapshotSha256:
|
||||
fixture.expectedSourceSnapshotSha256,
|
||||
inspectSourceArchive: () => stagedMismatch,
|
||||
inspectSnapPayload: (_snapPath, asset) =>
|
||||
fixture.snapPayloads[asset.name],
|
||||
validateRuntimeManifest: () => [],
|
||||
}
|
||||
),
|
||||
field === 'origin'
|
||||
? /origin must be "vendored-lgpl"/
|
||||
: /sourceBuildOrigin must be "vendored-lgpl-source-build"/
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('publishes only a stable verified asset snapshot with an exact receipt', async (t) => {
|
||||
@@ -1180,7 +1222,7 @@ test('hashes the final source archive bytes and reads the exact packaged Snap bi
|
||||
fs.unlinkSync(oversizedMemberPath);
|
||||
|
||||
const snapSourceRoot = path.join(temporaryRoot, 'snap-source');
|
||||
const appRoot = path.join(snapSourceRoot, 'usr', 'lib', 'iptvnator');
|
||||
const appRoot = snapSourceRoot;
|
||||
const nativeRoot = path.join(
|
||||
appRoot,
|
||||
'resources',
|
||||
@@ -1193,10 +1235,16 @@ test('hashes the final source archive bytes and reads the exact packaged Snap bi
|
||||
electronHeader.set([0x7f, 0x45, 0x4c, 0x46, 2, 1]);
|
||||
electronHeader.writeUInt16LE(62, 18);
|
||||
fs.writeFileSync(path.join(appRoot, 'iptvnator.bin'), electronHeader);
|
||||
const packagedSourceRuntime = {
|
||||
...sourceRuntime,
|
||||
origin: 'vendored-lgpl-source-build',
|
||||
};
|
||||
delete packagedSourceRuntime.sourceBuildOrigin;
|
||||
delete packagedSourceRuntime.stagedAt;
|
||||
const packagedManifest = {
|
||||
...fixture.snapPayloads['IPTVnator-amd64.snap'].manifest,
|
||||
sourceArchive,
|
||||
sourceRuntime,
|
||||
sourceRuntime: packagedSourceRuntime,
|
||||
};
|
||||
fs.writeFileSync(
|
||||
path.join(nativeRoot, 'embedded-mpv-runtime.json'),
|
||||
@@ -1326,7 +1374,7 @@ test('hashes the final source archive bytes and reads the exact packaged Snap bi
|
||||
assert.equal(staticValidationCalls.length, 1);
|
||||
assert.ok(
|
||||
staticValidationCalls[0].resourceDirectory.endsWith(
|
||||
['payload', 'usr', 'lib', 'iptvnator', 'resources'].join(path.sep)
|
||||
['payload', 'resources'].join(path.sep)
|
||||
)
|
||||
);
|
||||
assert.deepEqual(staticValidationCalls[0].options, {
|
||||
@@ -1484,6 +1532,41 @@ test('hashes the final source archive bytes and reads the exact packaged Snap bi
|
||||
/canonical frame-copy manifest/i
|
||||
);
|
||||
|
||||
const legacySourceRoot = path.join(temporaryRoot, 'legacy-snap-source');
|
||||
const legacyAppRoot = path.join(
|
||||
legacySourceRoot,
|
||||
'usr',
|
||||
'lib',
|
||||
'iptvnator'
|
||||
);
|
||||
fs.cpSync(appRoot, legacyAppRoot, { recursive: true });
|
||||
assert.throws(
|
||||
() =>
|
||||
sourceBindingHelper.inspectSnapPayload(
|
||||
snapPath,
|
||||
{ id: 1, name: 'IPTVnator-amd64.snap' },
|
||||
{
|
||||
runCommand: (command, args) => {
|
||||
assert.equal(command, 'unsquashfs');
|
||||
if (args[0] === '-lln') {
|
||||
return syntheticSquashfsListing();
|
||||
}
|
||||
const destinationIndex = args.indexOf('-dest') + 1;
|
||||
fs.cpSync(legacySourceRoot, args[destinationIndex], {
|
||||
recursive: true,
|
||||
});
|
||||
return '';
|
||||
},
|
||||
validatePackagedEmbeddedMpv: () => {
|
||||
throw new Error(
|
||||
'Static validation must not inspect a legacy usr/lib layout.'
|
||||
);
|
||||
},
|
||||
}
|
||||
),
|
||||
/canonical frame-copy manifest/i
|
||||
);
|
||||
|
||||
const manifestPath = path.join(nativeRoot, 'embedded-mpv-runtime.json');
|
||||
fs.truncateSync(manifestPath, 16 * 1024 * 1024 + 1);
|
||||
let oversizedStaticValidationCalls = 0;
|
||||
|
||||
@@ -41,6 +41,12 @@ const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
||||
const SAFE_BASENAME_PATTERN = /^[A-Za-z0-9_+.-]+$/;
|
||||
const FRAME_COPY_MANIFEST_NAME = 'embedded-mpv-runtime.json';
|
||||
const FRAME_COPY_UNAVAILABLE_MARKER_NAME = 'embedded-mpv-unavailable.txt';
|
||||
// The archive carries the STAGED runtime manifest (`stage-runtime.mjs`):
|
||||
// origin "vendored-lgpl", with the from-source builder's origin preserved in
|
||||
// sourceBuildOrigin. The shared source-build validator still checks every
|
||||
// other field through the projected origin below.
|
||||
const STAGED_RUNTIME_ORIGIN = 'vendored-lgpl';
|
||||
const SOURCE_BUILD_RUNTIME_ORIGIN = 'vendored-lgpl-source-build';
|
||||
const SNAP_RELEASE_BOUNDARY_SCHEMA_VERSION = 1;
|
||||
const SNAP_RELEASE_BOUNDARY_HELPER_PATH = path.join(
|
||||
__dirname,
|
||||
@@ -1116,9 +1122,6 @@ function inspectSnapPayload(
|
||||
const payloads = collectSnapNativePayloads(extractionRoot);
|
||||
const canonicalNativeDirectory = path.join(
|
||||
extractionRoot,
|
||||
'usr',
|
||||
'lib',
|
||||
'iptvnator',
|
||||
'resources',
|
||||
'app.asar.unpacked',
|
||||
'electron-backend',
|
||||
@@ -1148,20 +1151,8 @@ function inspectSnapPayload(
|
||||
);
|
||||
}
|
||||
const payloadPath = canonicalPayloads[0];
|
||||
const resourcesDirectory = path.join(
|
||||
extractionRoot,
|
||||
'usr',
|
||||
'lib',
|
||||
'iptvnator',
|
||||
'resources'
|
||||
);
|
||||
const electronPath = path.join(
|
||||
extractionRoot,
|
||||
'usr',
|
||||
'lib',
|
||||
'iptvnator',
|
||||
'iptvnator.bin'
|
||||
);
|
||||
const resourcesDirectory = path.join(extractionRoot, 'resources');
|
||||
const electronPath = path.join(extractionRoot, 'iptvnator.bin');
|
||||
const electronStat = fs.lstatSync(electronPath);
|
||||
if (
|
||||
!electronStat.isFile() ||
|
||||
@@ -1562,7 +1553,26 @@ function verifySnapReleaseSourceBinding(
|
||||
sourceIndex,
|
||||
sourceRuntime,
|
||||
} = sourceInspection;
|
||||
const runtimeErrors = validateRuntimeManifest(sourceRuntime);
|
||||
const stagedRuntimeErrors = [];
|
||||
if (sourceRuntime?.origin !== STAGED_RUNTIME_ORIGIN) {
|
||||
stagedRuntimeErrors.push(
|
||||
`Linux runtime manifest origin must be "${STAGED_RUNTIME_ORIGIN}".`
|
||||
);
|
||||
}
|
||||
if (sourceRuntime?.sourceBuildOrigin !== SOURCE_BUILD_RUNTIME_ORIGIN) {
|
||||
stagedRuntimeErrors.push(
|
||||
`Linux runtime manifest sourceBuildOrigin must be "${SOURCE_BUILD_RUNTIME_ORIGIN}".`
|
||||
);
|
||||
}
|
||||
const validatorErrors = isObject(sourceRuntime)
|
||||
? validateRuntimeManifest({
|
||||
...sourceRuntime,
|
||||
origin: SOURCE_BUILD_RUNTIME_ORIGIN,
|
||||
})
|
||||
: validateRuntimeManifest(sourceRuntime);
|
||||
const runtimeErrors = Array.isArray(validatorErrors)
|
||||
? [...stagedRuntimeErrors, ...validatorErrors]
|
||||
: validatorErrors;
|
||||
if (!Array.isArray(runtimeErrors) || runtimeErrors.length > 0) {
|
||||
throw new Error(
|
||||
`Source archive runtime manifest is invalid${
|
||||
@@ -1742,7 +1752,22 @@ function verifySnapReleaseSourceBinding(
|
||||
`x64 Snap ${payload.assetName} has an invalid frame-copy source archive binding.`
|
||||
);
|
||||
}
|
||||
if (!isDeepStrictEqual(manifest.sourceRuntime, sourceRuntime)) {
|
||||
// The snap bundles the from-source builder view of the runtime
|
||||
// manifest, while the archive carries its staged envelope
|
||||
// (origin renamed to "vendored-lgpl", sourceBuildOrigin and
|
||||
// stagedAt added). Project the envelope away before binding.
|
||||
const expectedSnapSourceRuntime = {
|
||||
...sourceRuntime,
|
||||
origin: SOURCE_BUILD_RUNTIME_ORIGIN,
|
||||
};
|
||||
delete expectedSnapSourceRuntime.sourceBuildOrigin;
|
||||
delete expectedSnapSourceRuntime.stagedAt;
|
||||
if (
|
||||
!isDeepStrictEqual(
|
||||
manifest.sourceRuntime,
|
||||
expectedSnapSourceRuntime
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`Snap source runtime does not match source archive: ${payload.assetName}`
|
||||
);
|
||||
|
||||
@@ -18,14 +18,7 @@ export function validateExtractedSnapReleaseBoundary(
|
||||
{ asarListPackage = listAsarPackageEntries } = {}
|
||||
) {
|
||||
const errors = [...validateExtractedSnapMetadata(extractionRoot)];
|
||||
const asarPath = path.join(
|
||||
extractionRoot,
|
||||
'usr',
|
||||
'lib',
|
||||
'iptvnator',
|
||||
'resources',
|
||||
'app.asar'
|
||||
);
|
||||
const asarPath = path.join(extractionRoot, 'resources', 'app.asar');
|
||||
let asarStat;
|
||||
try {
|
||||
asarStat = fs.lstatSync(asarPath);
|
||||
|
||||
Reference in new issue
Block a user