Skip to content
This repository has been archived by the owner on Nov 4, 2023. It is now read-only.

Commit

Permalink
Fix utf8 support for dataUri base64 (#15)
Browse files Browse the repository at this point in the history
Co-authored-by: ZHAO Jinxiang <xiaoxiangmoe@gmail.com>
  • Loading branch information
lydell and xiaoxiangmoe committed Dec 28, 2019
1 parent 858cd9e commit ff057d4
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 24 deletions.
3 changes: 2 additions & 1 deletion .jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"define": false,
"window": false,
"atob": true,
"JSON": false
"JSON": false,
"TextDecoder": true
}
}
3 changes: 2 additions & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
The MIT License (MIT)

Copyright (c) 2014, 2015, 2016, 2017 Simon Lydell
Copyright (c) 2014, 2015, 2016, 2017, 2019 Simon Lydell
Copyright (c) 2019 ZHAO Jinxiang

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
57 changes: 50 additions & 7 deletions lib/source-map-resolve-node.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright 2014, 2015, 2016, 2017 Simon Lydell
// Copyright 2014, 2015, 2016, 2017, 2019 Simon Lydell
// Copyright 2019 ZHAO Jinxiang
// X11 (“MIT”) Licensed. (See LICENSE.)

var sourceMappingURL = require("source-map-url")
Expand Down Expand Up @@ -71,8 +72,45 @@ function resolveSourceMapSync(code, codeUrl, read) {
}

var dataUriRegex = /^data:([^,;]*)(;[^,;]*)*(?:,(.*))?$/

/**
* The media type for JSON text is application/json.
*
* {@link https://tools.ietf.org/html/rfc8259#section-11 | IANA Considerations }
*
* `text/json` is non-standard media type
*/
var jsonMimeTypeRegex = /^(?:application|text)\/json$/

/**
* JSON text exchanged between systems that are not part of a closed ecosystem
* MUST be encoded using UTF-8.
*
* {@link https://tools.ietf.org/html/rfc8259#section-8.1 | Character Encoding}
*/
var jsonCharacterEncoding = "utf-8"

function base64ToBuf(b64) {
var binStr = atob(b64)
var len = binStr.length
var arr = new Uint8Array(len)
for (var i = 0; i < len; i++) {
arr[i] = binStr.charCodeAt(i)
}
return arr
}

function decodeBase64String(b64) {
if (typeof TextDecoder === "undefined" || typeof Uint8Array === "undefined") {
return atob(b64)
}
var buf = base64ToBuf(b64);
// Note: `decoder.decode` method will throw a `DOMException` with the
// `"EncodingError"` value when an coding error is found.
var decoder = new TextDecoder(jsonCharacterEncoding, {fatal: true})
return decoder.decode(buf);
}

