4
0
mirror of https://github.com/cwinfo/matterbridge.git synced 2025-07-01 18:16:17 +00:00

Add dependencies/vendor (whatsapp)

This commit is contained in:
Wim
2022-01-31 00:27:37 +01:00
parent e7b193788a
commit e3cafeaf92
1074 changed files with 3091569 additions and 26075 deletions

View File

@ -0,0 +1,177 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package binary
import (
"fmt"
"strconv"
"go.mau.fi/whatsmeow/types"
)
// AttrUtility is a helper struct for reading multiple XML attributes and checking for errors afterwards.
//
// The functions return values directly and append any decoding errors to the Errors slice. The
// slice can then be checked after all necessary attributes are read, instead of having to check
// each attribute for errors separately.
type AttrUtility struct {
Attrs Attrs
Errors []error
}
// AttrGetter returns the AttrUtility for this Node.
func (n *Node) AttrGetter() *AttrUtility {
return &AttrUtility{Attrs: n.Attrs, Errors: make([]error, 0)}
}
func (au *AttrUtility) GetJID(key string, require bool) (jidVal types.JID, ok bool) {
var val interface{}
if val, ok = au.Attrs[key]; !ok {
if require {
au.Errors = append(au.Errors, fmt.Errorf("didn't find required JID attribute '%s'", key))
}
} else if jidVal, ok = val.(types.JID); !ok {
au.Errors = append(au.Errors, fmt.Errorf("expected attribute '%s' to be JID, but was %T", key, val))
}
return
}
// OptionalJID returns the JID under the given key. If there's no valid JID under the given key, this will return nil.
// However, if the attribute is completely missing, this will not store an error.
func (au *AttrUtility) OptionalJID(key string) *types.JID {
jid, ok := au.GetJID(key, false)
if ok {
return &jid
}
return nil
}
// OptionalJIDOrEmpty returns the JID under the given key. If there's no valid JID under the given key, this will return an empty JID.
// However, if the attribute is completely missing, this will not store an error.
func (au *AttrUtility) OptionalJIDOrEmpty(key string) types.JID {
jid, ok := au.GetJID(key, false)
if ok {
return jid
}
return types.EmptyJID
}
// JID returns the JID under the given key.
// If there's no valid JID under the given key, an error will be stored and a blank JID struct will be returned.
func (au *AttrUtility) JID(key string) types.JID {
jid, _ := au.GetJID(key, true)
return jid
}
func (au *AttrUtility) GetString(key string, require bool) (strVal string, ok bool) {
var val interface{}
if val, ok = au.Attrs[key]; !ok {
if require {
au.Errors = append(au.Errors, fmt.Errorf("didn't find required attribute '%s'", key))
}
} else if strVal, ok = val.(string); !ok {
au.Errors = append(au.Errors, fmt.Errorf("expected attribute '%s' to be string, but was %T", key, val))
}
return
}
func (au *AttrUtility) GetInt64(key string, require bool) (int64, bool) {
if strVal, ok := au.GetString(key, require); !ok {
return 0, false
} else if intVal, err := strconv.ParseInt(strVal, 10, 64); err != nil {
au.Errors = append(au.Errors, fmt.Errorf("failed to parse int in attribute '%s': %w", key, err))
return 0, false
} else {
return intVal, true
}
}
func (au *AttrUtility) GetUint64(key string, require bool) (uint64, bool) {
if strVal, ok := au.GetString(key, require); !ok {
return 0, false
} else if intVal, err := strconv.ParseUint(strVal, 10, 64); err != nil {
au.Errors = append(au.Errors, fmt.Errorf("failed to parse uint in attribute '%s': %w", key, err))
return 0, false
} else {
return intVal, true
}
}
func (au *AttrUtility) GetBool(key string, require bool) (bool, bool) {
if strVal, ok := au.GetString(key, require); !ok {
return false, false
} else if boolVal, err := strconv.ParseBool(strVal); err != nil {
au.Errors = append(au.Errors, fmt.Errorf("failed to parse bool in attribute '%s': %w", key, err))
return false, false
} else {
return boolVal, true
}
}
// OptionalString returns the string under the given key.
func (au *AttrUtility) OptionalString(key string) string {
strVal, _ := au.GetString(key, false)
return strVal
}
// String returns the string under the given key.
// If there's no valid string under the given key, an error will be stored and an empty string will be returned.
func (au *AttrUtility) String(key string) string {
strVal, _ := au.GetString(key, true)
return strVal
}
func (au *AttrUtility) OptionalInt(key string) int {
val, _ := au.GetInt64(key, false)
return int(val)
}
func (au *AttrUtility) Int(key string) int {
val, _ := au.GetInt64(key, true)
return int(val)
}
func (au *AttrUtility) Int64(key string) int64 {
val, _ := au.GetInt64(key, true)
return val
}
func (au *AttrUtility) Uint64(key string) uint64 {
val, _ := au.GetUint64(key, true)
return val
}
func (au *AttrUtility) OptionalBool(key string) bool {
val, _ := au.GetBool(key, false)
return val
}
func (au *AttrUtility) Bool(key string) bool {
val, _ := au.GetBool(key, true)
return val
}
// OK returns true if there are no errors.
func (au *AttrUtility) OK() bool {
return len(au.Errors) == 0
}
// Error returns the list of errors as a single error interface, or nil if there are no errors.
func (au *AttrUtility) Error() error {
if au.OK() {
return nil
}
return ErrorList(au.Errors)
}
// ErrorList is a list of errors that implements the error interface itself.
type ErrorList []error
// Error returns all the errors in the list as a string.
func (el ErrorList) Error() string {
return fmt.Sprintf("%+v", []error(el))
}

