| // 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, tag: e.tag} | |
| 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(msgFirst) | |
| 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") | |
| } | |
| } |