function resolveSourceMapHelper(code, codeUrl) {
codeUrl = urix(codeUrl)

Expand All @@ -83,7 +121,7 @@ function resolveSourceMapHelper(code, codeUrl) {

var dataUri = url.match(dataUriRegex)
if (dataUri) {
var mimeType = dataUri[1]
var mimeType = dataUri[1] || "text/plain"
var lastParameter = dataUri[2] || ""
var encoded = dataUri[3] || ""
var data = {
Expand All @@ -93,14 +131,19 @@ function resolveSourceMapHelper(code, codeUrl) {
map: encoded
}
if (!jsonMimeTypeRegex.test(mimeType)) {
var error = new Error("Unuseful data uri mime type: " + (mimeType || "text/plain"))
var error = new Error("Unuseful data uri mime type: " + mimeType)
error.sourceMapData = data
throw error
}
try {
data.map = parseMapToJSON(
lastParameter === ";base64" ? decodeBase64String(encoded) : decodeURIComponent(encoded),
data
)
} catch (error) {
error.sourceMapData = data
throw error
}
data.map = parseMapToJSON(
lastParameter === ";base64" ? atob(encoded) : decodeURIComponent(encoded),
data
)
return data
}

Expand Down
54 changes: 48 additions & 6 deletions source-map-resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,45 @@ void (function(root, factory) {
}

var dataUriRegex = /^data:([^,;]*)(;[^,;]*)*(?:,(.*))?$/

/**
* The media type for JSON text is application/json.
*
* {@link https://tools.ietf.org/html/rfc8259#section-11 | IANA Considerations }
*
* `text/json` is non-standard media type
*/
var jsonMimeTypeRegex = /^(?:application|text)\/json$/

/**
* JSON text exchanged between systems that are not part of a closed ecosystem
* MUST be encoded using UTF-8.
*
* {@link https://tools.ietf.org/html/rfc8259#section-8.1 | Character Encoding}
*/
var jsonCharacterEncoding = "utf-8"

function base64ToBuf(b64) {
var binStr = atob(b64)
var len = binStr.length
var arr = new Uint8Array(len)
for (var i = 0; i < len; i++) {
arr[i] = binStr.charCodeAt(i)
}
return arr
}

function decodeBase64String(b64) {
if (typeof TextDecoder === "undefined" || typeof Uint8Array === "undefined") {
return atob(b64)
}
var buf = base64ToBuf(b64);
// Note: `decoder.decode` method will throw a `DOMException` with the
// `"EncodingError"` value when an coding error is found.
var decoder = new TextDecoder(jsonCharacterEncoding, {fatal: true})
return decoder.decode(buf);
}

function resolveSourceMapHelper(code, codeUrl) {
var url = sourceMappingURL.getFrom(code)
if (!url) {
Expand All @@ -89,7 +126,7 @@ void (function(root, factory) {

var dataUri = url.match(dataUriRegex)
if (dataUri) {
var mimeType = dataUri[1]
var mimeType = dataUri[1] || "text/plain"
var lastParameter = dataUri[2] || ""
var encoded = dataUri[3] || ""
var data = {
Expand All @@ -99,14 +136,19 @@ void (function(root, factory) {
map: encoded
}
if (!jsonMimeTypeRegex.test(mimeType)) {
var error = new Error("Unuseful data uri mime type: " + (mimeType || "text/plain"))
var error = new Error("Unuseful data uri mime type: " + mimeType)
error.sourceMapData = data
throw error
}
try {
data.map = parseMapToJSON(
lastParameter === ";base64" ? decodeBase64String(encoded) : decodeURIComponent(encoded),
data
)
} catch (error) {
error.sourceMapData = data
throw error
}
data.map = parseMapToJSON(
lastParameter === ";base64" ? atob(encoded) : decodeURIComponent(encoded),
data
)
return data
}

Expand Down
79 changes: 70 additions & 9 deletions test/source-map-resolve.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright 2014, 2015, 2016, 2017 Simon Lydell
// Copyright 2014, 2015, 2016, 2017, 2019 Simon Lydell
// Copyright 2019 ZHAO Jinxiang
// X11 (“MIT”) Licensed. (See LICENSE.)

var test = require("tape")
Expand Down Expand Up @@ -61,6 +62,12 @@ var map = {
sources: [],
names: []
},
utf8 : {
mappings: "AAAA",
sources: ["foo.js"],
sourcesContent: ["中文😊"],
names: []
},
empty: {}
}
map.simpleString = JSON.stringify(map.simple)
Expand All @@ -75,7 +82,8 @@ var code = {
"%7B%22mappings%22%3A%22AAAA%22%2C%22sources%22%3A%5B%22" +
"foo.js%22%5D%2C%22names%22%3A%5B%5D%7D"),
base64: u("data:application/json;base64," +
"eyJtYXBwaW5ncyI6IkFBQUEiLCJzb3VyY2VzIjpbImZvby5qcyJdLCJuYW1lcyI6W119"),
"eyJtYXBwaW5ncyI6IkFBQUEiLCJzb3VyY2VzIjpbImZvby5qcyJdLCJzb3VyY2VzQ29udGVudCI6WyLkuK3mlofwn5iKIl0sIm5hbWVzIjpbXX0="), // jshint ignore:line
base64InvalidUtf8: u("data:application/json;base64,abc"),
dataUriText: u("data:text/json," +
"%7B%22mappings%22%3A%22AAAA%22%2C%22sources%22%3A%5B%22" +
"foo.js%22%5D%2C%22names%22%3A%5B%5D%7D"),
Expand All @@ -85,6 +93,7 @@ var code = {
dataUriNoMime: u("data:,foo"),
dataUriInvalidMime: u("data:text/html,foo"),
dataUriInvalidJSON: u("data:application/json,foo"),
dataUriInvalidCode: u("data:application/json,%"),
dataUriXSSIsafe: u("data:application/json," + ")%5D%7D%27" +
"%7B%22mappings%22%3A%22AAAA%22%2C%22sources%22%3A%5B%22" +
"foo.js%22%5D%2C%22names%22%3A%5B%5D%7D"),
Expand All @@ -99,7 +108,7 @@ function testResolveSourceMap(method, sync) {

var codeUrl = "http://example.com/a/b/c/foo.js"

t.plan(1 + 12*3 + 6*4)
t.plan(1 + 12*3 + 8*4)

t.equal(typeof method, "function", "is a function")

Expand Down Expand Up @@ -171,14 +180,27 @@ function testResolveSourceMap(method, sync) {
t.error(error)
t.deepEqual(result, {
sourceMappingURL: "data:application/json;base64," +
"eyJtYXBwaW5ncyI6IkFBQUEiLCJzb3VyY2VzIjpbImZvby5qcyJdLCJuYW1lcyI6W119",
"eyJtYXBwaW5ncyI6IkFBQUEiLCJzb3VyY2VzIjpbImZvby5qcyJdLCJzb3VyY2VzQ29udGVudCI6WyLkuK3mlofwn5iKIl0sIm5hbWVzIjpbXX0=", // jshint ignore:line
url: null,
sourcesRelativeTo: codeUrl,
map: map.simple
map: map.utf8
}, "base64")
isAsync()
})

method(code.base64InvalidUtf8, codeUrl, wrap(Throws), function(error, result) {
t.deepEqual(error.sourceMapData, {
sourceMappingURL: "data:application/json;base64,abc",
url: null,
sourcesRelativeTo: codeUrl,
map: "abc"
}, "base64InvalidUtf8 .sourceMapData")
t.ok(error instanceof TypeError && error.message !== "data:application/json;base64,abc",
"base64InvalidUtf8")
t.notOk(result)
isAsync()
})

method(code.dataUriText, codeUrl, wrap(Throws), function(error, result) {
t.error(error)
t.deepEqual(result, {
Expand Down Expand Up @@ -242,6 +264,19 @@ function testResolveSourceMap(method, sync) {
isAsync()
})

method(code.dataUriInvalidCode, codeUrl, wrap(Throws), function(error, result) {
t.deepEqual(error.sourceMapData, {
sourceMappingURL: "data:application/json,%",
url: null,
sourcesRelativeTo: codeUrl,
map: "%"
}, "dataUriInvalidCode .sourceMapData")
t.ok(error instanceof URIError && error.message !== "data:application/json,%",
"dataUriInvalidCode")
t.notOk(result)
isAsync()
})

method(code.dataUriXSSIsafe, codeUrl, wrap(Throws), function(error, result) {
t.error(error)
t.deepEqual(result, {
Expand Down Expand Up @@ -599,7 +634,7 @@ function testResolve(method, sync) {

var codeUrl = "http://example.com/a/b/c/foo.js"

t.plan(1 + 15*3 + 21*4 + 4)
t.plan(1 + 15*3 + 23*4 + 4)

t.equal(typeof method, "function", "is a function")

Expand Down Expand Up @@ -683,16 +718,29 @@ function testResolve(method, sync) {
t.error(error)
t.deepEqual(result, {
sourceMappingURL: "data:application/json;base64," +
"eyJtYXBwaW5ncyI6IkFBQUEiLCJzb3VyY2VzIjpbImZvby5qcyJdLCJuYW1lcyI6W119",
"eyJtYXBwaW5ncyI6IkFBQUEiLCJzb3VyY2VzIjpbImZvby5qcyJdLCJzb3VyY2VzQ29udGVudCI6WyLkuK3mlofwn5iKIl0sIm5hbWVzIjpbXX0=", // jshint ignore:line
url: null,
sourcesRelativeTo: codeUrl,
map: map.simple,
map: map.utf8,
sourcesResolved: ["http://example.com/a/b/c/foo.js"],
sourcesContent: ["http://example.com/a/b/c/foo.js"]
sourcesContent: ["中文😊"]
}, "base64")
isAsync()
})

method(code.base64InvalidUtf8, codeUrl, wrap(Throws), function(error, result) {
t.deepEqual(error.sourceMapData, {
sourceMappingURL: "data:application/json;base64,abc",
url: null,
sourcesRelativeTo: codeUrl,
map: "abc"
}, "base64InvalidUtf8 .sourceMapData")
t.ok(error instanceof TypeError && error.message !== "data:application/json;base64,abc",
"base64InvalidUtf8")
t.notOk(result)
isAsync()
})

method(code.dataUriText, codeUrl, wrapMap(Throws, identity), function(error, result) {
t.error(error)
t.deepEqual(result, {
Expand Down Expand Up @@ -760,6 +808,19 @@ function testResolve(method, sync) {
isAsync()
})

method(code.dataUriInvalidCode, codeUrl, wrap(Throws), function(error, result) {
t.deepEqual(error.sourceMapData, {
sourceMappingURL: "data:application/json,%",
url: null,
sourcesRelativeTo: codeUrl,
map: "%"
}, "dataUriInvalidCode .sourceMapData")
t.ok(error instanceof URIError && error.message !== "data:application/json,%",
"dataUriInvalidCode")
t.notOk(result)
isAsync()
})

method(code.dataUriXSSIsafe, codeUrl, wrapMap(Throws, identity), function(error, result) {
t.error(error)
t.deepEqual(result, {
Expand Down

0 comments on commit ff057d4

Please sign in to comment.