View File

@ -0,0 +1,353 @@
package binary
import (
"fmt"
"io"
"strings"
"go.mau.fi/whatsmeow/binary/token"
"go.mau.fi/whatsmeow/types"
)
type binaryDecoder struct {
data []byte
index int
}
func newDecoder(data []byte) *binaryDecoder {
return &binaryDecoder{data, 0}
}
func (r *binaryDecoder) checkEOS(length int) error {
if r.index+length > len(r.data) {
return io.EOF
}
return nil
}
func (r *binaryDecoder) readByte() (byte, error) {
if err := r.checkEOS(1); err != nil {
return 0, err
}
b := r.data[r.index]
r.index++
return b, nil
}
func (r *binaryDecoder) readIntN(n int, littleEndian bool) (int, error) {
if err := r.checkEOS(n); err != nil {
return 0, err
}
var ret int
for i := 0; i < n; i++ {
var curShift int
if littleEndian {
curShift = i
} else {
curShift = n - i - 1
}
ret |= int(r.data[r.index+i]) << uint(curShift*8)
}
r.index += n
return ret, nil
}
func (r *binaryDecoder) readInt8(littleEndian bool) (int, error) {
return r.readIntN(1, littleEndian)
}
func (r *binaryDecoder) readInt16(littleEndian bool) (int, error) {
return r.readIntN(2, littleEndian)
}
func (r *binaryDecoder) readInt20() (int, error) {
if err := r.checkEOS(3); err != nil {
return 0, err
}
ret := ((int(r.data[r.index]) & 15) << 16) + (int(r.data[r.index+1]) << 8) + int(r.data[r.index+2])
r.index += 3
return ret, nil
}
func (r *binaryDecoder) readInt32(littleEndian bool) (int, error) {
return r.readIntN(4, littleEndian)
}
func (r *binaryDecoder) readPacked8(tag int) (string, error) {
startByte, err := r.readByte()
if err != nil {
return "", err
}
var build strings.Builder
for i := 0; i < int(startByte&127); i++ {
currByte, err := r.readByte()
if err != nil {
return "", err
}
lower, err := unpackByte(tag, currByte&0xF0>>4)
if err != nil {
return "", err
}
upper, err := unpackByte(tag, currByte&0x0F)
if err != nil {
return "", err
}
build.WriteByte(lower)
build.WriteByte(upper)
}
ret := build.String()
if startByte>>7 != 0 {
ret = ret[:len(ret)-1]
}
return ret, nil
}
func unpackByte(tag int, value byte) (byte, error) {
switch tag {
case token.Nibble8:
return unpackNibble(value)
case token.Hex8:
return unpackHex(value)
default:
return 0, fmt.Errorf("unpackByte with unknown tag %d", tag)
}
}
func unpackNibble(value byte) (byte, error) {
switch {
case value < 10:
return '0' + value, nil
case value == 10:
return '-', nil
case value == 11:
return '.', nil
case value == 15:
return 0, nil
default:
return 0, fmt.Errorf("unpackNibble with value %d", value)
}
}
func unpackHex(value byte) (byte, error) {
switch {
case value < 10:
return '0' + value, nil
case value < 16:
return 'A' + value - 10, nil
default:
return 0, fmt.Errorf("unpackHex with value %d", value)
}
}
func (r *binaryDecoder) readListSize(tag int) (int, error) {
switch tag {
case token.ListEmpty:
return 0, nil
case token.List8:
return r.readInt8(false)
case token.List16:
return r.readInt16(false)
default:
return 0, fmt.Errorf("readListSize with unknown tag %d at position %d", tag, r.index)
}
}
func (r *binaryDecoder) read(string bool) (interface{}, error) {
tagByte, err := r.readByte()
if err != nil {
return nil, err
}
tag := int(tagByte)
switch tag {
case token.ListEmpty:
return nil, nil
case token.List8, token.List16:
return r.readList(tag)
case token.Binary8:
size, err := r.readInt8(false)
if err != nil {
return nil, err
}
return r.readBytesOrString(size, string)
case token.Binary20:
size, err := r.readInt20()
if err != nil {
return nil, err
}
return r.readBytesOrString(size, string)
case token.Binary32:
size, err := r.readInt32(false)
if err != nil {
return nil, err
}
return r.readBytesOrString(size, string)
case token.Dictionary0, token.Dictionary1, token.Dictionary2, token.Dictionary3:
i, err := r.readInt8(false)
if err != nil {
return "", err
}
return token.GetDoubleToken(tag-token.Dictionary0, i)
case token.JIDPair:
return r.readJIDPair()
case token.ADJID:
return r.readADJID()
case token.Nibble8, token.Hex8:
return r.readPacked8(tag)
default:
if tag >= 1 && tag < len(token.SingleByteTokens) {
return token.SingleByteTokens[tag], nil
}
return "", fmt.Errorf("%w %d at position %d", ErrInvalidToken, tag, r.index)
}
}
func (r *binaryDecoder) readJIDPair() (interface{}, error) {
user, err := r.read(true)
if err != nil {
return nil, err
}
server, err := r.read(true)
if err != nil {
return nil, err
} else if server == nil {
return nil, ErrInvalidJIDType
} else if user == nil {
return types.NewJID("", server.(string)), nil
}
return types.NewJID(user.(string), server.(string)), nil
}
func (r *binaryDecoder) readADJID() (interface{}, error) {
agent, err := r.readByte()
if err != nil {
return nil, err
}
device, err := r.readByte()
if err != nil {
return nil, err
}
user, err := r.read(true)
if err != nil {
return nil, err
}
return types.NewADJID(user.(string), agent, device), nil
}
func (r *binaryDecoder) readAttributes(n int) (Attrs, error) {
if n == 0 {
return nil, nil
}
ret := make(Attrs)
for i := 0; i < n; i++ {
keyIfc, err := r.read(true)
if err != nil {
return nil, err
}
key, ok := keyIfc.(string)
if !ok {
return nil, fmt.Errorf("%[1]w at position %[3]d (%[2]T): %+[2]v", ErrNonStringKey, key, r.index)
}
ret[key], err = r.read(true)
if err != nil {
return nil, err
}
}
return ret, nil
}
func (r *binaryDecoder) readList(tag int) ([]Node, error) {
size, err := r.readListSize(tag)
if err != nil {
return nil, err
}
ret := make([]Node, size)
for i := 0; i < size; i++ {
n, err := r.readNode()
if err != nil {
return nil, err
}
ret[i] = *n
}
return ret, nil
}
func (r *binaryDecoder) readNode() (*Node, error) {
ret := &Node{}
size, err := r.readInt8(false)
if err != nil {
return nil, err
}
listSize, err := r.readListSize(size)
if err != nil {
return nil, err
}
rawDesc, err := r.read(true)
if err != nil {
return nil, err
}
ret.Tag = rawDesc.(string)
if listSize == 0 || ret.Tag == "" {
return nil, ErrInvalidNode
}
ret.Attrs, err = r.readAttributes((listSize - 1) >> 1)
if err != nil {
return nil, err
}
if listSize%2 == 1 {
return ret, nil
}
ret.Content, err = r.read(false)
return ret, err
}
func (r *binaryDecoder) readBytesOrString(length int, asString bool) (interface{}, error) {
data, err := r.readRaw(length)
if err != nil {
return nil, err
}
if asString {
return string(data), nil
}
return data, nil
}
func (r *binaryDecoder) readRaw(length int) ([]byte, error) {
if err := r.checkEOS(length); err != nil {
return nil, err
}
ret := r.data[r.index : r.index+length]
r.index += length
return ret, nil
}

