feat: publish av-tools 0.1.0
This commit is contained in:
150
scripts/checksum-release.mjs
Normal file
150
scripts/checksum-release.mjs
Normal file
@@ -0,0 +1,150 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { access, lstat, readFile, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const repositoryRoot = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'..'
|
||||
);
|
||||
|
||||
export function sha256(data) {
|
||||
return createHash('sha256').update(data).digest('hex');
|
||||
}
|
||||
|
||||
export async function sha256File(file) {
|
||||
return sha256(await readFile(file));
|
||||
}
|
||||
|
||||
async function requireRegularFile(file, label) {
|
||||
const details = await lstat(file);
|
||||
if (details.isSymbolicLink() || !details.isFile()) {
|
||||
throw new Error(`${label} must be a regular file: ${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function checksumLine(digest, archiveName) {
|
||||
if (!/^[a-f0-9]{64}$/.test(digest)) {
|
||||
throw new Error('SHA-256 digest must be 64 lowercase hexadecimal digits.');
|
||||
}
|
||||
if (
|
||||
typeof archiveName !== 'string' ||
|
||||
archiveName.length === 0 ||
|
||||
archiveName.includes('/') ||
|
||||
archiveName.includes('\\') ||
|
||||
archiveName.includes('\n') ||
|
||||
archiveName.includes('\r')
|
||||
) {
|
||||
throw new Error('Checksum archive name must be one safe basename.');
|
||||
}
|
||||
return `${digest} ${archiveName}\n`;
|
||||
}
|
||||
|
||||
export function parseChecksumLine(contents) {
|
||||
const match = /^([a-f0-9]{64}) ([^/\\\r\n]+)\n?$/.exec(contents);
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
'Checksum sidecar must contain `<lowercase-sha256> <archive-basename>`.'
|
||||
);
|
||||
}
|
||||
return { digest: match[1], archiveName: match[2] };
|
||||
}
|
||||
|
||||
export async function verifyChecksum(archivePath, sidecarPath) {
|
||||
await requireRegularFile(archivePath, 'Release archive');
|
||||
await requireRegularFile(sidecarPath, 'Checksum sidecar');
|
||||
const expected = parseChecksumLine(await readFile(sidecarPath, 'utf8'));
|
||||
if (expected.archiveName !== path.basename(archivePath)) {
|
||||
throw new Error(
|
||||
`Checksum names ${expected.archiveName}, not ${path.basename(archivePath)}.`
|
||||
);
|
||||
}
|
||||
const actual = await sha256File(archivePath);
|
||||
if (actual !== expected.digest) {
|
||||
throw new Error(
|
||||
`Checksum mismatch for ${path.basename(archivePath)}: expected ${expected.digest}, received ${actual}.`
|
||||
);
|
||||
}
|
||||
return actual;
|
||||
}
|
||||
|
||||
export async function writeChecksum(archivePath, options = {}) {
|
||||
const sidecarPath = options.sidecarPath ?? `${archivePath}.sha256`;
|
||||
await requireRegularFile(archivePath, 'Release archive');
|
||||
if (options.force && (await exists(sidecarPath))) {
|
||||
await requireRegularFile(sidecarPath, 'Existing checksum sidecar');
|
||||
}
|
||||
const digest = await sha256File(archivePath);
|
||||
await writeFile(
|
||||
sidecarPath,
|
||||
checksumLine(digest, path.basename(archivePath)),
|
||||
{
|
||||
encoding: 'utf8',
|
||||
flag: options.force ? 'w' : 'wx',
|
||||
}
|
||||
);
|
||||
return { digest, sidecarPath };
|
||||
}
|
||||
|
||||
async function exists(file) {
|
||||
try {
|
||||
await access(file);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error && typeof error === 'object' && error.code === 'ENOENT') {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const packageJson = JSON.parse(
|
||||
await readFile(path.join(repositoryRoot, 'package.json'), 'utf8')
|
||||
);
|
||||
const defaultArchive = path.join(
|
||||
repositoryRoot,
|
||||
'release',
|
||||
`av-tools-${packageJson.version}.zip`
|
||||
);
|
||||
const argumentsList = process.argv.slice(2);
|
||||
const write = argumentsList.includes('--write');
|
||||
const force = argumentsList.includes('--force');
|
||||
const positional = argumentsList.filter(
|
||||
(argument) => argument !== '--write' && argument !== '--force'
|
||||
);
|
||||
if (positional.length > 1) {
|
||||
throw new Error(
|
||||
'Usage: node scripts/checksum-release.mjs [archive.zip] [--write] [--force]'
|
||||
);
|
||||
}
|
||||
|
||||
const archivePath = path.resolve(
|
||||
repositoryRoot,
|
||||
positional[0] ?? defaultArchive
|
||||
);
|
||||
const sidecarPath = `${archivePath}.sha256`;
|
||||
if (write) {
|
||||
const result = await writeChecksum(archivePath, { force, sidecarPath });
|
||||
console.log(
|
||||
`Wrote ${path.relative(repositoryRoot, result.sidecarPath)} (${result.digest})`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!(await exists(sidecarPath))) {
|
||||
throw new Error(
|
||||
`${path.relative(repositoryRoot, sidecarPath)} is missing; pass --write to create it.`
|
||||
);
|
||||
}
|
||||
const digest = await verifyChecksum(archivePath, sidecarPath);
|
||||
console.log(
|
||||
`Verified ${path.relative(repositoryRoot, archivePath)} (${digest})`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
process.argv[1] &&
|
||||
import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href
|
||||
) {
|
||||
await main();
|
||||
}
|
||||
Reference in New Issue
Block a user