Skip to content

Commit f3fd1fc

Browse files
committed
feat(compartment-mapper): expose findUnknownCanonicalNames
Expose `findUnknownCanonicalNames()` from new export `@endo/compartment-mapper/policy.js`. Should be used _after_ `captureFromMap` when called without policy, since unknown canonical names are not emitted by the `UnknownCanonicalNameHook` unless a policy is present.
1 parent 49177f5 commit f3fd1fc

5 files changed

Lines changed: 145 additions & 11 deletions

File tree

.changeset/cozy-bushes-allow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@endo/compartment-mapper': minor
3+
---
4+
5+
Expose `findUnknownCanonicalNames()` from new export `@endo/compartment-mapper/policy.js`.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// eslint-disable-next-line import/export -- just types
2+
export * from './src/types-external.js';
3+
4+
export { findUnknownCanonicalNames } from './src/policy.js';

packages/compartment-mapper/src/node-modules.js

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ import {
2323
ENTRY_COMPARTMENT,
2424
generateCanonicalName,
2525
} from './policy-format.js';
26-
import { dependencyAllowedByPolicy, makePackagePolicy } from './policy.js';
26+
import {
27+
dependencyAllowedByPolicy,
28+
makePackagePolicy,
29+
findUnknownCanonicalNames,
30+
} from './policy.js';
2731
import { unpackReadPowers } from './powers.js';
2832
import { search, searchDescriptor } from './search.js';
2933
import { GenericGraph, makeShortestPath } from './generic-graph.js';
@@ -1268,16 +1272,16 @@ const finalizeGraph = (
12681272
};
12691273