View File

@ -0,0 +1,293 @@
package binary
import (
"fmt"
"math"
"strconv"
"go.mau.fi/whatsmeow/binary/token"
"go.mau.fi/whatsmeow/types"
)
type binaryEncoder struct {
data []byte
}
func newEncoder() *binaryEncoder {
return &binaryEncoder{[]byte{0}}
}
func (w *binaryEncoder) getData() []byte {
return w.data
}
func (w *binaryEncoder) pushByte(b byte) {
w.data = append(w.data, b)
}
func (w *binaryEncoder) pushBytes(bytes []byte) {
w.data = append(w.data, bytes...)
}
func (w *binaryEncoder) pushIntN(value, n int, littleEndian bool) {
for i := 0; i < n; i++ {
var curShift int
if littleEndian {
curShift = i
} else {
curShift = n - i - 1
}
w.pushByte(byte((value >> uint(curShift*8)) & 0xFF))
}
}
func (w *binaryEncoder) pushInt20(value int) {
w.pushBytes([]byte{byte((value >> 16) & 0x0F), byte((value >> 8) & 0xFF), byte(value & 0xFF)})
}
func (w *binaryEncoder) pushInt8(value int) {
w.pushIntN(value, 1, false)
}
func (w *binaryEncoder) pushInt16(value int) {
w.pushIntN(value, 2, false)
}
func (w *binaryEncoder) pushInt32(value int) {
w.pushIntN(value, 4, false)
}
func (w *binaryEncoder) pushString(value string) {
w.pushBytes([]byte(value))
}
func (w *binaryEncoder) writeByteLength(length int) {
if length < 256 {
w.pushByte(token.Binary8)
w.pushInt8(length)
} else if length < (1 << 20) {
w.pushByte(token.Binary20)
w.pushInt20(length)
} else if length < math.MaxInt32 {
w.pushByte(token.Binary32)
w.pushInt32(length)
} else {
panic(fmt.Errorf("length is too large: %d", length))
}
}
const tagSize = 1
func (w *binaryEncoder) writeNode(n Node) {
if n.Tag == "0" {
w.pushByte(token.List8)
w.pushByte(token.ListEmpty)
return
}
hasContent := 0
if n.Content != nil {
hasContent = 1
}
w.writeListStart(2*len(n.Attrs) + tagSize + hasContent)
w.writeString(n.Tag)
w.writeAttributes(n.Attrs)
if n.Content != nil {
w.write(n.Content)
}
}
func (w *binaryEncoder) write(data interface{}) {
switch typedData := data.(type) {
case nil:
w.pushByte(token.ListEmpty)
case types.JID:
w.writeJID(typedData)
case string:
w.writeString(typedData)
case int:
w.writeString(strconv.Itoa(typedData))
case int32:
w.writeString(strconv.FormatInt(int64(typedData), 10))
case uint:
w.writeString(strconv.FormatUint(uint64(typedData), 10))
case uint32:
w.writeString(strconv.FormatUint(uint64(typedData), 10))
case int64:
w.writeString(strconv.FormatInt(typedData, 10))
case uint64:
w.writeString(strconv.FormatUint(typedData, 10))
case bool:
w.writeString(strconv.FormatBool(typedData))
case []byte:
w.writeBytes(typedData)
case []Node:
w.writeListStart(len(typedData))
for _, n := range typedData {
w.writeNode(n)
}
default:
panic(fmt.Errorf("%w: %T", ErrInvalidType, typedData))
}
}
func (w *binaryEncoder) writeString(data string) {
var dictIndex byte
if tokenIndex, ok := token.IndexOfSingleToken(data); ok {
w.pushByte(tokenIndex)
} else if dictIndex, tokenIndex, ok = token.IndexOfDoubleByteToken(data); ok {
w.pushByte(token.Dictionary0 + dictIndex)
w.pushByte(tokenIndex)
} else if validateNibble(data) {
w.writePackedBytes(data, token.Nibble8)
} else if validateHex(data) {
w.writePackedBytes(data, token.Hex8)
} else {
w.writeStringRaw(data)
}
}
func (w *binaryEncoder) writeBytes(value []byte) {
w.writeByteLength(len(value))
w.pushBytes(value)
}
func (w *binaryEncoder) writeStringRaw(value string) {
w.writeByteLength(len(value))
w.pushString(value)
}
func (w *binaryEncoder) writeJID(jid types.JID) {
if jid.AD {
w.pushByte(token.ADJID)
w.pushByte(jid.Agent)
w.pushByte(jid.Device)
w.writeString(jid.User)
} else {
w.pushByte(token.JIDPair)
if len(jid.User) == 0 {
w.pushByte(token.ListEmpty)
} else {
w.write(jid.User)
}
w.write(jid.Server)
}
}
func (w *binaryEncoder) writeAttributes(attributes Attrs) {
if attributes == nil {
return
}
for key, val := range attributes {
if val == "" || val == nil {
continue
}
w.writeString(key)
w.write(val)
}
}
func (w *binaryEncoder) writeListStart(listSize int) {
if listSize == 0 {
w.pushByte(byte(token.ListEmpty))
} else if listSize < 256 {
w.pushByte(byte(token.List8))
w.pushInt8(listSize)
} else {
w.pushByte(byte(token.List16))
w.pushInt16(listSize)
}
}
func (w *binaryEncoder) writePackedBytes(value string, dataType int) {
if len(value) > token.PackedMax {
panic(fmt.Errorf("too many bytes to pack: %d", len(value)))
}
w.pushByte(byte(dataType))
roundedLength := byte(math.Ceil(float64(len(value)) / 2.0))
if len(value)%2 != 0 {
roundedLength |= 128
}
w.pushByte(roundedLength)
var packer func(byte) byte
if dataType == token.Nibble8 {
packer = packNibble
} else if dataType == token.Hex8 {
packer = packHex
} else {
// This should only be called with the correct values
panic(fmt.Errorf("invalid packed byte data type %v", dataType))
}
for i, l := 0, len(value)/2; i < l; i++ {
w.pushByte(w.packBytePair(packer, value[2*i], value[2*i+1]))
}
if len(value)%2 != 0 {
w.pushByte(w.packBytePair(packer, value[len(value)-1], '\x00'))
}
}
func (w *binaryEncoder) packBytePair(packer func(byte) byte, part1, part2 byte) byte {
return (packer(part1) << 4) | packer(part2)
}
func validateNibble(value string) bool {
if len(value) > token.PackedMax {
return false
}
for _, char := range value {
if !(char >= '0' && char <= '9') && char != '-' && char != '.' {
return false
}
}
return true
}
func packNibble(value byte) byte {
switch value {
case '-':
return 10
case '.':
return 11
case 0:
return 15
default:
if value >= '0' && value <= '9' {
return value - '0'
}
// This should be validated beforehand
panic(fmt.Errorf("invalid string to pack as nibble: %d / '%s'", value, string(value)))
}
}
func validateHex(value string) bool {
if len(value) > token.PackedMax {
return false
}
for _, char := range value {
if !(char >= '0' && char <= '9') && !(char >= 'A' && char <= 'F') && !(char >= 'a' && char <= 'f') {
return false
}
}
return true
}
func packHex(value byte) byte {
switch {
case value >= '0' && value <= '9':
return value - '0'
case value >= 'A' && value <= 'F':
return 10 + value - 'A'
case value >= 'a' && value <= 'f':
return 10 + value - 'a'
case value == 0:
return 15
default:
// This should be validated beforehand
panic(fmt.Errorf("invalid string to pack as hex: %d / '%s'", value, string(value)))
}
}

