588 lines
14 KiB
Go
588 lines
14 KiB
Go
package dataurl
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"reflect"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type dataURLTest struct {
|
|
InputRawDataURL string
|
|
ExpectedItems []item
|
|
ExpectedDataURL DataURL
|
|
}
|
|
|
|
func genTestTable() []dataURLTest {
|
|
return []dataURLTest{
|
|
dataURLTest{
|
|
`data:;base64,aGV5YQ==`,
|
|
[]item{
|
|
item{itemDataPrefix, dataPrefix},
|
|
item{itemParamSemicolon, ";"},
|
|
item{itemBase64Enc, "base64"},
|
|
item{itemDataComma, ","},
|
|
item{itemData, "aGV5YQ=="},
|
|
item{itemEOF, ""},
|
|
},
|
|
DataURL{
|
|
defaultMediaType(),
|
|
EncodingBase64,
|
|
[]byte("heya"),
|
|
},
|
|
},
|
|
dataURLTest{
|
|
`data:text/plain;base64,aGV5YQ==`,
|
|
[]item{
|
|
item{itemDataPrefix, dataPrefix},
|
|
item{itemMediaType, "text"},
|
|
item{itemMediaSep, "/"},
|
|
item{itemMediaSubType, "plain"},
|
|
item{itemParamSemicolon, ";"},
|
|
item{itemBase64Enc, "base64"},
|
|
item{itemDataComma, ","},
|
|
item{itemData, "aGV5YQ=="},
|
|
item{itemEOF, ""},
|
|
},
|
|
DataURL{
|
|
MediaType{
|
|
"text",
|
|
"plain",
|
|
map[string]string{},
|
|
},
|
|
EncodingBase64,
|
|
[]byte("heya"),
|
|
},
|
|
},
|
|
dataURLTest{
|
|
`data:text/plain;charset=utf-8;base64,aGV5YQ==`,
|
|
[]item{
|
|
item{itemDataPrefix, dataPrefix},
|
|
item{itemMediaType, "text"},
|
|
item{itemMediaSep, "/"},
|
|
item{itemMediaSubType, "plain"},
|
|
item{itemParamSemicolon, ";"},
|
|
item{itemParamAttr, "charset"},
|
|
item{itemParamEqual, "="},
|
|
item{itemParamVal, "utf-8"},
|
|
item{itemParamSemicolon, ";"},
|
|
item{itemBase64Enc, "base64"},
|
|
item{itemDataComma, ","},
|
|
item{itemData, "aGV5YQ=="},
|
|
item{itemEOF, ""},
|
|
},
|
|
DataURL{
|
|
MediaType{
|
|
"text",
|
|
"plain",
|
|
map[string]string{
|
|
"charset": "utf-8",
|
|
},
|
|
},
|
|
EncodingBase64,
|
|
[]byte("heya"),
|
|
},
|
|
},
|
|
dataURLTest{
|
|
`data:text/plain;charset=utf-8;foo=bar;base64,aGV5YQ==`,
|
|
[]item{
|
|
item{itemDataPrefix, dataPrefix},
|
|
item{itemMediaType, "text"},
|
|
item{itemMediaSep, "/"},
|
|
item{itemMediaSubType, "plain"},
|
|
item{itemParamSemicolon, ";"},
|
|
item{itemParamAttr, "charset"},
|
|
item{itemParamEqual, "="},
|
|
item{itemParamVal, "utf-8"},
|
|
item{itemParamSemicolon, ";"},
|
|
item{itemParamAttr, "foo"},
|
|
item{itemParamEqual, "="},
|
|
item{itemParamVal, "bar"},
|
|
item{itemParamSemicolon, ";"},
|
|
item{itemBase64Enc, "base64"},
|
|
item{itemDataComma, ","},
|
|
item{itemData, "aGV5YQ=="},
|
|
item{itemEOF, ""},
|
|
},
|
|
DataURL{
|
|
MediaType{
|
|
"text",
|
|
"plain",
|
|
map[string]string{
|
|
"charset": "utf-8",
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
EncodingBase64,
|
|
[]byte("heya"),
|
|
},
|
|
},
|
|
dataURLTest{
|
|
`data:application/json;charset=utf-8;foo="b\"<@>\"r";style=unformatted%20json;base64,eyJtc2ciOiAiaGV5YSJ9`,
|
|
[]item{
|
|
item{itemDataPrefix, dataPrefix},
|
|
item{itemMediaType, "application"},
|
|
item{itemMediaSep, "/"},
|
|
item{itemMediaSubType, "json"},
|
|
item{itemParamSemicolon, ";"},
|
|
item{itemParamAttr, "charset"},
|
|
item{itemParamEqual, "="},
|
|
item{itemParamVal, "utf-8"},
|
|
item{itemParamSemicolon, ";"},
|
|
item{itemParamAttr, "foo"},
|
|
item{itemParamEqual, "="},
|
|
item{itemLeftStringQuote, "\""},
|
|
item{itemParamVal, `b\"<@>\"r`},
|
|
item{itemRightStringQuote, "\""},
|
|
item{itemParamSemicolon, ";"},
|
|
item{itemParamAttr, "style"},
|
|
item{itemParamEqual, "="},
|
|
item{itemParamVal, "unformatted%20json"},
|
|
item{itemParamSemicolon, ";"},
|
|
item{itemBase64Enc, "base64"},
|
|
item{itemDataComma, ","},
|
|
item{itemData, "eyJtc2ciOiAiaGV5YSJ9"},
|
|
item{itemEOF, ""},
|
|
},
|
|
DataURL{
|
|
MediaType{
|
|
"application",
|
|
"json",
|
|
map[string]string{
|
|
"charset": "utf-8",
|
|
"foo": `b"<@>"r`,
|
|
"style": "unformatted json",
|
|
},
|
|
},
|
|
EncodingBase64,
|
|
[]byte(`{"msg": "heya"}`),
|
|
},
|
|
},
|
|
dataURLTest{
|
|
`data:xxx;base64,aGV5YQ==`,
|
|
[]item{
|
|
item{itemDataPrefix, dataPrefix},
|
|
item{itemError, "invalid character for media type"},
|
|
},
|
|
DataURL{},
|
|
},
|
|
dataURLTest{
|
|
`data:,`,
|
|
[]item{
|
|
item{itemDataPrefix, dataPrefix},
|
|
item{itemDataComma, ","},
|
|
item{itemEOF, ""},
|
|
},
|
|
DataURL{
|
|
defaultMediaType(),
|
|
EncodingASCII,
|
|
[]byte(""),
|
|
},
|
|
},
|
|
dataURLTest{
|
|
`data:,A%20brief%20note`,
|
|
[]item{
|
|
item{itemDataPrefix, dataPrefix},
|
|
item{itemDataComma, ","},
|
|
item{itemData, "A%20brief%20note"},
|
|
item{itemEOF, ""},
|
|
},
|
|
DataURL{
|
|
defaultMediaType(),
|
|
EncodingASCII,
|
|
[]byte("A brief note"),
|
|
},
|
|
},
|
|
dataURLTest{
|
|
`data:image/svg+xml-im.a.fake;base64,cGllLXN0b2NrX1RoaXJ0eQ==`,
|
|
[]item{
|
|
item{itemDataPrefix, dataPrefix},
|
|
item{itemMediaType, "image"},
|
|
item{itemMediaSep, "/"},
|
|
item{itemMediaSubType, "svg+xml-im.a.fake"},
|
|
item{itemParamSemicolon, ";"},
|
|
item{itemBase64Enc, "base64"},
|
|
item{itemDataComma, ","},
|
|
item{itemData, "cGllLXN0b2NrX1RoaXJ0eQ=="},
|
|
item{itemEOF, ""},
|
|
},
|
|
DataURL{
|
|
MediaType{
|
|
"image",
|
|
"svg+xml-im.a.fake",
|
|
map[string]string{},
|
|
},
|
|
EncodingBase64,
|
|
[]byte("pie-stock_Thirty"),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func expectItems(expected, actual []item) bool {
|
|
if len(expected) != len(actual) {
|
|
return false
|
|
}
|
|
for i := range expected {
|
|
if expected[i].t != actual[i].t {
|
|
return false
|
|
}
|
|
if expected[i].val != actual[i].val {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func equal(du1, du2 *DataURL) (bool, error) {
|
|
if !reflect.DeepEqual(du1.MediaType, du2.MediaType) {
|
|
return false, nil
|
|
}
|
|
if du1.Encoding != du2.Encoding {
|
|
return false, nil
|
|
}
|
|
|
|
if du1.Data == nil || du2.Data == nil {
|
|
return false, fmt.Errorf("nil Data")
|
|
}
|
|
|
|
if !bytes.Equal(du1.Data, du2.Data) {
|
|
return false, nil
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func TestLexDataURLs(t *testing.T) {
|
|
for _, test := range genTestTable() {
|
|
l := lex(test.InputRawDataURL)
|
|
var items []item
|
|
for item := range l.items {
|
|
items = append(items, item)
|
|
}
|
|
if !expectItems(test.ExpectedItems, items) {
|
|
t.Errorf("Expected %v, got %v", test.ExpectedItems, items)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testDataURLs(t *testing.T, factory func(string) (*DataURL, error)) {
|
|
for _, test := range genTestTable() {
|
|
var expectedItemError string
|
|
for _, item := range test.ExpectedItems {
|
|
if item.t == itemError {
|
|
expectedItemError = item.String()
|
|
break
|
|
}
|
|
}
|
|
dataURL, err := factory(test.InputRawDataURL)
|
|
if expectedItemError == "" && err != nil {
|
|
t.Error(err)
|
|
continue
|
|
} else if expectedItemError != "" && err == nil {
|
|
t.Errorf("Expected error \"%s\", got nil", expectedItemError)
|
|
continue
|
|
} else if expectedItemError != "" && err != nil {
|
|
if err.Error() != expectedItemError {
|
|
t.Errorf("Expected error \"%s\", got \"%s\"", expectedItemError, err.Error())
|
|
}
|
|
continue
|
|
}
|
|
|
|
if ok, err := equal(dataURL, &test.ExpectedDataURL); err != nil {
|
|
t.Error(err)
|
|
} else if !ok {
|
|
t.Errorf("Expected %v, got %v", test.ExpectedDataURL, *dataURL)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDataURLsWithDecode(t *testing.T) {
|
|
testDataURLs(t, func(s string) (*DataURL, error) {
|
|
return Decode(strings.NewReader(s))
|
|
})
|
|
}
|
|
|
|
func TestDataURLsWithDecodeString(t *testing.T) {
|
|
testDataURLs(t, func(s string) (*DataURL, error) {
|
|
return DecodeString(s)
|
|
})
|
|
}
|
|
|
|
func TestDataURLsWithUnmarshalText(t *testing.T) {
|
|
testDataURLs(t, func(s string) (*DataURL, error) {
|
|
d := &DataURL{}
|
|
err := d.UnmarshalText([]byte(s))
|
|
return d, err
|
|
})
|
|
}
|
|
|
|
func TestRoundTrip(t *testing.T) {
|
|
tests := []struct {
|
|
s string
|
|
roundTripOk bool
|
|
}{
|
|
{`data:text/plain;charset=utf-8;foo=bar;base64,aGV5YQ==`, true},
|
|
{`data:;charset=utf-8;foo=bar;base64,aGV5YQ==`, false},
|
|
{`data:text/plain;charset=utf-8;foo="bar";base64,aGV5YQ==`, false},
|
|
{`data:text/plain;charset=utf-8;foo="bar",A%20brief%20note`, false},
|
|
{`data:text/plain;charset=utf-8;foo=bar,A%20brief%20note`, true},
|
|
}
|
|
for _, test := range tests {
|
|
dataURL, err := DecodeString(test.s)
|
|
if err != nil {
|
|
t.Error(err)
|
|
continue
|
|
}
|
|
dus := dataURL.String()
|
|
if test.roundTripOk && dus != test.s {
|
|
t.Errorf("Expected %s, got %s", test.s, dus)
|
|
} else if !test.roundTripOk && dus == test.s {
|
|
t.Errorf("Found %s, expected something else", test.s)
|
|
}
|
|
|
|
txt, err := dataURL.MarshalText()
|
|
if err != nil {
|
|
t.Error(err)
|
|
continue
|
|
}
|
|
if test.roundTripOk && string(txt) != test.s {
|
|
t.Errorf("MarshalText roundtrip: got '%s', want '%s'", txt, test.s)
|
|
} else if !test.roundTripOk && string(txt) == test.s {
|
|
t.Errorf("MarshalText roundtrip: got '%s', want something else", txt)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNew(t *testing.T) {
|
|
tests := []struct {
|
|
Data []byte
|
|
MediaType string
|
|
ParamPairs []string
|
|
WillPanic bool
|
|
ExpectedDataURL *DataURL
|
|
}{
|
|
{
|
|
[]byte(`{"msg": "heya"}`),
|
|
"application/json",
|
|
[]string{},
|
|
false,
|
|
&DataURL{
|
|
MediaType{
|
|
"application",
|
|
"json",
|
|
map[string]string{},
|
|
},
|
|
EncodingBase64,
|
|
[]byte(`{"msg": "heya"}`),
|
|
},
|
|
},
|
|
{
|
|
[]byte(``),
|
|
"application//json",
|
|
[]string{},
|
|
true,
|
|
nil,
|
|
},
|
|
{
|
|
[]byte(``),
|
|
"",
|
|
[]string{},
|
|
true,
|
|
nil,
|
|
},
|
|
{
|
|
[]byte(`{"msg": "heya"}`),
|
|
"text/plain",
|
|
[]string{"charset", "utf-8"},
|
|
false,
|
|
&DataURL{
|
|
MediaType{
|
|
"text",
|
|
"plain",
|
|
map[string]string{
|
|
"charset": "utf-8",
|
|
},
|
|
},
|
|
EncodingBase64,
|
|
[]byte(`{"msg": "heya"}`),
|
|
},
|
|
},
|
|
{
|
|
[]byte(`{"msg": "heya"}`),
|
|
"text/plain",
|
|
[]string{"charset", "utf-8", "name"},
|
|
true,
|
|
nil,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
var dataURL *DataURL
|
|
func() {
|
|
defer func() {
|
|
if test.WillPanic {
|
|
if e := recover(); e == nil {
|
|
t.Error("Expected panic didn't happen")
|
|
}
|
|
} else {
|
|
if e := recover(); e != nil {
|
|
t.Errorf("Unexpected panic: %v", e)
|
|
}
|
|
}
|
|
}()
|
|
dataURL = New(test.Data, test.MediaType, test.ParamPairs...)
|
|
}()
|
|
if test.WillPanic {
|
|
if dataURL != nil {
|
|
t.Error("Expected nil DataURL")
|
|
}
|
|
} else {
|
|
if ok, err := equal(dataURL, test.ExpectedDataURL); err != nil {
|
|
t.Error(err)
|
|
} else if !ok {
|
|
t.Errorf("Expected %v, got %v", test.ExpectedDataURL, *dataURL)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var golangFavicon = strings.Replace(`AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
AAAAAAD///8AVE44//7hdv/+4Xb//uF2//7hdv/+4Xb//uF2//7hdv/+4Xb//uF2//7hdv/+4Xb/
|
|
/uF2/1ROOP////8A////AFROOP/+4Xb//uF2//7hdv/+4Xb//uF2//7hdv/+4Xb//uF2//7hdv/+
|
|
4Xb//uF2//7hdv9UTjj/////AP///wBUTjj//uF2//7hdv/+4Xb//uF2//7hdv/+4Xb//uF2//7h
|
|
dv/+4Xb//uF2//7hdv/+4Xb/VE44/////wD///8AVE44//7hdv/+4Xb//uF2//7hdv/+4Xb//uF2
|
|
//7hdv/+4Xb//uF2//7hdv/+4Xb//uF2/1ROOP////8A////AFROOP/+4Xb//uF2//7hdv/+4Xb/
|
|
/uF2//7hdv/+4Xb//uF2//7hdv/+4Xb//uF2//7hdv9UTjj/////AP///wBUTjj//uF2//7hdv/+
|
|
4Xb//uF2//7hdv/+4Xb//uF2//7hdv/+4Xb//uF2//7hdv/+4Xb/VE44/////wD///8AVE44//7h
|
|
dv/+4Xb//uF2//7hdv/+4Xb/z7t5/8Kyev/+4Xb//993///dd///3Xf//uF2/1ROOP////8A////
|
|
AFROOP/+4Xb//uF2//7hdv//4Hn/dIzD//v8///7/P//dIzD//7hdv//3Xf//913//7hdv9UTjj/
|
|
////AP///wBUTjj//uF2///fd//+4Xb//uF2/6ajif90jMP/dIzD/46Zpv/+4Xb//+F1///feP/+
|
|
4Xb/VE44/////wD///8AVE44//7hdv/z1XT////////////Is3L/HyAj/x8gI//Is3L/////////
|
|
///z1XT//uF2/1ROOP////8A19nd/1ROOP/+4Xb/5+HS//v+//8RExf/Liwn//7hdv/+4Xb/5+HS
|
|
//v8//8RExf/Liwn//7hdv9UTjj/19nd/1ROOP94aDT/yKdO/+fh0v//////ERMX/y4sJ//+4Xb/
|
|
/uF2/+fh0v//////ERMX/y4sJ//Ip07/dWU3/1ROOP9UTjj/yKdO/6qSSP/Is3L/9fb7//f6///I
|
|
s3L//uF2//7hdv/Is3L////////////Is3L/qpJI/8inTv9UTjj/19nd/1ROOP97c07/qpJI/8in
|
|
Tv/Ip07//uF2//7hdv/+4Xb//uF2/8zBlv/Kv4//pZJU/3tzTv9UTjj/19nd/////wD///8A4eLl
|
|
/6CcjP97c07/e3NO/1dOMf9BOiX/TkUn/2VXLf97c07/e3NO/6CcjP/h4uX/////AP///wD///8A
|
|
////AP///wD///8A////AP///wDq6/H/3N/j/9fZ3f/q6/H/////AP///wD///8A////AP///wD/
|
|
//8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
AAAAAAAAAAAAAA==`, "\n", "", -1)
|
|
|
|
func TestEncodeBytes(t *testing.T) {
|
|
mustDecode := func(s string) []byte {
|
|
data, err := base64.StdEncoding.DecodeString(s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return data
|
|
}
|
|
tests := []struct {
|
|
Data []byte
|
|
ExpectedString string
|
|
}{
|
|
{
|
|
[]byte(`A brief note`),
|
|
"data:text/plain;charset=utf-8;base64,QSBicmllZiBub3Rl",
|
|
},
|
|
{
|
|
[]byte{0xA, 0xFF, 0x99, 0x34, 0x56, 0x34, 0x00},
|
|
`data:application/octet-stream;base64,Cv+ZNFY0AA==`,
|
|
},
|
|
{
|
|
mustDecode(golangFavicon),
|
|
`data:image/vnd.microsoft.icon;base64,` + golangFavicon,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
str := EncodeBytes(test.Data)
|
|
if str != test.ExpectedString {
|
|
t.Errorf("Expected %s, got %s", test.ExpectedString, str)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkLex(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
for _, test := range genTestTable() {
|
|
l := lex(test.InputRawDataURL)
|
|
for _ = range l.items {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const rep = `^data:(?P<mediatype>\w+/[\w\+\-\.]+)?(?P<parameter>(?:;[\w\-]+="?[\w\-\\<>@,";:%]*"?)+)?(?P<base64>;base64)?,(?P<data>.*)$`
|
|
|
|
func TestRegexp(t *testing.T) {
|
|
re, err := regexp.Compile(rep)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, test := range genTestTable() {
|
|
shouldMatch := true
|
|
for _, item := range test.ExpectedItems {
|
|
if item.t == itemError {
|
|
shouldMatch = false
|
|
break
|
|
}
|
|
}
|
|
// just test it matches, do not parse
|
|
if re.MatchString(test.InputRawDataURL) && !shouldMatch {
|
|
t.Error("doesn't match", test.InputRawDataURL)
|
|
} else if !re.MatchString(test.InputRawDataURL) && shouldMatch {
|
|
t.Error("match", test.InputRawDataURL)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkRegexp(b *testing.B) {
|
|
re, err := regexp.Compile(rep)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
for i := 0; i < b.N; i++ {
|
|
for _, test := range genTestTable() {
|
|
_ = re.FindStringSubmatch(test.InputRawDataURL)
|
|
}
|
|
}
|
|
}
|
|
|
|
func ExampleDecodeString() {
|
|
dataURL, err := DecodeString(`data:text/plain;charset=utf-8;base64,aGV5YQ==`)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
fmt.Printf("%s, %s", dataURL.MediaType.ContentType(), string(dataURL.Data))
|
|
// Output: text/plain, heya
|
|
}
|
|
|
|
func ExampleDecode() {
|
|
r, err := http.NewRequest(
|
|
"POST", "/",
|
|
strings.NewReader(`data:image/vnd.microsoft.icon;name=golang%20favicon;base64,`+golangFavicon),
|
|
)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
|
|
var dataURL *DataURL
|
|
h := func(w http.ResponseWriter, r *http.Request) {
|
|
var err error
|
|
dataURL, err = Decode(r.Body)
|
|
defer r.Body.Close()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
}
|
|
w := httptest.NewRecorder()
|
|
h(w, r)
|
|
fmt.Printf("%s: %s", dataURL.Params["name"], dataURL.ContentType())
|
|
// Output: golang favicon: image/vnd.microsoft.icon
|
|
}
|