@snyk/cocoapods-lockfile-parser
Version:
Generate a Snyk dependency graph from a Podfile.lock file
197 lines • 8.67 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
const fs = require("fs");
const path = require("path");
const yaml = require("js-yaml");
const dep_graph_1 = require("@snyk/dep-graph");
const utils_1 = require("./utils");
class LockfileParser {
static async readFile(lockfilePath) {
const rootName = path.basename(path.dirname(path.resolve(lockfilePath)));
return new Promise((resolve, reject) => {
fs.readFile(lockfilePath, { encoding: 'utf8' }, (err, fileContents) => {
if (err) {
reject(err);
}
try {
const parser = this.readContents(fileContents, {
name: rootName,
version: '0.0.0',
});
resolve(parser);
}
catch (err) {
reject(err);
}
});
});
}
static readFileSync(lockfilePath) {
const fileContents = fs.readFileSync(lockfilePath, 'utf8');
const rootName = path.basename(path.dirname(path.resolve(lockfilePath)));
return this.readContents(fileContents, {
name: rootName,
version: '0.0.0',
});
}
static readContents(contents, rootPkgInfo) {
return new LockfileParser(yaml.safeLoad(contents), rootPkgInfo);
}
constructor(hash, rootPkgInfo) {
this.rootPkgInfo = undefined;
this.rootPkgInfo = rootPkgInfo;
this.internalData = hash;
}
toDepGraph() {
const builder = new dep_graph_1.DepGraphBuilder(this.pkgManager, this.rootPkgInfo);
const allDeps = {};
// Add all package nodes first, but collect dependencies
this.internalData.PODS.forEach((elem) => {
let pkgInfo;
let pkgDeps;
if (typeof elem === 'string') {
// When there are NO dependencies. This equals in yaml e.g.
// - Expecta (1.0.5)
pkgInfo = (0, utils_1.pkgInfoFromSpecificationString)(elem);
pkgDeps = [];
}
else {
// When there are dependencies. This equals in yaml e.g.
// - React/Core (0.59.2):
// - yoga (= 0.59.2.React)
const objKey = Object.keys(elem)[0];
pkgInfo = (0, utils_1.pkgInfoFromSpecificationString)(objKey);
pkgDeps = elem[objKey].map(utils_1.pkgInfoFromDependencyString);
}
const nodeId = this.nodeIdForPkgInfo(pkgInfo);
builder.addPkgNode(pkgInfo, nodeId, {
labels: this.nodeInfoLabelsForPod(pkgInfo.name),
});
allDeps[nodeId] = pkgDeps;
});
// Connect explicitly in the manifest (`Podfile`)
// declared dependencies to the root node.
this.internalData.DEPENDENCIES.map(utils_1.pkgInfoFromDependencyString).forEach((pkgInfo) => {
builder.connectDep(builder.rootNodeId, this.nodeIdForPkgInfo(pkgInfo));
});
// Now we can start to connect dependencies
Object.entries(allDeps).forEach(([nodeId, pkgDeps]) => pkgDeps.forEach((pkgInfo) => {
const depNodeId = this.nodeIdForPkgInfo(pkgInfo);
if (!allDeps[depNodeId]) {
// The pod is not a direct dependency of any targets of the integration,
// which can happen for platform-specific transitives, when their platform
// is not used in any target. (e.g. PromiseKit/UIKit is iOS-specific and is
// a transitive of PromiseKit, but won't be included for a macOS project.)
return;
}
builder.connectDep(nodeId, depNodeId);
}));
return builder.build();
}
/// CocoaPods guarantees that every pod is only present in one version,
/// so we can use just the pod name as node ID.
nodeIdForPkgInfo(pkgInfo) {
return pkgInfo.name;
}
/// Gathers relevant info from the lockfile and transform
/// them into the expected labels data structure.
nodeInfoLabelsForPod(podName) {
let nodeInfoLabels = {
checksum: this.checksumForPod(podName),
};
const repository = this.repositoryForPod(podName);
if (repository) {
nodeInfoLabels = Object.assign(Object.assign({}, nodeInfoLabels), { repository });
}
const externalSourceInfo = this.externalSourceInfoForPod(podName);
if (externalSourceInfo) {
nodeInfoLabels = Object.assign(Object.assign({}, nodeInfoLabels), { externalSourcePodspec: externalSourceInfo[':podspec'], externalSourcePath: externalSourceInfo[':path'], externalSourceGit: externalSourceInfo[':git'], externalSourceTag: externalSourceInfo[':tag'], externalSourceCommit: externalSourceInfo[':commit'], externalSourceBranch: externalSourceInfo[':branch'] });
}
const checkoutOptions = this.checkoutOptionsForPod(podName);
if (checkoutOptions) {
nodeInfoLabels = Object.assign(Object.assign({}, nodeInfoLabels), { checkoutOptionsPodspec: checkoutOptions[':podspec'], checkoutOptionsPath: checkoutOptions[':path'], checkoutOptionsGit: checkoutOptions[':git'], checkoutOptionsTag: checkoutOptions[':tag'], checkoutOptionsCommit: checkoutOptions[':commit'], checkoutOptionsBranch: checkoutOptions[':branch'] });
}
// Sanitize labels by removing null fields
// (as they don't survive a serialization/parse cycle and break tests)
Object.entries(nodeInfoLabels).forEach(([key, value]) => {
if (value === null || value === undefined) {
delete nodeInfoLabels[key];
}
});
return nodeInfoLabels;
}
/// The checksum of the pod.
checksumForPod(podName) {
const rootName = (0, utils_1.rootSpecName)(podName);
return this.internalData['SPEC CHECKSUMS'][rootName];
}
/// This can be either an URL or the local repository name.
repositoryForPod(podName) {
// Older Podfile.lock might not have this section yet.
const specRepos = this.internalData['SPEC REPOS'];
if (!specRepos) {
return undefined;
}
const rootName = (0, utils_1.rootSpecName)(podName);
const specRepoEntry = Object.entries(specRepos).find(([, deps]) => deps.includes(rootName));
if (specRepoEntry) {
return specRepoEntry[0];
}
return undefined;
}
/// Extracts the external source info for a given pod, if there is any.
externalSourceInfoForPod(podName) {
// Older Podfile.lock might not have this section yet.
const externalSources = this.internalData['EXTERNAL SOURCES'];
if (!externalSources) {
return undefined;
}
const externalSourceEntry = externalSources[(0, utils_1.rootSpecName)(podName)];
if (externalSourceEntry) {
return externalSourceEntry;
}
return undefined;
}
/// Extracts the checkout options for a given pod, if there is any.
checkoutOptionsForPod(podName) {
// Older Podfile.lock might not have this section yet.
const checkoutOptions = this.internalData['CHECKOUT OPTIONS'];
if (!checkoutOptions) {
return undefined;
}
const checkoutOptionsEntry = checkoutOptions[(0, utils_1.rootSpecName)(podName)];
if (checkoutOptionsEntry) {
return checkoutOptionsEntry;
}
return undefined;
}
get repositories() {
// Older Podfile.lock might not have this section yet.
const specRepos = this.internalData['SPEC REPOS'];
if (!specRepos) {
return [];
}
return Object.keys(specRepos).map((nameOrUrl) => {
return { alias: nameOrUrl };
});
}
get pkgManager() {
return {
name: 'cocoapods',
version: this.cocoapodsVersion,
repositories: this.repositories,
};
}
/// The CocoaPods version encoded in the lockfile which was used to
/// create this resolution.
get cocoapodsVersion() {
return this.internalData.COCOAPODS || 'unknown';
}
/// The checksum of the Podfile, which was used when resolving this integration.
/// - Note: this was not tracked by earlier versions of CocoaPods.
get podfileChecksum() {
return this.internalData['PODFILE CHECKSUM'];
}
}
exports.default = LockfileParser;
//# sourceMappingURL=lockfile-parser.js.map
;