@shockpkg/ria-packager
Version:
Package for creating Adobe AIR packages
510 lines (459 loc) • 11.5 kB
JavaScript
import { lstat, readFile } from 'node:fs/promises';
import { join as pathJoin, basename } from 'node:path';
import { fsWalk, pathNormalize } from '@shockpkg/archive-files';
import { Signature } from "./signature.mjs";
import { HasherSha256 } from "./hasher/sha256.mjs";
/**
* Options for adding resources.
*/
/**
* Packager object.
*/
export class Packager {
/**
* Make a debug build.
*/
debug = false;
/**
* Keystore object to use for signing.
*/
keystore = null;
/**
* Timestamp URL.
*/
timestampUrl = null;
/**
* Application descriptor file data.
*/
descriptorData = null;
/**
* Application descriptor file path.
*/
descriptorFile = null;
/**
* File and directory names to exclude when added a directory.
*/
excludes = [/^\./, /^ehthumbs\.db$/i, /^Thumbs\.db$/i];
/**
* Set the nobrowse option on mounted disk images.
*/
nobrowse = false;
/**
* Output path.
*/
/**
* Open flag.
*/
_isOpen = false;
/**
* Adding a resource flag.
*/
_isAddingResource = false;
/**
* Hasher object.
*/
/**
* Signature object.
*/
_signature = null;
/**
* Packager constructor.
*
* @param path Output path.
*/
constructor(path) {
this._hasher = this._createHasher();
this.path = path;
}
/**
* Check if output open.
*
* @returns Returns true if open, else false.
*/
get isOpen() {
return this._isOpen;
}
/**
* Open with application descriptor XML data.
*/
async open() {
if (this._isOpen) {
throw new Error('Already open');
}
const descriptorData = await this._getDescriptorData();
this._applicationInfoInit(descriptorData);
await this._open();
this._isOpen = true;
this._hasher.reset();
this._signature = null;
if (this.signed) {
this._signature = this._createSignature();
this._signature.timestampUrl = this.timestampUrl;
const keystore = this._getKeystore();
this._signature.certificate = keystore.getCertificate();
this._signature.privateKey = keystore.getPrivateKey();
}
await this._addMetaResourcesStart(descriptorData);
}
/**
* Close output.
*/
async close() {
if (!this._isOpen) {
throw new Error('Not open');
}
await this._addMetaResourcesEnd();
await this._close();
this._isOpen = false;
this._applicationInfoClear();
this._hasher.reset();
this._signature = null;
}
/**
* Run asyncronous function with automatic open and close.
*
* @param func Async function.
* @returns Return value of the async function.
*/
async write(func) {
await this.open();
let r;
try {
r = await func.call(this, this);
} finally {
await this.close();
}
return r;
}
/**
* Check if name is excluded file.
*
* @param name File name.
* @returns Returns true if excluded, else false.
*/
isExcludedFile(name) {
for (const exclude of this.excludes) {
if (exclude.test(name)) {
return true;
}
}
return false;
}
/**
* Add resource with file.
*
* @param source File path.
* @param destination Packaged file relative destination.
* @param options Resource options.
*/
async addResourceFile(source, destination = null, options = null) {
const opts = options || {};
const dest = destination === null ? source : destination;
const stat = await lstat(source);
// Symlinks would only be allowed in a macOS native extension.
// Throw an error like the official packager does.
if (stat.isSymbolicLink()) {
throw new Error(`Cannot add symlink: ${source}`);
}
// Throw if not a regular file.
if (!stat.isFile()) {
throw new Error(`Unsupported file type: ${source}`);
}
let {
executable
} = opts;
if (executable !== true && executable !== false) {
// eslint-disable-next-line no-bitwise
executable = !!(stat.mode & 0b001000000);
}
const data = await readFile(source);
await this.addResource(dest, data, {
executable,
mtime: opts.mtime || stat.mtime
});
}
/**
* Add resource with directory.
* Walks the directory looking for files to add, skips excluded file names.
*
* @param source Directory path.
* @param destination Packaged directory relative destination.
* @param options Resource options.
*/
async addResourceDirectory(source, destination = null, options = null) {
const dest = destination === null ? source : destination;
await fsWalk(source, async (path, stat) => {
// If this name is excluded, skip without descending.
if (this.isExcludedFile(basename(path))) {
return false;
}
// Ignore directories, but descend into them.
// Only files are listed in the ZIP packages.
// Created automatically for files in any other package.
if (stat.isDirectory()) {
return true;
}
// Anything else assume file.
await this.addResourceFile(pathJoin(source, path), pathJoin(dest, path), options);
return true;
});
}
/**
* Add resource with data.
*
* @param destination Packaged file relative destination.
* @param data Resource data.
* @param options Resource options.
*/
async addResource(destination, data, options = null) {
if (!this._isOpen) {
throw new Error('Not open');
}
if (this._isAddingResource) {
throw new Error('Resources must be added sequentially');
}
this._isAddingResource = true;
await this._addResource(pathNormalize(destination), data, options || {}, true, true);
this._isAddingResource = false;
}
/**
* Create Hasher object.
*
* @returns Hasher object.
*/
_createHasher() {
return new HasherSha256();
}
/**
* Create Signature object.
*
* @returns Hasher object.
*/
_createSignature() {
return new Signature();
}
/**
* Path of the mimetype meta resource.
*
* @returns Resource path.
*/
get _metaResourceMimetypePath() {
return 'mimetype';
}
/**
* Path of the application meta resource.
*
* @returns Resource path.
*/
get _metaResourceApplicationPath() {
return 'META-INF/AIR/application.xml';
}
/**
* Path of the hash meta resource.
*
* @returns Resource path.
*/
get _metaResourceHashPath() {
return 'META-INF/AIR/hash';
}
/**
* Path of the debug meta resource.
*
* @returns Resource path.
*/
get _metaResourceDebugPath() {
return 'META-INF/AIR/debug';
}
/**
* Path of the signatures meta resource.
*
* @returns Resource path.
*/
get _metaResourceSignaturesPath() {
return 'META-INF/signatures.xml';
}
/**
* Get application descriptor data or throw.
*
* @returns Application descriptor XML data.
*/
async _getDescriptorData() {
const {
descriptorData,
descriptorFile
} = this;
if (descriptorData) {
switch (typeof descriptorData) {
case 'function':
{
const d = await descriptorData();
return typeof d === 'string' ? new TextEncoder().encode(d) : d;
}
case 'string':
{
return new TextEncoder().encode(descriptorData);
}
default:
{
return descriptorData;
}
}
}
if (descriptorFile !== null) {
const d = await readFile(descriptorFile);
return new Uint8Array(d.buffer, d.byteOffset, d.byteLength);
}
throw new Error('Missing application descriptor data');
}
/**
* Get encoded mimetype data.
*
* @returns Mimetype data.
*/
_getMimetypeData() {
// The mimetype is UTF-8.
return new TextEncoder().encode(this.mimetype);
}
/**
* Get the keystore object.
*
* @returns Keystore object.
*/
_getKeystore() {
const r = this.keystore;
if (!r) {
throw new Error('A keystore not set');
}
return r;
}
/**
* Add resource with data, with options controlling hashing and signing.
*
* @param destination Packaged file relative destination.
* @param data Resource data.
* @param options Resource options.
* @param hashed This file is hashed.
* @param signed This file is signed.
*/
async _addResource(destination, data, options, hashed, signed) {
if (hashed) {
this._hasher.update(data);
}
if (signed) {
const signature = this._signature;
if (signature) {
signature.addFile(destination, data);
}
}
await this._writeResource(destination, data, options);
}
/**
* Add meta resources start.
*
* @param applicationData XML data.
*/
async _addMetaResourcesStart(applicationData) {
await this._addMetaResourceMimetype();
await this._addMetaResourceApplication(applicationData);
await this._addMetaResourceHash();
if (this.debug) {
await this._addMetaResourceDebug();
}
}
/**
* Add meta resources end.
*/
async _addMetaResourcesEnd() {
if (this.signed) {
await this._addMetaResourceSignatures();
}
}
/**
* Add meta resource for the mimetype.
*/
async _addMetaResourceMimetype() {
const path = this._metaResourceMimetypePath;
const data = this._getMimetypeData();
await this._addResource(path, data, {}, true, true);
}
/**
* Add meta resource for the application descriptor.
*
* @param applicationData The application descriptor data.
*/
async _addMetaResourceApplication(applicationData) {
const path = this._metaResourceApplicationPath;
await this._addResource(path, applicationData, {}, true, true);
}
/**
* Add meta resource for the hash (needs updating on close).
*/
async _addMetaResourceHash() {
const path = this._metaResourceHashPath;
const data = new Uint8Array(this._hasher.bytes);
await this._addResource(path, data, {}, false, false);
}
/**
* Add meta resource for debug.
*/
async _addMetaResourceDebug() {
const path = this._metaResourceDebugPath;
const data = new Uint8Array(0);
await this._addResource(path, data, {}, true, true);
}
/**
* Add resource for signatures.
*/
async _addMetaResourceSignatures() {
const path = this._metaResourceSignaturesPath;
const signature = this._signature;
if (!signature) {
throw new Error('Internal error');
}
signature.digest();
signature.sign();
if (signature.timestampUrl) {
await signature.timestamp();
}
const data = signature.encode();
await this._addResource(path, data, {}, false, false);
}
/**
* Init application info from descriptor data.
*
* @param applicationData The application descriptor data.
*/
_applicationInfoInit(applicationData) {
// Do nothing.
}
/**
* Clear application info from descriptor data.
*/
_applicationInfoClear() {
// Do nothing.
}
/**
* Package mimetype.
*
* @returns Mimetype string.
*/
/**
* Package signed.
*
* @returns Boolean for if package is signed or not.
*/
/**
* Open implementation.
*/
/**
* Close implementation.
*/
/**
* Write resource with data implementation.
*
* @param destination Packaged file relative destination.
* @param data Resource data.
* @param options Resource options.
*/
}
//# sourceMappingURL=packager.mjs.map