Skip to content

Commit ce50aa8

Browse files
fix: handle invalid files in imports and exports field properly
2 parents 7b6834c + d7a3003 commit ce50aa8

File tree

13 files changed

+586
-271
lines changed

13 files changed

+586
-271
lines changed

Diff for: .github/workflows/test.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
fail-fast: false
2727
matrix:
2828
os: [ubuntu-latest, windows-latest, macos-latest]
29-
node-version: [10.x, 12.x, 14.x, 16.x, 18.x, 20.x, 22.x]
29+
node-version: [10.x, 12.x, 14.x, 16.x, 18.x, 20.x, 22.4]
3030
runs-on: ${{ matrix.os }}
3131
steps:
3232
- uses: actions/checkout@v4

Diff for: lib/ExportsFieldPlugin.js

+34-5
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ const DescriptionFileUtils = require("./DescriptionFileUtils");
99
const forEachBail = require("./forEachBail");
1010
const { processExportsField } = require("./util/entrypoints");
1111
const { parseIdentifier } = require("./util/identifier");
12-
const { checkImportsExportsFieldTarget } = require("./util/path");
12+
const {
13+
invalidSegmentRegEx,
14+
deprecatedInvalidSegmentRegEx
15+
} = require("./util/path");
1316

1417
/** @typedef {import("./Resolver")} Resolver */
1518
/** @typedef {import("./Resolver").JsonObject} JsonObject */
@@ -79,6 +82,8 @@ module.exports = class ExportsFieldPlugin {
7982

8083
/** @type {string[]} */
8184
let paths;
85+
/** @type {string | null} */
86+
let usedField;
8287

8388
try {
8489
// We attach the cache to the description file instead of the exportsField value
@@ -94,7 +99,10 @@ module.exports = class ExportsFieldPlugin {
9499
fieldProcessor
95100
);
96101
}
97-
paths = fieldProcessor(remainingRequest, this.conditionNames);
102+
[paths, usedField] = fieldProcessor(
103+
remainingRequest,
104+
this.conditionNames
105+
);
98106
} catch (/** @type {unknown} */ err) {
99107
if (resolveContext.log) {
100108
resolveContext.log(
@@ -126,10 +134,31 @@ module.exports = class ExportsFieldPlugin {
126134

127135
const [relativePath, query, fragment] = parsedIdentifier;
128136

129-
const error = checkImportsExportsFieldTarget(relativePath);
137+
if (relativePath.length === 0 || !relativePath.startsWith("./")) {
138+
if (paths.length === 1) {
139+
return callback(
140+
new Error(
141+
`Invalid "exports" target "${p}" defined for "${usedField}" in the package config ${request.descriptionFilePath}, targets must start with "./"`
142+
)
143+
);
144+
}
145+
146+
return callback();
147+
}
130148

131-
if (error) {
132-
return callback(error);
149+
if (
150+
invalidSegmentRegEx.exec(relativePath.slice(2)) !== null &&
151+
deprecatedInvalidSegmentRegEx.test(relativePath.slice(2)) !== null
152+
) {
153+
if (paths.length === 1) {
154+
return callback(
155+
new Error(
156+
`Trying to access out of package scope. Requesting ${relativePath}`
157+
)
158+
);
159+
}
160+
161+
return callback();
133162
}
134163

135164
/** @type {ResolveRequest} */

Diff for: lib/ImportsFieldPlugin.js

+20-8
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ const DescriptionFileUtils = require("./DescriptionFileUtils");
99
const forEachBail = require("./forEachBail");
1010
const { processImportsField } = require("./util/entrypoints");
1111
const { parseIdentifier } = require("./util/identifier");
12-
const { checkImportsExportsFieldTarget } = require("./util/path");
12+
const {
13+
invalidSegmentRegEx,
14+
deprecatedInvalidSegmentRegEx
15+
} = require("./util/path");
1316

1417
/** @typedef {import("./Resolver")} Resolver */
1518
/** @typedef {import("./Resolver").JsonObject} JsonObject */
@@ -97,7 +100,7 @@ module.exports = class ImportsFieldPlugin {
97100
fieldProcessor
98101
);
99102
}
100-
paths = fieldProcessor(remainingRequest, this.conditionNames);
103+
[paths] = fieldProcessor(remainingRequest, this.conditionNames);
101104
} catch (/** @type {unknown} */ err) {
102105
if (resolveContext.log) {
103106
resolveContext.log(
@@ -129,15 +132,24 @@ module.exports = class ImportsFieldPlugin {
129132

130133
const [path_, query, fragment] = parsedIdentifier;
131134

132-
const error = checkImportsExportsFieldTarget(path_);
133-
134-
if (error) {
135-
return callback(error);
136-
}
137-
138135
switch (path_.charCodeAt(0)) {
139136
// should be relative
140137
case dotCode: {
138+
if (
139+
invalidSegmentRegEx.exec(path_.slice(2)) !== null &&
140+
deprecatedInvalidSegmentRegEx.test(path_.slice(2)) !== null
141+
) {
142+
if (paths.length === 1) {
143+
return callback(
144+
new Error(
145+
`Trying to access out of package scope. Requesting ${path_}`
146+
)
147+
);
148+
}
149+
150+
return callback();
151+
}
152+
141153
/** @type {ResolveRequest} */
142154
const obj = {
143155
...request,

Diff for: lib/util/entrypoints.js

+38-63
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
* @callback FieldProcessor
1717
* @param {string} request request
1818
* @param {Set<string>} conditionNames condition names
19-
* @returns {string[]} resolved paths
19+
* @returns {[string[], string | null]} resolved paths with used field
2020
*/
2121

2222
/*
@@ -72,6 +72,7 @@ Conditional mapping nested in another conditional mapping is called nested mappi
7272
7373
*/
7474

75+
const { parseIdentifier } = require("./identifier");
7576
const slashCode = "/".charCodeAt(0);
7677
const dotCode = ".".charCodeAt(0);
7778
const hashCode = "#".charCodeAt(0);
@@ -100,7 +101,7 @@ module.exports.processImportsField = function processImportsField(
100101
importsField
101102
) {
102103
return createFieldProcessor(
103-
buildImportsField(importsField),
104+
importsField,
104105
request => "#" + request,
105106
assertImportsFieldRequest,
106107
assertImportTarget
@@ -125,9 +126,10 @@ function createFieldProcessor(
125126

126127
const match = findMatch(normalizeRequest(request), field);
127128

128-
if (match === null) return [];
129+
if (match === null) return [[], null];
129130

130-
const [mapping, remainingRequest, isSubpathMapping, isPattern] = match;
131+
const [mapping, remainingRequest, isSubpathMapping, isPattern, usedField] =
132+
match;
131133

132134
/** @type {DirectMapping|null} */
133135
let direct = null;
@@ -139,19 +141,22 @@ function createFieldProcessor(
139141
);
140142

141143
// matching not found
142-
if (direct === null) return [];
144+
if (direct === null) return [[], null];
143145
} else {
144146
direct = /** @type {DirectMapping} */ (mapping);
145147
}
146148

147-
return directMapping(
148-
remainingRequest,
149-
isPattern,
150-
isSubpathMapping,
151-
direct,
152-
conditionNames,
153-
assertTarget
154-
);
149+
return [
150+
directMapping(
151+
remainingRequest,
152+
isPattern,
153+
isSubpathMapping,
154+
direct,
155+
conditionNames,
156+
assertTarget
157+
),
158+
usedField
159+
];
155160
};
156161
}
157162

@@ -200,18 +205,15 @@ function assertImportsFieldRequest(request) {
200205
* @param {boolean} expectFolder is folder expected
201206
*/
202207
function assertExportTarget(exp, expectFolder) {
203-
if (
204-
exp.charCodeAt(0) === slashCode ||
205-
(exp.charCodeAt(0) === dotCode && exp.charCodeAt(1) !== slashCode)
206-
) {
207-
throw new Error(
208-
`Export should be relative path and start with "./", got ${JSON.stringify(
209-
exp
210-
)}.`
211-
);
208+
const parsedIdentifier = parseIdentifier(exp);
209+
210+
if (!parsedIdentifier) {
211+
return;
212212
}
213213

214-
const isFolder = exp.charCodeAt(exp.length - 1) === slashCode;
214+
const [relativePath] = parsedIdentifier;
215+
const isFolder =
216+
relativePath.charCodeAt(relativePath.length - 1) === slashCode;
215217

216218
if (isFolder !== expectFolder) {
217219
throw new Error(
@@ -231,7 +233,15 @@ function assertExportTarget(exp, expectFolder) {
231233
* @param {boolean} expectFolder is folder expected
232234
*/
233235
function assertImportTarget(imp, expectFolder) {
234-
const isFolder = imp.charCodeAt(imp.length - 1) === slashCode;
236+
const parsedIdentifier = parseIdentifier(imp);
237+
238+
if (!parsedIdentifier) {
239+
return;
240+
}
241+
242+
const [relativePath] = parsedIdentifier;
243+
const isFolder =
244+
relativePath.charCodeAt(relativePath.length - 1) === slashCode;
235245

236246
if (isFolder !== expectFolder) {
237247
throw new Error(
@@ -271,7 +281,7 @@ function patternKeyCompare(a, b) {
271281
* Trying to match request to field
272282
* @param {string} request request
273283
* @param {ExportsField | ImportsField} field exports or import field
274-
* @returns {[MappingValue, string, boolean, boolean]|null} match or null, number is negative and one less when it's a folder mapping, number is request.length + 1 for direct mappings
284+
* @returns {[MappingValue, string, boolean, boolean, string]|null} match or null, number is negative and one less when it's a folder mapping, number is request.length + 1 for direct mappings
275285
*/
276286
function findMatch(request, field) {
277287
if (
@@ -281,7 +291,7 @@ function findMatch(request, field) {
281291
) {
282292
const target = /** @type {{[k: string]: MappingValue}} */ (field)[request];
283293

284-
return [target, "", false, false];
294+
return [target, "", false, false, request];
285295
}
286296

287297
/** @type {string} */
@@ -332,7 +342,8 @@ function findMatch(request, field) {
332342
target,
333343
/** @type {string} */ (bestMatchSubpath),
334344
isSubpathMapping,
335-
isPattern
345+
isPattern,
346+
bestMatch
336347
];
337348
}
338349

@@ -560,39 +571,3 @@ function buildExportsField(field) {
560571

561572
return field;
562573
}
563-
564-
/**
565-
* @param {ImportsField} field imports field
566-
* @returns {ImportsField} normalized imports field
567-
*/
568-
function buildImportsField(field) {
569-
const keys = Object.keys(field);
570-
571-
for (let i = 0; i < keys.length; i++) {
572-
const key = keys[i];
573-
574-
if (key.charCodeAt(0) !== hashCode) {
575-
throw new Error(
576-
`Imports field key should start with "#" (key: ${JSON.stringify(key)})`
577-
);
578-
}
579-
580-
if (key.length === 1) {
581-
throw new Error(
582-
`Imports field key should have at least 2 characters (key: ${JSON.stringify(
583-
key
584-
)})`
585-
);
586-
}
587-
588-
if (key.charCodeAt(1) === slashCode) {
589-
throw new Error(
590-
`Imports field key should not start with "#/" (key: ${JSON.stringify(
591-
key
592-
)})`
593-
);
594-
}
595-
}
596-
597-
return field;
598-
}

Diff for: lib/util/path.js

+8-34
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ const PathType = Object.freeze({
3333
});
3434
exports.PathType = PathType;
3535

36+
const invalidSegmentRegEx =
37+
/(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))?(\\|\/|$)/i;
38+
exports.invalidSegmentRegEx = invalidSegmentRegEx;
39+
40+
const deprecatedInvalidSegmentRegEx =
41+
/(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))(\\|\/|$)/i;
42+
exports.deprecatedInvalidSegmentRegEx = deprecatedInvalidSegmentRegEx;
43+
3644
/**
3745
* @param {string} p a path
3846
* @returns {PathType} type of path
@@ -193,37 +201,3 @@ const cachedJoin = (rootPath, request) => {
193201
return cacheEntry;
194202
};
195203
exports.cachedJoin = cachedJoin;
196-
197-
/**
198-
* @param {string} relativePath relative path
199-
* @returns {undefined|Error} nothing or an error
200-
*/
201-
const checkImportsExportsFieldTarget = relativePath => {
202-
let lastNonSlashIndex = 0;
203-
let slashIndex = relativePath.indexOf("/", 1);
204-
let cd = 0;
205-
206-
while (slashIndex !== -1) {
207-
const folder = relativePath.slice(lastNonSlashIndex, slashIndex);
208-
209-
switch (folder) {
210-
case "..": {
211-
cd--;
212-
if (cd < 0)
213-
return new Error(
214-
`Trying to access out of package scope. Requesting ${relativePath}`
215-
);
216-
break;
217-
}
218-
case ".":
219-
break;
220-
default:
221-
cd++;
222-
break;
223-
}
224-
225-
lastNonSlashIndex = slashIndex + 1;
226-
slashIndex = relativePath.indexOf("/", lastNonSlashIndex);
227-
}
228-
};
229-
exports.checkImportsExportsFieldTarget = checkImportsExportsFieldTarget;

Diff for: package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
"pretty": "prettier --loglevel warn --write \"lib/**/*.{js,json}\" \"test/*.js\"",
5555
"pretest": "yarn lint",
5656
"spelling": "cspell \"**\"",
57-
"test:only": "jest",
57+
"test:only": "node_modules/.bin/jest",
5858
"test:watch": "yarn test:only -- --watch",
5959
"test:coverage": "yarn test:only -- --collectCoverageFrom=\"lib/**/*.js\" --coverage",
6060
"test": "yarn test:coverage",

0 commit comments

Comments
 (0)