Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
policy: increase tests via permutation matrix
Browse files Browse the repository at this point in the history
bmeck committed Aug 5, 2020

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 861a54c commit 928bdad
Showing 7 changed files with 437 additions and 438 deletions.
7 changes: 1 addition & 6 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
@@ -276,18 +276,13 @@ function readPackage(requestPath) {
const existing = packageJsonCache.get(jsonPath);
if (existing !== undefined) return existing;

const result = packageJsonReader.read(path.toNamespacedPath(jsonPath));
const result = packageJsonReader.read(jsonPath);
const json = result.containsKeys === false ? '{}' : result.string;
if (json === undefined) {
packageJsonCache.set(jsonPath, false);
return false;
}

if (manifest) {
const jsonURL = pathToFileURL(jsonPath);
manifest.assertIntegrity(jsonURL, json);
}

try {
const parsed = JSONParse(json);
const filtered = {
21 changes: 14 additions & 7 deletions lib/internal/modules/esm/get_source.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
'use strict';

const { getOptionValue } = require('internal/options');
const manifest = getOptionValue('--experimental-policy') ?
require('internal/process/policy').manifest :
null;

const { Buffer } = require('buffer');

const fs = require('fs');
@@ -15,20 +20,22 @@ const DATA_URL_PATTERN = /^[^/]+\/[^,;]+(?:[^,]*?)(;base64)?,([\s\S]*)$/;

async function defaultGetSource(url, { format } = {}, defaultGetSource) {
const parsed = new URL(url);
let source;
if (parsed.protocol === 'file:') {
return {
source: await readFileAsync(parsed)
};
source = await readFileAsync(parsed);
} else if (parsed.protocol === 'data:') {
const match = DATA_URL_PATTERN.exec(parsed.pathname);
if (!match) {
throw new ERR_INVALID_URL(url);
}
const [ , base64, body ] = match;
return {
source: Buffer.from(body, base64 ? 'base64' : 'utf8')
};
source = Buffer.from(body, base64 ? 'base64' : 'utf8');
} else {
throw new ERR_INVALID_URL_SCHEME(['file', 'data']);
}
if (manifest) {
manifest.assertIntegrity(parsed, source);
}
throw new ERR_INVALID_URL_SCHEME(['file', 'data']);
return { source };
}
exports.defaultGetSource = defaultGetSource;
26 changes: 20 additions & 6 deletions lib/internal/modules/package_json_reader.js
Original file line number Diff line number Diff line change
@@ -2,21 +2,35 @@

const { SafeMap } = primordials;
const { internalModuleReadJSON } = internalBinding('fs');
const { pathToFileURL } = require('url');
const { toNamespacedPath } = require('path');

const cache = new SafeMap();

/**
*
* @param {string} path
* @param {string} jsonPath
*/
function read(path) {
if (cache.has(path)) {
return cache.get(path);
function read(jsonPath) {
if (cache.has(jsonPath)) {
return cache.get(jsonPath);
}

const [string, containsKeys] = internalModuleReadJSON(path);
const [string, containsKeys] = internalModuleReadJSON(
toNamespacedPath(jsonPath)
);
const result = { string, containsKeys };
cache.set(path, result);
const { getOptionValue } = require('internal/options');
if (string !== undefined) {
const manifest = getOptionValue('--experimental-policy') ?
require('internal/process/policy').manifest :
null;
if (manifest) {
const jsonURL = pathToFileURL(jsonPath);
manifest.assertIntegrity(jsonURL, string);
}
}
cache.set(jsonPath, result);
return result;
}

3 changes: 3 additions & 0 deletions lib/internal/worker.js
Original file line number Diff line number Diff line change
@@ -205,6 +205,9 @@ class Worker extends EventEmitter {
cwdCounter: cwdCounter || workerIo.sharedCwdCounter,
workerData: options.workerData,
publicPort: port2,
manifestURL: getOptionValue('--experimental-policy') ?
require('internal/process/policy').url :
null,
manifestSrc: getOptionValue('--experimental-policy') ?
require('internal/process/policy').src :
null,
414 changes: 0 additions & 414 deletions test/parallel/test-policy-integrity.js

This file was deleted.

14 changes: 9 additions & 5 deletions test/parallel/test-policy-parse-integrity.js
Original file line number Diff line number Diff line change
@@ -19,24 +19,28 @@ function hash(algo, body) {
return h.digest('base64');
}

const policyFilepath = path.join(tmpdir.path, 'policy');
const tmpdirPath = path.join(tmpdir.path, 'test-policy-parse-integrity');
fs.rmdirSync(tmpdirPath, { maxRetries: 3, recursive: true });
fs.mkdirSync(tmpdirPath, { recursive: true });

const parentFilepath = path.join(tmpdir.path, 'parent.js');
const policyFilepath = path.join(tmpdirPath, 'policy');

const parentFilepath = path.join(tmpdirPath, 'parent.js');
const parentBody = "require('./dep.js')";

const depFilepath = path.join(tmpdir.path, 'dep.js');
const depFilepath = path.join(tmpdirPath, 'dep.js');
const depURL = pathToFileURL(depFilepath);
const depBody = '';

fs.writeFileSync(parentFilepath, parentBody);
fs.writeFileSync(depFilepath, depBody);

const tmpdirURL = pathToFileURL(tmpdir.path);
const tmpdirURL = pathToFileURL(tmpdirPath);
if (!tmpdirURL.pathname.endsWith('/')) {
tmpdirURL.pathname += '/';
}

const packageFilepath = path.join(tmpdir.path, 'package.json');
const packageFilepath = path.join(tmpdirPath, 'package.json');
const packageURL = pathToFileURL(packageFilepath);
const packageBody = '{"main": "dep.js"}';

390 changes: 390 additions & 0 deletions test/pummel/test-policy-integrity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,390 @@
'use strict';

const common = require('../common');
if (!common.hasCrypto) common.skip('missing crypto');

const { debuglog } = require('util');
const debug = debuglog('test');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const { spawnSync, spawn } = require('child_process');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { pathToFileURL } = require('url');

function hash(algo, body) {
const values = [];
{
const h = crypto.createHash(algo);
h.update(body);
values.push(`${algo}-${h.digest('base64')}`);
}
{
const h = crypto.createHash(algo);
h.update(body.replace('\n', '\r\n'));
values.push(`${algo}-${h.digest('base64')}`);
}
return values;
}

const policyPath = './policy.json';
const parentBody = {
commonjs: `
if (!process.env.DEP_FILE) {
console.error(
'missing required DEP_FILE env to determine dependency'
);
process.exit(33);
}
require(process.env.DEP_FILE)
`,
module: `
if (!process.env.DEP_FILE) {
console.error(
'missing required DEP_FILE env to determine dependency'
);
process.exit(33);
}
import(process.env.DEP_FILE)
`,
};
const workerSpawningBody = `
const path = require('path');
const { Worker } = require('worker_threads');
if (!process.env.PARENT_FILE) {
console.error(
'missing required PARENT_FILE env to determine worker entry point'
);
process.exit(33);
}
if (!process.env.DELETABLE_POLICY_FILE) {
console.error(
'missing required DELETABLE_POLICY_FILE env to check reloading'
);
process.exit(33);
}
const w = new Worker(path.resolve(process.env.PARENT_FILE));
w.on('exit', (status) => process.exit(status === 0 ? 0 : 1));
`;

let nextTestId = 1;
function newTestId() {
return nextTestId++;
}
tmpdir.refresh();

let spawned = 0;
const toSpawn = [];
function queueSpawn(opts) {
toSpawn.push(opts);
drainQueue();
}

function drainQueue() {
if (spawned > 50) {
return;
}
if (toSpawn.length) {
const config = toSpawn.shift();
const {
shouldSucceed, // = (() => { throw new Error('required')})(),
preloads, // = (() =>{ throw new Error('required')})(),
entryPath, // = (() => { throw new Error('required')})(),
willDeletePolicy, // = (() => { throw new Error('required')})(),
onError, // = (() => { throw new Error('required')})(),
resources, // = (() => { throw new Error('required')})(),
parentPath,
depPath,
} = config;
const testId = newTestId();
const configDirPath = path.join(
tmpdir.path,
`test-policy-integrity-permutation-${testId}`
);
const tmpPolicyPath = path.join(
tmpdir.path,
`deletable-policy-${testId}.json`
);
const cliPolicy = willDeletePolicy ? tmpPolicyPath : policyPath;
fs.rmdirSync(configDirPath, { maxRetries: 3, recursive: true });
fs.mkdirSync(configDirPath, { recursive: true });
const manifest = {
onerror: onError,
resources: {},
};
const manifestPath = path.join(configDirPath, policyPath);
for (const [resourcePath, { body, integrities }] of Object.entries(
resources
)) {
const filePath = path.join(configDirPath, resourcePath);
if (integrities !== null) {
manifest.resources[pathToFileURL(filePath).href] = {
integrity: integrities.join(' '),
dependencies: true,
};
}
fs.writeFileSync(filePath, body, 'utf8');
}
const manifestBody = JSON.stringify(manifest);
fs.writeFileSync(manifestPath, manifestBody);
if (cliPolicy === tmpPolicyPath) {
fs.writeFileSync(tmpPolicyPath, manifestBody);
}
const spawnArgs = [
process.execPath,
[
'--unhandled-rejections=strict',
'--experimental-policy',
cliPolicy,
...preloads.flatMap((m) => ['-r', m]),
entryPath,
'--',
testId,
configDirPath,
],
{
env: {
...process.env,
DELETABLE_POLICY_FILE: tmpPolicyPath,
PARENT_FILE: parentPath,
DEP_FILE: depPath,
},
cwd: configDirPath,
stdio: 'pipe',
},
];
spawned++;
const stdout = [];
const stderr = [];
const child = spawn(...spawnArgs);
child.stdout.on('data', (d) => stdout.push(d));
child.stderr.on('data', (d) => stderr.push(d));
child.on('exit', (status, signal) => {
spawned--;
try {
if (shouldSucceed) {
assert.strictEqual(status, 0);
} else {
assert.notStrictEqual(status, 0);
}
} catch (e) {
console.log(
'permutation',
testId,
'failed'
);
console.dir(
{ config, manifest },
{ depth: null }
);
console.log('exit code:', status, 'signal:', signal);
console.log(`stdout: ${Buffer.concat(stdout)}`);
console.log(`stderr: ${Buffer.concat(stderr)}`);
throw e;
}
fs.rmdirSync(configDirPath, { maxRetries: 3, recursive: true });
drainQueue();
});
}
}

{
const { status } = spawnSync(
process.execPath,
['--experimental-policy', policyPath, '--experimental-policy', policyPath],
{
stdio: 'pipe',
}
);
assert.notStrictEqual(status, 0, 'Should not allow multiple policies');
}
{
const enoentFilepath = path.join(tmpdir.path, 'enoent');
try {
fs.unlinkSync(enoentFilepath);
} catch { }
const { status } = spawnSync(
process.execPath,
['--experimental-policy', enoentFilepath, '-e', ''],
{
stdio: 'pipe',
}
);
assert.notStrictEqual(status, 0, 'Should not allow missing policies');
}

/**
* @template {Record<string, Array<string | string[] | boolean>>} T
* @param {T} configurations
* @param {object} path
* @returns {Array<{[key: keyof T]: T[keyof configurations]}>}
*/
function permutations(configurations, path = {}) {
const keys = Object.keys(configurations);
if (keys.length === 0) {
return path;
}
const config = keys[0];
const { [config]: values, ...otherConfigs } = configurations;
return values.flatMap((value) => {
return permutations(otherConfigs, { ...path, [config]: value });
});
}
const tests = new Set();
function fileExtensionFormat(extension, packageType) {
if (extension === '.js') {
return packageType === 'module' ? 'module' : 'commonjs';
} else if (extension === '.mjs') {
return 'module';
} else if (extension === '.cjs') {
return 'commonjs';
}
throw new Error('unknown format ' + extension);
}
for (const permutation of permutations({
entry: ['worker', 'parent', 'dep'],
preloads: [[], ['parent'], ['dep']],
onError: ['log', 'exit'],
parentExtension: ['.js', '.mjs', '.cjs'],
parentIntegrity: ['match', 'invalid', 'missing'],
depExtension: ['.js', '.mjs', '.cjs'],
depIntegrity: ['match', 'invalid', 'missing'],
packageType: ['no-package-json', 'module', 'commonjs'],
packageIntegrity: ['match', 'invalid', 'missing'],
})) {
let shouldSucceed = true;
const parentPath = `./parent${permutation.parentExtension}`;
const effectivePackageType =
permutation.packageType === 'module' ? 'module' : 'commonjs';
const parentFormat = fileExtensionFormat(
permutation.parentExtension,
effectivePackageType
);
const depFormat = fileExtensionFormat(
permutation.depExtension,
effectivePackageType
);
// non-sensical attempt to require ESM
if (depFormat === 'module' && parentFormat === 'commonjs') {
continue;
}
const depPath = `./dep${permutation.depExtension}`;
const workerSpawnerPath = './worker-spawner.cjs';
const entryPath = {
dep: depPath,
parent: parentPath,
worker: workerSpawnerPath,
}[permutation.entry];
const packageJSON = {
main: entryPath,
type: permutation.packageType,
};
if (permutation.packageType === 'no-field') {
delete packageJSON.type;
}
const resources = {
[depPath]: {
body: '',
integrities: hash('sha256', ''),
},
};
if (permutation.depIntegrity === 'invalid') {
resources[depPath].body += '\n// INVALID INTEGRITY';
shouldSucceed = false;
} else if (permutation.depIntegrity === 'missing') {
resources[depPath].integrities = null;
shouldSucceed = false;
} else if (permutation.depIntegrity === 'match') {
} else {
throw new Error('unreachable');
}
if (parentFormat !== 'commonjs') {
permutation.preloads = permutation.preloads.filter((_) => _ !== 'parent');
}
const hasParent =
permutation.entry !== 'dep' || permutation.preloads.includes('parent');
if (hasParent) {
resources[parentPath] = {
body: parentBody[parentFormat],
integrities: hash('sha256', parentBody[parentFormat]),
};
if (permutation.parentIntegrity === 'invalid') {
resources[parentPath].body += '\n// INVALID INTEGRITY';
shouldSucceed = false;
} else if (permutation.parentIntegrity === 'missing') {
resources[parentPath].integrities = null;
shouldSucceed = false;
} else if (permutation.parentIntegrity === 'match') {
} else {
throw new Error('unreachable');
}
}
if (permutation.entry === 'worker') {
resources[workerSpawnerPath] = {
body: workerSpawningBody,
integrities: hash('sha256', workerSpawningBody),
};
}
if (permutation.packageType !== 'no-package-json') {
let packageBody = JSON.stringify(packageJSON, null, 2);
let packageIntegrities = hash('sha256', packageBody);
if (
permutation.parentExtension !== '.js' ||
permutation.depExtension !== '.js'
) {
// NO PACKAGE LOOKUP
continue;
}
if (permutation.packageIntegrity === 'invalid') {
packageJSON['//'] = 'INVALID INTEGRITY';
packageBody = JSON.stringify(packageJSON, null, 2);
shouldSucceed = false;
} else if (permutation.packageIntegrity === 'missing') {
packageIntegrities = [];
shouldSucceed = false;
} else if (permutation.packageIntegrity === 'match') {
} else {
throw new Error('unreachable');
}
resources['./package.json'] = {
body: packageBody,
integrities: packageIntegrities,
};
}
const willDeletePolicy = permutation.entry === 'worker';
if (permutation.onError === 'log') {
shouldSucceed = true;
}
tests.add(
JSON.stringify({
// hasParent,
// original: permutation,
onError: permutation.onError,
shouldSucceed,
entryPath,
willDeletePolicy,
preloads: permutation.preloads
.map((_) => {
return {
'': '',
'parent': parentFormat === 'commonjs' ? parentPath : '',
'dep': depFormat === 'commonjs' ? depPath : '',
}[_];
})
.filter(Boolean),
parentPath,
depPath,
resources,
})
);
}
debug(`spawning ${tests.size} policy integrity permutations`);
debug(
'use NODE_DEBUG=test:policy-integrity:NUMBER to log a specific permutation'
);
for (const config of tests) {
const parsed = JSON.parse(config);
tests.delete(config);
queueSpawn(parsed);
}

0 comments on commit 928bdad

Please sign in to comment.