150 lines
6.0 KiB
JavaScript
150 lines
6.0 KiB
JavaScript
import zlib from 'node:zlib';
|
|
import { basenameAny, normalizePackagePath } from '../utils/pathUtils.js';
|
|
|
|
// Assembles a PSADT v4 package tree from a rendered profile script plus its linked
|
|
// Asset Library files, and zips it without an archiver dependency. The layout
|
|
// planning and the ZIP writer are pure and unit-tested; disk reads stay in the
|
|
// controller.
|
|
|
|
export const ENTRY_SCRIPT_NAME = 'Invoke-AppDeployToolkit.ps1';
|
|
|
|
// Link role -> default PSADT v4 subfolder when the link carries no explicit
|
|
// package path. Installers/payload go under Files, supporting content under
|
|
// SupportFiles, icons under Assets.
|
|
const ROLE_FOLDER = {
|
|
installer: 'Files',
|
|
'package-file': 'Files',
|
|
'support-file': 'SupportFiles',
|
|
'detection-script': 'SupportFiles',
|
|
icon: 'Assets',
|
|
reference: 'SupportFiles'
|
|
};
|
|
|
|
// Pure: decide where the rendered script and each linked asset land in the
|
|
// package tree. An explicit link packagePath wins; otherwise the role picks a
|
|
// standard folder and the asset's own file name. Collisions are dropped with a
|
|
// warning so a package never silently overwrites a file.
|
|
export function planPackageLayout(links = []) {
|
|
const warnings = [];
|
|
const used = new Map();
|
|
used.set(ENTRY_SCRIPT_NAME.toLowerCase(), ENTRY_SCRIPT_NAME);
|
|
const assets = [];
|
|
|
|
for (const link of links) {
|
|
const folder = ROLE_FOLDER[link.linkRole] || 'SupportFiles';
|
|
const name = basenameAny(link.packagePath || link.originalName || link.assetId);
|
|
let target = normalizePackagePath(link.packagePath, `${folder}/${name}`) || `${folder}/${name}`;
|
|
if (!target.includes('/')) target = `${folder}/${target}`;
|
|
|
|
const key = target.toLowerCase();
|
|
if (used.has(key)) {
|
|
warnings.push(`Skipped "${link.originalName || link.assetId}": another file already maps to ${target}.`);
|
|
continue;
|
|
}
|
|
used.set(key, link.originalName || link.assetId);
|
|
assets.push({ assetId: link.assetId, packagePath: target, role: link.linkRole, name: link.originalName });
|
|
}
|
|
|
|
const directories = ['Files', 'SupportFiles'];
|
|
if (assets.some((asset) => asset.packagePath.startsWith('Assets/'))) directories.push('Assets');
|
|
return { scriptPath: ENTRY_SCRIPT_NAME, assets, directories, warnings };
|
|
}
|
|
|
|
// Pure: pick the installer the .intunewin build should treat as its entry setup
|
|
// file (relative to the package root). An explicit choice wins; otherwise prefer
|
|
// an installer-role asset, then the first file under Files/.
|
|
export function resolveSetupFile(layout, explicit = '') {
|
|
const explicitClean = normalizePackagePath(explicit, '');
|
|
if (explicitClean) return explicitClean;
|
|
const installer = layout.assets.find((asset) => asset.role === 'installer');
|
|
if (installer) return installer.packagePath;
|
|
const firstFile = layout.assets.find((asset) => asset.packagePath.startsWith('Files/'));
|
|
return firstFile ? firstFile.packagePath : '';
|
|
}
|
|
|
|
let CRC_TABLE;
|
|
function crc32(buf) {
|
|
if (!CRC_TABLE) {
|
|
CRC_TABLE = new Int32Array(256);
|
|
for (let n = 0; n < 256; n += 1) {
|
|
let c = n;
|
|
for (let k = 0; k < 8; k += 1) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
|
|
CRC_TABLE[n] = c;
|
|
}
|
|
}
|
|
let crc = -1;
|
|
for (let i = 0; i < buf.length; i += 1) crc = (crc >>> 8) ^ CRC_TABLE[(crc ^ buf[i]) & 0xff];
|
|
return (crc ^ -1) >>> 0;
|
|
}
|
|
|
|
// Pure: build a standard PKZIP buffer from entries. Files are deflate-compressed
|
|
// (method 8), directories and empty files stored (method 0). A fixed 1980 DOS
|
|
// timestamp keeps output reproducible. Round-trips through zlib.inflateRawSync.
|
|
export function buildZip(entries = []) {
|
|
const local = [];
|
|
const central = [];
|
|
let offset = 0;
|
|
|
|
for (const entry of entries) {
|
|
const isDir = Boolean(entry.isDirectory);
|
|
let name = String(entry.path).replace(/\\/g, '/');
|
|
if (isDir && !name.endsWith('/')) name += '/';
|
|
const nameBytes = Buffer.from(name, 'utf8');
|
|
const raw = isDir ? Buffer.alloc(0) : (Buffer.isBuffer(entry.data) ? entry.data : Buffer.from(entry.data || '', 'utf8'));
|
|
const deflated = raw.length ? zlib.deflateRawSync(raw) : Buffer.alloc(0);
|
|
const useDeflate = raw.length > 0 && deflated.length < raw.length;
|
|
const method = useDeflate ? 8 : 0;
|
|
const stored = useDeflate ? deflated : raw;
|
|
const crc = crc32(raw);
|
|
|
|
const localHeader = Buffer.alloc(30);
|
|
localHeader.writeUInt32LE(0x04034b50, 0);
|
|
localHeader.writeUInt16LE(20, 4);
|
|
localHeader.writeUInt16LE(0, 6);
|
|
localHeader.writeUInt16LE(method, 8);
|
|
localHeader.writeUInt16LE(0, 10);
|
|
localHeader.writeUInt16LE(0x21, 12);
|
|
localHeader.writeUInt32LE(crc, 14);
|
|
localHeader.writeUInt32LE(stored.length, 18);
|
|
localHeader.writeUInt32LE(raw.length, 22);
|
|
localHeader.writeUInt16LE(nameBytes.length, 26);
|
|
localHeader.writeUInt16LE(0, 28);
|
|
local.push(localHeader, nameBytes, stored);
|
|
|
|
const centralHeader = Buffer.alloc(46);
|
|
centralHeader.writeUInt32LE(0x02014b50, 0);
|
|
centralHeader.writeUInt16LE(20, 4);
|
|
centralHeader.writeUInt16LE(20, 6);
|
|
centralHeader.writeUInt16LE(0, 8);
|
|
centralHeader.writeUInt16LE(method, 10);
|
|
centralHeader.writeUInt16LE(0, 12);
|
|
centralHeader.writeUInt16LE(0x21, 14);
|
|
centralHeader.writeUInt32LE(crc, 16);
|
|
centralHeader.writeUInt32LE(stored.length, 20);
|
|
centralHeader.writeUInt32LE(raw.length, 24);
|
|
centralHeader.writeUInt16LE(nameBytes.length, 28);
|
|
centralHeader.writeUInt16LE(0, 30);
|
|
centralHeader.writeUInt16LE(0, 32);
|
|
centralHeader.writeUInt16LE(0, 34);
|
|
centralHeader.writeUInt16LE(0, 36);
|
|
centralHeader.writeUInt32LE(isDir ? 0x10 : 0, 38);
|
|
centralHeader.writeUInt32LE(offset, 42);
|
|
central.push(centralHeader, nameBytes);
|
|
|
|
offset += localHeader.length + nameBytes.length + stored.length;
|
|
}
|
|
|
|
const centralBuffer = Buffer.concat(central);
|
|
const end = Buffer.alloc(22);
|
|
end.writeUInt32LE(0x06054b50, 0);
|
|
end.writeUInt16LE(0, 4);
|
|
end.writeUInt16LE(0, 6);
|
|
end.writeUInt16LE(entries.length, 8);
|
|
end.writeUInt16LE(entries.length, 10);
|
|
end.writeUInt32LE(centralBuffer.length, 12);
|
|
end.writeUInt32LE(offset, 16);
|
|
end.writeUInt16LE(0, 20);
|
|
|
|
return Buffer.concat([...local, centralBuffer, end]);
|
|
}
|