// 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 ().
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 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 ")
}
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 %q", s)
case "double":
f, err := strconv.ParseFloat(strings.TrimSpace(s), 64)
if err != nil {
return nil, fmt.Errorf("xmlrpc: bad %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 : %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 ", 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 ", 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 elements up to .
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 ", 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 ")
}
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 or ")
}
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("\n\n")
xml.EscapeText(&b, []byte(c.Method))
b.WriteString("\n")
writeParams(&b, c.Params)
b.WriteString("\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("\n\n")
if r.Fault != nil {
b.WriteString("\n")
writeValue(&b, Struct{{"faultCode", r.Fault.Code}, {"faultString", r.Fault.String}})
b.WriteString("\n")
} else {
writeParams(&b, r.Params)
}
b.WriteString("\n")
return b.Bytes()
}
func writeParams(b *bytes.Buffer, ps []Value) {
b.WriteString("\n")
for _, v := range ps {
b.WriteString("\n")
writeValue(b, v)
b.WriteString("\n")
}
b.WriteString("\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("")
switch v := v.(type) {
case nil:
b.WriteString("")
case int64:
fmt.Fprintf(b, "%d", v)
case int:
fmt.Fprintf(b, "%d", v)
case bool:
if v {
b.WriteString("1")
} else {
b.WriteString("0")
}
case string:
b.WriteString("")
writeText(b, v)
b.WriteString("")
case float64:
// Shortest round-trip form; Python's float() parses it, NaN and Inf too.
fmt.Fprintf(b, "%s", strconv.FormatFloat(v, 'g', -1, 64))
case []byte:
fmt.Fprintf(b, "%s", base64.StdEncoding.EncodeToString(v))
case DateTime:
b.WriteString("")
writeText(b, string(v))
b.WriteString("")
case []Value:
b.WriteString("\n")
for _, e := range v {
writeValue(b, e)
}
b.WriteString("")
case Struct:
b.WriteString("\n")
for _, m := range v {
b.WriteString("\n")
writeText(b, m.Name)
b.WriteString("\n")
writeValue(b, m.Value)
b.WriteString("\n")
}
b.WriteString("")
default:
panic(fmt.Sprintf("xmlrpc: unsupported type %T", v))
}
b.WriteString("\n")
}