12701274
/**
1271-
* Returns an array of "issue" objects if any resources referenced in `policy`
1272-
* are unknown.
1275+
* Returns an array of issue objects for each unknown canonical name referenced
1276+
* in `policy`, using the precomputed `unknownSet` to drive the check.
12731277
*
12741278
* @param {Set<CanonicalName>} canonicalNames Set of all known canonical names
12751279
* @param {SomePolicy} policy Policy to validate
1280+
* @param {Set<CanonicalName>} unknownSet Set of unknown canonical names from {@link findUnknownCanonicalNames}
12761281
* @returns {Array<{canonicalName: CanonicalName, message: string, path:
1277-
* string[], suggestion?: CanonicalName}>} Array of issue objects, or `undefined` if no issues were
1278-
* found
1282+
* string[], suggestion?: CanonicalName}>} Array of issue objects
12791283
*/
1280-
const validatePolicyResources = (canonicalNames, policy) => {
1284+
const validatePolicyResources = (canonicalNames, policy, unknownSet) => {
12811285
/**
12821286
* Finds a suggestion for `badName` if it is a suffix of any
12831287
* canonical name in `canonicalNames`.
@@ -1299,7 +1303,7 @@ const validatePolicyResources = (canonicalNames, policy) => {
12991303
for (const [resourceName, resourcePolicy] of entries(
13001304
policy.resources ?? {},
13011305
)) {
1302-
if (!canonicalNames.has(resourceName)) {
1306+
if (unknownSet.has(resourceName)) {
13031307
const issueMessage = `Resource ${q(resourceName)} was not found`;
13041308
const suggestion = findSuggestion(resourceName);
13051309
const issue = {
@@ -1314,7 +1318,7 @@ const validatePolicyResources = (canonicalNames, policy) => {
13141318
}
13151319
if (typeof resourcePolicy?.packages === 'object') {
13161320
for (const packageName of keys(resourcePolicy.packages)) {
1317-
if (!canonicalNames.has(packageName)) {
1321+
if (unknownSet.has(packageName)) {
13181322
const issueMessage = `Resource ${q(packageName)} from resource ${q(resourceName)} was not found`;
13191323
const suggestion = findSuggestion(packageName);
13201324
const issue = {
@@ -1471,8 +1475,19 @@ export const compartmentMapForNodeModules_ = async (
14711475
// of known canonical names and fire the `unknownCanonicalName` hook for each
14721476
// unknown resource, if found
14731477
if (policy) {
1474-
const canonicalNames = new Set(canonicalNameMap.keys());
1475-
const issues = validatePolicyResources(canonicalNames, policy) ?? [];
1478+
const knownCanonicalNames = new Set(canonicalNameMap.keys());
1479+
const unknownCanonicalNames = findUnknownCanonicalNames(
1480+
knownCanonicalNames,
1481+
policy,
1482+
);
1483+
const issues =
1484+
unknownCanonicalNames.size > 0
1485+
? validatePolicyResources(
1486+
knownCanonicalNames,
1487+
policy,
1488+
unknownCanonicalNames,
1489+
)
1490+
: [];
14761491
// Call default handler first if policy exists
14771492
for (const { message, canonicalName, path, suggestion } of issues) {
14781493
const hookInput = {

packages/compartment-mapper/src/policy.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,3 +557,35 @@ export const attenuateModuleHook = async (
557557
originalModuleRecord,
558558
});
559559
};
560+
561+
/**
562+
* Returns the set of canonical names referenced in `policy` that do not exist
563+
* in the provided `canonicalNames` set.
564+
*
565+
* @param {Set<CanonicalName>} canonicalNames Set of all known canonical names
566+
* @param {SomePolicy} policy Policy to inspect
567+
* @returns {Set<CanonicalName>} Set of unknown canonical names found in the policy
568+
*/
569+
570+
export const findUnknownCanonicalNames = (canonicalNames, policy) => {
571+
/** @type {Set<CanonicalName>} */
572+
const unknown = new Set();
573+
for (const [resourceName, resourcePolicy] of entries(
574+
policy.resources ?? {},
575+
)) {
576+
if (!canonicalNames.has(resourceName)) {
577+
unknown.add(resourceName);
578+
}
579+
if (
580+
resourcePolicy?.packages &&
581+
typeof resourcePolicy.packages === 'object'
582+
) {
583+
for (const packageName of keys(resourcePolicy.packages)) {
584+
if (!canonicalNames.has(packageName)) {
585+
unknown.add(packageName);
586+
}
587+
}
588+
}
589+
}
590+
return unknown;
591+
};

packages/compartment-mapper/test/policy.test.js

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
ATTENUATORS_COMPARTMENT,
99
ENTRY_COMPARTMENT,
1010
} from '../src/policy-format.js';
11-
import { makePackagePolicy } from '../src/policy.js';
11+
import { makePackagePolicy, findUnknownCanonicalNames } from '../src/policy.js';
1212

1313
function combineAssertions(...assertionFunctions) {
1414
return async (...args) => {
@@ -633,3 +633,81 @@ test('makePackagePolicy() - empty resources object returns empty package policy'
633633

634634
t.deepEqual(result, {});
635635
});
636+
637+
test('findUnknownCanonicalNames() - returns empty set when all names are known', t => {
638+
const known = new Set(['alice', 'alice>carol']);
639+
const testPolicy = {
640+
entry: {},
641+
resources: {
642+
alice: {
643+
packages: { 'alice>carol': true },
644+
},
645+
},
646+
};
647+
648+
const result = findUnknownCanonicalNames(known, testPolicy);
649+
650+
t.deepEqual(result, new Set());
651+
});
652+
653+
test('findUnknownCanonicalNames() - detects unknown top-level resource names', t => {
654+
const known = new Set(['alice']);
655+
const testPolicy = {
656+
entry: {},
657+
resources: {
658+
alice: {},
659+
ghost: {},
660+
},
661+
};
662+
663+
const result = findUnknownCanonicalNames(known, testPolicy);
664+
665+
t.deepEqual(result, new Set(['ghost']));
666+
});
667+
668+
test('findUnknownCanonicalNames() - detects unknown names nested in packages', t => {
669+
const known = new Set(['alice', 'bob']);
670+
const testPolicy = {
671+
entry: {},
672+
resources: {
673+
alice: {
674+
packages: {
675+
bob: true,
676+
'alice>nobody': true,
677+
},
678+
},
679+
},
680+
};
681+
682+
const result = findUnknownCanonicalNames(known, testPolicy);
683+
684+
t.deepEqual(result, new Set(['alice>nobody']));
685+
});
686+
687+
test('findUnknownCanonicalNames() - deduplicates names referenced multiple times', t => {
688+
const known = new Set(['alice']);
689+
const testPolicy = {
690+
entry: {},
691+
resources: {
692+
alice: {
693+
packages: { ghost: true },
694+
},
695+
ghost: {
696+
packages: { ghost: true },
697+
},
698+
},
699+
};
700+
701+
const result = findUnknownCanonicalNames(known, testPolicy);
702+
703+
t.deepEqual(result, new Set(['ghost']));
704+
});
705+
706+
test('findUnknownCanonicalNames() - returns empty set for policy with no resources', t => {
707+
const known = new Set(['alice']);
708+
const testPolicy = { entry: {} };
709+
710+
const result = findUnknownCanonicalNames(known, testPolicy);
711+
712+
t.deepEqual(result, new Set());
713+
});

0 commit comments

Comments
 (0)