407 lines
10 KiB
Go
Executable file
407 lines
10 KiB
Go
Executable file
// Copyright 2017 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package catmsg
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"golang.org/x/text/language"
|
|
)
|
|
|
|
// A Renderer renders a Message.
|
|
type Renderer interface {
|
|
// Render renders the given string. The given string may be interpreted as a
|
|
// format string, such as the one used by the fmt package or a template.
|
|
Render(s string)
|
|
|
|
// Arg returns the i-th argument passed to format a message. This method
|
|
// should return nil if there is no such argument. Messages need access to
|
|
// arguments to allow selecting a message based on linguistic features of
|
|
// those arguments.
|
|
Arg(i int) interface{}
|
|
}
|
|
|
|
// A Dictionary specifies a source of messages, including variables or macros.
|
|
type Dictionary interface {
|
|
// Lookup returns the message for the given key. It returns false for ok if
|
|
// such a message could not be found.
|
|
Lookup(key string) (data string, ok bool)
|
|
|
|
// TODO: consider returning an interface, instead of a string. This will
|
|
// allow implementations to do their own message type decoding.
|
|
}
|
|
|
|
// An Encoder serializes a Message to a string.
|
|
type Encoder struct {
|
|
// The root encoder is used for storing encoded variables.
|
|
root *Encoder
|
|
// The parent encoder provides the surrounding scopes for resolving variable
|
|
// names.
|
|
parent *Encoder
|
|
|
|
tag language.Tag
|
|
|
|
// buf holds the encoded message so far. After a message completes encoding,
|
|
// the contents of buf, prefixed by the encoded length, are flushed to the
|
|
// parent buffer.
|
|
buf []byte
|
|
|
|
// vars is the lookup table of variables in the current scope.
|
|
vars []keyVal
|
|
|
|
err error
|
|
inBody bool // if false next call must be EncodeMessageType
|
|
}
|
|
|
|
type keyVal struct {
|
|
key string
|
|
offset int
|
|
}
|
|
|
|
// Language reports the language for which the encoded message will be stored
|
|
// in the Catalog.
|
|
func (e *Encoder) Language() language.Tag { return e.tag }
|
|
|
|
func (e *Encoder) setError(err error) {
|
|
if e.root.err == nil {
|
|
e.root.err = err
|
|
}
|
|
}
|
|
|
|
// EncodeUint encodes x.
|
|
func (e *Encoder) EncodeUint(x uint64) {
|
|
e.checkInBody()
|
|
var buf [maxVarintBytes]byte
|
|
n := encodeUint(buf[:], x)
|
|
e.buf = append(e.buf, buf[:n]...)
|
|
}
|
|
|
|
// EncodeString encodes s.
|
|
func (e *Encoder) EncodeString(s string) {
|
|
e.checkInBody()
|
|
e.EncodeUint(uint64(len(s)))
|
|
e.buf = append(e.buf, s...)
|
|
}
|
|
|
|
// EncodeMessageType marks the current message to be of type h.
|
|
//
|
|
// It must be the first call of a Message's Compile method.
|
|
func (e *Encoder) EncodeMessageType(h Handle) {
|
|
if e.inBody {
|
|
panic("catmsg: EncodeMessageType not the first method called")
|
|
}
|
|
e.inBody = true
|
|
e.EncodeUint(uint64(h))
|
|
}
|
|
|
|
// EncodeMessage serializes the given message inline at the current position.
|
|
func (e *Encoder) EncodeMessage(m Message) error {
|
|
e = &Encoder{root: e.root, parent: e}
|
|
err := m.Compile(e)
|
|
if _, ok := m.(*Var); !ok {
|
|
e.flushTo(e.parent)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (e *Encoder) checkInBody() {
|
|
if !e.inBody {
|
|
panic("catmsg: expected prior call to EncodeMessageType")
|
|
}
|
|
}
|
|
|
|
// stripPrefix indicates the number of prefix bytes that must be stripped to
|
|
// turn a single-element sequence into a message that is just this single member
|
|
// without its size prefix. If the message can be stripped, b[1:n] contains the
|
|
// size prefix.
|
|
func stripPrefix(b []byte) (n int) {
|
|
if len(b) > 0 && Handle(b[0]) == msgFirst {
|
|
x, n, _ := decodeUint(b[1:])
|
|
if 1+n+int(x) == len(b) {
|
|
return 1 + n
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (e *Encoder) flushTo(dst *Encoder) {
|
|
data := e.buf
|
|
p := stripPrefix(data)
|
|
if p > 0 {
|
|
data = data[1:]
|
|
} else {
|
|
// Prefix the size.
|
|
dst.EncodeUint(uint64(len(data)))
|
|
}
|
|
dst.buf = append(dst.buf, data...)
|
|
}
|
|
|
|
func (e *Encoder) addVar(key string, m Message) error {
|
|
for _, v := range e.parent.vars {
|
|
if v.key == key {
|
|
err := fmt.Errorf("catmsg: duplicate variable %q", key)
|
|
e.setError(err)
|
|
return err
|
|
}
|
|
}
|
|
scope := e.parent
|
|
// If a variable message is Incomplete, and does not evaluate to a message
|
|
// during execution, we fall back to the variable name. We encode this by
|
|
// appending the variable name if the message reports it's incomplete.
|
|
|
|
err := m.Compile(e)
|
|
if err != ErrIncomplete {
|
|
e.setError(err)
|
|
}
|
|
switch {
|
|
case len(e.buf) == 1 && Handle(e.buf[0]) == msgFirst: // empty sequence
|
|
e.buf = e.buf[:0]
|
|
e.inBody = false
|
|
fallthrough
|
|
case len(e.buf) == 0:
|
|
// Empty message.
|
|
if err := String(key).Compile(e); err != nil {
|
|
e.setError(err)
|
|
}
|
|
case err == ErrIncomplete:
|
|
if Handle(e.buf[0]) != msgFirst {
|
|
seq := &Encoder{root: e.root, parent: e}
|
|
seq.EncodeMessageType(First)
|
|
e.flushTo(seq)
|
|
e = seq
|
|
}
|
|
// e contains a sequence; append the fallback string.
|
|
e.EncodeMessage(String(key))
|
|
}
|
|
|
|
// Flush result to variable heap.
|
|
offset := len(e.root.buf)
|
|
e.flushTo(e.root)
|
|
e.buf = e.buf[:0]
|
|
|
|
// Record variable offset in current scope.
|
|
scope.vars = append(scope.vars, keyVal{key: key, offset: offset})
|
|
return err
|
|
}
|
|
|
|
const (
|
|
substituteVar = iota
|
|
substituteMacro
|
|
substituteError
|
|
)
|
|
|
|
// EncodeSubstitution inserts a resolved reference to a variable or macro.
|
|
//
|
|
// This call must be matched with a call to ExecuteSubstitution at decoding
|
|
// time.
|
|
func (e *Encoder) EncodeSubstitution(name string, arguments ...int) {
|
|
if arity := len(arguments); arity > 0 {
|
|
// TODO: also resolve macros.
|
|
e.EncodeUint(substituteMacro)
|
|
e.EncodeString(name)
|
|
for _, a := range arguments {
|
|
e.EncodeUint(uint64(a))
|
|
}
|
|
return
|
|
}
|
|
for scope := e; scope != nil; scope = scope.parent {
|
|
for _, v := range scope.vars {
|
|
if v.key != name {
|
|
continue
|
|
}
|
|
e.EncodeUint(substituteVar) // TODO: support arity > 0
|
|
e.EncodeUint(uint64(v.offset))
|
|
return
|
|
}
|
|
}
|
|
// TODO: refer to dictionary-wide scoped variables.
|
|
e.EncodeUint(substituteError)
|
|
e.EncodeString(name)
|
|
e.setError(fmt.Errorf("catmsg: unknown var %q", name))
|
|
}
|
|
|
|
// A Decoder deserializes and evaluates messages that are encoded by an encoder.
|
|
type Decoder struct {
|
|
tag language.Tag
|
|
dst Renderer
|
|
macros Dictionary
|
|
|
|
err error
|
|
vars string
|
|
data string
|
|
|
|
macroArg int // TODO: allow more than one argument
|
|
}
|
|
|
|
// NewDecoder returns a new Decoder.
|
|
//
|
|
// Decoders are designed to be reused for multiple invocations of Execute.
|
|
// Only one goroutine may call Execute concurrently.
|
|
func NewDecoder(tag language.Tag, r Renderer, macros Dictionary) *Decoder {
|
|
return &Decoder{
|
|
tag: tag,
|
|
dst: r,
|
|
macros: macros,
|
|
}
|
|
}
|
|
|
|
func (d *Decoder) setError(err error) {
|
|
if d.err == nil {
|
|
d.err = err
|
|
}
|
|
}
|
|
|
|
// Language returns the language in which the message is being rendered.
|
|
//
|
|
// The destination language may be a child language of the language used for
|
|
// encoding. For instance, a decoding language of "pt-PT"" is consistent with an
|
|
// encoding language of "pt".
|
|
func (d *Decoder) Language() language.Tag { return d.tag }
|
|
|
|
// Done reports whether there are more bytes to process in this message.
|
|
func (d *Decoder) Done() bool { return len(d.data) == 0 }
|
|
|
|
// Render implements Renderer.
|
|
func (d *Decoder) Render(s string) { d.dst.Render(s) }
|
|
|
|
// Arg implements Renderer.
|
|
//
|
|
// During evaluation of macros, the argument positions may be mapped to
|
|
// arguments that differ from the original call.
|
|
func (d *Decoder) Arg(i int) interface{} {
|
|
if d.macroArg != 0 {
|
|
if i != 1 {
|
|
panic("catmsg: only macros with single argument supported")
|
|
}
|
|
i = d.macroArg
|
|
}
|
|
return d.dst.Arg(i)
|
|
}
|
|
|
|
// DecodeUint decodes a number that was encoded with EncodeUint and advances the
|
|
// position.
|
|
func (d *Decoder) DecodeUint() uint64 {
|
|
x, n, err := decodeUintString(d.data)
|
|
d.data = d.data[n:]
|
|
if err != nil {
|
|
d.setError(err)
|
|
}
|
|
return x
|
|
}
|
|
|
|
// DecodeString decodes a string that was encoded with EncodeString and advances
|
|
// the position.
|
|
func (d *Decoder) DecodeString() string {
|
|
size := d.DecodeUint()
|
|
s := d.data[:size]
|
|
d.data = d.data[size:]
|
|
return s
|
|
}
|
|
|
|
// SkipMessage skips the message at the current location and advances the
|
|
// position.
|
|
func (d *Decoder) SkipMessage() {
|
|
n := int(d.DecodeUint())
|
|
d.data = d.data[n:]
|
|
}
|
|
|
|
// Execute decodes and evaluates msg.
|
|
//
|
|
// Only one goroutine may call execute.
|
|
func (d *Decoder) Execute(msg string) error {
|
|
d.err = nil
|
|
if !d.execute(msg) {
|
|
return ErrNoMatch
|
|
}
|
|
return d.err
|
|
}
|
|
|
|
func (d *Decoder) execute(msg string) bool {
|
|
saved := d.data
|
|
d.data = msg
|
|
ok := d.executeMessage()
|
|
d.data = saved
|
|
return ok
|
|
}
|
|
|
|
// executeMessageFromData is like execute, but also decodes a leading message
|
|
// size and clips the given string accordingly.
|
|
//
|
|
// It reports the number of bytes consumed and whether a message was selected.
|
|
func (d *Decoder) executeMessageFromData(s string) (n int, ok bool) {
|
|
saved := d.data
|
|
d.data = s
|
|
size := int(d.DecodeUint())
|
|
n = len(s) - len(d.data)
|
|
// Sanitize the setting. This allows skipping a size argument for
|
|
// RawString and method Done.
|
|
d.data = d.data[:size]
|
|
ok = d.executeMessage()
|
|
n += size - len(d.data)
|
|
d.data = saved
|
|
return n, ok
|
|
}
|
|
|
|
var errUnknownHandler = errors.New("catmsg: string contains unsupported handler")
|
|
|
|
// executeMessage reads the handle id, initializes the decoder and executes the
|
|
// message. It is assumed that all of d.data[d.p:] is the single message.
|
|
func (d *Decoder) executeMessage() bool {
|
|
if d.Done() {
|
|
// We interpret no data as a valid empty message.
|
|
return true
|
|
}
|
|
handle := d.DecodeUint()
|
|
|
|
var fn Handler
|
|
mutex.Lock()
|
|
if int(handle) < len(handlers) {
|
|
fn = handlers[handle]
|
|
}
|
|
mutex.Unlock()
|
|
if fn == nil {
|
|
d.setError(errUnknownHandler)
|
|
d.execute(fmt.Sprintf("\x02$!(UNKNOWNMSGHANDLER=%#x)", handle))
|
|
return true
|
|
}
|
|
return fn(d)
|
|
}
|
|
|
|
// ExecuteMessage decodes and executes the message at the current position.
|
|
func (d *Decoder) ExecuteMessage() bool {
|
|
n, ok := d.executeMessageFromData(d.data)
|
|
d.data = d.data[n:]
|
|
return ok
|
|
}
|
|
|
|
// ExecuteSubstitution executes the message corresponding to the substitution
|
|
// as encoded by EncodeSubstitution.
|
|
func (d *Decoder) ExecuteSubstitution() {
|
|
switch x := d.DecodeUint(); x {
|
|
case substituteVar:
|
|
offset := d.DecodeUint()
|
|
d.executeMessageFromData(d.vars[offset:])
|
|
case substituteMacro:
|
|
name := d.DecodeString()
|
|
data, ok := d.macros.Lookup(name)
|
|
old := d.macroArg
|
|
// TODO: support macros of arity other than 1.
|
|
d.macroArg = int(d.DecodeUint())
|
|
switch {
|
|
case !ok:
|
|
// TODO: detect this at creation time.
|
|
d.setError(fmt.Errorf("catmsg: undefined macro %q", name))
|
|
fallthrough
|
|
case !d.execute(data):
|
|
d.dst.Render(name) // fall back to macro name.
|
|
}
|
|
d.macroArg = old
|
|
case substituteError:
|
|
d.dst.Render(d.DecodeString())
|
|
default:
|
|
panic("catmsg: unreachable")
|
|
}
|
|
}
|