Skip to content

Commit 541f9c5

Browse files
broofawesleytodd
andauthored
feat!: resolve extension conflicts with mime-score, close #116 (#119)
* feat!: resolve extension conflicts with mime-score, close #116 * feat!: resolve extension conflicts with mime-score, close #116 * chore: fix lint issues * wip: lint, logic, and test fixup * test: tweak log output * fix: update history.md --------- Co-authored-by: Wes Todd <[email protected]>
1 parent 540d9f3 commit 541f9c5

File tree

4 files changed

+119
-33
lines changed

4 files changed

+119
-33
lines changed

HISTORY.md

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
2.1.35 / 2022-03-12
1+
unreleased
22
===================
33

4-
5-
- Add extensions from IANA for more `image/*` types
6-
- Add extension `.asc` to `application/pgp-keys`
7-
- Add extensions to various XML types
8-
- Add new upstream MIME types
9-
10-
2.1.34 / 2021-11-08
11-
===================
12-
13-
14-
- Add new upstream MIME types
4+
* resolve extension conflicts with mime-score (#119)
5+
* asc -> application/pgp-signature is now application/pgp-keys
6+
* mpp -> application/vnd.ms-project is now application/dash-patch+xml
7+
* ac -> application/vnd.nokia.n-gage.ac+xml is now application/pkix-attr-cert
8+
* bdoc -> application/x-bdoc is now application/bdoc
9+
* wmz -> application/x-msmetafile is now application/x-ms-wmz
10+
* xsl -> application/xslt+xml is now application/xml
11+
* wav -> audio/wave is now audio/wav
12+
* rtf -> text/rtf is now application/rtf
13+
* xml -> text/xml is now application/xml
14+
* mp4 -> video/mp4 is now application/mp4
15+
* mpg4 -> video/mp4 is now application/mp4
1516

1617
2.1.33 / 2021-10-01
1718
===================

index.js

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
var db = require('mime-db')
1616
var extname = require('path').extname
17+
var mimeScore = require('./mimeScore')
1718

1819
/**
1920
* Module variables.
@@ -35,6 +36,7 @@ exports.extension = extension
3536
exports.extensions = Object.create(null)
3637
exports.lookup = lookup
3738
exports.types = Object.create(null)
39+
exports._extensionConflicts = []
3840

3941
// Populate the extensions/types maps
4042
populateMaps(exports.extensions, exports.types)
@@ -80,9 +82,7 @@ function contentType (str) {
8082
return false
8183
}
8284

83-
var mime = str.indexOf('/') === -1
84-
? exports.lookup(str)
85-
: str
85+
var mime = str.indexOf('/') === -1 ? exports.lookup(str) : str
8686

8787
if (!mime) {
8888
return false
@@ -152,9 +152,6 @@ function lookup (path) {
152152
*/
153153

154154
function populateMaps (extensions, types) {
155-
// source preference (least -> most)
156-
var preference = ['nginx', 'apache', undefined, 'iana']
157-
158155
Object.keys(db).forEach(function forEachMimeType (type) {
159156
var mime = db[type]
160157
var exts = mime.extensions
@@ -169,20 +166,46 @@ function populateMaps (extensions, types) {
169166
// extension -> mime
170167
for (var i = 0; i < exts.length; i++) {
171168
var extension = exts[i]
172-
173-
if (types[extension]) {
174-
var from = preference.indexOf(db[types[extension]].source)
175-
var to = preference.indexOf(mime.source)
176-
177-
if (types[extension] !== 'application/octet-stream' &&
178-
(from > to || (from === to && types[extension].slice(0, 12) === 'application/'))) {
179-
// skip the remapping
180-
continue
181-
}
169+
types[extension] = _preferredType(extension, types[extension], type)
170+
171+
// DELETE (eventually): Capture extension->type maps that change as a
172+
// result of switching to mime-score. This is just to help make reviewing
173+
// PR #119 easier, and can be removed once that PR is approved.
174+
const legacyType = _preferredTypeLegacy(
175+
extension,
176+
types[extension],
177+
type
178+
)
179+
if (legacyType !== types[extension]) {
180+
exports._extensionConflicts.push([extension, legacyType, types[extension]])
182181
}
183-
184-
// set the extension -> mime
185-
types[extension] = type
186182
}
187183
})
188184
}
185+
186+
// Resolve type conflict using mime-score
187+
function _preferredType (ext, type0, type1) {
188+
var score0 = type0 ? mimeScore(type0, db[type0].source) : 0
189+
var score1 = type1 ? mimeScore(type1, db[type1].source) : 0
190+
191+
return score0 > score1 ? type0 : type1
192+
}
193+
194+
// Resolve type conflict using pre-mime-score logic
195+
function _preferredTypeLegacy (ext, type0, type1) {
196+
var SOURCE_RANK = ['nginx', 'apache', undefined, 'iana']
197+
198+
var score0 = type0 ? SOURCE_RANK.indexOf(db[type0].source) : 0
199+
var score1 = type1 ? SOURCE_RANK.indexOf(db[type1].source) : 0
200+
201+
if (
202+
exports.types[extension] !== 'application/octet-stream' &&
203+
(score0 > score1 ||
204+
(score0 === score1 &&
205+
exports.types[extension]?.slice(0, 12) === 'application/'))
206+
) {
207+
return type0
208+
}
209+
210+
return score0 > score1 ? type0 : type1
211+
}

mimeScore.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// 'mime-score' back-ported to CommonJS
2+
3+
// Score RFC facets (see https://tools.ietf.org/html/rfc6838#section-3)
4+
var FACET_SCORES = {
5+
'prs.': 100,
6+
'x-': 200,
7+
'x.': 300,
8+
'vnd.': 400,
9+
default: 900
10+
}
11+
12+
// Score mime source (Logic originally from `jshttp/mime-types` module)
13+
var SOURCE_SCORES = {
14+
nginx: 10,
15+
apache: 20,
16+
iana: 40,
17+
default: 30 // definitions added by `jshttp/mime-db` project?
18+
}
19+
20+
var TYPE_SCORES = {
21+
// prefer application/xml over text/xml
22+
// prefer application/rtf over text/rtf
23+
application: 1,
24+
25+
// prefer font/woff over application/font-woff
26+
font: 2,
27+
28+
default: 0
29+
}
30+
31+
/**
32+
* Get each component of the score for a mime type. The sum of these is the
33+
* total score. The higher the score, the more "official" the type.
34+
*/
35+
module.exports = function mimeScore (mimeType, source = 'default') {
36+
if (mimeType === 'application/octet-stream') {
37+
return 0
38+
}
39+
40+
const [type, subtype] = mimeType.split('/')
41+
42+
const facet = subtype.replace(/(\.|x-).*/, '$1')
43+
44+
const facetScore = FACET_SCORES[facet] || FACET_SCORES.default
45+
const sourceScore = SOURCE_SCORES[source] || SOURCE_SCORES.default
46+
const typeScore = TYPE_SCORES[type] || TYPE_SCORES.default
47+
48+
// All else being equal prefer shorter types
49+
const lengthScore = 1 - mimeType.length / 100
50+
51+
return facetScore + sourceScore + typeScore + lengthScore
52+
}

test/test.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
var assert = require('assert')
32
var mimeTypes = require('..')
43

@@ -220,8 +219,19 @@ describe('mimeTypes', function () {
220219
})
221220

222221
it('should return mime type when there is extension, but no path', function () {
223-
assert.strictEqual(mimeTypes.lookup('.config.json'), 'application/json')
222+
assert.strictEqual(
223+
mimeTypes.lookup('.config.json'),
224+
'application/json'
225+
)
224226
})
225227
})
226228
})
229+
230+
// Note the changes in extension->type mapping that result from using mime-score
231+
describe('extension conflicts', function () {
232+
console.warn('Mime-score logic changes extension->type mappings for the following:')
233+
for (var [extension, legacy, current] of mimeTypes._extensionConflicts) {
234+
console.warn(`* ${extension} -> ${legacy} is now ${current}`)
235+
}
236+
})
227237
})

0 commit comments

Comments
 (0)