Parses and writes calls and responses with Python 2.7 xmlrpclib's and IMVU's PHP server's types, keeping struct member order. Co-Authored-By: Claude Opus 5.5 <[email protected]>
304 lines
8.5 KiB
Go
304 lines
8.5 KiB
Go
package xmlrpc
|
|
|
|
import (
|
|
"bytes"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Python 2.7 xmlrpclib.dumps(..., methodname=...) output, synthetic values.
|
|
const authorizeCall = `<?xml version='1.0'?>
|
|
<methodCall>
|
|
<methodName>test.authorizeProducts2</methodName>
|
|
<params>
|
|
<param>
|
|
<value><struct>
|
|
<member>
|
|
<name>auths</name>
|
|
<value><array><data>
|
|
<value><array><data>
|
|
<value><string>userId</string></value>
|
|
<value><string>productId</string></value>
|
|
</data></array></value>
|
|
<value><array><data>
|
|
<value><int>123</int></value>
|
|
<value><int>456</int></value>
|
|
</data></array></value>
|
|
</data></array></value>
|
|
</member>
|
|
<member>
|
|
<name>use_extracted_assets_if_available</name>
|
|
<value><boolean>1</boolean></value>
|
|
</member>
|
|
<member>
|
|
<name>max_supported_format</name>
|
|
<value><int>9</int></value>
|
|
</member>
|
|
</struct></value>
|
|
</param>
|
|
</params>
|
|
</methodCall>
|
|
`
|
|
|
|
// PHP-style response: declaration with encoding, indentation, i4, bare text.
|
|
const phpResponse = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<methodResponse>
|
|
<params>
|
|
<param>
|
|
<value>
|
|
<array>
|
|
<data>
|
|
<value>
|
|
<array>
|
|
<data>
|
|
<value><i4>1001</i4></value>
|
|
<value><string> Spaced & <name> </string></value>
|
|
<value><boolean>0</boolean></value>
|
|
</data>
|
|
</array>
|
|
</value>
|
|
<value>
|
|
<array>
|
|
<data>
|
|
<value><int>-7</int></value>
|
|
<value>bare text</value>
|
|
<value><boolean>1</boolean></value>
|
|
<value><i8>9007199254740993</i8></value>
|
|
<value><double>0.1</double></value>
|
|
<value><dateTime.iso8601>20261029T12:34:56</dateTime.iso8601></value>
|
|
<value><base64>aGVs
|
|
bG8=</base64></value>
|
|
<value><nil/></value>
|
|
<value></value>
|
|
<value/>
|
|
<value><string/></value>
|
|
<value><struct></struct></value>
|
|
<value><array><data></data></array></value>
|
|
</data>
|
|
</array>
|
|
</value>
|
|
</data>
|
|
</array>
|
|
</value>
|
|
</param>
|
|
</params>
|
|
</methodResponse>
|
|
`
|
|
|
|
const faultResponse = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<methodResponse>
|
|
<fault>
|
|
<value>
|
|
<struct>
|
|
<member><name>faultCode</name><value><int>4</int></value></member>
|
|
<member><name>faultString</name><value><string>Too many parameters.</string></value></member>
|
|
</struct>
|
|
</value>
|
|
</fault>
|
|
</methodResponse>
|
|
`
|
|
|
|
const unicodeCall = "<?xml version='1.0'?>\n<methodCall><methodName>test.echo</methodName><params>" +
|
|
"<param><value><string>café 日本 \U0001F600 ☺</string></value></param>" +
|
|
"</params></methodCall>"
|
|
|
|
func TestParseCall(t *testing.T) {
|
|
c, err := ParseCall([]byte(authorizeCall))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := &Call{Method: "test.authorizeProducts2", Params: []Value{Struct{
|
|
{"auths", []Value{[]Value{"userId", "productId"}, []Value{int64(123), int64(456)}}},
|
|
{"use_extracted_assets_if_available", true},
|
|
{"max_supported_format", int64(9)},
|
|
}}}
|
|
if !reflect.DeepEqual(c, want) {
|
|
t.Fatalf("got %#v", c)
|
|
}
|
|
roundTripCall(t, c)
|
|
}
|
|
|
|
func TestParseResponse(t *testing.T) {
|
|
r, err := ParseResponse([]byte(phpResponse))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := &Response{Params: []Value{[]Value{
|
|
[]Value{int64(1001), " Spaced & <name> ", false},
|
|
[]Value{int64(-7), "bare text", true, int64(9007199254740993), 0.1,
|
|
DateTime("20261029T12:34:56"), []byte("hello"), nil, "", "", "", Struct{}, []Value{}},
|
|
}}}
|
|
if !reflect.DeepEqual(r, want) {
|
|
t.Fatalf("got %#v", r)
|
|
}
|
|
roundTripResponse(t, r)
|
|
}
|
|
|
|
func TestFault(t *testing.T) {
|
|
r, err := ParseResponse([]byte(faultResponse))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if r.Fault == nil || *r.Fault != (Fault{4, "Too many parameters."}) || r.Params != nil {
|
|
t.Fatalf("got %#v", r)
|
|
}
|
|
out := r.Marshal()
|
|
if !bytes.Contains(out, []byte("<methodResponse>\n<fault>")) {
|
|
t.Fatalf("marshal: %s", out)
|
|
}
|
|
roundTripResponse(t, r)
|
|
}
|
|
|
|
func TestUnicode(t *testing.T) {
|
|
c, err := ParseCall([]byte(unicodeCall))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s := c.Params[0]; s != "café 日本 \U0001F600 ☺" {
|
|
t.Fatalf("got %q", s)
|
|
}
|
|
roundTripCall(t, c)
|
|
}
|
|
|
|
func TestWhitespaceInString(t *testing.T) {
|
|
r := &Response{Params: []Value{" a\tb\r\nc ", Struct{{"k\n", "\r"}}, 1.5e300, -0.0}}
|
|
roundTripResponse(t, r)
|
|
}
|
|
|
|
func TestMarshalFormat(t *testing.T) {
|
|
c := &Call{Method: "m", Params: []Value{int64(1), true, "a<&>", nil, 2.5}}
|
|
want := "<?xml version='1.0'?>\n<methodCall>\n<methodName>m</methodName>\n<params>\n" +
|
|
"<param>\n<value><int>1</int></value>\n</param>\n" +
|
|
"<param>\n<value><boolean>1</boolean></value>\n</param>\n" +
|
|
"<param>\n<value><string>a<&></string></value>\n</param>\n" +
|
|
"<param>\n<value><nil/></value>\n</param>\n" +
|
|
"<param>\n<value><double>2.5</double></value>\n</param>\n" +
|
|
"</params>\n</methodCall>\n"
|
|
if got := string(c.Marshal()); got != want {
|
|
t.Fatalf("got\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestCallWithoutParams(t *testing.T) {
|
|
c, err := ParseCall([]byte("<methodCall><methodName>x</methodName></methodCall>"))
|
|
if err != nil || c.Method != "x" || len(c.Params) != 0 {
|
|
t.Fatal(c, err)
|
|
}
|
|
}
|
|
|
|
func TestErrors(t *testing.T) {
|
|
bad := []string{
|
|
"",
|
|
"<methodCall>",
|
|
"<methodResponse><params><param><value><int>x</int></value></param></params></methodResponse>",
|
|
"<methodResponse><params><param><value><boolean>2</boolean></value></param></params></methodResponse>",
|
|
"<methodResponse><params><param><value><double>z</double></value></param></params></methodResponse>",
|
|
"<methodResponse><params><param><value><base64>!!</base64></value></param></params></methodResponse>",
|
|
"<methodResponse><params><param><value><foo/></value></param></params></methodResponse>",
|
|
"<methodResponse><params><param><value>x<int>1</int></value></param></params></methodResponse>",
|
|
"<methodResponse><params><param><value><int>1</int></value></param></params></methodResponse><x/>",
|
|
"<methodResponse><fault><value><int>1</int></value></fault></methodResponse>",
|
|
"<methodResponse><params><param><value><string>a</value></param></params></methodResponse>",
|
|
"<methodResponse></methodResponse>",
|
|
"<methodCall><methodName>x</methodName><params>junk</params></methodCall>",
|
|
`<?xml version="1.0" encoding="ISO-8859-1"?><methodCall><methodName>x</methodName></methodCall>`,
|
|
}
|
|
for _, s := range bad {
|
|
_, err1 := ParseResponse([]byte(s))
|
|
_, err2 := ParseCall([]byte(s))
|
|
if err1 == nil && err2 == nil {
|
|
t.Errorf("accepted %q", s)
|
|
}
|
|
}
|
|
}
|
|
|
|
func nested(n int) string {
|
|
return "<methodResponse><params><param><value>" +
|
|
strings.Repeat("<array><data><value>", n) + "<int>1</int>" +
|
|
strings.Repeat("</value></data></array>", n) +
|
|
"</value></param></params></methodResponse>"
|
|
}
|
|
|
|
func TestDepth(t *testing.T) {
|
|
if _, err := ParseResponse([]byte(nested(MaxDepth))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := ParseResponse([]byte(nested(MaxDepth + 1))); err == nil {
|
|
t.Fatal("accepted too deep")
|
|
}
|
|
}
|
|
|
|
func TestSize(t *testing.T) {
|
|
big := make([]byte, MaxSize+1)
|
|
if _, err := ParseCall(big); err == nil {
|
|
t.Fatal("accepted too large")
|
|
}
|
|
}
|
|
|
|
func roundTripCall(t *testing.T, c *Call) {
|
|
t.Helper()
|
|
c2, err := ParseCall(c.Marshal())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(c, c2) {
|
|
t.Fatalf("round trip:\n%#v\n%#v", c, c2)
|
|
}
|
|
}
|
|
|
|
func roundTripResponse(t *testing.T, r *Response) {
|
|
t.Helper()
|
|
r2, err := ParseResponse(r.Marshal())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if r.Params == nil && r.Fault == nil {
|
|
r.Params = []Value{}
|
|
}
|
|
if !reflect.DeepEqual(r, r2) {
|
|
t.Fatalf("round trip:\n%#v\n%#v", r, r2)
|
|
}
|
|
}
|
|
|
|
// Fuzzed documents must not panic, and whatever parses must re-marshal stably
|
|
// (bytes, not values, so NaN compares equal).
|
|
func FuzzParseCall(f *testing.F) {
|
|
f.Add([]byte(authorizeCall))
|
|
f.Add([]byte(unicodeCall))
|
|
f.Fuzz(func(t *testing.T, data []byte) {
|
|
c, err := ParseCall(data)
|
|
if err != nil {
|
|
return
|
|
}
|
|
out := c.Marshal()
|
|
c2, err := ParseCall(out)
|
|
if err != nil {
|
|
t.Fatalf("reparse: %v\n%s", err, out)
|
|
}
|
|
if !bytes.Equal(out, c2.Marshal()) {
|
|
t.Fatalf("unstable:\n%s", out)
|
|
}
|
|
})
|
|
}
|
|
|
|
func FuzzParseResponse(f *testing.F) {
|
|
f.Add([]byte(phpResponse))
|
|
f.Add([]byte(faultResponse))
|
|
f.Add([]byte(nested(3)))
|
|
f.Fuzz(func(t *testing.T, data []byte) {
|
|
r, err := ParseResponse(data)
|
|
if err != nil {
|
|
return
|
|
}
|
|
out := r.Marshal()
|
|
r2, err := ParseResponse(out)
|
|
if err != nil {
|
|
t.Fatalf("reparse: %v\n%s", err, out)
|
|
}
|
|
if !bytes.Equal(out, r2.Marshal()) {
|
|
t.Fatalf("unstable:\n%s", out)
|
|
}
|
|
})
|
|
}
|