View File

@ -0,0 +1,12 @@
package binary
import "errors"
// Errors returned by the binary XML decoder.
var (
ErrInvalidType = errors.New("unsupported payload type")
ErrInvalidJIDType = errors.New("invalid JID type")
ErrInvalidNode = errors.New("invalid node")
ErrInvalidToken = errors.New("invalid token with tag")
ErrNonStringKey = errors.New("non-string key")
)

View File

@ -0,0 +1,83 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package binary implements encoding and decoding documents in WhatsApp's binary XML format.
package binary
// Attrs is a type alias for the attributes of an XML element (Node).
type Attrs = map[string]interface{}
// Node represents an XML element.
type Node struct {
Tag string // The tag of the element.
Attrs Attrs // The attributes of the element.
Content interface{} // The content inside the element. Can be nil, a list of Nodes, or a byte array.
}
// GetChildren returns the Content of the node as a list of nodes. If the content is not a list of nodes, this returns nil.
func (n *Node) GetChildren() []Node {
if n.Content == nil {
return nil
}
children, ok := n.Content.([]Node)
if !ok {
return nil
}
return children
}
// GetChildrenByTag returns the same list as GetChildren, but filters it by tag first.
func (n *Node) GetChildrenByTag(tag string) (children []Node) {
for _, node := range n.GetChildren() {
if node.Tag == tag {
children = append(children, node)
}
}
return
}
// GetOptionalChildByTag finds the first child with the given tag and returns it.
// Each provided tag will recurse in, so this is useful for getting a specific nested element.
func (n *Node) GetOptionalChildByTag(tags ...string) (val Node, ok bool) {
val = *n
Outer:
for _, tag := range tags {
for _, child := range val.GetChildren() {
if child.Tag == tag {
val = child
continue Outer
}
}
// If no matching children are found, return false
return
}
// All iterations of loop found a matching child, return it
ok = true
return
}
// GetChildByTag does the same thing as GetOptionalChildByTag, but returns the Node directly without the ok boolean.
func (n *Node) GetChildByTag(tags ...string) Node {
node, _ := n.GetOptionalChildByTag(tags...)
return node
}
// Marshal encodes an XML element (Node) into WhatsApp's binary XML representation.
func Marshal(n Node) ([]byte, error) {
w := newEncoder()
w.writeNode(n)
return w.getData(), nil
}
// Unmarshal decodes WhatsApp's binary XML representation into a Node.
func Unmarshal(data []byte) (*Node, error) {
r := newDecoder(data)
n, err := r.readNode()
if err != nil {
return nil, err
}
return n, nil
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
// Package proto contains the compiled protobuf structs from WhatsApp's protobuf schema.
package proto

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,31 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package binary
import (
"bytes"
"compress/zlib"
"fmt"
"io"
)
// Unpack unpacks the given decrypted data from the WhatsApp web API.
//
// It checks the first byte to decide whether to uncompress the data with zlib or just return as-is
// (without the first byte). There's currently no corresponding Pack function because Marshal
// already returns the data with a leading zero (i.e. not compressed).
func Unpack(data []byte) ([]byte, error) {
dataType, data := data[0], data[1:]
if 2&dataType > 0 {
if decompressor, err := zlib.NewReader(bytes.NewReader(data)); err != nil {
return nil, fmt.Errorf("failed to create zlib reader: %w", err)
} else if data, err = io.ReadAll(decompressor); err != nil {
return nil, err
}
}
return data, nil
}

108
vendor/go.mau.fi/whatsmeow/binary/xml.go vendored Normal file
View File

@ -0,0 +1,108 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package binary
import (
"encoding/hex"
"fmt"
"sort"
"strings"
"unicode"
"unicode/utf8"
)
// Options to control how Node.XMLString behaves.
var (
IndentXML = false
MaxBytesToPrintAsHex = 128
)
// XMLString converts the Node to its XML representation
func (n *Node) XMLString() string {
content := n.contentString()
if len(content) == 0 {
return fmt.Sprintf("<%[1]s%[2]s/>", n.Tag, n.attributeString())
}
newline := "\n"
if len(content) == 1 || !IndentXML {
newline = ""
}
return fmt.Sprintf("<%[1]s%[2]s>%[4]s%[3]s%[4]s</%[1]s>", n.Tag, n.attributeString(), strings.Join(content, newline), newline)
}
func (n *Node) attributeString() string {
if len(n.Attrs) == 0 {
return ""
}
stringAttrs := make([]string, len(n.Attrs)+1)
i := 1
for key, value := range n.Attrs {
stringAttrs[i] = fmt.Sprintf(`%s="%v"`, key, value)
i++
}
sort.Strings(stringAttrs)
return strings.Join(stringAttrs, " ")
}
func printable(data []byte) string {
if !utf8.Valid(data) {
return ""
}
str := string(data)
for _, c := range str {
if !unicode.IsPrint(c) {
return ""
}
}
return str
}
func (n *Node) contentString() []string {
split := make([]string, 0)
switch content := n.Content.(type) {
case []Node:
for _, item := range content {
split = append(split, strings.Split(item.XMLString(), "\n")...)
}
case []byte:
if strContent := printable(content); len(strContent) > 0 {
if IndentXML {
split = append(split, strings.Split(string(content), "\n")...)
} else {
split = append(split, strings.ReplaceAll(string(content), "\n", "\\n"))
}
} else if len(content) > MaxBytesToPrintAsHex {
split = append(split, fmt.Sprintf("<!-- %d bytes -->", len(content)))
} else if !IndentXML {
split = append(split, hex.EncodeToString(content))
} else {
hexData := hex.EncodeToString(content)
for i := 0; i < len(hexData); i += 80 {
if len(hexData) < i+80 {
split = append(split, hexData[i:])
} else {
split = append(split, hexData[i:i+80])
}
}
}
case nil:
// don't append anything
default:
strContent := fmt.Sprintf("%s", content)
if IndentXML {
split = append(split, strings.Split(strContent, "\n")...)
} else {
split = append(split, strings.ReplaceAll(strContent, "\n", "\\n"))
}
}
if len(split) > 1 && IndentXML {
for i, line := range split {
split[i] = " " + line
}
}
return split
}