summaryrefslogtreecommitdiffhomepage
path: root/vendor/golang.org/x/text/internal/catmsg/codec.go
blob: 49c9fc9789acd056a8305971d5bbecfbdbfab21b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
// 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")
	}
}