From ff057d4c26b6cb72a9684f6370faa28d7bcb8bdb Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 28 Dec 2019 20:10:42 +0100 Subject: [PATCH] Fix utf8 support for dataUri base64 (#15) Co-authored-by: ZHAO Jinxiang --- .jshintrc | 3 +- LICENSE | 3 +- lib/source-map-resolve-node.js | 57 +++++++++++++++++++++--- source-map-resolve.js | 54 ++++++++++++++++++++--- test/source-map-resolve.js | 79 ++++++++++++++++++++++++++++++---- 5 files changed, 172 insertions(+), 24 deletions(-) diff --git a/.jshintrc b/.jshintrc index 4a29289..53fb51a 100644 --- a/.jshintrc +++ b/.jshintrc @@ -41,6 +41,7 @@ "define": false, "window": false, "atob": true, - "JSON": false + "JSON": false, + "TextDecoder": true } } diff --git a/LICENSE b/LICENSE index 748f42e..3e33dff 100644 --- a/LICENSE +++ b/LICENSE @@ -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 diff --git a/lib/source-map-resolve-node.js b/lib/source-map-resolve-node.js index f80953d..21754b4 100644 --- a/lib/source-map-resolve-node.js +++ b/lib/source-map-resolve-node.js @@ -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") @@ -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) @@ -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 = { @@ -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 } diff --git a/source-map-resolve.js b/source-map-resolve.js index 387fc19..abafa00 100644 --- a/source-map-resolve.js +++ b/source-map-resolve.js @@ -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) { @@ -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 = { @@ -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 } diff --git a/test/source-map-resolve.js b/test/source-map-resolve.js index f61c006..c64e891 100644 --- a/test/source-map-resolve.js +++ b/test/source-map-resolve.js @@ -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") @@ -61,6 +62,12 @@ var map = { sources: [], names: [] }, + utf8 : { + mappings: "AAAA", + sources: ["foo.js"], + sourcesContent: ["中文😊"], + names: [] + }, empty: {} } map.simpleString = JSON.stringify(map.simple) @@ -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"), @@ -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"), @@ -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") @@ -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, { @@ -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, { @@ -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") @@ -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, { @@ -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, {