mirror of
https://github.com/nimbold/Firelink.git
synced 2026-10-25 21:19:49 +00:00
eea313b45f
- Inspect launcher permissions from stored SquashFS metadata. - Pin and checksum the runtime embedded during AppImage packaging. - Cover metadata validation and release tool inputs with regression tests.
187 lines
6.4 KiB
JavaScript
187 lines
6.4 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
const APP_RUN_WRAPPED_EXEC = /^[\t ]*exec[\t ]+"?\$this_dir"?\/AppRun\.wrapped(?=[\t ]|$)/m;
|
|
const APP_RUN_GTK_HOOK = /^[\t ]*source[\t ]+"\$this_dir"\/apprun-hooks\/"linuxdeploy-plugin-gtk\.sh"(?=[\t ]|$)/m;
|
|
|
|
function appDirEntry(root, name, label, type) {
|
|
const entry = name === '.' ? root : path.join(root, name);
|
|
let info;
|
|
try {
|
|
info = fs.lstatSync(entry);
|
|
} catch (error) {
|
|
throw new Error(`${label} is missing or cannot be inspected: ${error.message}`, { cause: error });
|
|
}
|
|
|
|
if (info.isSymbolicLink()) {
|
|
throw new Error(`${label} must not be a symbolic link.`);
|
|
}
|
|
if (type === 'directory' && !info.isDirectory()) {
|
|
throw new Error(`${label} is not a directory.`);
|
|
}
|
|
if (type === 'file' && !info.isFile()) {
|
|
throw new Error(`${label} is not a regular file.`);
|
|
}
|
|
|
|
return entry;
|
|
}
|
|
|
|
function permissionBits(entry) {
|
|
return fs.lstatSync(entry).mode & 0o777;
|
|
}
|
|
|
|
function assertOtherReadableExecutable(entry, label) {
|
|
const mode = permissionBits(entry);
|
|
if ((mode & 0o005) !== 0o005) {
|
|
throw new Error(
|
|
`${label} must be readable and executable by other users for root-owned AppImage mounts; found mode 0${mode.toString(8).padStart(3, '0')}.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertOtherDirectoryAccess(entry, label) {
|
|
const mode = permissionBits(entry);
|
|
if ((mode & 0o005) !== 0o005) {
|
|
throw new Error(
|
|
`${label} must be readable and traversable by other users for root-owned AppImage mounts; found mode 0${mode.toString(8).padStart(3, '0')}.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertOtherReadable(entry, label) {
|
|
const mode = permissionBits(entry);
|
|
if ((mode & 0o004) !== 0o004) {
|
|
throw new Error(
|
|
`${label} must be readable by other users for root-owned AppImage mounts; found mode 0${mode.toString(8).padStart(3, '0')}.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function chmod(entry, mode, label) {
|
|
try {
|
|
fs.chmodSync(entry, mode);
|
|
} catch (error) {
|
|
throw new Error(`${label} permissions could not be normalized: ${error.message}`, { cause: error });
|
|
}
|
|
}
|
|
|
|
function checkAppRunPermissions(root, label, normalize) {
|
|
const appDir = appDirEntry(root, '.', `${label} AppDir root`, 'directory');
|
|
const appRun = appDirEntry(root, 'AppRun', `${label} AppRun`, 'file');
|
|
const appRunWrapped = appDirEntry(root, 'AppRun.wrapped', `${label} AppRun.wrapped launcher`, 'file');
|
|
const hookDirectory = appDirEntry(root, 'apprun-hooks', `${label} AppRun hook directory`, 'directory');
|
|
const gtkHook = appDirEntry(
|
|
root,
|
|
path.join('apprun-hooks', 'linuxdeploy-plugin-gtk.sh'),
|
|
`${label} GTK AppRun hook`,
|
|
'file',
|
|
);
|
|
|
|
const appRunContents = fs.readFileSync(appRun, 'utf8');
|
|
if (!APP_RUN_WRAPPED_EXEC.test(appRunContents)) {
|
|
throw new Error(
|
|
`${label} AppRun does not invoke the supported AppRun.wrapped launcher; inspect the new launcher structure before packaging.`,
|
|
);
|
|
}
|
|
if (!APP_RUN_GTK_HOOK.test(appRunContents)) {
|
|
throw new Error(
|
|
`${label} AppRun does not source the supported GTK hook; inspect the new launcher structure before packaging.`,
|
|
);
|
|
}
|
|
|
|
if (normalize) {
|
|
chmod(appDir, 0o755, `${label} AppDir root`);
|
|
chmod(appRun, 0o755, `${label} AppRun`);
|
|
chmod(hookDirectory, 0o755, `${label} AppRun hook directory`);
|
|
chmod(gtkHook, 0o644, `${label} GTK AppRun hook`);
|
|
chmod(appRunWrapped, 0o755, `${label} AppRun.wrapped launcher`);
|
|
}
|
|
|
|
assertOtherDirectoryAccess(appDir, `${label} AppDir root`);
|
|
assertOtherReadableExecutable(appRun, `${label} AppRun`);
|
|
assertOtherDirectoryAccess(hookDirectory, `${label} AppRun hook directory`);
|
|
assertOtherReadable(gtkHook, `${label} GTK AppRun hook`);
|
|
assertOtherReadableExecutable(appRunWrapped, `${label} AppRun.wrapped launcher`);
|
|
return true;
|
|
}
|
|
|
|
export function normalizeAppRunPermissions(root, label = 'AppDir') {
|
|
return checkAppRunPermissions(root, label, true);
|
|
}
|
|
|
|
export function verifyAppRunPermissions(root, label = 'AppImage') {
|
|
return checkAppRunPermissions(root, label, false);
|
|
}
|
|
|
|
function squashfsEntryMode(listing, relativePath, label, expectedType) {
|
|
const matchingLines = listing
|
|
.split(/\r?\n/)
|
|
.filter(line => {
|
|
const columns = line.trim().split(/\s+/);
|
|
return columns.at(-1)?.replace(/\/$/, '') === relativePath;
|
|
});
|
|
|
|
if (matchingLines.length !== 1) {
|
|
throw new Error(`${label} ${relativePath} must appear exactly once in the SquashFS permission listing.`);
|
|
}
|
|
|
|
const mode = /^([bcdlps-][rwxStTs-]{9})\s/.exec(matchingLines[0])?.[1];
|
|
if (!mode) {
|
|
throw new Error(`${label} ${relativePath} has an invalid SquashFS permission record.`);
|
|
}
|
|
|
|
if (mode[0] !== expectedType) {
|
|
throw new Error(`${label} ${relativePath} has the wrong SquashFS file type.`);
|
|
}
|
|
|
|
return mode.slice(1);
|
|
}
|
|
|
|
function assertListedOtherReadableExecutable(mode, label) {
|
|
if (mode[6] !== 'r' || !['x', 't'].includes(mode[8])) {
|
|
throw new Error(`${label} must be readable and executable by other users in the SquashFS image; found mode ${mode}.`);
|
|
}
|
|
}
|
|
|
|
function assertListedOtherDirectoryAccess(mode, label) {
|
|
if (mode[6] !== 'r' || !['x', 't'].includes(mode[8])) {
|
|
throw new Error(`${label} must be readable and traversable by other users in the SquashFS image; found mode ${mode}.`);
|
|
}
|
|
}
|
|
|
|
function assertListedOtherReadable(mode, label) {
|
|
if (mode[6] !== 'r') {
|
|
throw new Error(`${label} must be readable by other users in the SquashFS image; found mode ${mode}.`);
|
|
}
|
|
}
|
|
|
|
export function verifySquashfsAppRunPermissions(listing, label = 'AppImage') {
|
|
const rootMode = squashfsEntryMode(listing, 'squashfs-root', `${label} AppDir root`, 'd');
|
|
const appRunMode = squashfsEntryMode(listing, 'squashfs-root/AppRun', `${label} AppRun`, '-');
|
|
const wrappedMode = squashfsEntryMode(
|
|
listing,
|
|
'squashfs-root/AppRun.wrapped',
|
|
`${label} AppRun.wrapped launcher`,
|
|
'-',
|
|
);
|
|
const hookDirectoryMode = squashfsEntryMode(
|
|
listing,
|
|
'squashfs-root/apprun-hooks',
|
|
`${label} AppRun hook directory`,
|
|
'd',
|
|
);
|
|
const gtkHookMode = squashfsEntryMode(
|
|
listing,
|
|
'squashfs-root/apprun-hooks/linuxdeploy-plugin-gtk.sh',
|
|
`${label} GTK AppRun hook`,
|
|
'-',
|
|
);
|
|
|
|
assertListedOtherDirectoryAccess(rootMode, `${label} AppDir root`);
|
|
assertListedOtherReadableExecutable(appRunMode, `${label} AppRun`);
|
|
assertListedOtherReadableExecutable(wrappedMode, `${label} AppRun.wrapped launcher`);
|
|
assertListedOtherDirectoryAccess(hookDirectoryMode, `${label} AppRun hook directory`);
|
|
assertListedOtherReadable(gtkHookMode, `${label} GTK AppRun hook`);
|
|
return true;
|
|
}
|