feat(ses): Retract evaluate name option, use sourceURL
MODIFY
packages/ses/NEWS.md
packages/ses/NEWS.md
@@ -4,10 +4,8 @@ User-visible changes in SES:
* Modules and evaluated code that contains the censored substrings
for dynamic eval, dynamic import, and HTML comments will now
- throw errors that contain the name of the originating source or merely
- `<unknown>`.
- The `compartment.evaluate` method gains a `name` option to thread
- the name of the evaluated source for these errors.
+ throw errors that contain the `sourceURL` from any `//#sourceURL=` comment
+ toward the end of the source or merely `<unknown>`.
## Release 0.11.0 (3-November-2020)

MODIFY
packages/ses/src/compartment-shim.js
packages/ses/src/compartment-shim.js
@@ -48,7 +48,6 @@ export const CompartmentPrototype = {
transforms = [],
sloppyGlobalsMode = false,
__moduleShimLexicals__ = undefined,
- name = '<unknown>',
} = options;
const localTransforms = [...transforms];
@@ -79,7 +78,6 @@ export const CompartmentPrototype = {
globalTransforms,
localTransforms,
sloppyGlobalsMode,
- name,
});
},

MODIFY
packages/ses/src/evaluate.js
packages/ses/src/evaluate.js
@@ -23,16 +23,15 @@ export function performEval(
localTransforms = [],
globalTransforms = [],
sloppyGlobalsMode = false,
- name = '<unknown>',
} = {},
) {
// Execute the mandatory transforms last to ensure that any rewritten code
// meets those mandatory requirements.
- source = applyTransforms(
- source,
- [...localTransforms, ...globalTransforms, mandatoryTransforms],
- name,
- );
+ source = applyTransforms(source, [
+ ...localTransforms,
+ ...globalTransforms,
+ mandatoryTransforms,
+ ]);
const scopeHandler = createScopeHandler(globalObject, localObject, {
sloppyGlobalsMode,

ADD
packages/ses/src/get-source-url.js
+// Captures a key and value of the form #key=value or @key=value
+const sourceMetaEntryRegExp =
+ '\\s*[@#]\\s*([a-zA-Z][a-zA-Z0-9]*)\\s*=\\s*([^\\s\\*]*)';
+// Captures either a one-line or multi-line comment containing
+// one #key=value or @key=value.
+// Produces two pairs of capture groups, but the initial two may be undefined.
+// On account of the mechanics of regular expressions, scanning from the end
+// does not allow us to capture every pair, so getSourceURL must capture and
+// trim until there are no matching comments.
+const sourceMetaEntriesRegExp = new RegExp(
+ `(?:\\s*//${sourceMetaEntryRegExp}|/\\*${sourceMetaEntryRegExp}\\s*\\*/)\\s*$`,
+);
+
+export function getSourceURL(src) {
+ let sourceURL = '<unknown>';
+
+ // Our regular expression matches the last one or two comments with key value
+ // pairs at the end of the source, avoiding a scan over the entire length of
+ // the string, but at the expense of being able to capture all the (key,
+ // value) pair meta comments at the end of the source, which may include
+ // sourceMapURL in addition to sourceURL.
+ // So, we sublimate the comments out of the source until no source or no
+ // comments remain.
+ while (src.length > 0) {
+ const match = sourceMetaEntriesRegExp.exec(src);
+ if (match === null) {
+ break;
+ }
+ src = src.slice(0, src.length - match[0].length);
+
+ // We skip $0 since it contains the entire match.
+ // The match contains four capture groups,
+ // two (key, value) pairs, the first of which
+ // may be undefined.
+ // On the off-chance someone put two sourceURL comments in their code with
+ // different commenting conventions, the latter has precedence.
+ if (match[3] === 'sourceURL') {
+ sourceURL = match[4];
+ } else if (match[1] === 'sourceURL') {
+ sourceURL = match[2];
+ }
+ }
+
+ return sourceURL;
+}

MODIFY
packages/ses/src/module-instance.js
packages/ses/src/module-instance.js
@@ -25,7 +25,7 @@ export const makeModuleInstance = (
liveExportMap,
exportAlls,
} = moduleAnalysis;
- const { compartment, moduleSpecifier, moduleLocation } = moduleRecord;
+ const { compartment, moduleSpecifier } = moduleRecord;
const compartmentFields = privateFields.get(compartment);
@@ -322,7 +322,6 @@ export const makeModuleInstance = (
globalObject,
transforms: __shimTransforms__,
__moduleShimLexicals__: localLexicals,
- name: moduleLocation,
});
let didThrow = false;
let thrownError;

MODIFY
packages/ses/src/transforms.js
packages/ses/src/transforms.js
import { stringSearch, stringSlice, stringSplit } from './commons.js';
+import { getSourceURL } from './get-source-url.js';
// Find the first occurence of the given pattern and return
// the location as the approximate line number.
@@ -32,11 +33,12 @@ function getLineNumber(src, pattern) {
const htmlCommentPattern = new RegExp(`(?:${'<'}!--|--${'>'})`);
-export function rejectHtmlComments(src, name) {
+export function rejectHtmlComments(src) {
const lineNumber = getLineNumber(src, htmlCommentPattern);
if (lineNumber < 0) {
return src;
}
+ const name = getSourceURL(src);
throw new SyntaxError(
`SES3: Possible HTML comment rejected at ${name}:${lineNumber}`,
);
@@ -66,11 +68,12 @@ export function rejectHtmlComments(src, name) {
const importPattern = new RegExp('\\bimport\\s*(?:\\(|/[/*])');
-export function rejectImportExpressions(src, name) {
+export function rejectImportExpressions(src) {
const lineNumber = getLineNumber(src, importPattern);
if (lineNumber < 0) {
return src;
}
+ const name = getSourceURL(src);
throw new SyntaxError(
`SES2: Possible import expression rejected at ${name}:${lineNumber}`,
);
@@ -96,26 +99,27 @@ export function rejectImportExpressions(src, name) {
const someDirectEvalPattern = new RegExp('\\beval\\s*(?:\\(|/[/*])');
// Exported for unit tests.
-export function rejectSomeDirectEvalExpressions(src, name) {
+export function rejectSomeDirectEvalExpressions(src) {
const lineNumber = getLineNumber(src, someDirectEvalPattern);
if (lineNumber < 0) {
return src;
}
+ const name = getSourceURL(src);
throw new SyntaxError(
`SES1: Possible direct eval expression rejected at ${name}:${lineNumber}`,
);
}
-export function mandatoryTransforms(source, name) {
- source = rejectHtmlComments(source, name);
- source = rejectImportExpressions(source, name);
- source = rejectSomeDirectEvalExpressions(source, name);
+export function mandatoryTransforms(source) {
+ source = rejectHtmlComments(source);
+ source = rejectImportExpressions(source);
+ source = rejectSomeDirectEvalExpressions(source);
return source;
}
-export function applyTransforms(source, transforms, name) {
+export function applyTransforms(source, transforms) {
for (const transform of transforms) {
- source = transform(source, name);
+ source = transform(source);
}
return source;
}

ADD
packages/ses/test/get-source-url.test.js
+/* eslint max-depth: "off" */
+import tap from 'tap';
+import { getSourceURL } from '../src/get-source-url.js';
+
+const { test } = tap;
+
+test('getSourceURL', t => {
+ t.plan(2 + 1 * 2 * 2 * 2 * 3 * 2);
+
+ t.equal(getSourceURL(''), '<unknown>');
+ t.equal(getSourceURL(`//@sourceURL=path/file.js`), 'path/file.js');
+
+ for (const fileName of ['path/to/file.js']) {
+ for (const [startComment, endComment] of [
+ ['//', '\n'],
+ ['/*', '*/'],
+ ]) {
+ for (const prefix of ['@', '#']) {
+ for (const delimiter of ['', ' ']) {
+ for (const header of ['', 'const ignoreMe = 10;', 'ignoreMe()\n\n']) {
+ for (const footer of [
+ '',
+ delimiter +
+ [
+ startComment,
+ prefix,
+ 'sourceMapURL',
+ '=',
+ `${fileName}.json`,
+ endComment,
+ ].join(delimiter),
+ ]) {
+ const source =
+ header +
+ [
+ startComment,
+ prefix,
+ 'sourceURL',
+ '=',
+ fileName,
+ endComment,
+ ].join(delimiter) +
+ footer;
+ t.equal(getSourceURL(source), fileName, JSON.stringify(source));
+ }
+ }
+ }
+ }
+ }
+ }
+});

MODIFY
packages/ses/test/reject-direct-eval.test.js
packages/ses/test/reject-direct-eval.test.js
@@ -109,10 +109,9 @@ test('reject direct eval expressions with name', t => {
t.plan(2);
const c = new Compartment();
- const code = 'eval("evil")';
t.throws(
- () => c.evaluate(code),
+ () => c.evaluate('eval("evil")'),
{
name: 'SyntaxError',
message: 'SES1: Possible direct eval expression rejected at <unknown>:1',
@@ -122,9 +121,9 @@ test('reject direct eval expressions with name', t => {
t.throws(
() =>
- c.evaluate(code, {
- name: 'contrived://example',
- }),
+ c.evaluate(
+ 'eval("evil") /* #sourceURL=contrived://example */ // @sourceMapURL=ignore/me.json',
+ ),
{
name: 'SyntaxError',
message:

MODIFY
packages/ses/test/reject-html-comment.test.js
packages/ses/test/reject-html-comment.test.js
@@ -123,25 +123,24 @@ test('reject HTML comment expressions with name', t => {
t.plan(2);
const c = new Compartment();
- const code = '<!-- -->';
t.throws(
- () => c.evaluate(code),
+ () => c.evaluate('\n<!-- -->'),
{
name: 'SyntaxError',
- message: 'SES3: Possible HTML comment rejected at <unknown>:1',
+ message: 'SES3: Possible HTML comment rejected at <unknown>:2',
},
'htmlCloseComment without name',
);
t.throws(
() =>
- c.evaluate(code, {
- name: 'bogus://contrived',
- }),
+ c.evaluate(
+ '\n\n<!-- stuff -->/* @sourceURL=bogus://contrived\n*/ // #sourceMap=ignore/me',
+ ),
{
name: 'SyntaxError',
- message: 'SES3: Possible HTML comment rejected at bogus://contrived:1',
+ message: 'SES3: Possible HTML comment rejected at bogus://contrived:3',
},
'htmlCloseComment with name',
);

MODIFY
packages/ses/test/reject-import-expression.test.js
packages/ses/test/reject-import-expression.test.js
@@ -104,13 +104,10 @@ test('reject import expressions with error messages', t => {
);
t.throws(
- () =>
- c.evaluate(code, {
- name: 'never://land',
- }),
+ () => c.evaluate(`\n${code}//#sourceURL= never://land`),
{
name: 'SyntaxError',
- message: 'SES2: Possible import expression rejected at never://land:1',
+ message: 'SES2: Possible import expression rejected at never://land:2',
},
'with name',
);

feat(ses): Retract evaluate name option, use sourceURL (#521)