diff --git a/internal/bundler/bundler.go b/internal/bundler/bundler.go index a33af8bf88b..a5cf943785a 100644 --- a/internal/bundler/bundler.go +++ b/internal/bundler/bundler.go @@ -5,9 +5,11 @@ import ( "crypto/sha1" "encoding/base32" "encoding/base64" + "errors" "fmt" "mime" "net/http" + "net/url" "sort" "strings" "sync" @@ -524,27 +526,14 @@ func extractSourceMapFromComment( comment js_ast.Span, absResolveDir string, ) (logger.Path, *string) { - // Data URL + // Support data URLs if strings.HasPrefix(comment.Text, "data:") { - if strings.HasPrefix(comment.Text, "data:application/json;") { - // Scan for the base64 part to support URLs like "data:application/json;charset=utf-8;base64," - if index := strings.Index(comment.Text, ";base64,"); index != -1 { - n := int32(index + len(";base64,")) - encoded := comment.Text[n:] - decoded, err := base64.StdEncoding.DecodeString(encoded) - if err != nil { - r := logger.Range{Loc: logger.Loc{Start: comment.Range.Loc.Start + n}, Len: comment.Range.Len - n} - log.AddRangeWarning(source, r, "Invalid base64 data in source map") - return logger.Path{}, nil - } - contents := string(decoded) - return logger.Path{Text: source.PrettyPath + ".sourceMappingURL"}, &contents - } + if contents, err := dataFromDataURL(comment.Text); err == nil { + return logger.Path{Text: source.PrettyPath, IgnoredSuffix: "#sourceMappingURL"}, &contents + } else { + log.AddRangeWarning(source, comment.Range, fmt.Sprintf("Unsupported source map comment: %s", err.Error())) + return logger.Path{}, nil } - - // Anything else is unsupported - log.AddRangeWarning(source, comment.Range, "Unsupported source map comment") - return logger.Path{}, nil } // Relative path in a file with an absolute path @@ -568,6 +557,32 @@ func extractSourceMapFromComment( return logger.Path{}, nil } +func dataFromDataURL(dataURL string) (string, error) { + if strings.HasPrefix(dataURL, "data:") { + if comma := strings.IndexByte(dataURL, ','); comma != -1 { + b64 := ";base64," + + // Try to read base64 data + if pos := comma - len(b64) + 1; pos >= 0 && dataURL[pos:pos+len(b64)] == b64 { + bytes, err := base64.StdEncoding.DecodeString(dataURL[comma+1:]) + if err != nil { + return "", fmt.Errorf("Could not decode base64 data: %s", err.Error()) + } + return string(bytes), nil + } + + // Try to read percent-escaped data + content, err := url.QueryUnescape(dataURL[comma+1:]) + if err != nil { + return "", fmt.Errorf("Could not decode percent-escaped data: %s", err.Error()) + } + return content, nil + } + } + + return "", errors.New("Invalid data URL") +} + func sanetizeLocation(res resolver.Resolver, loc *logger.MsgLocation) { if loc != nil { if loc.Namespace == "" { diff --git a/scripts/verify-source-map.js b/scripts/verify-source-map.js index b1b80f28a18..0b16590912b 100644 --- a/scripts/verify-source-map.js +++ b/scripts/verify-source-map.js @@ -225,6 +225,21 @@ console.log("entry"); `, } +const testCasePartialMappingsPercentEscape = { + // The "mappings" value is "A,Q,I;A,Q,I;A,Q,I;AAMA,QAAQ,IAAI;" which contains + // partial mappings without original locations. This used to throw things off. + 'entry.js': `console.log(1); +console.log(2); +console.log(3); +console.log("entry"); +//# sourceMappingURL=data:,%7B%22version%22%3A3%2C%22sources%22%3A%5B%22entr` + + `y.js%22%5D%2C%22sourcesContent%22%3A%5B%22console.log(1)%5Cn%5Cnconsole` + + `.log(2)%5Cn%5Cnconsole.log(3)%5Cn%5Cnconsole.log(%5C%22entry%5C%22)%5Cn` + + `%22%5D%2C%22mappings%22%3A%22A%2CQ%2CI%3BA%2CQ%2CI%3BA%2CQ%2CI%3BAAMA%2` + + `CQAAQ%2CIAAI%3B%22%2C%22names%22%3A%5B%5D%7D +`, +} + const toSearchPartialMappings = { entry: 'entry.js', } @@ -445,6 +460,11 @@ async function main() { entryPoints: ['entry.js'], crlf, }), + check('dummy' + suffix, testCasePartialMappingsPercentEscape, toSearchPartialMappings, { + flags: flags.concat('--outfile=out.js', '--bundle'), + entryPoints: ['entry.js'], + crlf, + }), check('banner-footer' + suffix, testCaseES6, toSearchBundle, { flags: flags.concat('--outfile=out.js', '--bundle', '--banner="/* LICENSE abc */"', '--footer="/* end of file banner */"'), entryPoints: ['a.js'],