mirror of
https://github.com/marcogll/passkit-generator.git
synced 2026-03-16 09:25:41 +00:00
369 lines
8.7 KiB
TypeScript
369 lines
8.7 KiB
TypeScript
import * as path from "path";
|
|
import forge from "node-forge";
|
|
import formatMessage, { ERROR, DEBUG } from "./messages";
|
|
import * as Schemas from "./schemas";
|
|
import {
|
|
removeHidden,
|
|
splitBufferBundle,
|
|
getAllFilesWithName,
|
|
hasFilesWithName,
|
|
deletePersonalization,
|
|
} from "./utils";
|
|
import { promises as fs } from "fs";
|
|
import debug from "debug";
|
|
|
|
const prsDebug = debug("Personalization");
|
|
|
|
/**
|
|
* Performs checks on the passed model to
|
|
* determine how to parse it
|
|
* @param model
|
|
*/
|
|
|
|
export async function getModelContents(model: Schemas.FactoryOptions["model"]) {
|
|
let modelContents: Schemas.PartitionedBundle;
|
|
|
|
if (typeof model === "string") {
|
|
modelContents = await getModelFolderContents(model);
|
|
} else if (typeof model === "object" && Object.keys(model).length) {
|
|
modelContents = getModelBufferContents(model);
|
|
} else {
|
|
throw new Error(formatMessage(ERROR.MODEL_NOT_VALID));
|
|
}
|
|
|
|
const modelFiles = Object.keys(modelContents.bundle);
|
|
const isModelInitialized =
|
|
modelFiles.includes("pass.json") &&
|
|
hasFilesWithName("icon", modelFiles, "startsWith");
|
|
|
|
if (!isModelInitialized) {
|
|
throw new Error(
|
|
formatMessage(ERROR.MODEL_UNINITIALIZED, "parse result"),
|
|
);
|
|
}
|
|
|
|
// ======================= //
|
|
// *** Personalization *** //
|
|
// ======================= //
|
|
|
|
const personalizationJsonFile = "personalization.json";
|
|
|
|
if (!modelFiles.includes(personalizationJsonFile)) {
|
|
return modelContents;
|
|
}
|
|
|
|
const logoFullNames = getAllFilesWithName(
|
|
"personalizationLogo",
|
|
modelFiles,
|
|
"startsWith",
|
|
);
|
|
if (
|
|
!(
|
|
logoFullNames.length &&
|
|
modelContents.bundle[personalizationJsonFile].length
|
|
)
|
|
) {
|
|
deletePersonalization(modelContents.bundle, logoFullNames);
|
|
return modelContents;
|
|
}
|
|
|
|
try {
|
|
const parsedPersonalization = JSON.parse(
|
|
modelContents.bundle[personalizationJsonFile].toString("utf8"),
|
|
);
|
|
const isPersonalizationValid = Schemas.isValid(
|
|
parsedPersonalization,
|
|
Schemas.Personalization,
|
|
);
|
|
|
|
if (!isPersonalizationValid) {
|
|
[...logoFullNames, personalizationJsonFile].forEach(
|
|
(file) => delete modelContents.bundle[file],
|
|
);
|
|
|
|
return modelContents;
|
|
}
|
|
} catch (err) {
|
|
prsDebug(formatMessage(DEBUG.PRS_INVALID, err));
|
|
deletePersonalization(modelContents.bundle, logoFullNames);
|
|
}
|
|
|
|
return modelContents;
|
|
}
|
|
|
|
/**
|
|
* Reads the model folder contents
|
|
*
|
|
* @param model
|
|
* @returns A promise of an object containing all
|
|
* filePaths and the relative buffer
|
|
*/
|
|
|
|
export async function getModelFolderContents(
|
|
model: string,
|
|
): Promise<{ [filePath: string]: Buffer }> {
|
|
try {
|
|
const modelPath = `${model}${(!path.extname(model) && ".pass") || ""}`;
|
|
const modelFilesList = await fs.readdir(modelPath);
|
|
|
|
// No dot-starting files, manifest and signature
|
|
const filteredModelRecords = removeHidden(modelFilesList).filter(
|
|
(f) =>
|
|
!/(manifest|signature)/i.test(f) &&
|
|
/.+$/.test(path.parse(f).ext),
|
|
);
|
|
|
|
/* const isModelInitialized =
|
|
filteredFiles.length &&
|
|
hasFilesWithName("icon", filteredFiles, "startsWith");
|
|
|
|
// Icon is required to proceed
|
|
if (!isModelInitialized) {
|
|
throw new Error(
|
|
formatMessage(
|
|
ERROR.MODEL_UNINITIALIZED,
|
|
path.parse(model).name,
|
|
),
|
|
);
|
|
} */
|
|
|
|
const modelRecords = (
|
|
await Promise.all(
|
|
/**
|
|
* Obtaining flattened array of buffer records
|
|
* containing file name and the buffer itself.
|
|
*
|
|
* This goes also to read every nested l10n
|
|
* subfolder.
|
|
*/
|
|
|
|
filteredModelRecords.map((fileOrDirectoryPath) => {
|
|
const fullPath = path.resolve(
|
|
modelPath,
|
|
fileOrDirectoryPath,
|
|
);
|
|
|
|
return readFileOrDirectory(fullPath);
|
|
}),
|
|
)
|
|
)
|
|
.flat(1)
|
|
.reduce((acc, current) => ({ ...acc, ...current }), {});
|
|
|
|
return modelRecords;
|
|
} catch (err) {
|
|
if (err?.code === "ENOENT") {
|
|
if (err.syscall === "open") {
|
|
// file opening failed
|
|
throw new Error(
|
|
formatMessage(ERROR.MODELF_NOT_FOUND, err.path),
|
|
);
|
|
} else if (err.syscall === "scandir") {
|
|
// directory reading failed
|
|
const pathContents = (err.path as string).split(/(\/|\\\?)/);
|
|
throw new Error(
|
|
formatMessage(
|
|
ERROR.MODELF_FILE_NOT_FOUND,
|
|
pathContents[pathContents.length - 1],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads sequentially
|
|
* @param filePath
|
|
* @returns
|
|
*/
|
|
|
|
async function readFileOrDirectory(filePath: string) {
|
|
if ((await fs.lstat(filePath)).isDirectory()) {
|
|
return Promise.all(await readDirectory(filePath));
|
|
} else {
|
|
return fs
|
|
.readFile(filePath)
|
|
.then((content) => getObjectFromModelFile(filePath, content, 1));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns an object containing the parsed fileName
|
|
* from a path along with its content.
|
|
*
|
|
* @param filePath
|
|
* @param content
|
|
* @param depthFromEnd - used to preserve localization lproj content
|
|
* @returns
|
|
*/
|
|
|
|
function getObjectFromModelFile(
|
|
filePath: string,
|
|
content: Buffer,
|
|
depthFromEnd: number,
|
|
) {
|
|
const fileComponents = filePath.split(path.sep);
|
|
const fileName = fileComponents
|
|
.slice(fileComponents.length - depthFromEnd)
|
|
.join(path.sep);
|
|
|
|
return { [fileName]: content };
|
|
}
|
|
|
|
/**
|
|
* Reads a directory and returns all the files in it
|
|
* as an Array<Promise>
|
|
*
|
|
* @param filePath
|
|
* @returns
|
|
*/
|
|
|
|
async function readDirectory(filePath: string) {
|
|
const dirContent = await fs.readdir(filePath).then(removeHidden);
|
|
|
|
return dirContent.map(async (fileName) => {
|
|
const content = await fs.readFile(path.resolve(filePath, fileName));
|
|
return getObjectFromModelFile(filePath, content, 1);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Analyzes the passed buffer model and splits it to
|
|
* return buffers and localization files buffers.
|
|
* @param model
|
|
*/
|
|
|
|
export function getModelBufferContents(
|
|
model: Schemas.BundleUnit,
|
|
): Schemas.PartitionedBundle {
|
|
const rawBundle = removeHidden(
|
|
Object.keys(model),
|
|
).reduce<Schemas.BundleUnit>((acc, current) => {
|
|
// Checking if current file is one of the autogenerated ones or if its
|
|
// content is not available
|
|
|
|
if (/(manifest|signature)/.test(current) || !model[current]) {
|
|
return acc;
|
|
}
|
|
|
|
return { ...acc, [current]: model[current] };
|
|
}, {});
|
|
|
|
const bundleKeys = Object.keys(rawBundle);
|
|
|
|
const isModelInitialized =
|
|
bundleKeys.length && hasFilesWithName("icon", bundleKeys, "startsWith");
|
|
|
|
// Icon is required to proceed
|
|
if (!isModelInitialized) {
|
|
throw new Error(formatMessage(ERROR.MODEL_UNINITIALIZED, "Buffers"));
|
|
}
|
|
|
|
// separing localization folders from bundle files
|
|
const [l10nBundle, bundle] = splitBufferBundle(rawBundle);
|
|
|
|
return {
|
|
bundle,
|
|
l10nBundle,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Reads certificate contents, if the passed content is a path,
|
|
* and parses them as a PEM.
|
|
* @param options
|
|
*/
|
|
|
|
type flatCertificates = Omit<Schemas.Certificates, "signerKey"> & {
|
|
signerKey: string;
|
|
};
|
|
|
|
export async function readCertificatesFromOptions(
|
|
options: Schemas.Certificates,
|
|
): Promise<Schemas.CertificatesSchema> {
|
|
if (
|
|
!(
|
|
options &&
|
|
Object.keys(options).length &&
|
|
Schemas.isValid(options, Schemas.CertificatesSchema)
|
|
)
|
|
) {
|
|
throw new Error(formatMessage(ERROR.CP_NO_CERTS));
|
|
}
|
|
|
|
let signerKey: string;
|
|
|
|
if (typeof options.signerKey === "object") {
|
|
signerKey = options.signerKey?.keyFile;
|
|
} else {
|
|
signerKey = options.signerKey;
|
|
}
|
|
|
|
// if the signerKey is an object, we want to get
|
|
// all the real contents and don't care of passphrase
|
|
const flattenedDocs = Object.assign({}, options, {
|
|
signerKey,
|
|
}) as flatCertificates;
|
|
|
|
// We read the contents
|
|
const rawContentsPromises = Object.keys(flattenedDocs).map((key) => {
|
|
const content = flattenedDocs[key];
|
|
|
|
if (!!path.parse(content).ext) {
|
|
// The content is a path to the document
|
|
return fs.readFile(path.resolve(content), { encoding: "utf8" });
|
|
} else {
|
|
// Content is the real document content
|
|
return Promise.resolve(content);
|
|
}
|
|
});
|
|
|
|
try {
|
|
const parsedContents = await Promise.all(rawContentsPromises);
|
|
const pemParsedContents = parsedContents.map((file, index) => {
|
|
const certName = Object.keys(options)[index];
|
|
const passphrase =
|
|
(typeof options.signerKey === "object" &&
|
|
options.signerKey?.passphrase) ||
|
|
undefined;
|
|
|
|
const pem = parsePEM(certName, file, passphrase);
|
|
|
|
if (!pem) {
|
|
throw new Error(formatMessage(ERROR.INVALID_CERTS, certName));
|
|
}
|
|
|
|
return { [certName]: pem };
|
|
});
|
|
|
|
return Object.assign({}, ...pemParsedContents);
|
|
} catch (err) {
|
|
if (!err.path) {
|
|
throw err;
|
|
}
|
|
|
|
throw new Error(
|
|
formatMessage(ERROR.INVALID_CERT_PATH, path.parse(err.path).base),
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses the PEM-formatted passed text (certificates)
|
|
*
|
|
* @param element - Text content of .pem files
|
|
* @param passphrase - passphrase for the key
|
|
* @returns The parsed certificate or key in node forge format
|
|
*/
|
|
|
|
function parsePEM(pemName: string, element: string, passphrase?: string) {
|
|
if (pemName === "signerKey" && passphrase) {
|
|
return forge.pki.decryptRsaPrivateKey(element, String(passphrase));
|
|
} else {
|
|
return forge.pki.certificateFromPem(element);
|
|
}
|
|
}
|