Merge branch 'xmlrpc' into products
This commit is contained in:
commit
b2eed27b99
2 files changed
+828
No files matched your search
@@ -0,0 +1,525 @@
|
|||||||
|
// Package xmlrpc parses and writes XML-RPC calls and responses, keeping
|
||||||
|
// value types and struct member order exactly.
|
||||||
|
package xmlrpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MaxSize is the largest document accepted.
|
||||||
|
const MaxSize = 64 << 20
|
||||||
|
|
||||||
|
// MaxDepth is the deepest nesting of arrays and structs accepted.
|
||||||
|
const MaxDepth = 64
|
||||||
|
|
||||||
|
// Value is one of: int64, bool, string, float64, []byte (base64), DateTime,
|
||||||
|
// []Value (array), Struct, or nil (<nil/>).
|
||||||
|
type Value = any
|
||||||
|
|
||||||
|
// DateTime is dateTime.iso8601 text, kept as is.
|
||||||
|
type DateTime string
|
||||||
|
|
||||||
|
// Member is a struct member.
|
||||||
|
type Member struct {
|
||||||
|
Name string
|
||||||
|
Value Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Struct is an XML-RPC struct, in document order.
|
||||||
|
type Struct []Member
|
||||||
|
|
||||||
|
// Get returns the value of the first member with the name.
|
||||||
|
func (s Struct) Get(name string) (Value, bool) {
|
||||||
|
for _, m := range s {
|
||||||
|
if m.Name == name {
|
||||||
|
return m.Value, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call is a methodCall.
|
||||||
|
type Call struct {
|
||||||
|
Method string
|
||||||
|
Params []Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response is a methodResponse: Params, or Fault if it's set.
|
||||||
|
type Response struct {
|
||||||
|
Params []Value
|
||||||
|
Fault *Fault
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fault is a fault response's code and message.
|
||||||
|
type Fault struct {
|
||||||
|
Code int64
|
||||||
|
String string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fault) Error() string { return fmt.Sprintf("xmlrpc fault %d: %s", f.Code, f.String) }
|
||||||
|
|
||||||
|
type parser struct{ d *xml.Decoder }
|
||||||
|
|
||||||
|
func newParser(data []byte) (*parser, error) {
|
||||||
|
if len(data) > MaxSize {
|
||||||
|
return nil, errors.New("xmlrpc: document too large")
|
||||||
|
}
|
||||||
|
d := xml.NewDecoder(bytes.NewReader(data))
|
||||||
|
d.Strict = true
|
||||||
|
d.CharsetReader = func(label string, r io.Reader) (io.Reader, error) {
|
||||||
|
switch strings.ToLower(label) {
|
||||||
|
case "utf-8", "utf8", "us-ascii", "ascii":
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("xmlrpc: unsupported encoding %q", label)
|
||||||
|
}
|
||||||
|
return &parser{d}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) token() (xml.Token, error) {
|
||||||
|
t, err := p.d.Token()
|
||||||
|
if err == io.EOF {
|
||||||
|
err = io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
return t, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// next returns the next start or end element, skipping whitespace,
|
||||||
|
// comments and processing instructions.
|
||||||
|
func (p *parser) next() (xml.Token, error) {
|
||||||
|
for {
|
||||||
|
t, err := p.token()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch t := t.(type) {
|
||||||
|
case xml.StartElement, xml.EndElement:
|
||||||
|
return t, nil
|
||||||
|
case xml.CharData:
|
||||||
|
if len(bytes.TrimSpace(t)) != 0 {
|
||||||
|
return nil, fmt.Errorf("xmlrpc: unexpected text %q", string(t))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) start(name string) error {
|
||||||
|
t, err := p.next()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s, ok := t.(xml.StartElement); !ok || s.Name.Local != name {
|
||||||
|
return fmt.Errorf("xmlrpc: expected <%s>", name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) end(name string) error {
|
||||||
|
t, err := p.next()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if e, ok := t.(xml.EndElement); !ok || e.Name.Local != name {
|
||||||
|
return fmt.Errorf("xmlrpc: expected </%s>", name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// text reads character data up to the current element's end.
|
||||||
|
func (p *parser) text() (string, error) {
|
||||||
|
var b strings.Builder
|
||||||
|
for {
|
||||||
|
t, err := p.token()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
switch t := t.(type) {
|
||||||
|
case xml.CharData:
|
||||||
|
b.Write(t)
|
||||||
|
case xml.EndElement:
|
||||||
|
return b.String(), nil
|
||||||
|
case xml.StartElement:
|
||||||
|
return "", fmt.Errorf("xmlrpc: unexpected <%s>", t.Name.Local)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eof checks that only whitespace follows the root element.
|
||||||
|
func (p *parser) eof() error {
|
||||||
|
for {
|
||||||
|
t, err := p.d.Token()
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch t := t.(type) {
|
||||||
|
case xml.StartElement, xml.EndElement:
|
||||||
|
return errors.New("xmlrpc: content after document")
|
||||||
|
case xml.CharData:
|
||||||
|
if len(bytes.TrimSpace(t)) != 0 {
|
||||||
|
return errors.New("xmlrpc: content after document")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// value parses a value's content; its <value> start was read.
|
||||||
|
// Bare text is a string, whitespace included.
|
||||||
|
func (p *parser) value(depth int) (Value, error) {
|
||||||
|
var text strings.Builder
|
||||||
|
for {
|
||||||
|
t, err := p.token()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch t := t.(type) {
|
||||||
|
case xml.CharData:
|
||||||
|
text.Write(t)
|
||||||
|
case xml.EndElement:
|
||||||
|
return text.String(), nil
|
||||||
|
case xml.StartElement:
|
||||||
|
if strings.TrimSpace(text.String()) != "" {
|
||||||
|
return nil, errors.New("xmlrpc: text mixed with elements in <value>")
|
||||||
|
}
|
||||||
|
v, err := p.typed(t.Name.Local, depth)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return v, p.end("value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) typed(name string, depth int) (Value, error) {
|
||||||
|
switch name {
|
||||||
|
case "array", "struct":
|
||||||
|
if depth >= MaxDepth {
|
||||||
|
return nil, errors.New("xmlrpc: nested too deeply")
|
||||||
|
}
|
||||||
|
if name == "array" {
|
||||||
|
return p.array(depth + 1)
|
||||||
|
}
|
||||||
|
return p.strct(depth + 1)
|
||||||
|
}
|
||||||
|
s, err := p.text()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch name {
|
||||||
|
case "nil":
|
||||||
|
return nil, nil
|
||||||
|
case "string":
|
||||||
|
return s, nil
|
||||||
|
case "int", "i4", "i8":
|
||||||
|
n, err := strconv.ParseInt(strings.TrimSpace(s), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("xmlrpc: bad <%s> %q", name, s)
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
case "boolean":
|
||||||
|
switch strings.TrimSpace(s) {
|
||||||
|
case "0":
|
||||||
|
return false, nil
|
||||||
|
case "1":
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("xmlrpc: bad <boolean> %q", s)
|
||||||
|
case "double":
|
||||||
|
f, err := strconv.ParseFloat(strings.TrimSpace(s), 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("xmlrpc: bad <double> %q", s)
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
case "dateTime.iso8601":
|
||||||
|
return DateTime(s), nil
|
||||||
|
case "base64":
|
||||||
|
b, err := base64.StdEncoding.DecodeString(strings.Join(strings.Fields(s), ""))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("xmlrpc: bad <base64>: %v", err)
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("xmlrpc: unknown type <%s>", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) array(depth int) (Value, error) {
|
||||||
|
if err := p.start("data"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
a := []Value{}
|
||||||
|
for {
|
||||||
|
t, err := p.next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch t := t.(type) {
|
||||||
|
case xml.EndElement:
|
||||||
|
return a, p.end("array")
|
||||||
|
case xml.StartElement:
|
||||||
|
if t.Name.Local != "value" {
|
||||||
|
return nil, fmt.Errorf("xmlrpc: unexpected <%s> in <data>", t.Name.Local)
|
||||||
|
}
|
||||||
|
v, err := p.value(depth)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
a = append(a, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) strct(depth int) (Value, error) {
|
||||||
|
s := Struct{}
|
||||||
|
for {
|
||||||
|
t, err := p.next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch t := t.(type) {
|
||||||
|
case xml.EndElement:
|
||||||
|
return s, nil
|
||||||
|
case xml.StartElement:
|
||||||
|
if t.Name.Local != "member" {
|
||||||
|
return nil, fmt.Errorf("xmlrpc: unexpected <%s> in <struct>", t.Name.Local)
|
||||||
|
}
|
||||||
|
if err := p.start("name"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
name, err := p.text()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := p.start("value"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
v, err := p.value(depth)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := p.end("member"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s = append(s, Member{name, v})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// params parses <param> elements up to </params>.
|
||||||
|
func (p *parser) params() ([]Value, error) {
|
||||||
|
ps := []Value{}
|
||||||
|
for {
|
||||||
|
t, err := p.next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch t := t.(type) {
|
||||||
|
case xml.EndElement:
|
||||||
|
return ps, nil
|
||||||
|
case xml.StartElement:
|
||||||
|
if t.Name.Local != "param" {
|
||||||
|
return nil, fmt.Errorf("xmlrpc: unexpected <%s> in <params>", t.Name.Local)
|
||||||
|
}
|
||||||
|
if err := p.start("value"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
v, err := p.value(0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := p.end("param"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ps = append(ps, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseCall parses a methodCall document.
|
||||||
|
func ParseCall(data []byte) (*Call, error) {
|
||||||
|
p, err := newParser(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := p.start("methodCall"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := p.start("methodName"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
method, err := p.text()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c := &Call{Method: strings.TrimSpace(method), Params: []Value{}}
|
||||||
|
t, err := p.next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if s, ok := t.(xml.StartElement); ok && s.Name.Local == "params" {
|
||||||
|
if c.Params, err = p.params(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := p.end("methodCall"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if e, ok := t.(xml.EndElement); !ok || e.Name.Local != "methodCall" {
|
||||||
|
return nil, errors.New("xmlrpc: expected <params>")
|
||||||
|
}
|
||||||
|
return c, p.eof()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseResponse parses a methodResponse document.
|
||||||
|
func ParseResponse(data []byte) (*Response, error) {
|
||||||
|
p, err := newParser(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := p.start("methodResponse"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
t, err := p.next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s, _ := t.(xml.StartElement)
|
||||||
|
r := &Response{}
|
||||||
|
switch s.Name.Local {
|
||||||
|
case "params":
|
||||||
|
if r.Params, err = p.params(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case "fault":
|
||||||
|
if r.Fault, err = p.fault(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, errors.New("xmlrpc: expected <params> or <fault>")
|
||||||
|
}
|
||||||
|
if err := p.end("methodResponse"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return r, p.eof()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) fault() (*Fault, error) {
|
||||||
|
if err := p.start("value"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
v, err := p.value(0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := p.end("fault"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
st, _ := v.(Struct)
|
||||||
|
code, _ := st.Get("faultCode")
|
||||||
|
msg, _ := st.Get("faultString")
|
||||||
|
f := &Fault{}
|
||||||
|
var ok1, ok2 bool
|
||||||
|
f.Code, ok1 = code.(int64)
|
||||||
|
f.String, ok2 = msg.(string)
|
||||||
|
if !ok1 || !ok2 {
|
||||||
|
return nil, errors.New("xmlrpc: bad fault")
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal writes the call. It panics on a value of an unsupported type.
|
||||||
|
func (c *Call) Marshal() []byte {
|
||||||
|
var b bytes.Buffer
|
||||||
|
b.WriteString("<?xml version='1.0'?>\n<methodCall>\n<methodName>")
|
||||||
|
xml.EscapeText(&b, []byte(c.Method))
|
||||||
|
b.WriteString("</methodName>\n")
|
||||||
|
writeParams(&b, c.Params)
|
||||||
|
b.WriteString("</methodCall>\n")
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal writes the response. It panics on a value of an unsupported type.
|
||||||
|
func (r *Response) Marshal() []byte {
|
||||||
|
var b bytes.Buffer
|
||||||
|
b.WriteString("<?xml version='1.0'?>\n<methodResponse>\n")
|
||||||
|
if r.Fault != nil {
|
||||||
|
b.WriteString("<fault>\n")
|
||||||
|
writeValue(&b, Struct{{"faultCode", r.Fault.Code}, {"faultString", r.Fault.String}})
|
||||||
|
b.WriteString("</fault>\n")
|
||||||
|
} else {
|
||||||
|
writeParams(&b, r.Params)
|
||||||
|
}
|
||||||
|
b.WriteString("</methodResponse>\n")
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeParams(b *bytes.Buffer, ps []Value) {
|
||||||
|
b.WriteString("<params>\n")
|
||||||
|
for _, v := range ps {
|
||||||
|
b.WriteString("<param>\n")
|
||||||
|
writeValue(b, v)
|
||||||
|
b.WriteString("</param>\n")
|
||||||
|
}
|
||||||
|
b.WriteString("</params>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeText escapes s; xml.EscapeText also escapes quotes and \t\n\r as
|
||||||
|
// character references, so whitespace survives exactly.
|
||||||
|
func writeText(b *bytes.Buffer, s string) { xml.EscapeText(b, []byte(s)) }
|
||||||
|
|
||||||
|
func writeValue(b *bytes.Buffer, v Value) {
|
||||||
|
b.WriteString("<value>")
|
||||||
|
switch v := v.(type) {
|
||||||
|
case nil:
|
||||||
|
b.WriteString("<nil/>")
|
||||||
|
case int64:
|
||||||
|
fmt.Fprintf(b, "<int>%d</int>", v)
|
||||||
|
case int:
|
||||||
|
fmt.Fprintf(b, "<int>%d</int>", v)
|
||||||
|
case bool:
|
||||||
|
if v {
|
||||||
|
b.WriteString("<boolean>1</boolean>")
|
||||||
|
} else {
|
||||||
|
b.WriteString("<boolean>0</boolean>")
|
||||||
|
}
|
||||||
|
case string:
|
||||||
|
b.WriteString("<string>")
|
||||||
|
writeText(b, v)
|
||||||
|
b.WriteString("</string>")
|
||||||
|
case float64:
|
||||||
|
// Shortest round-trip form; Python's float() parses it, NaN and Inf too.
|
||||||
|
fmt.Fprintf(b, "<double>%s</double>", strconv.FormatFloat(v, 'g', -1, 64))
|
||||||
|
case []byte:
|
||||||
|
fmt.Fprintf(b, "<base64>%s</base64>", base64.StdEncoding.EncodeToString(v))
|
||||||
|
case DateTime:
|
||||||
|
b.WriteString("<dateTime.iso8601>")
|
||||||
|
writeText(b, string(v))
|
||||||
|
b.WriteString("</dateTime.iso8601>")
|
||||||
|
case []Value:
|
||||||
|
b.WriteString("<array><data>\n")
|
||||||
|
for _, e := range v {
|
||||||
|
writeValue(b, e)
|
||||||
|
}
|
||||||
|
b.WriteString("</data></array>")
|
||||||
|
case Struct:
|
||||||
|
b.WriteString("<struct>\n")
|
||||||
|
for _, m := range v {
|
||||||
|
b.WriteString("<member>\n<name>")
|
||||||
|
writeText(b, m.Name)
|
||||||
|
b.WriteString("</name>\n")
|
||||||
|
writeValue(b, m.Value)
|
||||||
|
b.WriteString("</member>\n")
|
||||||
|
}
|
||||||
|
b.WriteString("</struct>")
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("xmlrpc: unsupported type %T", v))
|
||||||
|
}
|
||||||
|
b.WriteString("</value>\n")
|
||||||
|
}
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in new issue
Block a user