Skip to content

Commit 3fb43cd

Browse files
committed
Improvements:
* Added fallback for WeakSet * Disable fallbacks for ESM exports * Improved fuzzing coverage
1 parent 5c747b6 commit 3fb43cd

File tree

11 files changed

+315
-30
lines changed

11 files changed

+315
-30
lines changed

esbuild.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ await Promise.all(
4747
outExtension: {
4848
'.js': format === 'esm' ? '.mjs' : '.cjs',
4949
},
50+
define: {
51+
...buildOptionsBase.define,
52+
'import.meta.format': JSON.stringify(format),
53+
},
5054
});
5155
}),
5256
);

fuzz/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
"type": "commonjs",
66
"scripts": {
77
"start": "jsfuzz src/index.cjs corpus",
8-
"report": "nyc report --per-file --exclude-after-remap=false"
8+
"start:fast": "jsfuzz src/fast.cjs corpus",
9+
"report": "nyc report --per-file --exclude-after-remap=false",
10+
"report:html": "nyc report --reporter=html --per-file --exclude-after-remap=false"
911
},
1012
"devDependencies": {
1113
"jsfuzz": "^1.0.15"

fuzz/src/fast.cjs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/* Copyright © 2025 Apeleg Limited. All rights reserved.
2+
*
3+
* Permission to use, copy, modify, and distribute this software for any
4+
* purpose with or without fee is hereby granted, provided that the above
5+
* copyright notice and this permission notice appear in all copies.
6+
*
7+
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
8+
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
9+
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
10+
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
11+
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
12+
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
13+
* PERFORMANCE OF THIS SOFTWARE.
14+
*/
15+
/* eslint-disable @typescript-eslint/no-require-imports */
16+
17+
const fuzzFactory = require('./fuzzFactory.cjs');
18+
const fuzz = fuzzFactory(require);
19+
20+
module.exports = { fuzz };

fuzz/src/fuzzFactory.cjs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/* Copyright © 2025 Apeleg Limited. All rights reserved.
2+
*
3+
* Permission to use, copy, modify, and distribute this software for any
4+
* purpose with or without fee is hereby granted, provided that the above
5+
* copyright notice and this permission notice appear in all copies.
6+
*
7+
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
8+
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
9+
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
10+
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
11+
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
12+
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
13+
* PERFORMANCE OF THIS SOFTWARE.
14+
*/
15+
16+
function fuzzFactory(require, pre, post) {
17+
return function (buf) {
18+
if (buf.length < 1) return;
19+
20+
pre?.(buf);
21+
22+
// This line for coverage purposes (use all getters)
23+
void { ...require('../../dist/index.cjs') };
24+
25+
const {
26+
fuzz: negotiateMediaType,
27+
} = require('./negotiateMediaType.cjs');
28+
const { fuzz: parseAcceptHeader } = require('./parseAcceptHeader.cjs');
29+
const { fuzz: parseMediaType } = require('./parseMediaType.cjs');
30+
31+
switch (buf[0] & 0b1100_0000) {
32+
case 0b0000_0000:
33+
negotiateMediaType(buf);
34+
break;
35+
case 0b0100_0000:
36+
parseAcceptHeader(buf);
37+
break;
38+
case 0b1000_0000:
39+
parseMediaType(buf);
40+
break;
41+
}
42+
43+
post?.(buf);
44+
};
45+
}
46+
47+
module.exports = fuzzFactory;

fuzz/src/index.cjs

Lines changed: 108 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,115 @@
1313
* PERFORMANCE OF THIS SOFTWARE.
1414
*/
1515
/* eslint-disable @typescript-eslint/no-require-imports */
16-
const { fuzz: negotiateMediaType } = require('./negotiateMediaType.cjs');
17-
const { fuzz: parseAcceptHeader } = require('./parseAcceptHeader.cjs');
18-
const { fuzz: parseMediaType } = require('./parseMediaType.cjs');
19-
20-
function fuzz(buf) {
21-
if (buf.length < 1) return;
22-
23-
switch (buf[0] & 0b11000000) {
24-
case 0b00000000:
25-
negotiateMediaType(buf);
26-
break;
27-
case 0b01000000:
28-
parseAcceptHeader(buf);
29-
break;
30-
case 0b10000000:
31-
parseMediaType(buf);
32-
break;
16+
17+
const fuzzFactory = require('./fuzzFactory.cjs');
18+
19+
const {
20+
enableSet,
21+
isSetEnabled,
22+
enableStringIncludes,
23+
isStringIncludesEnabled,
24+
enableWeakMap,
25+
isWeakMapEnabled,
26+
} = (() => {
27+
const { Set, WeakMap } = globalThis;
28+
const { includes: String_includes } = String.prototype;
29+
30+
let SetEnabled = true,
31+
StringIncludesEnabled = true,
32+
WeakMapEnabled = true;
33+
34+
const enableSet = (v) => {
35+
SetEnabled = v;
36+
};
37+
const isSetEnabled = () => SetEnabled;
38+
const enableStringIncludes = (v) => {
39+
StringIncludesEnabled = v;
40+
};
41+
const isStringIncludesEnabled = () => StringIncludesEnabled;
42+
const enableWeakMap = (v) => {
43+
WeakMapEnabled = v;
44+
};
45+
const isWeakMapEnabled = () => WeakMapEnabled;
46+
47+
Object.defineProperties(globalThis, {
48+
Set: {
49+
get: () => {
50+
return SetEnabled ? Set : undefined;
51+
},
52+
},
53+
WeakMap: {
54+
get: () => {
55+
return WeakMapEnabled ? WeakMap : undefined;
56+
},
57+
},
58+
});
59+
60+
Object.defineProperties(String.prototype, {
61+
includes: {
62+
get: () => {
63+
return StringIncludesEnabled ? String_includes : undefined;
64+
},
65+
},
66+
});
67+
68+
return {
69+
enableSet,
70+
isSetEnabled,
71+
enableStringIncludes,
72+
isStringIncludesEnabled,
73+
enableWeakMap,
74+
isWeakMapEnabled,
75+
};
76+
})();
77+
78+
function hotRequire(id) {
79+
delete require.cache[require.resolve(id)];
80+
81+
const SetEnabled = isSetEnabled();
82+
const WeakMapEnabled = isWeakMapEnabled();
83+
const StringIncludesEnabled = isStringIncludesEnabled();
84+
85+
enableSet(true);
86+
enableWeakMap(true);
87+
enableStringIncludes(true);
88+
89+
try {
90+
return require(id);
91+
} finally {
92+
enableSet(SetEnabled);
93+
enableWeakMap(WeakMapEnabled);
94+
enableStringIncludes(StringIncludesEnabled);
3395
}
3496
}
3597

98+
const fuzz = (() => {
99+
const map = Object.create(null);
100+
101+
const getFeatureKey = () => {
102+
return [isSetEnabled, isWeakMapEnabled, isStringIncludesEnabled]
103+
.map((v) => +!!v())
104+
.join('');
105+
};
106+
107+
const require_ = (id) => {
108+
const key = getFeatureKey();
109+
if (!Object.hasOwn(map, key)) {
110+
map[key] = Object.create(null);
111+
}
112+
const resolved = require.resolve(id);
113+
if (!Object.hasOwn(map[key], resolved)) {
114+
map[key][resolved] = hotRequire(resolved);
115+
}
116+
117+
return map[key][resolved];
118+
};
119+
120+
return fuzzFactory(require_, (buf) => {
121+
enableSet(!(buf[0] & 0b0010_0000));
122+
enableWeakMap(!(buf[0] & 0b0001_0000));
123+
enableStringIncludes(!(buf[0] & 0b0000_1000));
124+
});
125+
})();
126+
36127
module.exports = { fuzz };

fuzz/src/parseAcceptHeader.cjs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* PERFORMANCE OF THIS SOFTWARE.
1414
*/
1515
/* eslint-disable @typescript-eslint/no-require-imports */
16-
const { parseAcceptHeader } = require('../../dist/index.cjs');
16+
const { parseAcceptHeader, parseMediaType } = require('../../dist/index.cjs');
1717

1818
function fuzz(buf) {
1919
if (buf.length < 1) return;
@@ -22,7 +22,8 @@ function fuzz(buf) {
2222
const typesOnly = !!(buf[0] & 0b01);
2323
const permissive = !!(buf[0] & 0b10);
2424

25-
parseAcceptHeader(accept, typesOnly, permissive);
25+
const parsedTypes = parseAcceptHeader(accept, typesOnly, permissive);
26+
parsedTypes.forEach((type) => parseMediaType(type, permissive));
2627
}
2728

2829
module.exports = { fuzz };

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@apeleghq/http-media-type-negotiator",
3-
"version": "1.0.2",
3+
"version": "1.0.3",
44
"description": "RFC‑aware HTTP media type negotiator - parses and normalises Accept headers and media types (RFC 9110), supports q-value ranking, wildcard matching, and a permissive mode for real‑world headers.",
55
"type": "module",
66
"main": "dist/index.cjs",

src/global.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/* eslint-disable @typescript-eslint/naming-convention */
2+
3+
interface ImportMeta {
4+
readonly format: 'esm' | 'cjs' | 'iife';
5+
}

src/negotiateMediaTypeFactory.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { $subtype, $type } from './constants.js';
1717
import normaliseMediaType from './normaliseMediaType.js';
1818
import parseAcceptHeader from './parseAcceptHeader.js';
1919
import parseMediaType, { type TMediaType } from './parseMediaType.js';
20+
import { wm } from './utils.js';
2021

2122
const QVALUE_REGEX = /^(?:(?:0(?:\.\d{1,3})?)|(?:1(?:\.0{1,3})?))$/;
2223
const DEFAULT_Q = 1000;
@@ -124,7 +125,7 @@ const negotiateMediaTypeFactory = (availableMediaTypes: string[]) => {
124125
return () => null;
125126
}
126127

127-
const mapToOriginal = new Map<TMediaType, string>();
128+
const mapToOriginal = wm<TMediaType, string>();
128129
const parsedAvailableTypes = availableMediaTypes.map((mediaType) => {
129130
const value = normaliseMediaType(parseMediaType(mediaType));
130131
mapToOriginal.set(value, mediaType);
@@ -136,7 +137,7 @@ const negotiateMediaTypeFactory = (availableMediaTypes: string[]) => {
136137
return availableMediaTypes[0];
137138
}
138139

139-
const qMap = new WeakMap<TMediaType, number>();
140+
const qMap = wm<TMediaType, number>();
140141

141142
const parsedAcceptableTypes = parseAcceptHeader(
142143
accept,
@@ -164,10 +165,7 @@ const negotiateMediaTypeFactory = (availableMediaTypes: string[]) => {
164165
return qb - qa;
165166
}) as TMediaType[];
166167

167-
const map = new WeakMap<
168-
TMediaType,
169-
(typeof parsedAvailableTypes)[number]
170-
>();
168+
const map = wm<TMediaType, (typeof parsedAvailableTypes)[number]>();
171169

172170
// First pass: remove media types that aren't possible
173171
const overlappingTypes = parsedAcceptableTypes.filter(

0 commit comments

Comments
 (0)