blob: 2f6b85bb9a146e19676e9182dccad4e5c724a922 [file]
// Package unquote provides a function to unquote txtpb-formatted quoted string literals.
package unquote
import (
"errors"
"fmt"
"strconv"
"strings"
"unicode/utf8"
"github.com/protocolbuffers/txtpbfmt/ast"
)
// Unquote returns the value of the string node.
// Calling Unquote on non-string node doesn't panic, but is otherwise undefined.
func Unquote(n *ast.Node) (string, error) {
return unquoteValues(n.Values, unquote)
}
// Raw returns the raw value of the string node, with string escapes left in place.
// Calling UnquoteRaw on non-string node doesn't panic, but is otherwise undefined.
func Raw(n *ast.Node) (string, error) {
return unquoteValues(n.Values, unquoteRaw)
}
func unquoteValues(values []*ast.Value, unquoter func(string) (string, error)) (string, error) {
var ret strings.Builder
for _, v := range values {
uq, err := unquoter(v.Value)
if err != nil {
return "", err
}
ret.WriteString(uq)
}
return ret.String(), nil
}
// Returns the quote rune used in the given string (' or "). Returns an error if the string doesn't
// start and end with a matching pair of valid quotes.
func quoteRune(s string) (rune, error) {
if len(s) < 2 {
return 0, errors.New("not a quoted string")
}
quote := s[0]
if quote != '"' && quote != '\'' {
return 0, fmt.Errorf("invalid quote character %s", string(quote))
}
if s[len(s)-1] != quote {
return 0, errors.New("unmatched quote")
}
return rune(quote), nil
}
func unquote(s string) (string, error) {
quote, err := quoteRune(s)
if err != nil {
return "", err
}
return unquoteC(s[1:len(s)-1], quote)
}
func unquoteRaw(s string) (string, error) {
_, err := quoteRune(s) // Trigger validation, which guarantees this is a quote-wrapped string.
if err != nil {
return "", err
}
return s[1 : len(s)-1], nil
}
var (
errBadUTF8 = errors.New("bad UTF-8")
)
func unquoteC(s string, quote rune) (string, error) {
// Copied from third_party/golang/protobuf/proto/text_parser.go
// This is based on C++'s tokenizer.cc.
// Despite its name, this is *not* parsing C syntax.
// For instance, "\0" is an invalid quoted string.
// Avoid allocation in trivial cases.
simple := true
for _, r := range s {
if r == '\\' || r == quote {
simple = false
break
}
}
if simple {
return s, nil
}
buf := make([]byte, 0, 3*len(s)/2)
for len(s) > 0 {
r, n := utf8.DecodeRuneInString(s)
if r == utf8.RuneError && n == 1 {
return "", errBadUTF8
}
s = s[n:]
if r != '\\' {
if r < utf8.RuneSelf {
buf = append(buf, byte(r))
} else {
buf = append(buf, string(r)...)
}
continue
}
ch, tail, err := unescape(s)
if err != nil {
return "", err
}
buf = append(buf, ch...)
s = tail
}
return string(buf), nil
}
func unescape(s string) (ch string, tail string, err error) {
// Copied from third_party/golang/protobuf/proto/text_parser.go
r, n := utf8.DecodeRuneInString(s)
if r == utf8.RuneError && n == 1 {
return "", "", errBadUTF8
}
s = s[n:]
switch r {
case 'a':
return "\a", s, nil
case 'b':
return "\b", s, nil
case 'f':
return "\f", s, nil
case 'n':
return "\n", s, nil
case 'r':
return "\r", s, nil
case 't':
return "\t", s, nil
case 'v':
return "\v", s, nil
case '?':
return "?", s, nil // trigraph workaround
case '\'', '"', '\\':
return string(r), s, nil
case '0', '1', '2', '3', '4', '5', '6', '7':
if len(s) < 2 {
return "", "", fmt.Errorf(`\%c requires 2 following digits`, r)
}
ss := string(r) + s[:2]
s = s[2:]
i, err := strconv.ParseUint(ss, 8, 8)
if err != nil {
return "", "", fmt.Errorf(`\%s contains non-octal digits`, ss)
}
return string([]byte{byte(i)}), s, nil
case 'x', 'X', 'u', 'U':
var n int
switch r {
case 'x', 'X':
n = 2
case 'u':
n = 4
case 'U':
n = 8
}
if len(s) < n {
return "", "", fmt.Errorf(`\%c requires %d following digits`, r, n)
}
ss := s[:n]
s = s[n:]
i, err := strconv.ParseUint(ss, 16, 64)
if err != nil {
return "", "", fmt.Errorf(`\%c%s contains non-hexadecimal digits`, r, ss)
}
if r == 'x' || r == 'X' {
return string([]byte{byte(i)}), s, nil
}
if i > utf8.MaxRune {
return "", "", fmt.Errorf(`\%c%s is not a valid Unicode code point`, r, ss)
}
return strconv.FormatUint(i, 10), s, nil
}
return "", "", fmt.Errorf(`unknown escape \%c`, r)
}